/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package java.util.jar; import java.io.File; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import libcore.io.Streams; /** * {@code JarFile} is used to read jar entries and their associated data from * jar files. * * @see JarInputStream * @see JarEntry */ public class JarFile extends ZipFile { /** * The MANIFEST file name. */ public static final String MANIFEST_NAME = "META-INF/MANIFEST.MF"; // The directory containing the manifest. static final String META_DIR = "META-INF/"; // The manifest after it has been read from the JAR. private Manifest manifest; // The entry for the MANIFEST.MF file before the first call to getManifest(). private byte[] manifestBytes; JarVerifier verifier; private boolean closed = false; static final class JarFileInputStream extends FilterInputStream { private final JarVerifier.VerifierEntry entry; private long count; private boolean done = false; JarFileInputStream(InputStream is, long size, JarVerifier.VerifierEntry e) { super(is); entry = e; count = size; } @Override public int read() throws IOException { if (done) { return -1; } if (count > 0) { int r = super.read(); if (r != -1) { entry.write(r); count--; } else { count = 0; } if (count == 0) { done = true; entry.verify(); } return r; } else { done = true; entry.verify(); return -1; } } @Override public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException { if (done) { return -1; } if (count > 0) { int r = super.read(buffer, byteOffset, byteCount); if (r != -1) { int size = r; if (count < size) { size = (int) count; } entry.write(buffer, byteOffset, size); count -= size; } else { count = 0; } if (count == 0) { done = true; entry.verify(); } return r; } else { done = true; entry.verify(); return -1; } } @Override public int available() throws IOException { if (done) { return 0; } return super.available(); } @Override public long skip(long byteCount) throws IOException { return Streams.skipByReading(this, byteCount); } } static final class JarFileEnumerator implements Enumeration<JarEntry> { final Enumeration<? extends ZipEntry> ze; final JarFile jf; JarFileEnumerator(Enumeration<? extends ZipEntry> zenum, JarFile jf) { ze = zenum; this.jf = jf; } public boolean hasMoreElements() { return ze.hasMoreElements(); } public JarEntry nextElement() { return new JarEntry(ze.nextElement(), jf /* parentJar */); } } /** * Create a new {@code JarFile} using the contents of the specified file. * * @param file * the JAR file as {@link File}. * @throws IOException * If the file cannot be read. */ public JarFile(File file) throws IOException { this(file, true); } /** * Create a new {@code JarFile} using the contents of the specified file. * * @param file * the JAR file as {@link File}. * @param verify * if this JAR file is signed whether it must be verified. * @throws IOException * If the file cannot be read. */ public JarFile(File file, boolean verify) throws IOException { this(file, verify, ZipFile.OPEN_READ); } /** * Create a new {@code JarFile} using the contents of file. * * @param file * the JAR file as {@link File}. * @param verify * if this JAR filed is signed whether it must be verified. * @param mode * the mode to use, either {@link ZipFile#OPEN_READ OPEN_READ} or * {@link ZipFile#OPEN_DELETE OPEN_DELETE}. * @throws IOException * If the file cannot be read. */ public JarFile(File file, boolean verify, int mode) throws IOException { super(file, mode); // Step 1: Scan the central directory for meta entries (MANIFEST.mf // & possibly the signature files) and read them fully. HashMap<String, byte[]> metaEntries = readMetaEntries(this, verify); // Step 2: Construct a verifier with the information we have. // Verification is possible *only* if the JAR file contains a manifest // *AND* it contains signing related information (signature block // files and the signature files). // // TODO: Is this really the behaviour we want if verify == true ? // We silently skip verification for files that have no manifest or // no signatures. if (verify && metaEntries.containsKey(MANIFEST_NAME) && metaEntries.size() > 1) { // We create the manifest straight away, so that we can create // the jar verifier as well. manifest = new Manifest(metaEntries.get(MANIFEST_NAME), true); verifier = new JarVerifier(getName(), manifest, metaEntries); } else { verifier = null; manifestBytes = metaEntries.get(MANIFEST_NAME); } } /** * Create a new {@code JarFile} from the contents of the file specified by * filename. * * @param filename * the file name referring to the JAR file. * @throws IOException * if file name cannot be opened for reading. */ public JarFile(String filename) throws IOException { this(filename, true); } /** * Create a new {@code JarFile} from the contents of the file specified by * {@code filename}. * * @param filename * the file name referring to the JAR file. * @param verify * if this JAR filed is signed whether it must be verified. * @throws IOException * If file cannot be opened or read. */ public JarFile(String filename, boolean verify) throws IOException { this(new File(filename), verify, ZipFile.OPEN_READ); } /** * Return an enumeration containing the {@code JarEntrys} contained in this * {@code JarFile}. * * @return the {@code Enumeration} containing the JAR entries. * @throws IllegalStateException * if this {@code JarFile} is closed. */ @Override public Enumeration<JarEntry> entries() { return new JarFileEnumerator(super.entries(), this); } /** * Return the {@code JarEntry} specified by its name or {@code null} if no * such entry exists. * * @param name * the name of the entry in the JAR file. * @return the JAR entry defined by the name. */ public JarEntry getJarEntry(String name) { return (JarEntry) getEntry(name); } /** * Returns the {@code Manifest} object associated with this {@code JarFile} * or {@code null} if no MANIFEST entry exists. * * @return the MANIFEST. * @throws IOException * if an error occurs reading the MANIFEST file. * @throws IllegalStateException * if the jar file is closed. * @see Manifest */ public Manifest getManifest() throws IOException { if (closed) { throw new IllegalStateException("JarFile has been closed"); } if (manifest != null) { return manifest; } // If manifest == null && manifestBytes == null, there's no manifest. if (manifestBytes == null) { return null; } // We hit this code path only if the verification isn't necessary. If // we did decide to verify this file, we'd have created the Manifest and // the associated Verifier in the constructor itself. manifest = new Manifest(manifestBytes, false); manifestBytes = null; return manifest; } /** * Called by the JarFile constructors, Reads the contents of the * file's META-INF/ directory and picks out the MANIFEST.MF file and * verifier signature files if they exist. * * @throws IOException * if there is a problem reading the jar file entries. * @return a map of entry names to their {@code byte[]} content. */ static HashMap<String, byte[]> readMetaEntries(ZipFile zipFile, boolean verificationRequired) throws IOException { // Get all meta directory entries List<ZipEntry> metaEntries = getMetaEntries(zipFile); HashMap<String, byte[]> metaEntriesMap = new HashMap<String, byte[]>(); for (ZipEntry entry : metaEntries) { String entryName = entry.getName(); // Is this the entry for META-INF/MANIFEST.MF ? // // TODO: Why do we need the containsKey check ? Shouldn't we discard // files that contain duplicate entries like this as invalid ?. if (entryName.equalsIgnoreCase(MANIFEST_NAME) && !metaEntriesMap.containsKey(MANIFEST_NAME)) { metaEntriesMap.put(MANIFEST_NAME, Streams.readFully( zipFile.getInputStream(entry))); // If there is no verifier then we don't need to look any further. if (!verificationRequired) { break; } } else if (verificationRequired) { // Is this an entry that the verifier needs? if (endsWithIgnoreCase(entryName, ".SF") || endsWithIgnoreCase(entryName, ".DSA") || endsWithIgnoreCase(entryName, ".RSA") || endsWithIgnoreCase(entryName, ".EC")) { InputStream is = zipFile.getInputStream(entry); metaEntriesMap.put(entryName.toUpperCase(Locale.US), Streams.readFully(is)); } } } return metaEntriesMap; } private static boolean endsWithIgnoreCase(String s, String suffix) { return s.regionMatches(true, s.length() - suffix.length(), suffix, 0, suffix.length()); } /** * Return an {@code InputStream} for reading the decompressed contents of * ZIP entry. * * @param ze * the ZIP entry to be read. * @return the input stream to read from. * @throws IOException * if an error occurred while creating the input stream. */ @Override public InputStream getInputStream(ZipEntry ze) throws IOException { if (manifestBytes != null) { getManifest(); } if (verifier != null) { if (verifier.readCertificates()) { verifier.removeMetaEntries(); manifest.removeChunks(); if (!verifier.isSignedJar()) { verifier = null; } } } InputStream in = super.getInputStream(ze); if (in == null) { return null; } if (verifier == null || ze.getSize() == -1) { return in; } JarVerifier.VerifierEntry entry = verifier.initEntry(ze.getName()); if (entry == null) { return in; } return new JarFileInputStream(in, ze.getSize(), entry); } /** * Return the {@code JarEntry} specified by name or {@code null} if no such * entry exists. * * @param name * the name of the entry in the JAR file. * @return the ZIP entry extracted. */ @Override public ZipEntry getEntry(String name) { ZipEntry ze = super.getEntry(name); if (ze == null) { return ze; } return new JarEntry(ze, this /* parentJar */); } /** * Returns all the ZipEntry's that relate to files in the * JAR's META-INF directory. */ private static List<ZipEntry> getMetaEntries(ZipFile zipFile) { List<ZipEntry> list = new ArrayList<ZipEntry>(8); Enumeration<? extends ZipEntry> allEntries = zipFile.entries(); while (allEntries.hasMoreElements()) { ZipEntry ze = allEntries.nextElement(); if (ze.getName().startsWith(META_DIR) && ze.getName().length() > META_DIR.length()) { list.add(ze); } } return list; } /** * Closes this {@code JarFile}. * * @throws IOException * if an error occurs. */ @Override public void close() throws IOException { super.close(); closed = true; } }