Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 8a7c1606 authored by Przemyslaw Szczepaniak's avatar Przemyslaw Szczepaniak
Browse files

Move StrictJarFile from libcore to framework

Bug: 25337946
Change-Id: Ib4fac6fa9f534b8654e5ca158bbaedb2393772ba
(cherrypicked from 43ea2cc2a81926a6b2ca13d41f4eab089640129e)
parent 355a74f2
Loading
Loading
Loading
Loading
+2 −1
Original line number Diff line number Diff line
@@ -84,9 +84,10 @@ import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.jar.StrictJarFile;
import java.util.zip.ZipEntry;

import android.util.jar.StrictJarFile;

/**
 * Parser for package files (APKs) on disk. This supports apps packaged either
 * as a single "monolithic" APK, or apps packaged as a "cluster" of multiple
+427 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * Licensed 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 android.util.jar;

import dalvik.system.CloseGuard;
import java.io.ByteArrayInputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.security.cert.Certificate;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import libcore.io.IoUtils;
import libcore.io.Streams;

/**
 * A subset of the JarFile API implemented as a thin wrapper over
 * system/core/libziparchive.
 *
 * @hide for internal use only. Not API compatible (or as forgiving) as
 *        {@link java.util.jar.JarFile}
 */
public final class StrictJarFile {

    private final long nativeHandle;

    // NOTE: It's possible to share a file descriptor with the native
    // code, at the cost of some additional complexity.
    private final RandomAccessFile raf;

    private final StrictJarManifest manifest;
    private final StrictJarVerifier verifier;

    private final boolean isSigned;

    private final CloseGuard guard = CloseGuard.get();
    private boolean closed;

    public StrictJarFile(String fileName) throws IOException, SecurityException {
        this.nativeHandle = nativeOpenJarFile(fileName);
        this.raf = new RandomAccessFile(fileName, "r");

        try {
            // Read the MANIFEST and signature files up front and try to
            // parse them. We never want to accept a JAR File with broken signatures
            // or manifests, so it's best to throw as early as possible.
            HashMap<String, byte[]> metaEntries = getMetaEntries();
            this.manifest = new StrictJarManifest(metaEntries.get(JarFile.MANIFEST_NAME), true);
            this.verifier = new StrictJarVerifier(fileName, manifest, metaEntries);
            Set<String> files = manifest.getEntries().keySet();
            for (String file : files) {
                if (findEntry(file) == null) {
                    throw new SecurityException(fileName + ": File " + file + " in manifest does not exist");
                }
            }

            isSigned = verifier.readCertificates() && verifier.isSignedJar();
        } catch (IOException | SecurityException e) {
            nativeClose(this.nativeHandle);
            IoUtils.closeQuietly(this.raf);
            throw e;
        }

        guard.open("close");
    }

    public StrictJarManifest getManifest() {
        return manifest;
    }

    public Iterator<ZipEntry> iterator() throws IOException {
        return new EntryIterator(nativeHandle, "");
    }

    public ZipEntry findEntry(String name) {
        return nativeFindEntry(nativeHandle, name);
    }

    /**
     * Return all certificate chains for a given {@link ZipEntry} belonging to this jar.
     * This method MUST be called only after fully exhausting the InputStream belonging
     * to this entry.
     *
     * Returns {@code null} if this jar file isn't signed or if this method is
     * called before the stream is processed.
     */
    public Certificate[][] getCertificateChains(ZipEntry ze) {
        if (isSigned) {
            return verifier.getCertificateChains(ze.getName());
        }

        return null;
    }

    /**
     * Return all certificates for a given {@link ZipEntry} belonging to this jar.
     * This method MUST be called only after fully exhausting the InputStream belonging
     * to this entry.
     *
     * Returns {@code null} if this jar file isn't signed or if this method is
     * called before the stream is processed.
     *
     * @deprecated Switch callers to use getCertificateChains instead
     */
    @Deprecated
    public Certificate[] getCertificates(ZipEntry ze) {
        if (isSigned) {
            Certificate[][] certChains = verifier.getCertificateChains(ze.getName());

            // Measure number of certs.
            int count = 0;
            for (Certificate[] chain : certChains) {
                count += chain.length;
            }

            // Create new array and copy all the certs into it.
            Certificate[] certs = new Certificate[count];
            int i = 0;
            for (Certificate[] chain : certChains) {
                System.arraycopy(chain, 0, certs, i, chain.length);
                i += chain.length;
            }

            return certs;
        }

        return null;
    }

    public InputStream getInputStream(ZipEntry ze) {
        final InputStream is = getZipInputStream(ze);

        if (isSigned) {
            StrictJarVerifier.VerifierEntry entry = verifier.initEntry(ze.getName());
            if (entry == null) {
                return is;
            }

            return new JarFileInputStream(is, ze.getSize(), entry);
        }

        return is;
    }

