JarFile.java revision 0c1869ed7f46baf764f9daf4e64c77cd7fbd3516
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.HashMap;
27import java.util.List;
28import java.util.Locale;
29import java.util.zip.ZipEntry;
30import java.util.zip.ZipFile;
31import libcore.io.Streams;
32
33/**
34 * {@code JarFile} is used to read jar entries and their associated data from
35 * jar files.
36 *
37 * @see JarInputStream
38 * @see JarEntry
39 */
40public class JarFile extends ZipFile {
41
42    /**
43     * The MANIFEST file name.
44     */
45    public static final String MANIFEST_NAME = "META-INF/MANIFEST.MF";
46
47    // The directory containing the manifest.
48    static final String META_DIR = "META-INF/";
49
50    // The manifest after it has been read from the JAR.
51    private Manifest manifest;
52
53    // The entry for the MANIFEST.MF file before the first call to getManifest().
54    private byte[] manifestBytes;
55
56    JarVerifier verifier;
57
58    private boolean closed = false;
59
60    static final class JarFileInputStream extends FilterInputStream {
61        private final JarVerifier.VerifierEntry entry;
62
63        private long count;
64        private boolean done = false;
65
66        JarFileInputStream(InputStream is, long size, JarVerifier.VerifierEntry e) {
67            super(is);
68            entry = e;
69
70            count = size;
71        }
72
73        @Override
74        public int read() throws IOException {
75            if (done) {
76                return -1;
77            }
78            if (count > 0) {
79                int r = super.read();
80                if (r != -1) {
81                    entry.write(r);
82                    count--;
83                } else {
84                    count = 0;
85                }
86                if (count == 0) {
87                    done = true;
88                    entry.verify();
89                }
90                return r;
91            } else {
92                done = true;
93                entry.verify();
94                return -1;
95            }
96        }
97
98        @Override
99        public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException {
100            if (done) {
101                return -1;
102            }
103            if (count > 0) {
104                int r = super.read(buffer, byteOffset, byteCount);
105                if (r != -1) {
106                    int size = r;
107                    if (count < size) {
108                        size = (int) count;
109                    }
110                    entry.write(buffer, byteOffset, size);
111                    count -= size;
112                } else {
113                    count = 0;
114                }
115                if (count == 0) {
116                    done = true;
117                    entry.verify();
118                }
119                return r;
120            } else {
121                done = true;
122                entry.verify();
123                return -1;
124            }
125        }
126
127        @Override
128        public int available() throws IOException {
129            if (done) {
130                return 0;
131            }
132            return super.available();
133        }
134
135        @Override
136        public long skip(long byteCount) throws IOException {
137            return Streams.skipByReading(this, byteCount);
138        }
139    }
140
141    static final class JarFileEnumerator implements Enumeration<JarEntry> {
142        final Enumeration<? extends ZipEntry> ze;
143        final JarFile jf;
144
145        JarFileEnumerator(Enumeration<? extends ZipEntry> zenum, JarFile jf) {
146            ze = zenum;
147            this.jf = jf;
148        }
149
150        public boolean hasMoreElements() {
151            return ze.hasMoreElements();
152        }
153
154        public JarEntry nextElement() {
155            return new JarEntry(ze.nextElement(), jf /* parentJar */);
156        }
157    }
158
159    /**
160     * Create a new {@code JarFile} using the contents of the specified file.
161     *
162     * @param file
163     *            the JAR file as {@link File}.
164     * @throws IOException
165     *             If the file cannot be read.
166     */
167    public JarFile(File file) throws IOException {
168        this(file, true);
169    }
170
171    /**
172     * Create a new {@code JarFile} using the contents of the specified file.
173     *
174     * @param file
175     *            the JAR file as {@link File}.
176     * @param verify
177     *            if this JAR file is signed whether it must be verified.
178     * @throws IOException
179     *             If the file cannot be read.
180     */
181    public JarFile(File file, boolean verify) throws IOException {
182        this(file, verify, ZipFile.OPEN_READ);
183    }
184
185    /**
186     * Create a new {@code JarFile} using the contents of file.
187     *
188     * @param file
189     *            the JAR file as {@link File}.
190     * @param verify
191     *            if this JAR filed is signed whether it must be verified.
192     * @param mode
193     *            the mode to use, either {@link ZipFile#OPEN_READ OPEN_READ} or
194     *            {@link ZipFile#OPEN_DELETE OPEN_DELETE}.
195     * @throws IOException
196     *             If the file cannot be read.
197     */
198    public JarFile(File file, boolean verify, int mode) throws IOException {
199        super(file, mode);
200
201        // Step 1: Scan the central directory for meta entries (MANIFEST.mf
202        // & possibly the signature files) and read them fully.
203        HashMap<String, byte[]> metaEntries = readMetaEntries(this, verify);
204
205        // Step 2: Construct a verifier with the information we have.
206        // Verification is possible *only* if the JAR file contains a manifest
207        // *AND* it contains signing related information (signature block
208        // files and the signature files).
209        //
210        // TODO: Is this really the behaviour we want if verify == true ?
211        // We silently skip verification for files that have no manifest or
212        // no signatures.
213        if (verify && metaEntries.containsKey(MANIFEST_NAME) &&
214                metaEntries.size() > 1) {
215            // We create the manifest straight away, so that we can create
216            // the jar verifier as well.
217            manifest = new Manifest(metaEntries.get(MANIFEST_NAME), true);
218            verifier = new JarVerifier(getName(), manifest, metaEntries);
219        } else {
220            verifier = null;
221            manifestBytes = metaEntries.get(MANIFEST_NAME);
222        }
223    }
224
225    /**
226     * Create a new {@code JarFile} from the contents of the file specified by
227     * filename.
228     *
229     * @param filename
230     *            the file name referring to the JAR file.
231     * @throws IOException
232     *             if file name cannot be opened for reading.
233     */
234    public JarFile(String filename) throws IOException {
235        this(filename, true);
236    }
237
238    /**
239     * Create a new {@code JarFile} from the contents of the file specified by
240     * {@code filename}.
241     *
242     * @param filename
243     *            the file name referring to the JAR file.
244     * @param verify
245     *            if this JAR filed is signed whether it must be verified.
246     * @throws IOException
247     *             If file cannot be opened or read.
248     */
249    public JarFile(String filename, boolean verify) throws IOException {
250        this(new File(filename), verify, ZipFile.OPEN_READ);
251    }
252
253    /**
254     * Return an enumeration containing the {@code JarEntrys} contained in this
255     * {@code JarFile}.
256     *
257     * @return the {@code Enumeration} containing the JAR entries.
258     * @throws IllegalStateException
259     *             if this {@code JarFile} is closed.
260     */
261    @Override
262    public Enumeration<JarEntry> entries() {
263        return new JarFileEnumerator(super.entries(), this);
264    }
265
266    /**
267     * Return the {@code JarEntry} specified by its name or {@code null} if no
268     * such entry exists.
269     *
270     * @param name
271     *            the name of the entry in the JAR file.
272     * @return the JAR entry defined by the name.
273     */
274    public JarEntry getJarEntry(String name) {
275        return (JarEntry) getEntry(name);
276    }
277
278    /**
279     * Returns the {@code Manifest} object associated with this {@code JarFile}
280     * or {@code null} if no MANIFEST entry exists.
281     *
282     * @return the MANIFEST.
283     * @throws IOException
284     *             if an error occurs reading the MANIFEST file.
285     * @throws IllegalStateException
286     *             if the jar file is closed.
287     * @see Manifest
288     */
289    public Manifest getManifest() throws IOException {
290        if (closed) {
291            throw new IllegalStateException("JarFile has been closed");
292        }
293
294        if (manifest != null) {
295            return manifest;
296        }
297
298        // If manifest == null && manifestBytes == null, there's no manifest.
299        if (manifestBytes == null) {
300            return null;
301        }
302
303        // We hit this code path only if the verification isn't necessary. If
304        // we did decide to verify this file, we'd have created the Manifest and
305        // the associated Verifier in the constructor itself.
306        manifest = new Manifest(manifestBytes, false);
307        manifestBytes = null;
308
309        return manifest;
310    }
311
312    /**
313     * Called by the JarFile constructors, Reads the contents of the
314     * file's META-INF/ directory and picks out the MANIFEST.MF file and
315     * verifier signature files if they exist.
316     *
317     * @throws IOException
318     *             if there is a problem reading the jar file entries.
319     * @return a map of entry names to their {@code byte[]} content.
320     */
321    static HashMap<String, byte[]> readMetaEntries(ZipFile zipFile,
322            boolean verificationRequired) throws IOException {
323        // Get all meta directory entries
324        List<ZipEntry> metaEntries = getMetaEntries(zipFile);
325
326        HashMap<String, byte[]> metaEntriesMap = new HashMap<String, byte[]>();
327
328        for (ZipEntry entry : metaEntries) {
329            String entryName = entry.getName();
330            // Is this the entry for META-INF/MANIFEST.MF ?
331            //
332            // TODO: Why do we need the containsKey check ? Shouldn't we discard
333            // files that contain duplicate entries like this as invalid ?.
334            if (entryName.equalsIgnoreCase(MANIFEST_NAME) &&
335                    !metaEntriesMap.containsKey(MANIFEST_NAME)) {
336
337                metaEntriesMap.put(MANIFEST_NAME, Streams.readFully(
338                        zipFile.getInputStream(entry)));
339
340                // If there is no verifier then we don't need to look any further.
341                if (!verificationRequired) {
342                    break;
343                }
344            } else if (verificationRequired) {
345                // Is this an entry that the verifier needs?
346                if (endsWithIgnoreCase(entryName, ".SF")
347                        || endsWithIgnoreCase(entryName, ".DSA")
348                        || endsWithIgnoreCase(entryName, ".RSA")
349                        || endsWithIgnoreCase(entryName, ".EC")) {
350                    InputStream is = zipFile.getInputStream(entry);
351                    metaEntriesMap.put(entryName.toUpperCase(Locale.US), Streams.readFully(is));
352                }
353            }
354        }
355
356        return metaEntriesMap;
357    }
358
359    private static boolean endsWithIgnoreCase(String s, String suffix) {
360        return s.regionMatches(true, s.length() - suffix.length(), suffix, 0, suffix.length());
361    }
362
363    /**
364     * Return an {@code InputStream} for reading the decompressed contents of
365     * ZIP entry.
366     *
367     * @param ze
368     *            the ZIP entry to be read.
369     * @return the input stream to read from.
370     * @throws IOException
371     *             if an error occurred while creating the input stream.
372     */
373    @Override
374    public InputStream getInputStream(ZipEntry ze) throws IOException {
375        if (manifestBytes != null) {
376            getManifest();
377        }
378
379        if (verifier != null) {
380            if (verifier.readCertificates()) {
381                verifier.removeMetaEntries();
382                manifest.removeChunks();
383
384                if (!verifier.isSignedJar()) {
385                    verifier = null;
386                }
387            }
388        }
389
390        InputStream in = super.getInputStream(ze);
391        if (in == null) {
392            return null;
393        }
394        if (verifier == null || ze.getSize() == -1) {
395            return in;
396        }
397        JarVerifier.VerifierEntry entry = verifier.initEntry(ze.getName());
398        if (entry == null) {
399            return in;
400        }
401        return new JarFileInputStream(in, ze.getSize(), entry);
402    }
403
404    /**
405     * Return the {@code JarEntry} specified by name or {@code null} if no such
406     * entry exists.
407     *
408     * @param name
409     *            the name of the entry in the JAR file.
410     * @return the ZIP entry extracted.
411     */
412    @Override
413    public ZipEntry getEntry(String name) {
414        ZipEntry ze = super.getEntry(name);
415        if (ze == null) {
416            return ze;
417        }
418        return new JarEntry(ze, this /* parentJar */);
419    }
420
421    /**
422     * Returns all the ZipEntry's that relate to files in the
423     * JAR's META-INF directory.
424     */
425    private static List<ZipEntry> getMetaEntries(ZipFile zipFile) {
426        List<ZipEntry> list = new ArrayList<ZipEntry>(8);
427
428        Enumeration<? extends ZipEntry> allEntries = zipFile.entries();
429        while (allEntries.hasMoreElements()) {
430            ZipEntry ze = allEntries.nextElement();
431            if (ze.getName().startsWith(META_DIR)
432                    && ze.getName().length() > META_DIR.length()) {
433                list.add(ze);
434            }
435        }
436
437        return list;
438    }
439
440    /**
441     * Closes this {@code JarFile}.
442     *
443     * @throws IOException
444     *             if an error occurs.
445     */
446    @Override
447    public void close() throws IOException {
448        super.close();
449        closed = true;
450    }
451}
452