/* * 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.List; 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 it is read. private ZipEntry manifestEntry; JarVerifier verifier; private boolean closed = false; static final class JarFileInputStream extends FilterInputStream { private long count; private ZipEntry zipEntry; private JarVerifier.VerifierEntry entry; private boolean done = false; JarFileInputStream(InputStream is, ZipEntry ze, JarVerifier.VerifierEntry e) { super(is); zipEntry = ze; count = zipEntry.getSize(); entry = e; } @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[] buf, int off, int nbytes) throws IOException { if (done) { return -1; } if (count > 0) { int r = super.read(buf, off, nbytes); if (r != -1) { int size = r; if (count < size) { size = (int) count; } entry.write(buf, off, 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); } } /** * 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 { super(file); if (verify) { verifier = new JarVerifier(file.getPath()); } readMetaEntries(); } /** * 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); if (verify) { verifier = new JarVerifier(file.getPath()); } readMetaEntries(); } /** * 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 { super(filename); if (verify) { verifier = new JarVerifier(filename); } readMetaEntries(); } /** * 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() { class JarFileEnumerator implements Enumeration<JarEntry> { Enumeration<? extends ZipEntry> ze; JarFile jf; JarFileEnumerator(Enumeration<? extends ZipEntry> zenum, JarFile jf) { ze = zenum; this.jf = jf; } public boolean hasMoreElements() { return ze.hasMoreElements(); } public JarEntry nextElement() { JarEntry je = new JarEntry(ze.nextElement()); je.parentJar = jf; return je; } } 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; } try { InputStream is = super.getInputStream(manifestEntry); if (verifier != null) { verifier.addMetaEntry(manifestEntry.getName(), Streams.readFully(is)); is = super.getInputStream(manifestEntry); } try { manifest = new Manifest(is, verifier != null); } finally { is.close(); } manifestEntry = null; // Can discard the entry now. } catch (NullPointerException e) { manifestEntry = null; } return manifest; } /** * Called by the JarFile constructors, this method reads the contents of the * file's META-INF/ directory and picks out the MANIFEST.MF file and * verifier signature files if they exist. Any signature files found are * registered with the verifier. * * @throws IOException * if there is a problem reading the jar file entries. */ private void readMetaEntries() throws IOException { // Get all meta directory entries ZipEntry[] metaEntries = getMetaEntriesImpl(); if (metaEntries == null) { verifier = null; return; } boolean signed = false; for (ZipEntry entry : metaEntries) { String entryName = entry.getName(); // Is this the entry for META-INF/MANIFEST.MF ? if (manifestEntry == null && entryName.equalsIgnoreCase(MANIFEST_NAME)) { manifestEntry = entry; // If there is no verifier then we don't need to look any further. if (verifier == null) { break; } } else { // Is this an entry that the verifier needs? if (verifier != null && (endsWithIgnoreCase(entryName, ".SF") || endsWithIgnoreCase(entryName, ".DSA") || endsWithIgnoreCase(entryName, ".RSA"))) { signed = true; InputStream is = super.getInputStream(entry); verifier.addMetaEntry(entryName, Streams.readFully(is)); } } } // If there were no signature files, then no verifier work to do. if (!signed) { verifier = null; } } 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 (manifestEntry != null) { getManifest(); } if (verifier != null) { verifier.setManifest(getManifest()); if (manifest != null) { verifier.mainAttributesEnd = manifest.getMainAttributesEnd(); } if (verifier.readCertificates()) { verifier.removeMetaEntries(); if (manifest != null) { 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, 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; } JarEntry je = new JarEntry(ze); je.parentJar = this; return je; } /** * Returns all the ZipEntry's that relate to files in the * JAR's META-INF directory. * * @return the list of ZipEntry's or {@code null} if there are none. */ private ZipEntry[] getMetaEntriesImpl() { List<ZipEntry> list = new ArrayList<ZipEntry>(8); Enumeration<? extends ZipEntry> allEntries = entries(); while (allEntries.hasMoreElements()) { ZipEntry ze = allEntries.nextElement(); if (ze.getName().startsWith(META_DIR) && ze.getName().length() > META_DIR.length()) { list.add(ze); } } if (list.size() == 0) { return null; } ZipEntry[] result = new ZipEntry[list.size()]; list.toArray(result); return result; } /** * Closes this {@code JarFile}. * * @throws IOException * if an error occurs. */ @Override public void close() throws IOException { super.close(); closed = true; } }