1/*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements.  See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License.  You may obtain a copy of the License at
8 *
9 *     http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package java.util.jar;
19
20import java.io.File;
21import java.io.FilterInputStream;
22import java.io.IOException;
23import java.io.InputStream;
24import java.util.ArrayList;
25import java.util.Enumeration;
26import java.util.List;
27import java.util.zip.ZipEntry;
28import java.util.zip.ZipFile;
29
30import org.apache.harmony.archive.internal.nls.Messages;
31import org.apache.harmony.archive.util.Util;
32import org.apache.harmony.luni.util.InputStreamHelper;
33
34/**
35 * {@code JarFile} is used to read jar entries and their associated data from
36 * jar files.
37 *
38 * @see JarInputStream
39 * @see JarEntry
40 */
41public class JarFile extends ZipFile {
42
43    /**
44     * The MANIFEST file name.
45     */
46    public static final String MANIFEST_NAME = "META-INF/MANIFEST.MF"; //$NON-NLS-1$
47
48    // The directory containing the manifest.
49    static final String META_DIR = "META-INF/"; //$NON-NLS-1$
50
51    // The manifest after it has been read from the JAR.
52    private Manifest manifest;
53
54    // The entry for the MANIFEST.MF file before it is read.
55    private ZipEntry manifestEntry;
56
57    JarVerifier verifier;
58
59    private boolean closed = false;
60
61    static final class JarFileInputStream extends FilterInputStream {
62        private long count;
63
64        private ZipEntry zipEntry;
65
66        private JarVerifier.VerifierEntry entry;
67
68        private boolean done = false;
69
70        JarFileInputStream(InputStream is, ZipEntry ze,
71                JarVerifier.VerifierEntry e) {
72            super(is);
73            zipEntry = ze;
74            count = zipEntry.getSize();
75            entry = e;
76        }
77
78        @Override
79        public int read() throws IOException {
80            if (done) {
81                return -1;
82            }
83            if (count > 0) {
84                int r = super.read();
85                if (r != -1) {
86                    entry.write(r);
87                    count--;
88                } else {
89                    count = 0;
90                }
91                if (count == 0) {
92                    done = true;
93                    entry.verify();
94                }
95                return r;
96            } else {
97                done = true;
98                entry.verify();
99                return -1;
100            }
101        }
102
103        @Override
104        public int read(byte[] buf, int off, int nbytes) throws IOException {
105            if (done) {
106                return -1;
107            }
108            if (count > 0) {
109                int r = super.read(buf, off, nbytes);
110                if (r != -1) {
111                    int size = r;
112                    if (count < size) {
113                        size = (int) count;
114                    }
115                    entry.write(buf, off, size);
116                    count -= size;
117                } else {
118                    count = 0;
119                }
120                if (count == 0) {
121                    done = true;
122                    entry.verify();
123                }
124                return r;
125            } else {
126                done = true;
127                entry.verify();
128                return -1;
129            }
130        }
131
132        @Override
133        public int available() throws IOException {
134            if (done) {
135                return 0;
136            }
137            return super.available();
138        }
139
140        @Override
141        public long skip(long nbytes) throws IOException {
142            long cnt = 0, rem = 0;
143            byte[] buf = new byte[(int)Math.min(nbytes, 2048L)];
144            while (cnt < nbytes) {
145                int x = read(buf, 0,
146                        (rem = nbytes - cnt) > buf.length ? buf.length
147                                : (int) rem);
148                if (x == -1) {
149                    return cnt;
150                }
151                cnt += x;
152            }
153            return cnt;
154        }
155    }
156
157    /**
158     * Create a new {@code JarFile} using the contents of the specified file.
159     *
160     * @param file
161     *            the JAR file as {@link File}.
162     * @throws IOException
163     *             If the file cannot be read.
164     */
165    public JarFile(File file) throws IOException {
166        this(file, true);
167    }
168
169    /**
170     * Create a new {@code JarFile} using the contents of the specified file.
171     *
172     * @param file
173     *            the JAR file as {@link File}.
174     * @param verify
175     *            if this JAR file is signed whether it must be verified.
176     * @throws IOException
177     *             If the file cannot be read.
178     */
179    public JarFile(File file, boolean verify) throws IOException {
180        super(file);
181        if (verify) {
182            verifier = new JarVerifier(file.getPath());
183        }
184        readMetaEntries();
185    }
186
187    /**
188     * Create a new {@code JarFile} using the contents of file.
189     *
190     * @param file
191     *            the JAR file as {@link File}.
192     * @param verify
193     *            if this JAR filed is signed whether it must be verified.
194     * @param mode
195     *            the mode to use, either {@link ZipFile#OPEN_READ OPEN_READ} or
196     *            {@link ZipFile#OPEN_DELETE OPEN_DELETE}.
197     * @throws IOException
198     *             If the file cannot be read.
199     */
200    public JarFile(File file, boolean verify, int mode) throws IOException {
201        super(file, mode);
202        if (verify) {
203            verifier = new JarVerifier(file.getPath());
204        }
205        readMetaEntries();
206    }
207
208    /**
209     * Create a new {@code JarFile} from the contents of the file specified by
210     * filename.
211     *
212     * @param filename
213     *            the file name referring to the JAR file.
214     * @throws IOException
215     *             if file name cannot be opened for reading.
216     */
217    public JarFile(String filename) throws IOException {
218        this(filename, true);
219    }
220
221    /**
222     * Create a new {@code JarFile} from the contents of the file specified by
223     * {@code filename}.
224     *
225     * @param filename
226     *            the file name referring to the JAR file.
227     * @param verify
228     *            if this JAR filed is signed whether it must be verified.
229     * @throws IOException
230     *             If file cannot be opened or read.
231     */
232    public JarFile(String filename, boolean verify) throws IOException {
233        super(filename);
234        if (verify) {
235            verifier = new JarVerifier(filename);
236        }
237        readMetaEntries();
238    }
239
240    /**
241     * Return an enumeration containing the {@code JarEntrys} contained in this
242     * {@code JarFile}.
243     *
244     * @return the {@code Enumeration} containing the JAR entries.
245     * @throws IllegalStateException
246     *             if this {@code JarFile} is closed.
247     */
248    @Override
249    public Enumeration<JarEntry> entries() {
250        class JarFileEnumerator implements Enumeration<JarEntry> {
251            Enumeration<? extends ZipEntry> ze;
252
253            JarFile jf;
254
255            JarFileEnumerator(Enumeration<? extends ZipEntry> zenum, JarFile jf) {
256                ze = zenum;
257                this.jf = jf;
258            }
259
260            public boolean hasMoreElements() {
261                return ze.hasMoreElements();
262            }
263
264            public JarEntry nextElement() {
265                JarEntry je = new JarEntry(ze.nextElement());
266                je.parentJar = jf;
267                return je;
268            }
269        }
270        return new JarFileEnumerator(super.entries(), this);
271    }
272
273    /**
274     * Return the {@code JarEntry} specified by its name or {@code null} if no
275     * such entry exists.
276     *
277     * @param name
278     *            the name of the entry in the JAR file.
279     * @return the JAR entry defined by the name.
280     */
281    public JarEntry getJarEntry(String name) {
282        return (JarEntry) getEntry(name);
283    }
284
285    /**
286     * Returns the {@code Manifest} object associated with this {@code JarFile}
287     * or {@code null} if no MANIFEST entry exists.
288     *
289     * @return the MANIFEST.
290     * @throws IOException
291     *             if an error occurs reading the MANIFEST file.
292     * @throws IllegalStateException
293     *             if the jar file is closed.
294     * @see Manifest
295     */
296    public Manifest getManifest() throws IOException {
297        if (closed) {
298            // archive.35=JarFile has been closed
299            throw new IllegalStateException(Messages.getString("archive.35")); //$NON-NLS-1$
300        }
301        if (manifest != null) {
302            return manifest;
303        }
304        try {
305            InputStream is = super.getInputStream(manifestEntry);
306            if (verifier != null) {
307                verifier.addMetaEntry(manifestEntry.getName(),
308                        InputStreamHelper.readFullyAndClose(is));
309                is = super.getInputStream(manifestEntry);
310            }
311            try {
312                manifest = new Manifest(is, verifier != null);
313            } finally {
314                is.close();
315            }
316            manifestEntry = null;  // Can discard the entry now.
317        } catch (NullPointerException e) {
318            manifestEntry = null;
319        }
320        return manifest;
321    }
322
323    /**
324     * Called by the JarFile constructors, this method reads the contents of the
325     * file's META-INF/ directory and picks out the MANIFEST.MF file and
326     * verifier signature files if they exist. Any signature files found are
327     * registered with the verifier.
328     *
329     * @throws IOException
330     *             if there is a problem reading the jar file entries.
331     */
332    private void readMetaEntries() throws IOException {
333        // Get all meta directory entries
334        ZipEntry[] metaEntries = getMetaEntriesImpl();
335        if (metaEntries == null) {
336            verifier = null;
337            return;
338        }
339
340        boolean signed = false;
341
342        for (ZipEntry entry : metaEntries) {
343            String entryName = entry.getName();
344            // Is this the entry for META-INF/MANIFEST.MF ?
345            if (manifestEntry == null
346                    && Util.asciiEqualsIgnoreCase(MANIFEST_NAME, entryName)) {
347                manifestEntry = entry;
348                // If there is no verifier then we don't need to look any further.
349                if (verifier == null) {
350                    break;
351                }
352            } else {
353                // Is this an entry that the verifier needs?
354                if (verifier != null
355                        && (Util.asciiEndsWithIgnoreCase(entryName, ".SF")
356                                || Util.asciiEndsWithIgnoreCase(entryName, ".DSA")
357                                || Util.asciiEndsWithIgnoreCase(entryName, ".RSA"))) {
358                    signed = true;
359                    InputStream is = super.getInputStream(entry);
360                    byte[] buf = InputStreamHelper.readFullyAndClose(is);
361                    verifier.addMetaEntry(entryName, buf);
362                }
363            }
364        }
365
366        // If there were no signature files, then no verifier work to do.
367        if (!signed) {
368            verifier = null;
369        }
370    }
371
372    /**
373     * Return an {@code InputStream} for reading the decompressed contents of
374     * ZIP entry.
375     *
376     * @param ze
377     *            the ZIP entry to be read.
378     * @return the input stream to read from.
379     * @throws IOException
380     *             if an error occurred while creating the input stream.
381     */
382    @Override
383    public InputStream getInputStream(ZipEntry ze) throws IOException {
384        if (manifestEntry != null) {
385            getManifest();
386        }
387        if (verifier != null) {
388            verifier.setManifest(getManifest());
389            if (manifest != null) {
390                verifier.mainAttributesEnd = manifest.getMainAttributesEnd();
391            }
392            if (verifier.readCertificates()) {
393                verifier.removeMetaEntries();
394                if (manifest != null) {
395                    manifest.removeChunks();
396                }
397                if (!verifier.isSignedJar()) {
398                    verifier = null;
399                }
400            }
401        }
402        InputStream in = super.getInputStream(ze);
403        if (in == null) {
404            return null;
405        }
406        if (verifier == null || ze.getSize() == -1) {
407            return in;
408        }
409        JarVerifier.VerifierEntry entry = verifier.initEntry(ze.getName());
410        if (entry == null) {
411            return in;
412        }
413        return new JarFileInputStream(in, ze, entry);
414    }
415
416    /**
417     * Return the {@code JarEntry} specified by name or {@code null} if no such
418     * entry exists.
419     *
420     * @param name
421     *            the name of the entry in the JAR file.
422     * @return the ZIP entry extracted.
423     */
424    @Override
425    public ZipEntry getEntry(String name) {
426        ZipEntry ze = super.getEntry(name);
427        if (ze == null) {
428            return ze;
429        }
430        JarEntry je = new JarEntry(ze);
431        je.parentJar = this;
432        return je;
433    }
434
435    /**
436     * Returns all the ZipEntry's that relate to files in the
437     * JAR's META-INF directory.
438     *
439     * @return the list of ZipEntry's or {@code null} if there are none.
440     */
441    private ZipEntry[] getMetaEntriesImpl() {
442        List<ZipEntry> list = new ArrayList<ZipEntry>(8);
443        Enumeration<? extends ZipEntry> allEntries = entries();
444        while (allEntries.hasMoreElements()) {
445            ZipEntry ze = allEntries.nextElement();
446            if (ze.getName().startsWith(META_DIR)
447                    && ze.getName().length() > META_DIR.length()) {
448                list.add(ze);
449            }
450        }
451        if (list.size() == 0) {
452            return null;
453        }
454        ZipEntry[] result = new ZipEntry[list.size()];
455        list.toArray(result);
456        return result;
457    }
458
459    /**
460     * Closes this {@code JarFile}.
461     *
462     * @throws IOException
463     *             if an error occurs.
464     */
465    @Override
466    public void close() throws IOException {
467        super.close();
468        closed = true;
469    }
470}
471