/* * Copyright (C) 2014 The Android Open Source Project * Copyright (c) 1997, 2013, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package java.util.jar; import java.io.*; import java.lang.ref.SoftReference; import java.util.*; import java.util.stream.Stream; import java.util.stream.StreamSupport; import java.util.zip.*; import java.security.CodeSigner; import java.security.cert.Certificate; import java.security.AccessController; import sun.misc.IOUtils; import sun.security.action.GetPropertyAction; import sun.security.util.ManifestEntryVerifier; import sun.security.util.SignatureFileVerifier; /** * The JarFile class is used to read the contents of a jar file * from any file that can be opened with java.io.RandomAccessFile. * It extends the class java.util.zip.ZipFile with support * for reading an optional Manifest entry. The * Manifest can be used to specify meta-information about the * jar file and its entries. * *

Unless otherwise noted, passing a null argument to a constructor * or method in this class will cause a {@link NullPointerException} to be * thrown. * * If the verify flag is on when opening a signed jar file, the content of the * file is verified against its signature embedded inside the file. Please note * that the verification process does not include validating the signer's * certificate. A caller should inspect the return value of * {@link JarEntry#getCodeSigners()} to further determine if the signature * can be trusted. * * @author David Connelly * @see Manifest * @see java.util.zip.ZipFile * @see java.util.jar.JarEntry * @since 1.2 */ public class JarFile extends ZipFile { static final String META_DIR = "META-INF/"; private Manifest manifest; private JarEntry manEntry; private JarVerifier jv; private boolean jvInitialized; private boolean verify; // indicates if Class-Path attribute present (only valid if hasCheckedSpecialAttributes true) private boolean hasClassPathAttribute; // true if manifest checked for special attributes private volatile boolean hasCheckedSpecialAttributes; /** * The JAR manifest file name. */ public static final String MANIFEST_NAME = "META-INF/MANIFEST.MF"; /** * Creates a new JarFile to read from the specified * file name. The JarFile will be verified if * it is signed. * @param name the name of the jar file to be opened for reading * @throws IOException if an I/O error has occurred * @throws SecurityException if access to the file is denied * by the SecurityManager */ public JarFile(String name) throws IOException { this(new File(name), true, ZipFile.OPEN_READ); } /** * Creates a new JarFile to read from the specified * file name. * @param name the name of the jar file to be opened for reading * @param verify whether or not to verify the jar file if * it is signed. * @throws IOException if an I/O error has occurred * @throws SecurityException if access to the file is denied * by the SecurityManager */ public JarFile(String name, boolean verify) throws IOException { this(new File(name), verify, ZipFile.OPEN_READ); } /** * Creates a new JarFile to read from the specified * File object. The JarFile will be verified if * it is signed. * @param file the jar file to be opened for reading * @throws IOException if an I/O error has occurred * @throws SecurityException if access to the file is denied * by the SecurityManager */ public JarFile(File file) throws IOException { this(file, true, ZipFile.OPEN_READ); } /** * Creates a new JarFile to read from the specified * File object. * @param file the jar file to be opened for reading * @param verify whether or not to verify the jar file if * it is signed. * @throws IOException if an I/O error has occurred * @throws SecurityException if access to the file is denied * by the SecurityManager. */ public JarFile(File file, boolean verify) throws IOException { this(file, verify, ZipFile.OPEN_READ); } /** * Creates a new JarFile to read from the specified * File object in the specified mode. The mode argument * must be either OPEN_READ or OPEN_READ | OPEN_DELETE. * * @param file the jar file to be opened for reading * @param verify whether or not to verify the jar file if * it is signed. * @param mode the mode in which the file is to be opened * @throws IOException if an I/O error has occurred * @throws IllegalArgumentException * if the mode argument is invalid * @throws SecurityException if access to the file is denied * by the SecurityManager * @since 1.3 */ public JarFile(File file, boolean verify, int mode) throws IOException { super(file, mode); this.verify = verify; } /** * Returns the jar file manifest, or null if none. * * @return the jar file manifest, or null if none * * @throws IllegalStateException * may be thrown if the jar file has been closed * @throws IOException if an I/O error has occurred */ public Manifest getManifest() throws IOException { return getManifestFromReference(); } private synchronized Manifest getManifestFromReference() throws IOException { if (manifest == null) { JarEntry manEntry = getManEntry(); // If found then load the manifest if (manEntry != null) { if (verify) { byte[] b = getBytes(manEntry); manifest = new Manifest(new ByteArrayInputStream(b)); if (!jvInitialized) { jv = new JarVerifier(b); } } else { manifest = new Manifest(super.getInputStream(manEntry)); } } } return manifest; } private native String[] getMetaInfEntryNames(); /** * Returns the JarEntry for the given entry name or * null if not found. * * @param name the jar file entry name * @return the JarEntry for the given entry name or * null if not found. * * @throws IllegalStateException * may be thrown if the jar file has been closed * * @see java.util.jar.JarEntry */ public JarEntry getJarEntry(String name) { return (JarEntry)getEntry(name); } /** * Returns the ZipEntry for the given entry name or * null if not found. * * @param name the jar file entry name * @return the ZipEntry for the given entry name or * null if not found * * @throws IllegalStateException * may be thrown if the jar file has been closed * * @see java.util.zip.ZipEntry */ public ZipEntry getEntry(String name) { ZipEntry ze = super.getEntry(name); if (ze != null) { return new JarFileEntry(ze); } return null; } private class JarEntryIterator implements Enumeration, Iterator { final Enumeration e = JarFile.super.entries(); public boolean hasNext() { return e.hasMoreElements(); } public JarEntry next() { ZipEntry ze = e.nextElement(); return new JarFileEntry(ze); } public boolean hasMoreElements() { return hasNext(); } public JarEntry nextElement() { return next(); } } /** * Returns an enumeration of the zip file entries. */ public Enumeration entries() { return new JarEntryIterator(); } @Override public Stream stream() { return StreamSupport.stream(Spliterators.spliterator( new JarEntryIterator(), size(), Spliterator.ORDERED | Spliterator.DISTINCT | Spliterator.IMMUTABLE | Spliterator.NONNULL), false); } private class JarFileEntry extends JarEntry { JarFileEntry(ZipEntry ze) { super(ze); } public Attributes getAttributes() throws IOException { Manifest man = JarFile.this.getManifest(); if (man != null) { return man.getAttributes(getName()); } else { return null; } } public Certificate[] getCertificates() { try { maybeInstantiateVerifier(); } catch (IOException e) { throw new RuntimeException(e); } if (certs == null && jv != null) { certs = jv.getCerts(JarFile.this, this); } return certs == null ? null : certs.clone(); } public CodeSigner[] getCodeSigners() { try { maybeInstantiateVerifier(); } catch (IOException e) { throw new RuntimeException(e); } if (signers == null && jv != null) { signers = jv.getCodeSigners(JarFile.this, this); } return signers == null ? null : signers.clone(); } } /* * Ensures that the JarVerifier has been created if one is * necessary (i.e., the jar appears to be signed.) This is done as * a quick check to avoid processing of the manifest for unsigned * jars. */ private void maybeInstantiateVerifier() throws IOException { if (jv != null) { return; } if (verify) { String[] names = getMetaInfEntryNames(); if (names != null) { for (int i = 0; i < names.length; i++) { String name = names[i].toUpperCase(Locale.ENGLISH); if (name.endsWith(".DSA") || name.endsWith(".RSA") || name.endsWith(".EC") || name.endsWith(".SF")) { // Assume since we found a signature-related file // that the jar is signed and that we therefore // need a JarVerifier and Manifest getManifest(); return; } } } // No signature-related files; don't instantiate a // verifier verify = false; } } /* * Initializes the verifier object by reading all the manifest * entries and passing them to the verifier. */ private void initializeVerifier() { ManifestEntryVerifier mev = null; // Verify "META-INF/" entries... try { String[] names = getMetaInfEntryNames(); if (names != null) { for (int i = 0; i < names.length; i++) { String uname = names[i].toUpperCase(Locale.ENGLISH); if (MANIFEST_NAME.equals(uname) || SignatureFileVerifier.isBlockOrSF(uname)) { JarEntry e = getJarEntry(names[i]); if (e == null) { throw new JarException("corrupted jar file"); } if (mev == null) { mev = new ManifestEntryVerifier (getManifestFromReference()); } byte[] b = getBytes(e); if (b != null && b.length > 0) { jv.beginEntry(e, mev); jv.update(b.length, b, 0, b.length, mev); jv.update(-1, null, 0, 0, mev); } } } } } catch (IOException ex) { // if we had an error parsing any blocks, just // treat the jar file as being unsigned jv = null; verify = false; if (JarVerifier.debug != null) { JarVerifier.debug.println("jarfile parsing error!"); ex.printStackTrace(); } } // if after initializing the verifier we have nothing // signed, we null it out. if (jv != null) { jv.doneWithMeta(); if (JarVerifier.debug != null) { JarVerifier.debug.println("done with meta!"); } if (jv.nothingToVerify()) { if (JarVerifier.debug != null) { JarVerifier.debug.println("nothing to verify!"); } jv = null; verify = false; } } } /* * Reads all the bytes for a given entry. Used to process the * META-INF files. */ private byte[] getBytes(ZipEntry ze) throws IOException { try (InputStream is = super.getInputStream(ze)) { return IOUtils.readFully(is, (int)ze.getSize(), true); } } /** * Returns an input stream for reading the contents of the specified * zip file entry. * @param ze the zip file entry * @return an input stream for reading the contents of the specified * zip file entry * @throws ZipException if a zip file format error has occurred * @throws IOException if an I/O error has occurred * @throws SecurityException if any of the jar file entries * are incorrectly signed. * @throws IllegalStateException * may be thrown if the jar file has been closed */ public synchronized InputStream getInputStream(ZipEntry ze) throws IOException { maybeInstantiateVerifier(); if (jv == null) { return super.getInputStream(ze); } if (!jvInitialized) { initializeVerifier(); jvInitialized = true; // could be set to null after a call to // initializeVerifier if we have nothing to // verify if (jv == null) return super.getInputStream(ze); } // wrap a verifier stream around the real stream return new JarVerifier.VerifierStream( getManifestFromReference(), ze instanceof JarFileEntry ? (JarEntry) ze : getJarEntry(ze.getName()), super.getInputStream(ze), jv); } // Statics for hand-coded Boyer-Moore search private static final char[] CLASSPATH_CHARS = {'c','l','a','s','s','-','p','a','t','h'}; // The bad character shift for "class-path" private static final int[] CLASSPATH_LASTOCC; // The good suffix shift for "class-path" private static final int[] CLASSPATH_OPTOSFT; static { CLASSPATH_LASTOCC = new int[128]; CLASSPATH_OPTOSFT = new int[10]; CLASSPATH_LASTOCC[(int)'c'] = 1; CLASSPATH_LASTOCC[(int)'l'] = 2; CLASSPATH_LASTOCC[(int)'s'] = 5; CLASSPATH_LASTOCC[(int)'-'] = 6; CLASSPATH_LASTOCC[(int)'p'] = 7; CLASSPATH_LASTOCC[(int)'a'] = 8; CLASSPATH_LASTOCC[(int)'t'] = 9; CLASSPATH_LASTOCC[(int)'h'] = 10; for (int i=0; i<9; i++) CLASSPATH_OPTOSFT[i] = 10; CLASSPATH_OPTOSFT[9]=1; } private synchronized JarEntry getManEntry() { if (manEntry == null) { // First look up manifest entry using standard name manEntry = getJarEntry(MANIFEST_NAME); if (manEntry == null) { // If not found, then iterate through all the "META-INF/" // entries to find a match. String[] names = getMetaInfEntryNames(); if (names != null) { for (int i = 0; i < names.length; i++) { if (MANIFEST_NAME.equals( names[i].toUpperCase(Locale.ENGLISH))) { manEntry = getJarEntry(names[i]); break; } } } } } return manEntry; } /** * Returns {@code true} iff this JAR file has a manifest with the * Class-Path attribute * @hide */ public boolean hasClassPathAttribute() throws IOException { checkForSpecialAttributes(); return hasClassPathAttribute; } /** * Returns true if the pattern {@code src} is found in {@code b}. * The {@code lastOcc} and {@code optoSft} arrays are the precomputed * bad character and good suffix shifts. */ private boolean match(char[] src, byte[] b, int[] lastOcc, int[] optoSft) { int len = src.length; int last = b.length - len; int i = 0; next: while (i<=last) { for (int j=(len-1); j>=0; j--) { char c = (char) b[i+j]; c = (((c-'A')|('Z'-c)) >= 0) ? (char)(c + 32) : c; if (c != src[j]) { i += Math.max(j + 1 - lastOcc[c&0x7F], optoSft[j]); continue next; } } return true; } return false; } /** * On first invocation, check if the JAR file has the Class-Path * attribute. A no-op on subsequent calls. */ private void checkForSpecialAttributes() throws IOException { if (hasCheckedSpecialAttributes) return; // Android-changed: Doesn't make sense on android: //if (!isKnownNotToHaveSpecialAttributes()) { { JarEntry manEntry = getManEntry(); if (manEntry != null) { byte[] b = getBytes(manEntry); if (match(CLASSPATH_CHARS, b, CLASSPATH_LASTOCC, CLASSPATH_OPTOSFT)) hasClassPathAttribute = true; } } hasCheckedSpecialAttributes = true; } // Android-changed: Doesn't make sense on android: /*private static String javaHome; private static volatile String[] jarNames; private boolean isKnownNotToHaveSpecialAttributes() { // Optimize away even scanning of manifest for jar files we // deliver which don't have a class-path attribute. If one of // these jars is changed to include such an attribute this code // must be changed. if (javaHome == null) { javaHome = AccessController.doPrivileged( new GetPropertyAction("java.home")); } if (jarNames == null) { String[] names = new String[11]; String fileSep = File.separator; int i = 0; names[i++] = fileSep + "rt.jar"; names[i++] = fileSep + "jsse.jar"; names[i++] = fileSep + "jce.jar"; names[i++] = fileSep + "charsets.jar"; names[i++] = fileSep + "dnsns.jar"; names[i++] = fileSep + "zipfs.jar"; names[i++] = fileSep + "localedata.jar"; names[i++] = fileSep = "cldrdata.jar"; names[i++] = fileSep + "sunjce_provider.jar"; names[i++] = fileSep + "sunpkcs11.jar"; names[i++] = fileSep + "sunec.jar"; jarNames = names; } String name = getName(); String localJavaHome = javaHome; if (name.startsWith(localJavaHome)) { String[] names = jarNames; for (int i = 0; i < names.length; i++) { if (name.endsWith(names[i])) { return true; } } } return false; }*/ JarEntry newEntry(ZipEntry ze) { return new JarFileEntry(ze); } }