    public void close() throws IOException {
        if (!closed) {
            guard.close();

            nativeClose(nativeHandle);
            IoUtils.closeQuietly(raf);
            closed = true;
        }
    }

    private InputStream getZipInputStream(ZipEntry ze) {
        if (ze.getMethod() == ZipEntry.STORED) {
            return new RAFStream(raf, ze.getDataOffset(),
                    ze.getDataOffset() + ze.getSize());
        } else {
            final RAFStream wrapped = new RAFStream(
                    raf, ze.getDataOffset(), ze.getDataOffset() + ze.getCompressedSize());

            int bufSize = Math.max(1024, (int) Math.min(ze.getSize(), 65535L));
            return new ZipInflaterInputStream(wrapped, new Inflater(true), bufSize, ze);
        }
    }

    static final class EntryIterator implements Iterator<ZipEntry> {
        private final long iterationHandle;
        private ZipEntry nextEntry;

        EntryIterator(long nativeHandle, String prefix) throws IOException {
            iterationHandle = nativeStartIteration(nativeHandle, prefix);
        }

        public ZipEntry next() {
            if (nextEntry != null) {
                final ZipEntry ze = nextEntry;
                nextEntry = null;
                return ze;
            }

            return nativeNextEntry(iterationHandle);
        }

        public boolean hasNext() {
            if (nextEntry != null) {
                return true;
            }

            final ZipEntry ze = nativeNextEntry(iterationHandle);
            if (ze == null) {
                return false;
            }

            nextEntry = ze;
            return true;
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }
    }

    private HashMap<String, byte[]> getMetaEntries() throws IOException {
        HashMap<String, byte[]> metaEntries = new HashMap<String, byte[]>();

        Iterator<ZipEntry> entryIterator = new EntryIterator(nativeHandle, "META-INF/");
        while (entryIterator.hasNext()) {
            final ZipEntry entry = entryIterator.next();
            metaEntries.put(entry.getName(), Streams.readFully(getInputStream(entry)));
        }

        return metaEntries;
    }

    static final class JarFileInputStream extends FilterInputStream {
        private final StrictJarVerifier.VerifierEntry entry;

        private long count;
        private boolean done = false;

        JarFileInputStream(InputStream is, long size, StrictJarVerifier.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);
        }
    }

    /** @hide */
    public static class ZipInflaterInputStream extends InflaterInputStream {
        private final ZipEntry entry;
        private long bytesRead = 0;

        public ZipInflaterInputStream(InputStream is, Inflater inf, int bsize, ZipEntry entry) {
            super(is, inf, bsize);
            this.entry = entry;
        }

        @Override public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException {
            final int i;
            try {
                i = super.read(buffer, byteOffset, byteCount);
            } catch (IOException e) {
                throw new IOException("Error reading data for " + entry.getName() + " near offset "
                        + bytesRead, e);
            }
            if (i == -1) {
                if (entry.getSize() != bytesRead) {
                    throw new IOException("Size mismatch on inflated file: " + bytesRead + " vs "
                            + entry.getSize());
                }
            } else {
                bytesRead += i;
            }
            return i;
        }

        @Override public int available() throws IOException {
            if (closed) {
                // Our superclass will throw an exception, but there's a jtreg test that
                // explicitly checks that the InputStream returned from ZipFile.getInputStream
                // returns 0 even when closed.
                return 0;
            }
            return super.available() == 0 ? 0 : (int) (entry.getSize() - bytesRead);
        }
    }

    /**
     * Wrap a stream around a RandomAccessFile.  The RandomAccessFile is shared
     * among all streams returned by getInputStream(), so we have to synchronize
     * access to it.  (We can optimize this by adding buffering here to reduce
     * collisions.)
     *
     * <p>We could support mark/reset, but we don't currently need them.
     *
     * @hide
     */
    public static class RAFStream extends InputStream {
        private final RandomAccessFile sharedRaf;
        private long endOffset;
        private long offset;


        public RAFStream(RandomAccessFile raf, long initialOffset, long endOffset) {
            sharedRaf = raf;
            offset = initialOffset;
            this.endOffset = endOffset;
        }

        public RAFStream(RandomAccessFile raf, long initialOffset) throws IOException {
            this(raf, initialOffset, raf.length());
        }

        @Override public int available() throws IOException {
            return (offset < endOffset ? 1 : 0);
        }

        @Override public int read() throws IOException {
            return Streams.readSingleByte(this);
        }

        @Override public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException {
            synchronized (sharedRaf) {
                final long length = endOffset - offset;
                if (byteCount > length) {
                    byteCount = (int) length;
                }
                sharedRaf.seek(offset);
                int count = sharedRaf.read(buffer, byteOffset, byteCount);
                if (count > 0) {
                    offset += count;
                    return count;
                } else {
                    return -1;
                }
            }
        }

        @Override public long skip(long byteCount) throws IOException {
            if (byteCount > endOffset - offset) {
                byteCount = endOffset - offset;
            }
            offset += byteCount;
            return byteCount;
        }
    }


    private static native long nativeOpenJarFile(String fileName) throws IOException;
    private static native long nativeStartIteration(long nativeHandle, String prefix);
    private static native ZipEntry nativeNextEntry(long iterationHandle);
    private static native ZipEntry nativeFindEntry(long nativeHandle, String entryName);
    private static native void nativeClose(long nativeHandle);
}
+315 −0
Original line number Diff line number Diff line
/*
 * 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 android.util.jar;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CoderResult;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.jar.Attributes;
import libcore.io.Streams;

/**
 * The {@code StrictJarManifest} class is used to obtain attribute information for a
 * {@code StrictJarFile} and its entries.
 *
 * @hide
 */
