blob: 585c1351b95edcc63ba4cda98a2a7bd738c006bb [file] [log] [blame]
/*
* 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;
}
}