OSDN Git Service

APK signer primitive.
authorAlex Klyubin <klyubin@google.com>
Wed, 22 Jun 2016 21:03:06 +0000 (14:03 -0700)
committerAlex Klyubin <klyubin@google.com>
Mon, 27 Jun 2016 18:28:08 +0000 (11:28 -0700)
This adds an APK signer primitive which preserves as much of the input
APK as possible. For example, it preserves the order of APK entries
and preserves their contents, including compressed form and alignment
of data.

Bug: 27461702
Change-Id: I51d07c530480182a66379e70a00f680544ff6214

tools/apksigner/core/src/com/android/apksigner/core/ApkSigner.java [new file with mode: 0644]
tools/apksigner/core/src/com/android/apksigner/core/ApkSignerEngine.java
tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/V1SchemeVerifier.java
tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/V2SchemeVerifier.java
tools/apksigner/core/src/com/android/apksigner/core/internal/util/RandomAccessFileDataSink.java [new file with mode: 0644]
tools/apksigner/core/src/com/android/apksigner/core/internal/zip/CentralDirectoryRecord.java
tools/apksigner/core/src/com/android/apksigner/core/internal/zip/EocdRecord.java [new file with mode: 0644]
tools/apksigner/core/src/com/android/apksigner/core/internal/zip/LocalFileHeader.java [deleted file]
tools/apksigner/core/src/com/android/apksigner/core/internal/zip/LocalFileRecord.java [new file with mode: 0644]
tools/apksigner/core/src/com/android/apksigner/core/internal/zip/ZipUtils.java
tools/apksigner/core/src/com/android/apksigner/core/util/DataSinks.java

diff --git a/tools/apksigner/core/src/com/android/apksigner/core/ApkSigner.java b/tools/apksigner/core/src/com/android/apksigner/core/ApkSigner.java
new file mode 100644 (file)
index 0000000..2491302
--- /dev/null
@@ -0,0 +1,711 @@
+/*
+ * Copyright (C) 2016 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 com.android.apksigner.core;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SignatureException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import com.android.apksigner.core.apk.ApkUtils;
+import com.android.apksigner.core.internal.apk.v2.V2SchemeVerifier;
+import com.android.apksigner.core.internal.util.ByteBufferDataSource;
+import com.android.apksigner.core.internal.util.Pair;
+import com.android.apksigner.core.internal.zip.CentralDirectoryRecord;
+import com.android.apksigner.core.internal.zip.EocdRecord;
+import com.android.apksigner.core.internal.zip.LocalFileRecord;
+import com.android.apksigner.core.internal.zip.ZipUtils;
+import com.android.apksigner.core.util.DataSink;
+import com.android.apksigner.core.util.DataSinks;
+import com.android.apksigner.core.util.DataSource;
+import com.android.apksigner.core.util.DataSources;
+import com.android.apksigner.core.zip.ZipFormatException;
+
+/**
+ * APK signer.
+ *
+ * <p>The signer preserves as much of the input APK as possible. For example, it preserves the
+ * order of APK entries and preserves their contents, including compressed form and alignment of
+ * data.
+ *
+ * <p>Use {@link Builder} to obtain instances of this signer.
+ */
+public class ApkSigner {
+
+    /**
+     * Extensible data block/field header ID used for storing information about alignment of
+     * uncompressed entries as well as for aligning the entries's data. See ZIP appnote.txt section
+     * 4.5 Extensible data fields.
+     */
+    private static final short ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID = (short) 0xd935;
+
+    /**
+     * Minimum size (in bytes) of the extensible data block/field used for alignment of uncompressed
+     * entries.
+     */
+    private static final short ALIGNMENT_ZIP_EXTRA_DATA_FIELD_MIN_SIZE_BYTES = 6;
+
+    private final ApkSignerEngine mSignerEngine;
+
+    private final File mInputApkFile;
+    private final DataSource mInputApkDataSource;
+
+    private final File mOutputApkFile;
+    private final DataSink mOutputApkDataSink;
+    private final DataSource mOutputApkDataSource;
+
+    private ApkSigner(
+            ApkSignerEngine signerEngine,
+            File inputApkFile,
+            DataSource inputApkDataSource,
+            File outputApkFile,
+            DataSink outputApkDataSink,
+            DataSource outputApkDataSource) {
+        mSignerEngine = signerEngine;
+
+        mInputApkFile = inputApkFile;
+        mInputApkDataSource = inputApkDataSource;
+
+        mOutputApkFile = outputApkFile;
+        mOutputApkDataSink = outputApkDataSink;
+        mOutputApkDataSource = outputApkDataSource;
+    }
+
+    /**
+     * Signs the input APK and outputs the resulting signed APK. The input APK is not modified.
+     *
+     * @throws IOException if an I/O error is encountered while reading or writing the APKs
+     * @throws ZipFormatException if the input APK is malformed at ZIP format level
+     * @throws NoSuchAlgorithmException if the APK signatures cannot be produced or verified because
+     *         a required cryptographic algorithm implementation is missing
+     * @throws InvalidKeyException if a signature could not be generated because a signing key is
+     *         not suitable for generating the signature
+     * @throws SignatureException if an error occurred while generating or verifying a signature
+     * @throws IllegalStateException if this signer's configuration is missing required information
+     *         or if the signing engine is in an invalid state.
+     */
+    public void sign()
+            throws IOException, ZipFormatException, NoSuchAlgorithmException, InvalidKeyException,
+                    SignatureException, IllegalStateException {
+        Closeable in = null;
+        DataSource inputApk;
+        try {
+            if (mInputApkDataSource != null) {
+                inputApk = mInputApkDataSource;
+            } else if (mInputApkFile != null) {
+                RandomAccessFile inputFile = new RandomAccessFile(mInputApkFile, "r");
+                in = inputFile;
+                inputApk = DataSources.asDataSource(inputFile);
+            } else {
+                throw new IllegalStateException("Input APK not specified");
+            }
+
+            Closeable out = null;
+            try {
+                DataSink outputApkOut;
+                DataSource outputApkIn;
+                if (mOutputApkDataSink != null) {
+                    outputApkOut = mOutputApkDataSink;
+                    outputApkIn = mOutputApkDataSource;
+                } else if (mOutputApkFile != null) {
+                    RandomAccessFile outputFile = new RandomAccessFile(mOutputApkFile, "rw");
+                    out = outputFile;
+                    outputFile.setLength(0);
+                    outputApkOut = DataSinks.asDataSink(outputFile);
+                    outputApkIn = DataSources.asDataSource(outputFile);
+                } else {
+                    throw new IllegalStateException("Output APK not specified");
+                }
+
+                sign(mSignerEngine, inputApk, outputApkOut, outputApkIn);
+            } finally {
+                if (out != null) {
+                    out.close();
+                }
+            }
+        } finally {
+            if (in != null) {
+                in.close();
+            }
+        }
+    }
+
+    private static void sign(
+            ApkSignerEngine signerEngine,
+            DataSource inputApk,
+            DataSink outputApkOut,
+            DataSource outputApkIn)
+                    throws IOException, ZipFormatException, NoSuchAlgorithmException,
+                            InvalidKeyException, SignatureException {
+        // Step 1. Find input APK's main ZIP sections
+        ApkUtils.ZipSections inputZipSections = ApkUtils.findZipSections(inputApk);
+        long apkSigningBlockOffset = -1;
+        try {
+            Pair<DataSource, Long> apkSigningBlockAndOffset =
+                    V2SchemeVerifier.findApkSigningBlock(inputApk, inputZipSections);
+            signerEngine.inputApkSigningBlock(apkSigningBlockAndOffset.getFirst());
+            apkSigningBlockOffset = apkSigningBlockAndOffset.getSecond();
+        } catch (V2SchemeVerifier.SignatureNotFoundException e) {
+            // Input APK does not contain an APK Signing Block. That's OK. APKs are not required to
+            // contain this block. It's only needed if the APK is signed using APK Signature Scheme
+            // v2.
+        }
+
+        // Step 2. Parse the input APK's ZIP Central Directory
+        ByteBuffer inputCd = getZipCentralDirectory(inputApk, inputZipSections);
+        List<CentralDirectoryRecord> inputCdRecords =
+                parseZipCentralDirectory(inputCd, inputZipSections);
+
+        // Step 3. Iterate over input APK's entries and output the Local File Header + data of those
+        // entries which need to be output. Entries are iterated in the order in which their Local
+        // File Header records are stored in the file. This is to achieve better data locality in
+        // case Central Directory entries are in the wrong order.
+        List<CentralDirectoryRecord> inputCdRecordsSortedByLfhOffset =
+                new ArrayList<>(inputCdRecords);
+        Collections.sort(
+                inputCdRecordsSortedByLfhOffset,
+                CentralDirectoryRecord.BY_LOCAL_FILE_HEADER_OFFSET_COMPARATOR);
+        DataSource inputApkLfhSection =
+                inputApk.slice(
+                        0,
+                        (apkSigningBlockOffset != -1)
+                                ? apkSigningBlockOffset
+                                : inputZipSections.getZipCentralDirectoryOffset());
+        int lastModifiedDateForNewEntries = -1;
+        int lastModifiedTimeForNewEntries = -1;
+        long inputOffset = 0;
+        long outputOffset = 0;
+        Map<String, CentralDirectoryRecord> outputCdRecordsByName =
+                new HashMap<>(inputCdRecords.size());
+        for (final CentralDirectoryRecord inputCdRecord : inputCdRecordsSortedByLfhOffset) {
+            String entryName = inputCdRecord.getName();
+            ApkSignerEngine.InputJarEntryInstructions entryInstructions =
+                    signerEngine.inputJarEntry(entryName);
+            boolean shouldOutput;
+            switch (entryInstructions.getOutputPolicy()) {
+                case OUTPUT:
+                    shouldOutput = true;
+                    break;
+                case OUTPUT_BY_ENGINE:
+                case SKIP:
+                    shouldOutput = false;
+                    break;
+                default:
+                    throw new RuntimeException(
+                            "Unknown output policy: " + entryInstructions.getOutputPolicy());
+            }
+
+            long inputLocalFileHeaderStartOffset = inputCdRecord.getLocalFileHeaderOffset();
+            if (inputLocalFileHeaderStartOffset > inputOffset) {
+                // Unprocessed data in input starting at inputOffset and ending and the start of
+                // this record's LFH. We output this data verbatim because this signer is supposed
+                // to preserve as much of input as possible.
+                long chunkSize = inputLocalFileHeaderStartOffset - inputOffset;
+                inputApkLfhSection.feed(inputOffset, chunkSize, outputApkOut);
+                outputOffset += chunkSize;
+                inputOffset = inputLocalFileHeaderStartOffset;
+            }
+            LocalFileRecord inputLocalFileRecord =
+                    LocalFileRecord.getRecord(
+                            inputApkLfhSection, inputCdRecord, inputApkLfhSection.size());
+            inputOffset += inputLocalFileRecord.getSize();
+
+            ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
+                    entryInstructions.getInspectJarEntryRequest();
+            if (inspectEntryRequest != null) {
+                fulfillInspectInputJarEntryRequest(
+                        inputApkLfhSection, inputLocalFileRecord, inspectEntryRequest);
+            }
+
+            if (shouldOutput) {
+                // Find the max value of last modified, to be used for new entries added by the
+                // signer.
+                int lastModifiedDate = inputCdRecord.getLastModificationDate();
+                int lastModifiedTime = inputCdRecord.getLastModificationTime();
+                if ((lastModifiedDateForNewEntries == -1)
+                        || (lastModifiedDate > lastModifiedDateForNewEntries)
+                        || ((lastModifiedDate == lastModifiedDateForNewEntries)
+                                && (lastModifiedTime > lastModifiedTimeForNewEntries))) {
+                    lastModifiedDateForNewEntries = lastModifiedDate;
+                    lastModifiedTimeForNewEntries = lastModifiedTime;
+                }
+
+                inspectEntryRequest = signerEngine.outputJarEntry(entryName);
+                if (inspectEntryRequest != null) {
+                    fulfillInspectInputJarEntryRequest(
+                            inputApkLfhSection, inputLocalFileRecord, inspectEntryRequest);
+                }
+
+                // Output entry's Local File Header + data
+                long outputLocalFileHeaderOffset = outputOffset;
+                long outputLocalFileRecordSize =
+                        outputInputJarEntryLfhRecordPreservingDataAlignment(
+                                inputApkLfhSection,
+                                inputLocalFileRecord,
+                                outputApkOut,
+                                outputLocalFileHeaderOffset);
+                outputOffset += outputLocalFileRecordSize;
+
+                // Enqueue entry's Central Directory record for output
+                CentralDirectoryRecord outputCdRecord;
+                if (outputLocalFileHeaderOffset == inputLocalFileRecord.getStartOffsetInArchive()) {
+                    outputCdRecord = inputCdRecord;
+                } else {
+                    outputCdRecord =
+                            inputCdRecord.createWithModifiedLocalFileHeaderOffset(
+                                    outputLocalFileHeaderOffset);
+                }
+                outputCdRecordsByName.put(entryName, outputCdRecord);
+            }
+        }
+        long inputLfhSectionSize = inputApkLfhSection.size();
+        if (inputOffset < inputLfhSectionSize) {
+            // Unprocessed data in input starting at inputOffset and ending and the end of the input
+            // APK's LFH section. We output this data verbatim because this signer is supposed
+            // to preserve as much of input as possible.
+            long chunkSize = inputLfhSectionSize - inputOffset;
+            inputApkLfhSection.feed(inputOffset, chunkSize, outputApkOut);
+            outputOffset += chunkSize;
+            inputOffset = inputLfhSectionSize;
+        }
+
+        // Step 4. Sort output APK's Central Directory records in the order in which they should
+        // appear in the output
+        List<CentralDirectoryRecord> outputCdRecords = new ArrayList<>(inputCdRecords.size() + 10);
+        for (CentralDirectoryRecord inputCdRecord : inputCdRecords) {
+            String entryName = inputCdRecord.getName();
+            CentralDirectoryRecord outputCdRecord = outputCdRecordsByName.get(entryName);
+            if (outputCdRecord != null) {
+                outputCdRecords.add(outputCdRecord);
+            }
+        }
+
+        // Step 5. Generate and output JAR signatures, if necessary. This may output more Local File
+        // Header + data entries and add to the list of output Central Directory records.
+        ApkSignerEngine.OutputJarSignatureRequest outputJarSignatureRequest =
+                signerEngine.outputJarEntries();
+        if (outputJarSignatureRequest != null) {
+            if (lastModifiedDateForNewEntries == -1) {
+                lastModifiedDateForNewEntries = 0x3a21; // Jan 1 2009 (DOS)
+                lastModifiedTimeForNewEntries = 0;
+            }
+            for (ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry :
+                    outputJarSignatureRequest.getAdditionalJarEntries()) {
+                String entryName = entry.getName();
+                byte[] uncompressedData = entry.getData();
+                ZipUtils.DeflateResult deflateResult =
+                        ZipUtils.deflate(ByteBuffer.wrap(uncompressedData));
+                byte[] compressedData = deflateResult.output;
+                long uncompressedDataCrc32 = deflateResult.inputCrc32;
+
+                ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
+                        signerEngine.outputJarEntry(entryName);
+                if (inspectEntryRequest != null) {
+                    inspectEntryRequest.getDataSink().consume(
+                            uncompressedData, 0, uncompressedData.length);
+                    inspectEntryRequest.done();
+                }
+
+                long localFileHeaderOffset = outputOffset;
+                outputOffset +=
+                        LocalFileRecord.outputRecordWithDeflateCompressedData(
+                                entryName,
+                                lastModifiedTimeForNewEntries,
+                                lastModifiedDateForNewEntries,
+                                compressedData,
+                                uncompressedDataCrc32,
+                                uncompressedData.length,
+                                outputApkOut);
+
+
+                outputCdRecords.add(
+                        CentralDirectoryRecord.createWithDeflateCompressedData(
+                                entryName,
+                                lastModifiedTimeForNewEntries,
+                                lastModifiedDateForNewEntries,
+                                uncompressedDataCrc32,
+                                compressedData.length,
+                                uncompressedData.length,
+                                localFileHeaderOffset));
+            }
+            outputJarSignatureRequest.done();
+        }
+
+        // Step 6. Construct output ZIP Central Directory in an in-memory buffer
+        long outputCentralDirSizeBytes = 0;
+        for (CentralDirectoryRecord record : outputCdRecords) {
+            outputCentralDirSizeBytes += record.getSize();
+        }
+        if (outputCentralDirSizeBytes > Integer.MAX_VALUE) {
+            throw new IOException(
+                    "Output ZIP Central Directory too large: " + outputCentralDirSizeBytes
+                            + " bytes");
+        }
+        ByteBuffer outputCentralDir = ByteBuffer.allocate((int) outputCentralDirSizeBytes);
+        for (CentralDirectoryRecord record : outputCdRecords) {
+            record.copyTo(outputCentralDir);
+        }
+        outputCentralDir.flip();
+        DataSource outputCentralDirDataSource = new ByteBufferDataSource(outputCentralDir);
+        long outputCentralDirStartOffset = outputOffset;
+        int outputCentralDirRecordCount = outputCdRecords.size();
+
+        // Step 7. Construct output ZIP End of Central Directory record in an in-memory buffer
+        ByteBuffer outputEocd =
+                EocdRecord.createWithModifiedCentralDirectoryInfo(
+                        inputZipSections.getZipEndOfCentralDirectory(),
+                        outputCentralDirRecordCount,
+                        outputCentralDirDataSource.size(),
+                        outputCentralDirStartOffset);
+
+        // Step 8. Generate and output APK Signature Scheme v2 signatures, if necessary. This may
+        // insert an APK Signing Block just before the output's ZIP Central Directory
+        ApkSignerEngine.OutputApkSigningBlockRequest outputApkSigingBlockRequest =
+                signerEngine.outputZipSections(
+                        outputApkIn,
+                        outputCentralDirDataSource,
+                        DataSources.asDataSource(outputEocd));
+        if (outputApkSigingBlockRequest != null) {
+            byte[] outputApkSigningBlock = outputApkSigingBlockRequest.getApkSigningBlock();
+            outputApkOut.consume(outputApkSigningBlock, 0, outputApkSigningBlock.length);
+            ZipUtils.setZipEocdCentralDirectoryOffset(
+                    outputEocd, outputCentralDirStartOffset + outputApkSigningBlock.length);
+            outputApkSigingBlockRequest.done();
+        }
+
+        // Step 9. Output ZIP Central Directory and ZIP End of Central Directory
+        outputCentralDirDataSource.feed(0, outputCentralDirDataSource.size(), outputApkOut);
+        outputApkOut.consume(outputEocd);
+        signerEngine.outputDone();
+    }
+
+    private static void fulfillInspectInputJarEntryRequest(
+            DataSource lfhSection,
+            LocalFileRecord localFileRecord,
+            ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest)
+                    throws IOException, ZipFormatException {
+        localFileRecord.outputUncompressedData(lfhSection, inspectEntryRequest.getDataSink());
+        inspectEntryRequest.done();
+    }
+
+    private static long outputInputJarEntryLfhRecordPreservingDataAlignment(
+            DataSource inputLfhSection,
+            LocalFileRecord inputRecord,
+            DataSink outputLfhSection,
+            long outputOffset) throws IOException {
+        long inputOffset = inputRecord.getStartOffsetInArchive();
+        if (inputOffset == outputOffset) {
+            // This record's data will be aligned same as in the input APK.
+            return inputRecord.outputRecord(inputLfhSection, outputLfhSection);
+        }
+        int dataAlignmentMultiple = getInputJarEntryDataAlignmentMultiple(inputRecord);
+        if ((dataAlignmentMultiple <= 1)
+                || ((inputOffset % dataAlignmentMultiple)
+                        == (outputOffset % dataAlignmentMultiple))) {
+            // This record's data will be aligned same as in the input APK.
+            return inputRecord.outputRecord(inputLfhSection, outputLfhSection);
+        }
+
+        long inputDataStartOffset = inputOffset + inputRecord.getDataStartOffsetInRecord();
+        if ((inputDataStartOffset % dataAlignmentMultiple) != 0) {
+            // This record's data is not aligned in the input APK. No need to align it in the
+            // output.
+            return inputRecord.outputRecord(inputLfhSection, outputLfhSection);
+        }
+
+        // This record's data needs to be re-aligned in the output. This is achieved using the
+        // record's extra field.
+        ByteBuffer aligningExtra =
+                createExtraFieldToAlignData(
+                        inputRecord.getExtra(),
+                        outputOffset + inputRecord.getExtraFieldStartOffsetInsideRecord(),
+                        dataAlignmentMultiple);
+        return inputRecord.outputRecordWithModifiedExtra(
+                inputLfhSection, aligningExtra, outputLfhSection);
+    }
+
+    private static int getInputJarEntryDataAlignmentMultiple(LocalFileRecord entry) {
+        if (entry.isDataCompressed()) {
+            // Compressed entries don't need to be aligned
+            return 1;
+        }
+
+        // Attempt to obtain the alignment multiple from the entry's extra field.
+        ByteBuffer extra = entry.getExtra();
+        if (extra.hasRemaining()) {
+            extra.order(ByteOrder.LITTLE_ENDIAN);
+            // FORMAT: sequence of fields. Each field consists of:
+            //   * uint16 ID
+            //   * uint16 size
+            //   * 'size' bytes: payload
+            while (extra.remaining() >= 4) {
+                short headerId  = extra.getShort();
+                int dataSize = ZipUtils.getUnsignedInt16(extra);
+                if (dataSize > extra.remaining()) {
+                    // Malformed field -- insufficient input remaining
+                    break;
+                }
+                if (headerId != ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID) {
+                    // Skip this field
+                    extra.position(extra.position() + dataSize);
+                    continue;
+                }
+                // This is APK alignment field.
+                // FORMAT:
+                //  * uint16 alignment multiple (in bytes)
+                //  * remaining bytes -- padding to achieve alignment of data which starts after
+                //    the extra field
+                if (dataSize < 2) {
+                    // Malformed
+                    break;
+                }
+                return ZipUtils.getUnsignedInt16(extra);
+            }
+        }
+
+        // Fall back to filename-based defaults
+        return (entry.getName().endsWith(".so")) ? 4096 : 4;
+    }
+
+    private static ByteBuffer createExtraFieldToAlignData(
+            ByteBuffer original,
+            long extraStartOffset,
+            int dataAlignmentMultiple) {
+        if (dataAlignmentMultiple <= 1) {
+            return original;
+        }
+
+        // In the worst case scenario, we'll increase the output size by 6 + dataAlignment - 1.
+        ByteBuffer result = ByteBuffer.allocate(original.remaining() + 5 + dataAlignmentMultiple);
+        result.order(ByteOrder.LITTLE_ENDIAN);
+
+        // Step 1. Output all extra fields other than the one which is to do with alignment
+        // FORMAT: sequence of fields. Each field consists of:
+        //   * uint16 ID
+        //   * uint16 size
+        //   * 'size' bytes: payload
+        while (original.remaining() >= 4) {
+            short headerId  = original.getShort();
+            int dataSize = ZipUtils.getUnsignedInt16(original);
+            if (dataSize > original.remaining()) {
+                // Malformed field -- insufficient input remaining
+                break;
+            }
+            if (((headerId == 0) && (dataSize == 0))
+                    || (headerId == ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID)) {
+                // Ignore the field if it has to do with the old APK data alignment method (filling
+                // the extra field with 0x00 bytes) or the new APK data alignment method.
+                original.position(original.position() + dataSize);
+                continue;
+            }
+            // Copy this field (including header) to the output
+            original.position(original.position() - 4);
+            int originalLimit = original.limit();
+            original.limit(original.position() + 4 + dataSize);
+            result.put(original);
+            original.limit(originalLimit);
+        }
+
+        // Step 2. Add alignment field
+        // FORMAT:
+        //  * uint16 extra header ID
+        //  * uint16 extra data size
+        //        Payload ('data size' bytes)
+        //      * uint16 alignment multiple (in bytes)
+        //      * remaining bytes -- padding to achieve alignment of data which starts after the
+        //        extra field
+        long dataMinStartOffset =
+                extraStartOffset + result.position()
+                        + ALIGNMENT_ZIP_EXTRA_DATA_FIELD_MIN_SIZE_BYTES;
+        int paddingSizeBytes =
+                (dataAlignmentMultiple - ((int) (dataMinStartOffset % dataAlignmentMultiple)))
+                        % dataAlignmentMultiple;
+        result.putShort(ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID);
+        ZipUtils.putUnsignedInt16(result, 2 + paddingSizeBytes);
+        ZipUtils.putUnsignedInt16(result, dataAlignmentMultiple);
+        result.position(result.position() + paddingSizeBytes);
+        result.flip();
+
+        return result;
+    }
+
+    private static ByteBuffer getZipCentralDirectory(
+            DataSource apk,
+            ApkUtils.ZipSections apkSections) throws IOException, ZipFormatException {
+        long cdSizeBytes = apkSections.getZipCentralDirectorySizeBytes();
+        if (cdSizeBytes > Integer.MAX_VALUE) {
+            throw new ZipFormatException("ZIP Central Directory too large: " + cdSizeBytes);
+        }
+        long cdOffset = apkSections.getZipCentralDirectoryOffset();
+        ByteBuffer cd = apk.getByteBuffer(cdOffset, (int) cdSizeBytes);
+        cd.order(ByteOrder.LITTLE_ENDIAN);
+        return cd;
+    }
+
+    private static List<CentralDirectoryRecord> parseZipCentralDirectory(
+            ByteBuffer cd,
+            ApkUtils.ZipSections apkSections) throws ZipFormatException {
+        long cdOffset = apkSections.getZipCentralDirectoryOffset();
+        int expectedCdRecordCount = apkSections.getZipCentralDirectoryRecordCount();
+        List<CentralDirectoryRecord> cdRecords = new ArrayList<>(expectedCdRecordCount);
+        Set<String> entryNames = new HashSet<>(expectedCdRecordCount);
+        for (int i = 0; i < expectedCdRecordCount; i++) {
+            CentralDirectoryRecord cdRecord;
+            int offsetInsideCd = cd.position();
+            try {
+                cdRecord = CentralDirectoryRecord.getRecord(cd);
+            } catch (ZipFormatException e) {
+                throw new ZipFormatException(
+                        "Failed to parse ZIP Central Directory record #" + (i + 1)
+                                + " at file offset " + (cdOffset + offsetInsideCd),
+                        e);
+            }
+            String entryName = cdRecord.getName();
+            if (!entryNames.add(entryName)) {
+                throw new ZipFormatException(
+                        "Malformed APK: multiple JAR entries with the same name: " + entryName);
+            }
+            cdRecords.add(cdRecord);
+        }
+        if (cd.hasRemaining()) {
+            throw new ZipFormatException(
+                    "Unused space at the end of ZIP Central Directory: " + cd.remaining()
+                        + " bytes starting at file offset " + (cdOffset + cd.position()));
+        }
+
+        return cdRecords;
+    }
+
+    /**
+     * Builder of {@link ApkSigner} instances.
+     *
+     * <p>The following information is required to construct a working {@code ApkSigner}:
+     * <ul>
+     * <li>{@link ApkSignerEngine} -- provided in the constructor,</li>
+     * <li>APK to be signed -- see {@link #setInputApk(File) setInputApk} variants,</li>
+     * <li>where to store the signed APK -- see {@link #setOutputApk(File) setOutputApk} variants.
+     * </li>
+     * </ul>
+     */
+    public static class Builder {
+        private final ApkSignerEngine mSignerEngine;
+
+        private File mInputApkFile;
+        private DataSource mInputApkDataSource;
+
+        private File mOutputApkFile;
+        private DataSink mOutputApkDataSink;
+        private DataSource mOutputApkDataSource;
+
+        /**
+         * Constructs a new {@code Builder} which will make {@code ApkSigner} use the provided
+         * signing engine.
+         */
+        public Builder(ApkSignerEngine signerEngine) {
+            mSignerEngine = signerEngine;
+        }
+
+        /**
+         * Sets the APK to be signed.
+         *
+         * @see #setInputApk(DataSource)
+         */
+        public Builder setInputApk(File inputApk) {
+            if (inputApk == null) {
+                throw new NullPointerException("inputApk == null");
+            }
+            mInputApkFile = inputApk;
+            mInputApkDataSource = null;
+            return this;
+        }
+
+        /**
+         * Sets the APK to be signed.
+         *
+         * @see #setInputApk(File)
+         */
+        public Builder setInputApk(DataSource inputApk) {
+            if (inputApk == null) {
+                throw new NullPointerException("inputApk == null");
+            }
+            mInputApkDataSource = inputApk;
+            mInputApkFile = null;
+            return this;
+        }
+
+        /**
+         * Sets the location of the output (signed) APK. {@code ApkSigner} will create this file if
+         * it doesn't exist.
+         *
+         * @see #setOutputApk(DataSink, DataSource)
+         */
+        public Builder setOutputApk(File outputApk) {
+            if (outputApk == null) {
+                throw new NullPointerException("outputApk == null");
+            }
+            mOutputApkFile = outputApk;
+            mOutputApkDataSink = null;
+            mOutputApkDataSource = null;
+            return this;
+        }
+
+        /**
+         * Sets the sink which will receive the output (signed) APK. Data received by the
+         * {@code outputApkOut} sink must be visible through the {@code outputApkIn} data source.
+         *
+         * @see #setOutputApk(File)
+         */
+        public Builder setOutputApk(DataSink outputApkOut, DataSource outputApkIn) {
+            if (outputApkOut == null) {
+                throw new NullPointerException("outputApkOut == null");
+            }
+            if (outputApkIn == null) {
+                throw new NullPointerException("outputApkIn == null");
+            }
+            mOutputApkFile = null;
+            mOutputApkDataSink = outputApkOut;
+            mOutputApkDataSource = outputApkIn;
+            return this;
+        }
+
+        /**
+         * Returns a new {@code ApkSigner} instance initialized according to the configuration of
+         * this builder.
+         */
+        public ApkSigner build() {
+            return new ApkSigner(
+                    mSignerEngine,
+                    mInputApkFile,
+                    mInputApkDataSource,
+                    mOutputApkFile,
+                    mOutputApkDataSink,
+                    mOutputApkDataSource);
+        }
+    }
+}
index 6a148ca..21c2706 100644 (file)
@@ -33,9 +33,9 @@ import com.android.apksigner.core.util.DataSource;
  * <p><h3>Operating Model</h3>
  *
  * The abstract operating model is that there is an input APK which is being signed, thus producing
