Home | History | Annotate | Download | only in jar
      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 
     18 package java.util.jar;
     19 
     20 import java.io.File;
     21 import java.io.FilterInputStream;
     22 import java.io.IOException;
     23 import java.io.InputStream;
     24 import java.util.ArrayList;
     25 import java.util.Enumeration;
     26 import java.util.HashMap;
     27 import java.util.List;
     28 import java.util.Locale;
     29 import java.util.zip.ZipEntry;
     30 import java.util.zip.ZipFile;
     31 import 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  */
     40 public 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