blob: 6b147f6012d4dae2ff491834a4152d9437c0719e [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.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;
}
}