public class StrictJarManifest implements Cloneable {
    static final int LINE_LENGTH_LIMIT = 72;

    private static final byte[] LINE_SEPARATOR = new byte[] { '\r', '\n' };

    private static final byte[] VALUE_SEPARATOR = new byte[] { ':', ' ' };

    private final Attributes mainAttributes;
    private final HashMap<String, Attributes> entries;

    static final class Chunk {
        final int start;
        final int end;

        Chunk(int start, int end) {
            this.start = start;
            this.end = end;
        }
    }

    private HashMap<String, Chunk> chunks;

    /**
     * The end of the main attributes section in the manifest is needed in
     * verification.
     */
    private int mainEnd;

    /**
     * Creates a new {@code StrictJarManifest} instance.
     */
    public StrictJarManifest() {
        entries = new HashMap<String, Attributes>();
        mainAttributes = new Attributes();
    }

    /**
     * Creates a new {@code StrictJarManifest} instance using the attributes obtained
     * from the input stream.
     *
     * @param is
     *            {@code InputStream} to parse for attributes.
     * @throws IOException
     *             if an IO error occurs while creating this {@code StrictJarManifest}
     */
    public StrictJarManifest(InputStream is) throws IOException {
        this();
        read(Streams.readFully(is));
    }

    /**
     * Creates a new {@code StrictJarManifest} instance. The new instance will have the
     * same attributes as those found in the parameter {@code StrictJarManifest}.
     *
     * @param man
     *            {@code StrictJarManifest} instance to obtain attributes from.
     */
    @SuppressWarnings("unchecked")
    public StrictJarManifest(StrictJarManifest man) {
        mainAttributes = (Attributes) man.mainAttributes.clone();
        entries = (HashMap<String, Attributes>) ((HashMap<String, Attributes>) man
                .getEntries()).clone();
    }

    StrictJarManifest(byte[] manifestBytes, boolean readChunks) throws IOException {
        this();
        if (readChunks) {
            chunks = new HashMap<String, Chunk>();
        }
        read(manifestBytes);
    }

    /**
     * Resets the both the main attributes as well as the entry attributes
     * associated with this {@code StrictJarManifest}.
     */
    public void clear() {
        entries.clear();
        mainAttributes.clear();
    }

    /**
     * Returns the {@code Attributes} associated with the parameter entry
     * {@code name}.
     *
     * @param name
     *            the name of the entry to obtain {@code Attributes} from.
     * @return the Attributes for the entry or {@code null} if the entry does
     *         not exist.
     */
    public Attributes getAttributes(String name) {
        return getEntries().get(name);
    }

    /**
     * Returns a map containing the {@code Attributes} for each entry in the
     * {@code StrictJarManifest}.
     *
     * @return the map of entry attributes.
     */
    public Map<String, Attributes> getEntries() {
        return entries;
    }

    /**
     * Returns the main {@code Attributes} of the {@code JarFile}.
     *
     * @return main {@code Attributes} associated with the source {@code
     *         JarFile}.
     */
    public Attributes getMainAttributes() {
        return mainAttributes;
    }

    /**
     * Creates a copy of this {@code StrictJarManifest}. The returned {@code StrictJarManifest}
     * will equal the {@code StrictJarManifest} from which it was cloned.
     *
     * @return a copy of this instance.
     */
    @Override
    public Object clone() {
        return new StrictJarManifest(this);
    }

    /**
     * Writes this {@code StrictJarManifest}'s name/attributes pairs to the given {@code OutputStream}.
     * The {@code MANIFEST_VERSION} or {@code SIGNATURE_VERSION} attribute must be set before
     * calling this method, or no attributes will be written.
     *
     * @throws IOException
     *             If an error occurs writing the {@code StrictJarManifest}.
     */
    public void write(OutputStream os) throws IOException {
        write(this, os);
    }