- * an output APK. In reality, there may be just an output APK being built from scratch, or the input APK and
- * the output APK may be the same file. Because this engine does not deal with reading and writing
- * files, it can handle all of these scenarios.
+ * an output APK. In reality, there may be just an output APK being built from scratch, or the input
+ * APK and the output APK may be the same file. Because this engine does not deal with reading and
+ * writing files, it can handle all of these scenarios.
  *
  * <p>The engine is stateful and thus cannot be used for signing multiple APKs. However, once
  * the engine signed an APK, the engine can be used to re-sign the APK after it has been modified.
@@ -119,9 +119,10 @@ public interface ApkSignerEngine extends Closeable {
      * @param apkSigningBlock APK signing block of the input APK. The provided data source is
      *        guaranteed to not be used by the engine after this method terminates.
      *
+     * @throws IOException if an I/O error occurs while reading the APK Signing Block
      * @throws IllegalStateException if this engine is closed
      */
-    void inputApkSigningBlock(DataSource apkSigningBlock) throws IllegalStateException;
+    void inputApkSigningBlock(DataSource apkSigningBlock) throws IOException, IllegalStateException;
 
     /**
      * Indicates to this engine that the specified JAR entry was encountered in the input APK.
index 1bba313..752ba7e 100644 (file)
@@ -47,7 +47,7 @@ import com.android.apksigner.core.internal.util.AndroidSdkVersion;
 import com.android.apksigner.core.internal.util.InclusiveIntRange;
 import com.android.apksigner.core.internal.util.MessageDigestSink;
 import com.android.apksigner.core.internal.zip.CentralDirectoryRecord;
-import com.android.apksigner.core.internal.zip.LocalFileHeader;
+import com.android.apksigner.core.internal.zip.LocalFileRecord;
 import com.android.apksigner.core.util.DataSource;
 import com.android.apksigner.core.zip.ZipFormatException;
 
@@ -187,10 +187,7 @@ public abstract class V1SchemeVerifier {
 
             // Parse the JAR manifest and check that all JAR entries it references exist in the APK.
             byte[] manifestBytes =
-                    LocalFileHeader.getUncompressedData(
-                            apk, 0,
-                            manifestEntry,
-                            cdStartOffset);
+                    LocalFileRecord.getUncompressedData(apk, manifestEntry, cdStartOffset);
             Map<String, ManifestParser.Section> entryNameToManifestSection = null;
             ManifestParser manifest = new ManifestParser(manifestBytes);
             ManifestParser.Section manifestMainSection = manifest.readSection();
@@ -411,15 +408,9 @@ public abstract class V1SchemeVerifier {
                 DataSource apk, long cdStartOffset, int minSdkVersion, int maxSdkVersion)
                         throws IOException, ZipFormatException, NoSuchAlgorithmException {
             byte[] sigBlockBytes =
-                    LocalFileHeader.getUncompressedData(
-                            apk, 0,
-                            mSignatureBlockEntry,
-                            cdStartOffset);
+                    LocalFileRecord.getUncompressedData(apk, mSignatureBlockEntry, cdStartOffset);
             mSigFileBytes =
-                    LocalFileHeader.getUncompressedData(
-                            apk, 0,
-                            mSignatureFileEntry,
-                            cdStartOffset);
+                    LocalFileRecord.getUncompressedData(apk, mSignatureFileEntry, cdStartOffset);
             PKCS7 sigBlock;
             try {
                 sigBlock = new PKCS7(sigBlockBytes);
@@ -1412,8 +1403,8 @@ public abstract class V1SchemeVerifier {
             }
 
             try {
-                LocalFileHeader.sendUncompressedData(
-                        apk, 0,
+                LocalFileRecord.outputUncompressedData(
+                        apk,
                         cdRecord,
                         cdOffsetInApk,
                         new MessageDigestSink(mds));
index 0c303ee..5e1e8fb 100644 (file)
@@ -553,38 +553,35 @@ public abstract class V2SchemeVerifier {
     private static SignatureInfo findSignature(
             DataSource apk, ApkUtils.ZipSections zipSections, Result result)
                     throws IOException, SignatureNotFoundException {
-        long centralDirStartOffset = zipSections.getZipCentralDirectoryOffset();
-        long centralDirEndOffset =
-                centralDirStartOffset + zipSections.getZipCentralDirectorySizeBytes();
-        long eocdStartOffset = zipSections.getZipEndOfCentralDirectoryOffset();
-        if (centralDirEndOffset != eocdStartOffset) {
-            throw new SignatureNotFoundException(
-                    "ZIP Central Directory is not immediately followed by End of Central Directory"
-                            + ". CD end: " + centralDirEndOffset
-                            + ", EoCD start: " + eocdStartOffset);
-        }
-
         // Find the APK Signing Block. The block immediately precedes the Central Directory.
         ByteBuffer eocd = zipSections.getZipEndOfCentralDirectory();
-        Pair<ByteBuffer, Long> apkSigningBlockAndOffset =
-                findApkSigningBlock(apk, centralDirStartOffset);
-        ByteBuffer apkSigningBlock = apkSigningBlockAndOffset.getFirst();
+        Pair<DataSource, Long> apkSigningBlockAndOffset = findApkSigningBlock(apk, zipSections);
+        DataSource apkSigningBlock = apkSigningBlockAndOffset.getFirst();
         long apkSigningBlockOffset = apkSigningBlockAndOffset.getSecond();
+        ByteBuffer apkSigningBlockBuf =
+                apkSigningBlock.getByteBuffer(0, (int) apkSigningBlock.size());
+        apkSigningBlockBuf.order(ByteOrder.LITTLE_ENDIAN);
 
         // Find the APK Signature Scheme v2 Block inside the APK Signing Block.
         ByteBuffer apkSignatureSchemeV2Block =
-                findApkSignatureSchemeV2Block(apkSigningBlock, result);
+                findApkSignatureSchemeV2Block(apkSigningBlockBuf, result);
 
         return new SignatureInfo(
                 apkSignatureSchemeV2Block,
                 apkSigningBlockOffset,
-                centralDirStartOffset,
-                eocdStartOffset,
+                zipSections.getZipCentralDirectoryOffset(),
+                zipSections.getZipEndOfCentralDirectoryOffset(),
                 eocd);
     }
 
-    private static Pair<ByteBuffer, Long> findApkSigningBlock(
-            DataSource apk, long centralDirOffset) throws IOException, SignatureNotFoundException {
+    /**
+     * Returns the APK Signing Block and its offset in the provided APK.
+     *
+     * @throws SignatureNotFoundException if the APK does not contain an APK Signing Block
+     */
+    public static Pair<DataSource, Long> findApkSigningBlock(
+            DataSource apk, ApkUtils.ZipSections zipSections)
+                    throws IOException, SignatureNotFoundException {
         // FORMAT:
         // OFFSET       DATA TYPE  DESCRIPTION
         // * @+0  bytes uint64:    size in bytes (excluding this field)
@@ -592,15 +589,26 @@ public abstract class V2SchemeVerifier {
         // * @-24 bytes uint64:    size in bytes (same as the one above)
         // * @-16 bytes uint128:   magic
 
-        if (centralDirOffset < APK_SIG_BLOCK_MIN_SIZE) {
+        long centralDirStartOffset = zipSections.getZipCentralDirectoryOffset();
+        long centralDirEndOffset =
+                centralDirStartOffset + zipSections.getZipCentralDirectorySizeBytes();
+        long eocdStartOffset = zipSections.getZipEndOfCentralDirectoryOffset();
+        if (centralDirEndOffset != eocdStartOffset) {
+            throw new SignatureNotFoundException(
+                    "ZIP Central Directory is not immediately followed by End of Central Directory"
+                            + ". CD end: " + centralDirEndOffset
+                            + ", EoCD start: " + eocdStartOffset);
+        }
+
+        if (centralDirStartOffset < APK_SIG_BLOCK_MIN_SIZE) {
             throw new SignatureNotFoundException(
                     "APK too small for APK Signing Block. ZIP Central Directory offset: "
-                            + centralDirOffset);
+                            + centralDirStartOffset);
         }
         // Read the magic and offset in file from the footer section of the block:
         // * uint64:   size of block
         // * 16 bytes: magic
-        ByteBuffer footer = apk.getByteBuffer(centralDirOffset - 24, 24);
+        ByteBuffer footer = apk.getByteBuffer(centralDirStartOffset - 24, 24);
         footer.order(ByteOrder.LITTLE_ENDIAN);
         if ((footer.getLong(8) != APK_SIG_BLOCK_MAGIC_LO)
                 || (footer.getLong(16) != APK_SIG_BLOCK_MAGIC_HI)) {
@@ -615,12 +623,12 @@ public abstract class V2SchemeVerifier {
                     "APK Signing Block size out of range: " + apkSigBlockSizeInFooter);
         }
         int totalSize = (int) (apkSigBlockSizeInFooter + 8);
-        long apkSigBlockOffset = centralDirOffset - totalSize;
+        long apkSigBlockOffset = centralDirStartOffset - totalSize;
         if (apkSigBlockOffset < 0) {
             throw new SignatureNotFoundException(
                     "APK Signing Block offset out of range: " + apkSigBlockOffset);
         }
-        ByteBuffer apkSigBlock = apk.getByteBuffer(apkSigBlockOffset, totalSize);
+        ByteBuffer apkSigBlock = apk.getByteBuffer(apkSigBlockOffset, 8);
         apkSigBlock.order(ByteOrder.LITTLE_ENDIAN);
         long apkSigBlockSizeInHeader = apkSigBlock.getLong(0);
         if (apkSigBlockSizeInHeader != apkSigBlockSizeInFooter) {
@@ -628,7 +636,7 @@ public abstract class V2SchemeVerifier {
                     "APK Signing Block sizes in header and footer do not match: "
                             + apkSigBlockSizeInHeader + " vs " + apkSigBlockSizeInFooter);
         }
-        return Pair.of(apkSigBlock, apkSigBlockOffset);
+        return Pair.of(apk.slice(apkSigBlockOffset, totalSize), apkSigBlockOffset);
     }
 
     private static ByteBuffer findApkSignatureSchemeV2Block(
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/util/RandomAccessFileDataSink.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/util/RandomAccessFileDataSink.java
new file mode 100644 (file)
index 0000000..2198492
--- /dev/null
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2016 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 com.android.apksigner.core.internal.util;
+
+import com.android.apksigner.core.util.DataSink;
+
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+
+/**
+ * {@link DataSink} which outputs received data into the associated file, sequentially.
+ */
+public class RandomAccessFileDataSink implements DataSink {
+
+    private final RandomAccessFile mFile;
+    private final FileChannel mFileChannel;
+    private long mPosition;
+
+    /**
+     * Constructs a new {@code RandomAccessFileDataSink} which stores output starting from the
+     * beginning of the provided file.
+     */
+    public RandomAccessFileDataSink(RandomAccessFile file) {
+        this(file, 0);
+    }
+
+    /**
+     * Constructs a new {@code RandomAccessFileDataSink} which stores output starting from the
+     * specified position of the provided file.
+     */
+    public RandomAccessFileDataSink(RandomAccessFile file, long startPosition) {
+        if (file == null) {
+            throw new NullPointerException("file == null");
+        }
+        if (startPosition < 0) {
+            throw new IllegalArgumentException("startPosition: " + startPosition);
+        }
+        mFile = file;
+        mFileChannel = file.getChannel();
+        mPosition = startPosition;
+    }
+
+    @Override
+    public void consume(byte[] buf, int offset, int length) throws IOException {
+        if (length == 0) {
+            return;
+        }
+
+        synchronized (mFile) {
+            mFile.seek(mPosition);
+            mFile.write(buf, offset, length);
+            mPosition += length;
+        }
+    }
+
+    @Override
+    public void consume(ByteBuffer buf) throws IOException {
+        int length = buf.remaining();
+        if (length == 0) {
+            return;
+        }
+
+        synchronized (mFile) {
+            mFile.seek(mPosition);
+            while (buf.hasRemaining()) {
+                mFileChannel.write(buf);
+            }
+            mPosition += length;
+        }
+    }
+}
index 6a5b94c..141d01e 100644 (file)
@@ -20,6 +20,7 @@ import com.android.apksigner.core.zip.ZipFormatException;
 
 import java.nio.BufferUnderflowException;
 import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
 import java.nio.charset.StandardCharsets;
 import java.util.Comparator;
 
@@ -38,52 +39,59 @@ public class CentralDirectoryRecord {
     private static final int RECORD_SIGNATURE = 0x02014b50;
     private static final int HEADER_SIZE_BYTES = 46;
 
-    private static final int GP_FLAGS_OFFSET = 8;
-    private static final int COMPRESSION_METHOD_OFFSET = 10;
-    private static final int CRC32_OFFSET = 16;
-    private static final int COMPRESSED_SIZE_OFFSET = 20;
-    private static final int UNCOMPRESSED_SIZE_OFFSET = 24;
-    private static final int NAME_LENGTH_OFFSET = 28;
-    private static final int EXTRA_LENGTH_OFFSET = 30;
-    private static final int COMMENT_LENGTH_OFFSET = 32;
-    private static final int LOCAL_FILE_HEADER_OFFSET = 42;
+    private static final int LAST_MODIFICATION_TIME_OFFSET =  12;
+    private static final int LOCAL_FILE_HEADER_OFFSET_OFFSET = 42;
     private static final int NAME_OFFSET = HEADER_SIZE_BYTES;
 
-    private final short mGpFlags;
-    private final short mCompressionMethod;
+    private final ByteBuffer mData;
+    private final int mLastModificationTime;
+    private final int mLastModificationDate;
     private final long mCrc32;
     private final long mCompressedSize;
     private final long mUncompressedSize;
     private final long mLocalFileHeaderOffset;
     private final String mName;
+    private final int mNameSizeBytes;
 
     private CentralDirectoryRecord(
-            short gpFlags,
-            short compressionMethod,
+            ByteBuffer data,
+            int lastModificationTime,
+            int lastModificationDate,
             long crc32,
             long compressedSize,
             long uncompressedSize,
             long localFileHeaderOffset,
-            String name) {
-        mGpFlags = gpFlags;
-        mCompressionMethod = compressionMethod;
+            String name,
+            int nameSizeBytes) {
+        mData = data;
+        mLastModificationDate = lastModificationDate;
+        mLastModificationTime = lastModificationTime;
         mCrc32 = crc32;
         mCompressedSize = compressedSize;
         mUncompressedSize = uncompressedSize;
         mLocalFileHeaderOffset = localFileHeaderOffset;
         mName = name;
+        mNameSizeBytes = nameSizeBytes;
+    }
+
+    public int getSize() {
+        return mData.remaining();
     }
 
     public String getName() {
         return mName;
     }
 
-    public short getGpFlags() {
-        return mGpFlags;
+    public int getNameSizeBytes() {
+        return mNameSizeBytes;
+    }
+
+    public int getLastModificationTime() {
+        return mLastModificationTime;
     }
 
-    public short getCompressionMethod() {
-        return mCompressionMethod;
+    public int getLastModificationDate() {
+        return mLastModificationDate;
     }
 
     public long getCrc32() {
@@ -114,24 +122,25 @@ public class CentralDirectoryRecord {
                             + " bytes, available: " + buf.remaining() + " bytes",
                     new BufferUnderflowException());
         }
-        int bufPosition = buf.position();
-        int recordSignature = buf.getInt(bufPosition);
+        int originalPosition = buf.position();
+        int recordSignature = buf.getInt();
         if (recordSignature != RECORD_SIGNATURE) {
             throw new ZipFormatException(
                     "Not a Central Directory record. Signature: 0x"
                             + Long.toHexString(recordSignature & 0xffffffffL));
         }
-        short gpFlags = buf.getShort(bufPosition + GP_FLAGS_OFFSET);
-        short compressionMethod = buf.getShort(bufPosition + COMPRESSION_METHOD_OFFSET);
-        long crc32 = ZipUtils.getUnsignedInt32(buf, bufPosition + CRC32_OFFSET);
-        long compressedSize = ZipUtils.getUnsignedInt32(buf, bufPosition + COMPRESSED_SIZE_OFFSET);
-        long uncompressedSize =
-                ZipUtils.getUnsignedInt32(buf,  bufPosition + UNCOMPRESSED_SIZE_OFFSET);
-        int nameSize = ZipUtils.getUnsignedInt16(buf, bufPosition + NAME_LENGTH_OFFSET);
-        int extraSize = ZipUtils.getUnsignedInt16(buf, bufPosition + EXTRA_LENGTH_OFFSET);
-        int commentSize = ZipUtils.getUnsignedInt16(buf, bufPosition + COMMENT_LENGTH_OFFSET);
-        long localFileHeaderOffset =
-                ZipUtils.getUnsignedInt32(buf, bufPosition + LOCAL_FILE_HEADER_OFFSET);
+        buf.position(originalPosition + LAST_MODIFICATION_TIME_OFFSET);
+        int lastModificationTime = ZipUtils.getUnsignedInt16(buf);
+        int lastModificationDate = ZipUtils.getUnsignedInt16(buf);
+        long crc32 = ZipUtils.getUnsignedInt32(buf);
+        long compressedSize = ZipUtils.getUnsignedInt32(buf);
+        long uncompressedSize = ZipUtils.getUnsignedInt32(buf);
+        int nameSize = ZipUtils.getUnsignedInt16(buf);
+        int extraSize = ZipUtils.getUnsignedInt16(buf);
+        int commentSize = ZipUtils.getUnsignedInt16(buf);
+        buf.position(originalPosition + LOCAL_FILE_HEADER_OFFSET_OFFSET);
+        long localFileHeaderOffset = ZipUtils.getUnsignedInt32(buf);
+        buf.position(originalPosition);
         int recordSize = HEADER_SIZE_BYTES + nameSize + extraSize + commentSize;
         if (recordSize > buf.remaining()) {
             throw new ZipFormatException(
@@ -139,16 +148,99 @@ public class CentralDirectoryRecord {
                             + buf.remaining() + " bytes",
                     new BufferUnderflowException());
         }
-        String name = getName(buf, bufPosition + NAME_OFFSET, nameSize);
-        buf.position(bufPosition + recordSize);
+        String name = getName(buf, originalPosition + NAME_OFFSET, nameSize);
+        buf.position(originalPosition);
+        int originalLimit = buf.limit();
+        int recordEndInBuf = originalPosition + recordSize;
+        ByteBuffer recordBuf;
+        try {
+            buf.limit(recordEndInBuf);
+            recordBuf = buf.slice();
+        } finally {
+            buf.limit(originalLimit);
+        }
+        // Consume this record
+        buf.position(recordEndInBuf);
+        return new CentralDirectoryRecord(
+                recordBuf,
+                lastModificationTime,
+                lastModificationDate,
+                crc32,
+                compressedSize,
+                uncompressedSize,
+                localFileHeaderOffset,
+                name,
+                nameSize);
+    }
+
+    public void copyTo(ByteBuffer output) {
+        output.put(mData.slice());
+    }
+
+    public CentralDirectoryRecord createWithModifiedLocalFileHeaderOffset(
+            long localFileHeaderOffset) {
+        ByteBuffer result = ByteBuffer.allocate(mData.remaining());
+        result.put(mData.slice());
+        result.flip();
+        result.order(ByteOrder.LITTLE_ENDIAN);
+        ZipUtils.setUnsignedInt32(result, LOCAL_FILE_HEADER_OFFSET_OFFSET, localFileHeaderOffset);
+        return new CentralDirectoryRecord(
+                result,
+                mLastModificationTime,
+                mLastModificationDate,
+                mCrc32,
+                mCompressedSize,
+                mUncompressedSize,
+                localFileHeaderOffset,
+                mName,
+                mNameSizeBytes);
+    }
+
+    public static CentralDirectoryRecord createWithDeflateCompressedData(
+            String name,
+            int lastModifiedTime,
+            int lastModifiedDate,
+            long crc32,
+            long compressedSize,
+            long uncompressedSize,
+            long localFileHeaderOffset) {
+        byte[] nameBytes = name.getBytes(StandardCharsets.UTF_8);
+        int recordSize = HEADER_SIZE_BYTES + nameBytes.length;
+        ByteBuffer result = ByteBuffer.allocate(recordSize);
+        result.order(ByteOrder.LITTLE_ENDIAN);
+        result.putInt(RECORD_SIGNATURE);
+        ZipUtils.putUnsignedInt16(result, 0x14); // Version made by
+        ZipUtils.putUnsignedInt16(result, 0x14); // Minimum version needed to extract
+        result.putShort(ZipUtils.GP_FLAG_EFS); // UTF-8 character encoding used for entry name
+        result.putShort(ZipUtils.COMPRESSION_METHOD_DEFLATED);
+        ZipUtils.putUnsignedInt16(result, lastModifiedTime);
+        ZipUtils.putUnsignedInt16(result, lastModifiedDate);
+        ZipUtils.putUnsignedInt32(result, crc32);
+        ZipUtils.putUnsignedInt32(result, compressedSize);
+        ZipUtils.putUnsignedInt32(result, uncompressedSize);
+        ZipUtils.putUnsignedInt16(result, nameBytes.length);
+        ZipUtils.putUnsignedInt16(result, 0); // Extra field length
+        ZipUtils.putUnsignedInt16(result, 0); // File comment length
+        ZipUtils.putUnsignedInt16(result, 0); // Disk number
+        ZipUtils.putUnsignedInt16(result, 0); // Internal file attributes
+        ZipUtils.putUnsignedInt32(result, 0); // External file attributes
+        ZipUtils.putUnsignedInt32(result, localFileHeaderOffset);
+        result.put(nameBytes);
+
+        if (result.hasRemaining()) {
+            throw new RuntimeException("pos: " + result.position() + ", limit: " + result.limit());
+        }
+        result.flip();
         return new CentralDirectoryRecord(
-                gpFlags,
-                compressionMethod,
+                result,
+                lastModifiedTime,
+                lastModifiedDate,
                 crc32,
                 compressedSize,
                 uncompressedSize,
                 localFileHeaderOffset,
-                name);
+                name,
+                nameBytes.length);
     }
 
     static String getName(ByteBuffer record, int position, int nameLengthBytes) {
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/EocdRecord.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/EocdRecord.java
new file mode 100644 (file)
index 0000000..8777591
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2016 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 com.android.apksigner.core.internal.zip;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * ZIP End of Central Directory record.
+ */
+public class EocdRecord {
+    private static final int CD_RECORD_COUNT_ON_DISK_OFFSET = 8;
+    private static final int CD_RECORD_COUNT_TOTAL_OFFSET = 10;
+    private static final int CD_SIZE_OFFSET = 12;
+    private static final int CD_OFFSET_OFFSET = 16;
+
+    public static ByteBuffer createWithModifiedCentralDirectoryInfo(
+            ByteBuffer original,
+            int centralDirectoryRecordCount,
+            long centralDirectorySizeBytes,
+            long centralDirectoryOffset) {
+        ByteBuffer result = ByteBuffer.allocate(original.remaining());
+        result.order(ByteOrder.LITTLE_ENDIAN);
+        result.put(original.slice());
+        result.flip();
+        ZipUtils.setUnsignedInt16(
+                result, CD_RECORD_COUNT_ON_DISK_OFFSET, centralDirectoryRecordCount);
+        ZipUtils.setUnsignedInt16(
+                result, CD_RECORD_COUNT_TOTAL_OFFSET, centralDirectoryRecordCount);
+        ZipUtils.setUnsignedInt32(result, CD_SIZE_OFFSET, centralDirectorySizeBytes);
+        ZipUtils.setUnsignedInt32(result, CD_OFFSET_OFFSET, centralDirectoryOffset);
+        return result;
+    }
+}
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/LocalFileHeader.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/LocalFileHeader.java
deleted file mode 100644 (file)
index 99a606b..0000000
+++ /dev/null
@@ -1,282 +0,0 @@
-/*
- * Copyright (C) 2016 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 com.android.apksigner.core.internal.zip;
-
-import java.io.Closeable;
-import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.nio.charset.StandardCharsets;
-import java.util.zip.DataFormatException;
-import java.util.zip.Inflater;
-
-import com.android.apksigner.core.internal.util.ByteBufferSink;
-import com.android.apksigner.core.util.DataSink;
-import com.android.apksigner.core.util.DataSource;
-import com.android.apksigner.core.zip.ZipFormatException;
-
-/**
- * ZIP Local File Header.
- */
-public class LocalFileHeader {
-    private static final int RECORD_SIGNATURE = 0x04034b50;
-    private static final int HEADER_SIZE_BYTES = 30;
-
-    private static final int GP_FLAGS_OFFSET = 6;
-    private static final int COMPRESSION_METHOD_OFFSET = 8;
-    private static final int CRC32_OFFSET = 14;
-    private static final int COMPRESSED_SIZE_OFFSET = 18;
-    private static final int UNCOMPRESSED_SIZE_OFFSET = 22;
-    private static final int NAME_LENGTH_OFFSET = 26;
-    private static final int EXTRA_LENGTH_OFFSET = 28;
-    private static final int NAME_OFFSET = HEADER_SIZE_BYTES;
-
-    private static final short GP_FLAG_DATA_DESCRIPTOR_USED = 0x08;
-
-    private LocalFileHeader() {}
-
-    /**
-     * Returns the uncompressed data pointed to by the provided ZIP Central Directory (CD) record.
-     */
-    public static byte[] getUncompressedData(
-            DataSource source,
-            long sourceOffsetInArchive,
-            CentralDirectoryRecord cdRecord,
-            long cdStartOffsetInArchive) throws ZipFormatException, IOException {
-        if (cdRecord.getUncompressedSize() > Integer.MAX_VALUE) {
-            throw new IOException(
-                    cdRecord.getName() + " too large: " + cdRecord.getUncompressedSize());
-        }
-        byte[] result = new byte[(int) cdRecord.getUncompressedSize()];
-        ByteBuffer resultBuf = ByteBuffer.wrap(result);
-        ByteBufferSink resultSink = new ByteBufferSink(resultBuf);
-        sendUncompressedData(
-                source,
-                sourceOffsetInArchive,
-                cdRecord,
-                cdStartOffsetInArchive,
-                resultSink);
-        if (resultBuf.hasRemaining()) {
-            throw new ZipFormatException(
-                    "Data of " + cdRecord.getName() + " shorter than specified in Central Directory"
-                            + ". Expected: " + result.length + " bytes,  read: "
-                            + resultBuf.position() + " bytes");
-        }
-        return result;
-    }
-
-    /**
-     * Sends the uncompressed data pointed to by the provided ZIP Central Directory (CD) record into
-     * the provided data sink.
-     */
-    public static void sendUncompressedData(
-            DataSource source,
-            long sourceOffsetInArchive,
-            CentralDirectoryRecord cdRecord,
-            long cdStartOffsetInArchive,
-            DataSink sink) throws ZipFormatException, IOException {
-
-        // IMPLEMENTATION NOTE: This method attempts to mimic the behavior of Android platform
-        // exhibited when reading an APK for the purposes of verifying its signatures.
-
-        String entryName = cdRecord.getName();
-        byte[] cdNameBytes = entryName.getBytes(StandardCharsets.UTF_8);
-        int headerSizeWithName = HEADER_SIZE_BYTES + cdNameBytes.length;
-        long localFileHeaderOffsetInArchive = cdRecord.getLocalFileHeaderOffset();
-        long headerEndInArchive = localFileHeaderOffsetInArchive + headerSizeWithName;
-        if (headerEndInArchive >= cdStartOffsetInArchive) {
-            throw new ZipFormatException(
-                    "Local File Header of " + entryName + " extends beyond start of Central"
-                            + " Directory. LFH end: " + headerEndInArchive
-                            + ", CD start: " + cdStartOffsetInArchive);
-        }
-        ByteBuffer header;
-        try {
-            header =
-                    source.getByteBuffer(
-                            localFileHeaderOffsetInArchive - sourceOffsetInArchive,
-                            headerSizeWithName);
-        } catch (IOException e) {
-            throw new IOException("Failed to read Local File Header of " + entryName, e);
-        }
-        header.order(ByteOrder.LITTLE_ENDIAN);
-
-        int recordSignature = header.getInt(0);
-        if (recordSignature != RECORD_SIGNATURE) {
-            throw new ZipFormatException(
-                    "Not a Local File Header record for entry " + entryName + ". Signature: 0x"
-                            + Long.toHexString(recordSignature & 0xffffffffL));
-        }
-        short gpFlags = header.getShort(GP_FLAGS_OFFSET);
-        if ((gpFlags & GP_FLAG_DATA_DESCRIPTOR_USED) == 0) {
-            long crc32 = ZipUtils.getUnsignedInt32(header, CRC32_OFFSET);
-            if (crc32 != cdRecord.getCrc32()) {
-                throw new ZipFormatException(
-                        "CRC-32 mismatch between Local File Header and Central Directory for entry "
-                                + entryName + ". LFH: " + crc32 + ", CD: " + cdRecord.getCrc32());
-            }
-            long compressedSize = ZipUtils.getUnsignedInt32(header, COMPRESSED_SIZE_OFFSET);
-            if (compressedSize != cdRecord.getCompressedSize()) {
-                throw new ZipFormatException(
-                        "Compressed size mismatch between Local File Header and Central Directory"
-                                + " for entry " + entryName + ". LFH: " + compressedSize
-                                + ", CD: " + cdRecord.getCompressedSize());
-            }
-            long uncompressedSize = ZipUtils.getUnsignedInt32(header, UNCOMPRESSED_SIZE_OFFSET);
-            if (uncompressedSize != cdRecord.getUncompressedSize()) {
-                throw new ZipFormatException(
-                        "Uncompressed size mismatch between Local File Header and Central Directory"
-                                + " for entry " + entryName + ". LFH: " + uncompressedSize
-                                + ", CD: " + cdRecord.getUncompressedSize());
-            }
-        }
-        int nameLength = ZipUtils.getUnsignedInt16(header, NAME_LENGTH_OFFSET);
-        if (nameLength > cdNameBytes.length) {
-            throw new ZipFormatException(
-                    "Name mismatch between Local File Header and Central Directory for entry"
-                            + entryName + ". LFH: " + nameLength
-                            + " bytes, CD: " + cdNameBytes.length + " bytes");
-        }
-        String name = CentralDirectoryRecord.getName(header, NAME_OFFSET, nameLength);
-        if (!entryName.equals(name)) {
-            throw new ZipFormatException(
-                    "Name mismatch between Local File Header and Central Directory. LFH: \""
-                            + name + "\", CD: \"" + entryName + "\"");
-        }
-        int extraLength = ZipUtils.getUnsignedInt16(header, EXTRA_LENGTH_OFFSET);
-
-        short compressionMethod = header.getShort(COMPRESSION_METHOD_OFFSET);
-        boolean compressed;
-        switch (compressionMethod) {
-            case ZipUtils.COMPRESSION_METHOD_STORED:
-                compressed = false;
-                break;
-            case ZipUtils.COMPRESSION_METHOD_DEFLATED:
-                compressed = true;
-                break;
-            default:
-                throw new ZipFormatException(
-                        "Unsupported compression method of entry " + entryName
-                                + ": " + (compressionMethod & 0xffff));
-        }
-
-        long dataStartOffsetInArchive =
-                localFileHeaderOffsetInArchive + HEADER_SIZE_BYTES + nameLength + extraLength;
-        long dataSize;
-        if (compressed) {
-            dataSize = cdRecord.getCompressedSize();
-        } else {
-            dataSize = cdRecord.getUncompressedSize();
-        }
-        long dataEndOffsetInArchive = dataStartOffsetInArchive + dataSize;
-        if (dataEndOffsetInArchive > cdStartOffsetInArchive) {
-            throw new ZipFormatException(
-                    "Local File Header data of " + entryName + " extends beyond Central Directory"
-                            + ". LFH data start: " + dataStartOffsetInArchive
-                            + ", LFH data end: " + dataEndOffsetInArchive
-                            + ", CD start: " + cdStartOffsetInArchive);
-        }
-
-        long dataOffsetInSource = dataStartOffsetInArchive - sourceOffsetInArchive;
-        try {
-            if (compressed) {
-                try (InflateSinkAdapter inflateAdapter = new InflateSinkAdapter(sink)) {
-                    source.feed(dataOffsetInSource, dataSize, inflateAdapter);
-                }
-            } else {
-                source.feed(dataOffsetInSource, dataSize, sink);
-            }
-        } catch (IOException e) {
-            throw new IOException(
-                    "Failed to read data of " + ((compressed) ? "compressed" : "uncompressed")
-                        + " entry " + entryName,
-                    e);
-        }
-        // Interestingly, Android doesn't check that uncompressed data's CRC-32 is as expected. We
-        // thus don't check either.
-    }
-
-    private static class InflateSinkAdapter implements DataSink, Closeable {
-        private final DataSink mDelegate;
-
-        private Inflater mInflater = new Inflater(true);
-        private byte[] mOutputBuffer;
-        private byte[] mInputBuffer;
-        private boolean mClosed;
-
-        private InflateSinkAdapter(DataSink delegate) {
-            mDelegate = delegate;
-        }
-
-        @Override
-        public void consume(byte[] buf, int offset, int length) throws IOException {
-            checkNotClosed();
-            mInflater.setInput(buf, offset, length);
-            if (mOutputBuffer == null) {
-                mOutputBuffer = new byte[65536];
-            }
-            while (!mInflater.finished()) {
-                int outputChunkSize;
-                try {
-                    outputChunkSize = mInflater.inflate(mOutputBuffer);
-                } catch (DataFormatException e) {
-                    throw new IOException("Failed to inflate data", e);
-                }
-                if (outputChunkSize == 0) {
-                    return;
-                }
-                // mDelegate.consume(mOutputBuffer, 0, outputChunkSize);
-                mDelegate.consume(ByteBuffer.wrap(mOutputBuffer, 0, outputChunkSize));
-            }
-        }
-
-        @Override
-        public void consume(ByteBuffer buf) throws IOException {
-            checkNotClosed();
-            if (buf.hasArray()) {
-                consume(buf.array(), buf.arrayOffset() + buf.position(), buf.remaining());
-                buf.position(buf.limit());
-            } else {
-                if (mInputBuffer == null) {
-                    mInputBuffer = new byte[65536];
-                }
-                while (buf.hasRemaining()) {
-                    int chunkSize = Math.min(buf.remaining(), mInputBuffer.length);
-                    buf.get(mInputBuffer, 0, chunkSize);
-                    consume(mInputBuffer, 0, chunkSize);
-                }
-            }
-        }
-
-        @Override
-        public void close() throws IOException {
-            mClosed = true;
-            mInputBuffer = null;
-            mOutputBuffer = null;
-            if (mInflater != null) {
-                mInflater.end();
-                mInflater = null;
-            }
-        }
-
-        private void checkNotClosed() {
-            if (mClosed) {
-                throw new IllegalStateException("Closed");
-            }
-        }
-    }
-}
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/LocalFileRecord.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/LocalFileRecord.java
new file mode 100644 (file)
index 0000000..397a450
--- /dev/null
@@ -0,0 +1,540 @@
+/*
+ * Copyright (C) 2016 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 com.android.apksigner.core.internal.zip;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
+import java.util.zip.DataFormatException;
+import java.util.zip.Inflater;
+
+import com.android.apksigner.core.internal.util.ByteBufferSink;
+import com.android.apksigner.core.util.DataSink;
+import com.android.apksigner.core.util.DataSource;
+import com.android.apksigner.core.zip.ZipFormatException;
+
+/**
+ * ZIP Local File record.
+ *
+ * <p>The record consists of the Local File Header, file data, and (if present) Data Descriptor.
+ */
+public class LocalFileRecord {
+    private static final int RECORD_SIGNATURE = 0x04034b50;
+    private static final int HEADER_SIZE_BYTES = 30;
+
+    private static final int GP_FLAGS_OFFSET = 6;
+    private static final int COMPRESSION_METHOD_OFFSET = 8;
+    private static final int CRC32_OFFSET = 14;
+    private static final int COMPRESSED_SIZE_OFFSET = 18;
+    private static final int UNCOMPRESSED_SIZE_OFFSET = 22;
+    private static final int NAME_LENGTH_OFFSET = 26;
+    private static final int EXTRA_LENGTH_OFFSET = 28;
+    private static final int NAME_OFFSET = HEADER_SIZE_BYTES;
+
+    private static final int DATA_DESCRIPTOR_SIZE_BYTES_WITHOUT_SIGNATURE = 12;
+    private static final int DATA_DESCRIPTOR_SIGNATURE = 0x08074b50;
+
+    private final String mName;
+    private final int mNameSizeBytes;
+    private final ByteBuffer mExtra;
+
+    private final long mStartOffsetInArchive;
+    private final long mSize;
+
+    private final int mDataStartOffset;
+    private final long mDataSize;
+    private final boolean mDataCompressed;
+    private final long mUncompressedDataSize;
+
+    private LocalFileRecord(
+            String name,
+            int nameSizeBytes,
+            ByteBuffer extra,
+            long startOffsetInArchive,
+            long size,
+            int dataStartOffset,
+            long dataSize,
+            boolean dataCompressed,
+            long uncompressedDataSize) {
+        mName = name;
+        mNameSizeBytes = nameSizeBytes;
+        mExtra = extra;
+        mStartOffsetInArchive = startOffsetInArchive;
+        mSize = size;
+        mDataStartOffset = dataStartOffset;
+        mDataSize = dataSize;
+        mDataCompressed = dataCompressed;
+        mUncompressedDataSize = uncompressedDataSize;
+    }
+
+    public String getName() {
+        return mName;
+    }
+
+    public ByteBuffer getExtra() {
+        return (mExtra.capacity() > 0) ? mExtra.slice() : mExtra;
+    }
+
+    public int getExtraFieldStartOffsetInsideRecord() {
+        return HEADER_SIZE_BYTES + mNameSizeBytes;
+    }
+
+    public long getStartOffsetInArchive() {
+        return mStartOffsetInArchive;
+    }
+
+    public int getDataStartOffsetInRecord() {
+        return mDataStartOffset;
+    }
+
+    /**
+     * Returns the size (in bytes) of this record.
+     */
+    public long getSize() {
+        return mSize;
+    }
+
+    /**
+     * Returns {@code true} if this record's file data is stored in compressed form.
+     */
+    public boolean isDataCompressed() {
+        return mDataCompressed;
+    }
+
+    /**
+     * Returns the Local File record starting at the current position of the provided buffer
+     * and advances the buffer's position immediately past the end of the record. The record
+     * consists of the Local File Header, data, and (if present) Data Descriptor.
+     */
+    public static LocalFileRecord getRecord(
+            DataSource apk,
+            CentralDirectoryRecord cdRecord,
+            long cdStartOffset) throws ZipFormatException, IOException {
+        return getRecord(
+                apk,
+                cdRecord,
+                cdStartOffset,
+                true, // obtain extra field contents
+                true // include Data Descriptor (if present)
+                );
+    }
+
+    /**
+     * Returns the Local File record starting at the current position of the provided buffer
+     * and advances the buffer's position immediately past the end of the record. The record
+     * consists of the Local File Header, data, and (if present) Data Descriptor.
+     */
+    private static LocalFileRecord getRecord(
+            DataSource apk,
+            CentralDirectoryRecord cdRecord,
+            long cdStartOffset,
+            boolean extraFieldContentsNeeded,
+            boolean dataDescriptorIncluded) throws ZipFormatException, IOException {
+        // IMPLEMENTATION NOTE: This method attempts to mimic the behavior of Android platform
+        // exhibited when reading an APK for the purposes of verifying its signatures.
+
+        String entryName = cdRecord.getName();
+        int cdRecordEntryNameSizeBytes = cdRecord.getNameSizeBytes();
+        int headerSizeWithName = HEADER_SIZE_BYTES + cdRecordEntryNameSizeBytes;
+        long headerStartOffset = cdRecord.getLocalFileHeaderOffset();
+        long headerEndOffset = headerStartOffset + headerSizeWithName;
+        if (headerEndOffset >= cdStartOffset) {
+            throw new ZipFormatException(
+                    "Local File Header of " + entryName + " extends beyond start of Central"
+                            + " Directory. LFH end: " + headerEndOffset
+                            + ", CD start: " + cdStartOffset);
+        }
+        ByteBuffer header;
+        try {
+            header = apk.getByteBuffer(headerStartOffset, headerSizeWithName);
+        } catch (IOException e) {
+            throw new IOException("Failed to read Local File Header of " + entryName, e);
+        }
+        header.order(ByteOrder.LITTLE_ENDIAN);
+
+        int recordSignature = header.getInt();
+        if (recordSignature != RECORD_SIGNATURE) {
+            throw new ZipFormatException(
+                    "Not a Local File Header record for entry " + entryName + ". Signature: 0x"
+                            + Long.toHexString(recordSignature & 0xffffffffL));
+        }
+        short gpFlags = header.getShort(GP_FLAGS_OFFSET);
+        boolean dataDescriptorUsed = (gpFlags & ZipUtils.GP_FLAG_DATA_DESCRIPTOR_USED) != 0;
+        long uncompressedDataCrc32FromCdRecord = cdRecord.getCrc32();
+        long compressedDataSizeFromCdRecord = cdRecord.getCompressedSize();
+        long uncompressedDataSizeFromCdRecord = cdRecord.getUncompressedSize();
+        if (!dataDescriptorUsed) {
+            long crc32 = ZipUtils.getUnsignedInt32(header, CRC32_OFFSET);
+            if (crc32 != uncompressedDataCrc32FromCdRecord) {
+                throw new ZipFormatException(
+                        "CRC-32 mismatch between Local File Header and Central Directory for entry "
+                                + entryName + ". LFH: " + crc32
+                                + ", CD: " + uncompressedDataCrc32FromCdRecord);
+            }
+            long compressedSize = ZipUtils.getUnsignedInt32(header, COMPRESSED_SIZE_OFFSET);
+            if (compressedSize != compressedDataSizeFromCdRecord) {
+                throw new ZipFormatException(
+                        "Compressed size mismatch between Local File Header and Central Directory"
+                                + " for entry " + entryName + ". LFH: " + compressedSize
+                                + ", CD: " + compressedDataSizeFromCdRecord);
+            }
+            long uncompressedSize = ZipUtils.getUnsignedInt32(header, UNCOMPRESSED_SIZE_OFFSET);
+            if (uncompressedSize != uncompressedDataSizeFromCdRecord) {
+                throw new ZipFormatException(
+                        "Uncompressed size mismatch between Local File Header and Central Directory"
+                                + " for entry " + entryName + ". LFH: " + uncompressedSize
+                                + ", CD: " + uncompressedDataSizeFromCdRecord);
+            }
+        }
+        int nameLength = ZipUtils.getUnsignedInt16(header, NAME_LENGTH_OFFSET);
+        if (nameLength > cdRecordEntryNameSizeBytes) {
+            throw new ZipFormatException(
+                    "Name mismatch between Local File Header and Central Directory for entry"
+                            + entryName + ". LFH: " + nameLength
+                            + " bytes, CD: " + cdRecordEntryNameSizeBytes + " bytes");
+        }
+        String name = CentralDirectoryRecord.getName(header, NAME_OFFSET, nameLength);
+        if (!entryName.equals(name)) {
+            throw new ZipFormatException(
+                    "Name mismatch between Local File Header and Central Directory. LFH: \""
+                            + name + "\", CD: \"" + entryName + "\"");
+        }
+        int extraLength = ZipUtils.getUnsignedInt16(header, EXTRA_LENGTH_OFFSET);
+
+        short compressionMethod = header.getShort(COMPRESSION_METHOD_OFFSET);
+        boolean compressed;
+        switch (compressionMethod) {
+            case ZipUtils.COMPRESSION_METHOD_STORED:
+                compressed = false;
+                break;
+            case ZipUtils.COMPRESSION_METHOD_DEFLATED:
+                compressed = true;
+                break;
+            default:
+                throw new ZipFormatException(
+                        "Unsupported compression method of entry " + entryName
+                                + ": " + (compressionMethod & 0xffff));
+        }
+
+        long dataStartOffset = headerStartOffset + HEADER_SIZE_BYTES + nameLength + extraLength;
+        long dataSize;
+        if (compressed) {
+            dataSize = compressedDataSizeFromCdRecord;
+        } else {
+            dataSize = uncompressedDataSizeFromCdRecord;
+        }
+        long dataEndOffset = dataStartOffset + dataSize;
+        if (dataEndOffset > cdStartOffset) {
+            throw new ZipFormatException(
+                    "Local File Header data of " + entryName + " overlaps with Central Directory"
+                            + ". LFH data start: " + dataStartOffset
+                            + ", LFH data end: " + dataEndOffset + ", CD start: " + cdStartOffset);
+        }
+
+        ByteBuffer extra = EMPTY_BYTE_BUFFER;
+        if ((extraFieldContentsNeeded) && (extraLength > 0)) {
+            extra = apk.getByteBuffer(
+                    headerStartOffset + HEADER_SIZE_BYTES + nameLength, extraLength);
+        }
+
+        long recordEndOffset = dataEndOffset;
+        // Include the Data Descriptor (if requested and present) into the record.
+        if ((dataDescriptorIncluded) && ((gpFlags & ZipUtils.GP_FLAG_DATA_DESCRIPTOR_USED) != 0)) {
+            // The record's data is supposed to be followed by the Data Descriptor. Unfortunately,
+            // the descriptor's size is not known in advance because the spec lets the signature
+            // field (the first four bytes) be omitted. Thus, there's no 100% reliable way to tell
+            // how long the Data Descriptor record is. Most parsers (including Android) check
+            // whether the first four bytes look like Data Descriptor record signature and, if so,
+            // assume that it is indeed the record's signature. However, this is the wrong
+            // conclusion if the record's CRC-32 (next field after the signature) has the same value
+            // as the signature. In any case, we're doing what Android is doing.
+            long dataDescriptorEndOffset =
+                    dataEndOffset + DATA_DESCRIPTOR_SIZE_BYTES_WITHOUT_SIGNATURE;
+            if (dataDescriptorEndOffset > cdStartOffset) {
+                throw new ZipFormatException(
+                        "Data Descriptor of " + entryName + " overlaps with Central Directory"
+                                + ". Data Descriptor end: " + dataEndOffset
+                                + ", CD start: " + cdStartOffset);
+            }
+            ByteBuffer dataDescriptorPotentialSig = apk.getByteBuffer(dataEndOffset, 4);
+            dataDescriptorPotentialSig.order(ByteOrder.LITTLE_ENDIAN);
+            if (dataDescriptorPotentialSig.getInt() == DATA_DESCRIPTOR_SIGNATURE) {
+                dataDescriptorEndOffset += 4;
+                if (dataDescriptorEndOffset > cdStartOffset) {
+                    throw new ZipFormatException(
+                            "Data Descriptor of " + entryName + " overlaps with Central Directory"
+                                    + ". Data Descriptor end: " + dataEndOffset
+                                    + ", CD start: " + cdStartOffset);
+                }
+            }
+            recordEndOffset = dataDescriptorEndOffset;
+        }
+
+        long recordSize = recordEndOffset - headerStartOffset;
+        int dataStartOffsetInRecord = HEADER_SIZE_BYTES + nameLength + extraLength;
+
+        return new LocalFileRecord(
+                entryName,
+                cdRecordEntryNameSizeBytes,
+                extra,
+                headerStartOffset,
+                recordSize,
+                dataStartOffsetInRecord,
+                dataSize,
+                compressed,
+                uncompressedDataSizeFromCdRecord);
+    }
+
+    /**
+     * Outputs this record and returns returns the number of bytes output.
+     */
+    public long outputRecord(DataSource sourceApk, DataSink output) throws IOException {
+        long size = getSize();
+        sourceApk.feed(getStartOffsetInArchive(), size, output);
+        return size;
+    }
+
+    /**
+     * Outputs this record, replacing its extra field with the provided one, and returns returns the
+     * number of bytes output.
+     */
+    public long outputRecordWithModifiedExtra(
+            DataSource sourceApk,
+            ByteBuffer extra,
+            DataSink output) throws IOException {
+        long recordStartOffsetInSource = getStartOffsetInArchive();
+        int extraStartOffsetInRecord = getExtraFieldStartOffsetInsideRecord();
+        int extraSizeBytes = extra.remaining();
+        int headerSize = extraStartOffsetInRecord + extraSizeBytes;
+        ByteBuffer header = ByteBuffer.allocate(headerSize);
+        header.order(ByteOrder.LITTLE_ENDIAN);
+        sourceApk.copyTo(recordStartOffsetInSource, extraStartOffsetInRecord, header);
+        header.put(extra.slice());
+        header.flip();
+        ZipUtils.setUnsignedInt16(header, EXTRA_LENGTH_OFFSET, extraSizeBytes);
+
+        long outputByteCount = header.remaining();
+        output.consume(header);
+        long remainingRecordSize = getSize() - mDataStartOffset;
+        sourceApk.feed(recordStartOffsetInSource + mDataStartOffset, remainingRecordSize, output);
+        outputByteCount += remainingRecordSize;
+        return outputByteCount;
+    }
+
+    /**
+     * Outputs the specified Local File Header record with its data and returns the number of bytes
+     * output.
+     */
+    public static long outputRecordWithDeflateCompressedData(
+            String name,
+            int lastModifiedTime,
+            int lastModifiedDate,
+            byte[] compressedData,
+            long crc32,
+            long uncompressedSize,
+            DataSink output) throws IOException {
+        byte[] nameBytes = name.getBytes(StandardCharsets.UTF_8);
+        int recordSize = HEADER_SIZE_BYTES + nameBytes.length;
+        ByteBuffer result = ByteBuffer.allocate(recordSize);
+        result.order(ByteOrder.LITTLE_ENDIAN);
+        result.putInt(RECORD_SIGNATURE);
+        ZipUtils.putUnsignedInt16(result,  0x14); // Minimum version needed to extract
+        result.putShort(ZipUtils.GP_FLAG_EFS); // General purpose flag: UTF-8 encoded name
+        result.putShort(ZipUtils.COMPRESSION_METHOD_DEFLATED);
+        ZipUtils.putUnsignedInt16(result, lastModifiedTime);
+        ZipUtils.putUnsignedInt16(result, lastModifiedDate);
+        ZipUtils.putUnsignedInt32(result, crc32);
+        ZipUtils.putUnsignedInt32(result, compressedData.length);
+        ZipUtils.putUnsignedInt32(result, uncompressedSize);
+        ZipUtils.putUnsignedInt16(result, nameBytes.length);
+        ZipUtils.putUnsignedInt16(result, 0); // Extra field length
+        result.put(nameBytes);
+        if (result.hasRemaining()) {
+            throw new RuntimeException("pos: " + result.position() + ", limit: " + result.limit());
+        }
+        result.flip();
+
+        long outputByteCount = result.remaining();
+        output.consume(result);
+        outputByteCount += compressedData.length;
+        output.consume(compressedData, 0, compressedData.length);
+        return outputByteCount;
+    }
+
+    private static final ByteBuffer EMPTY_BYTE_BUFFER = ByteBuffer.allocate(0);
+
+    /**
+     * Sends uncompressed data of this record into the the provided data sink.
+     */
+    public void outputUncompressedData(
+            DataSource lfhSection,
+            DataSink sink) throws IOException, ZipFormatException {
+        long dataStartOffsetInArchive = mStartOffsetInArchive + mDataStartOffset;
+        try {
+            if (mDataCompressed) {
+                try (InflateSinkAdapter inflateAdapter = new InflateSinkAdapter(sink)) {
+                    lfhSection.feed(dataStartOffsetInArchive, mDataSize, inflateAdapter);
+                    long actualUncompressedSize = inflateAdapter.getOutputByteCount();
+                    if (actualUncompressedSize != mUncompressedDataSize) {
+                        throw new ZipFormatException(
+                                "Unexpected size of uncompressed data of " + mName
+                                        + ". Expected: " + mUncompressedDataSize + " bytes"
+                                        + ", actual: " + actualUncompressedSize + " bytes");
+                    }
+                }
+            } else {
+                lfhSection.feed(dataStartOffsetInArchive, mDataSize, sink);
+                // No need to check whether output size is as expected because DataSource.feed is
+                // guaranteed to output exactly the number of bytes requested.
+            }
+        } catch (IOException e) {
+            throw new IOException(
+                    "Failed to read data of " + ((mDataCompressed) ? "compressed" : "uncompressed")
+                        + " entry " + mName,
+                    e);
+        }
+        // Interestingly, Android doesn't check that uncompressed data's CRC-32 is as expected. We
+        // thus don't check either.
+    }
+
+    /**
+     * Sends uncompressed data pointed to by the provided ZIP Central Directory (CD) record into the
+     * provided data sink.
+     */
+    public static void outputUncompressedData(
+            DataSource source,
+            CentralDirectoryRecord cdRecord,
+            long cdStartOffsetInArchive,
+            DataSink sink) throws ZipFormatException, IOException {
+        // IMPLEMENTATION NOTE: This method attempts to mimic the behavior of Android platform
+        // exhibited when reading an APK for the purposes of verifying its signatures.
+        // When verifying an APK, Android doesn't care reading the extra field or the Data
+        // Descriptor.
+        LocalFileRecord lfhRecord =
+                getRecord(
+                        source,
+                        cdRecord,
+                        cdStartOffsetInArchive,
+                        false, // don't care about the extra field
+                        false // don't read the Data Descriptor
+                        );
+        lfhRecord.outputUncompressedData(source, sink);
+    }
+
+    /**
+     * Returns the uncompressed data pointed to by the provided ZIP Central Directory (CD) record.
+     */
+    public static byte[] getUncompressedData(
+            DataSource source,
+            CentralDirectoryRecord cdRecord,
+            long cdStartOffsetInArchive) throws ZipFormatException, IOException {
+        if (cdRecord.getUncompressedSize() > Integer.MAX_VALUE) {
+            throw new IOException(
+                    cdRecord.getName() + " too large: " + cdRecord.getUncompressedSize());
+        }
+        byte[] result = new byte[(int) cdRecord.getUncompressedSize()];
+        ByteBuffer resultBuf = ByteBuffer.wrap(result);
+        ByteBufferSink resultSink = new ByteBufferSink(resultBuf);
+        outputUncompressedData(
+                source,
+                cdRecord,
+                cdStartOffsetInArchive,
+                resultSink);
+        return result;
+    }
+
+    /**
+     * {@link DataSink} which inflates received data and outputs the deflated data into the provided
+     * delegate sink.
+     */
+    private static class InflateSinkAdapter implements DataSink, Closeable {
+        private final DataSink mDelegate;
+
+        private Inflater mInflater = new Inflater(true);
+        private byte[] mOutputBuffer;
+        private byte[] mInputBuffer;
+        private long mOutputByteCount;
+        private boolean mClosed;
+
+        private InflateSinkAdapter(DataSink delegate) {
+            mDelegate = delegate;
+        }
+
+        @Override
+        public void consume(byte[] buf, int offset, int length) throws IOException {
+            checkNotClosed();
+            mInflater.setInput(buf, offset, length);
+            if (mOutputBuffer == null) {
+                mOutputBuffer = new byte[65536];
+            }
+            while (!mInflater.finished()) {
+                int outputChunkSize;
+                try {
+                    outputChunkSize = mInflater.inflate(mOutputBuffer);
+                } catch (DataFormatException e) {
+                    throw new IOException("Failed to inflate data", e);
+                }
+                if (outputChunkSize == 0) {
+                    return;
+                }
+                mDelegate.consume(mOutputBuffer, 0, outputChunkSize);
+                mOutputByteCount += outputChunkSize;
+            }
+        }
+
+        @Override
+        public void consume(ByteBuffer buf) throws IOException {
+            checkNotClosed();
+            if (buf.hasArray()) {
+                consume(buf.array(), buf.arrayOffset() + buf.position(), buf.remaining());
+                buf.position(buf.limit());
+            } else {
+                if (mInputBuffer == null) {
+                    mInputBuffer = new byte[65536];
+                }
+                while (buf.hasRemaining()) {
+                    int chunkSize = Math.min(buf.remaining(), mInputBuffer.length);
+                    buf.get(mInputBuffer, 0, chunkSize);
+                    consume(mInputBuffer, 0, chunkSize);
+                }
+            }
+        }
+
+        public long getOutputByteCount() {
+            return mOutputByteCount;
+        }
+
+        @Override
+        public void close() throws IOException {
+            mClosed = true;
+            mInputBuffer = null;
+            mOutputBuffer = null;
+            if (mInflater != null) {
+                mInflater.end();
+                mInflater = null;
+            }
+        }
+
+        private void checkNotClosed() {
+            if (mClosed) {
+                throw new IllegalStateException("Closed");
+            }
+        }
+    }
+}
index 118585a..6a0c501 100644 (file)
 
 package com.android.apksigner.core.internal.zip;
 
+import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
+import java.util.zip.CRC32;
+import java.util.zip.Deflater;
 
 import com.android.apksigner.core.internal.util.Pair;
 import com.android.apksigner.core.util.DataSource;
@@ -35,6 +38,9 @@ public abstract class ZipUtils {
     public static final short COMPRESSION_METHOD_STORED = 0;
     public static final short COMPRESSION_METHOD_DEFLATED = 8;
 
+    public static final short GP_FLAG_DATA_DESCRIPTOR_USED = 0x08;
+    public static final short GP_FLAG_EFS = 0x0800;
+
     private static final int ZIP_EOCD_REC_MIN_SIZE = 22;
     private static final int ZIP_EOCD_REC_SIG = 0x06054b50;
     private static final int ZIP_EOCD_CENTRAL_DIR_TOTAL_RECORD_COUNT_OFFSET = 10;
@@ -265,14 +271,83 @@ public abstract class ZipUtils {
         return buffer.getShort(offset) & 0xffff;
     }
 
-    private static void setUnsignedInt32(ByteBuffer buffer, int offset, long value) {
+    public static int getUnsignedInt16(ByteBuffer buffer) {
+        return buffer.getShort() & 0xffff;
+    }
+
+    static void setUnsignedInt16(ByteBuffer buffer, int offset, int value) {
+        if ((value < 0) || (value > 0xffff)) {
+            throw new IllegalArgumentException("uint16 value of out range: " + value);
+        }
+        buffer.putShort(offset, (short) value);
+    }
+
+    static void setUnsignedInt32(ByteBuffer buffer, int offset, long value) {
         if ((value < 0) || (value > 0xffffffffL)) {
             throw new IllegalArgumentException("uint32 value of out range: " + value);
         }
         buffer.putInt(offset, (int) value);
     }
 
+    public static void putUnsignedInt16(ByteBuffer buffer, int value) {
+        if ((value < 0) || (value > 0xffff)) {
+            throw new IllegalArgumentException("uint16 value of out range: " + value);
+        }
+        buffer.putShort((short) value);
+    }
+
     static long getUnsignedInt32(ByteBuffer buffer, int offset) {
         return buffer.getInt(offset) & 0xffffffffL;
     }
+
+    static long getUnsignedInt32(ByteBuffer buffer) {
+        return buffer.getInt() & 0xffffffffL;
+    }
+
+    static void putUnsignedInt32(ByteBuffer buffer, long value) {
+        if ((value < 0) || (value > 0xffffffffL)) {
+            throw new IllegalArgumentException("uint32 value of out range: " + value);
+        }
+        buffer.putInt((int) value);
+    }
+
+    public static DeflateResult deflate(ByteBuffer input) {
+        byte[] inputBuf;
+        int inputOffset;
+        int inputLength = input.remaining();
+        if (input.hasArray()) {
+            inputBuf = input.array();
+            inputOffset = input.arrayOffset() + input.position();
+            input.position(input.limit());
+        } else {
+            inputBuf = new byte[inputLength];
+            inputOffset = 0;
+            input.get(inputBuf);
+        }
+        CRC32 crc32 = new CRC32();
+        crc32.update(inputBuf, inputOffset, inputLength);
+        long crc32Value = crc32.getValue();
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        Deflater deflater = new Deflater(9, true);
+        deflater.setInput(inputBuf, inputOffset, inputLength);
+        deflater.finish();
+        byte[] buf = new byte[65536];
+        while (!deflater.finished()) {
+            int chunkSize = deflater.deflate(buf);
+            out.write(buf, 0, chunkSize);
+        }
+        return new DeflateResult(inputLength, crc32Value, out.toByteArray());
+    }
+
+    public static class DeflateResult {
+        public final int inputSizeBytes;
+        public final long inputCrc32;
+        public final byte[] output;
+
+        public DeflateResult(int inputSizeBytes, long inputCrc32, byte[] output) {
+            this.inputSizeBytes = inputSizeBytes;
+            this.inputCrc32 = inputCrc32;
+            this.output = output;
+        }
+    }
 }
\ No newline at end of file
index 8ee1f13..4aefedb 100644 (file)
 package com.android.apksigner.core.util;
 
 import java.io.OutputStream;
+import java.io.RandomAccessFile;
 
 import com.android.apksigner.core.internal.util.OutputStreamDataSink;
+import com.android.apksigner.core.internal.util.RandomAccessFileDataSink;
 
 /**
  * Utility methods for working with {@link DataSink} abstraction.
@@ -33,4 +35,12 @@ public abstract class DataSinks {
     public static DataSink asDataSink(OutputStream out) {
         return new OutputStreamDataSink(out);
     }
+
+    /**
+     * Returns a {@link DataSink} which outputs received data into the provided file, sequentially,
+     * starting at the beginning of the file.
+     */
+    public static DataSink asDataSink(RandomAccessFile file) {
+        return new RandomAccessFileDataSink(file);
+    }
 }