| /* |
| * 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.ByteArrayOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.util.HashMap; |
| import java.util.Locale; |
| import java.util.zip.ZipEntry; |
| import java.util.zip.ZipInputStream; |
| import libcore.io.Streams; |
| |
| /** |
| * The input stream from which the JAR file to be read may be fetched. It is |
| * used like the {@code ZipInputStream}. |
| * |
| * @see ZipInputStream |
| */ |
| // TODO: The semantics provided by this class are really weird. The jar file |
| // spec does not impose any ordering constraints on the entries of a jar file. |
| // In particular, the Manifest and META-INF directory *need not appear first*. This |
| // class will silently skip certificate checks for jar files where the manifest |
| // isn't the first entry. To do this correctly, we need O(input_stream_length) memory. |
| public class JarInputStream extends ZipInputStream { |
| |
| private Manifest manifest; |
| |
| private boolean verified = false; |
| |
| private JarEntry currentJarEntry; |
| |
| private JarEntry pendingJarEntry; |
| |
| private boolean isMeta; |
| |
| private JarVerifier verifier; |
| |
| private OutputStream verStream; |
| |
| /** |
| * Constructs a new {@code JarInputStream} from an input stream. |
| * |
| * @param stream |
| * the input stream containing the JAR file. |
| * @param verify |
| * if the file should be verified with a {@code JarVerifier}. |
| * @throws IOException |
| * If an error occurs reading entries from the input stream. |
| * @see ZipInputStream#ZipInputStream(InputStream) |
| */ |
| public JarInputStream(InputStream stream, boolean verify) throws IOException { |
| super(stream); |
| |
| verifier = null; |
| pendingJarEntry = null; |
| currentJarEntry = null; |
| |
| if (getNextJarEntry() == null) { |
| return; |
| } |
| |
| if (currentJarEntry.getName().equalsIgnoreCase(JarFile.META_DIR)) { |
| // Fetch the next entry, in the hope that it's the manifest file. |
| closeEntry(); |
| getNextJarEntry(); |
| } |
| |
| if (currentJarEntry.getName().equalsIgnoreCase(JarFile.MANIFEST_NAME)) { |
| final byte[] manifestBytes = Streams.readFullyNoClose(this); |
| manifest = new Manifest(manifestBytes, verify); |
| closeEntry(); |
| |
| if (verify) { |
| HashMap<String, byte[]> metaEntries = new HashMap<String, byte[]>(); |
| metaEntries.put(JarFile.MANIFEST_NAME, manifestBytes); |
| verifier = new JarVerifier("JarInputStream", manifest, metaEntries); |
| } |
| } |
| |
| // There was no manifest available, so we should return the current |
| // entry the next time getNextEntry is called. |
| pendingJarEntry = currentJarEntry; |
| currentJarEntry = null; |
| |
| // If the manifest isn't the first entry, we will not have enough |
| // information to perform verification on entries that precede it. |
| // |
| // TODO: Should we throw if verify == true in this case ? |
| // TODO: We need all meta entries to be placed before the manifest |
| // as well. |
| } |
| |
| /** |
| * Constructs a new {@code JarInputStream} from an input stream. |
| * |
| * @param stream |
| * the input stream containing the JAR file. |
| * @throws IOException |
| * If an error occurs reading entries from the input stream. |
| * @see ZipInputStream#ZipInputStream(InputStream) |
| */ |
| public JarInputStream(InputStream stream) throws IOException { |
| this(stream, true); |
| } |
| |
| /** |
| * Returns the {@code Manifest} object associated with this {@code |
| * JarInputStream} or {@code null} if no manifest entry exists. |
| * |
| * @return the MANIFEST specifying the contents of the JAR file. |
| */ |
| public Manifest getManifest() { |
| return manifest; |
| } |
| |
| /** |
| * Returns the next {@code JarEntry} contained in this stream or {@code |
| * null} if no more entries are present. |
| * |
| * @return the next JAR entry. |
| * @throws IOException |
| * if an error occurs while reading the entry. |
| */ |
| public JarEntry getNextJarEntry() throws IOException { |
| return (JarEntry) getNextEntry(); |
| } |
| |
| /** |
| * Reads up to {@code byteCount} bytes of decompressed data and stores it in |
| * {@code buffer} starting at {@code byteOffset}. Returns the number of uncompressed bytes read. |
| * |
| * @throws IOException |
| * if an IOException occurs. |
| */ |
| @Override |
| public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException { |
| if (currentJarEntry == null) { |
| return -1; |
| } |
| |
| int r = super.read(buffer, byteOffset, byteCount); |
| // verifier can be null if we've been asked not to verify or if |
| // the manifest wasn't found. |
| // |
| // verStream will be null if we're reading the manifest or if we have |
| // no signatures or if the digest for this entry isn't present in the |
| // manifest. |
| if (verifier != null && verStream != null && !verified) { |
| if (r == -1) { |
| // We've hit the end of this stream for the first time, so attempt |
| // a verification. |
| verified = true; |
| if (isMeta) { |
| verifier.addMetaEntry(currentJarEntry.getName(), |
| ((ByteArrayOutputStream) verStream).toByteArray()); |
| try { |
| verifier.readCertificates(); |
| } catch (SecurityException e) { |
| verifier = null; |
| throw e; |
| } |
| } else { |
| ((JarVerifier.VerifierEntry) verStream).verify(); |
| } |
| } else { |
| verStream.write(buffer, byteOffset, r); |
| } |
| } |
| |
| return r; |
| } |
| |
| /** |
| * Returns the next {@code ZipEntry} contained in this stream or {@code |
| * null} if no more entries are present. |
| * |
| * @return the next extracted ZIP entry. |
| * @throws IOException |
| * if an error occurs while reading the entry. |
| */ |
| @Override |
| public ZipEntry getNextEntry() throws IOException { |
| // NOTE: This function must update the value of currentJarEntry |
| // as a side effect. |
| |
| if (pendingJarEntry != null) { |
| JarEntry pending = pendingJarEntry; |
| pendingJarEntry = null; |
| currentJarEntry = pending; |
| return pending; |
| } |
| |
| currentJarEntry = (JarEntry) super.getNextEntry(); |
| if (currentJarEntry == null) { |
| return null; |
| } |
| |
| if (verifier != null) { |
| isMeta = currentJarEntry.getName().toUpperCase(Locale.US).startsWith(JarFile.META_DIR); |
| if (isMeta) { |
| final int entrySize = (int) currentJarEntry.getSize(); |
| verStream = new ByteArrayOutputStream(entrySize > 0 ? entrySize : 8192); |
| } else { |
| verStream = verifier.initEntry(currentJarEntry.getName()); |
| } |
| } |
| |
| verified = false; |
| return currentJarEntry; |
| } |
| |
| @Override |
| public void closeEntry() throws IOException { |
| // NOTE: This was the old behavior. A call to closeEntry() before the |
| // first call to getNextEntry should be a no-op. If we don't return early |
| // here, the super class will close pendingJarEntry for us and reads will |
| // fail. |
| if (pendingJarEntry != null) { |
| return; |
| } |
| |
| super.closeEntry(); |
| currentJarEntry = null; |
| } |
| |
| @Override |
| protected ZipEntry createZipEntry(String name) { |
| JarEntry entry = new JarEntry(name); |
| if (manifest != null) { |
| entry.setAttributes(manifest.getAttributes(name)); |
| } |
| return entry; |
| } |
| } |