    /**
     * Merges name/attribute pairs read from the input stream {@code is} into this manifest.
     *
     * @param is
     *            The {@code InputStream} to read from.
     * @throws IOException
     *             If an error occurs reading the manifest.
     */
    public void read(InputStream is) throws IOException {
        read(Streams.readFullyNoClose(is));
    }

    private void read(byte[] buf) throws IOException {
        if (buf.length == 0) {
            return;
        }

        StrictJarManifestReader im = new StrictJarManifestReader(buf, mainAttributes);
        mainEnd = im.getEndOfMainSection();
        im.readEntries(entries, chunks);
    }

    /**
     * Returns the hash code for this instance.
     *
     * @return this {@code StrictJarManifest}'s hashCode.
     */
    @Override
    public int hashCode() {
        return mainAttributes.hashCode() ^ getEntries().hashCode();
    }

    /**
     * Determines if the receiver is equal to the parameter object. Two {@code
     * StrictJarManifest}s are equal if they have identical main attributes as well as
     * identical entry attributes.
     *
     * @param o
     *            the object to compare against.
     * @return {@code true} if the manifests are equal, {@code false} otherwise
     */
    @Override
    public boolean equals(Object o) {
        if (o == null) {
            return false;
        }
        if (o.getClass() != this.getClass()) {
            return false;
        }
        if (!mainAttributes.equals(((StrictJarManifest) o).mainAttributes)) {
            return false;
        }
        return getEntries().equals(((StrictJarManifest) o).getEntries());
    }

    Chunk getChunk(String name) {
        return chunks.get(name);
    }

    void removeChunks() {
        chunks = null;
    }

    int getMainAttributesEnd() {
        return mainEnd;
    }

    /**
     * Writes out the attribute information of the specified manifest to the
     * specified {@code OutputStream}
     *
     * @param manifest
     *            the manifest to write out.
     * @param out
     *            The {@code OutputStream} to write to.
     * @throws IOException
     *             If an error occurs writing the {@code StrictJarManifest}.
     */
    static void write(StrictJarManifest manifest, OutputStream out) throws IOException {
        CharsetEncoder encoder = StandardCharsets.UTF_8.newEncoder();
        ByteBuffer buffer = ByteBuffer.allocate(LINE_LENGTH_LIMIT);

        Attributes.Name versionName = Attributes.Name.MANIFEST_VERSION;
        String version = manifest.mainAttributes.getValue(versionName);
        if (version == null) {
            versionName = Attributes.Name.SIGNATURE_VERSION;
            version = manifest.mainAttributes.getValue(versionName);
        }
        if (version != null) {
            writeEntry(out, versionName, version, encoder, buffer);
            Iterator<?> entries = manifest.mainAttributes.keySet().iterator();
            while (entries.hasNext()) {
                Attributes.Name name = (Attributes.Name) entries.next();
                if (!name.equals(versionName)) {
                    writeEntry(out, name, manifest.mainAttributes.getValue(name), encoder, buffer);
                }
            }
        }
        out.write(LINE_SEPARATOR);
        Iterator<String> i = manifest.getEntries().keySet().iterator();
        while (i.hasNext()) {
            String key = i.next();
            writeEntry(out, Attributes.Name.NAME, key, encoder, buffer);
            Attributes attributes = manifest.entries.get(key);
            Iterator<?> entries = attributes.keySet().iterator();
            while (entries.hasNext()) {
                Attributes.Name name = (Attributes.Name) entries.next();
                writeEntry(out, name, attributes.getValue(name), encoder, buffer);
            }
            out.write(LINE_SEPARATOR);
        }
    }

    private static void writeEntry(OutputStream os, Attributes.Name name,
            String value, CharsetEncoder encoder, ByteBuffer bBuf) throws IOException {
        String nameString = name.toString();
        os.write(nameString.getBytes(StandardCharsets.US_ASCII));
        os.write(VALUE_SEPARATOR);

        encoder.reset();
        bBuf.clear().limit(LINE_LENGTH_LIMIT - nameString.length() - 2);

        CharBuffer cBuf = CharBuffer.wrap(value);

        while (true) {
            CoderResult r = encoder.encode(cBuf, bBuf, true);
            if (CoderResult.UNDERFLOW == r) {
                r = encoder.flush(bBuf);
            }
            os.write(bBuf.array(), bBuf.arrayOffset(), bBuf.position());
            os.write(LINE_SEPARATOR);
            if (CoderResult.UNDERFLOW == r) {
                break;
            }
            os.write(' ');
            bBuf.clear().limit(LINE_LENGTH_LIMIT - 1);
        }
    }
}
+184 −0

File added.

Preview size limit exceeded, changes collapsed.

+456 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading