Merge "Support LOCAL_STRIP_MODULE := keep_symbols for prebuilts"
diff --git a/core/config_sanitizers.mk b/core/config_sanitizers.mk
index 9602626..7882fdf 100644
--- a/core/config_sanitizers.mk
+++ b/core/config_sanitizers.mk
@@ -51,6 +51,7 @@
 # Don't apply sanitizers to NDK code.
 ifdef LOCAL_SDK_VERSION
   my_sanitize :=
+  my_global_sanitize :=
 endif
 
 # Never always wins.
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
index 0000000..2491302
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/ApkSigner.java
@@ -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);
+        }
+    }
+}
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/ApkSignerEngine.java b/tools/apksigner/core/src/com/android/apksigner/core/ApkSignerEngine.java
index 6a148ca..21c2706 100644
--- a/tools/apksigner/core/src/com/android/apksigner/core/ApkSignerEngine.java
+++ b/tools/apksigner/core/src/com/android/apksigner/core/ApkSignerEngine.java
@@ -33,9 +33,9 @@
  * <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 @@
      * @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.
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/V1SchemeVerifier.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/V1SchemeVerifier.java
index 1bba313..752ba7e 100644
--- a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/V1SchemeVerifier.java
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/V1SchemeVerifier.java
@@ -47,7 +47,7 @@
 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 @@
 
             // 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 @@
                 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 @@
             }
 
             try {
-                LocalFileHeader.sendUncompressedData(
-                        apk, 0,
+                LocalFileRecord.outputUncompressedData(
+                        apk,
                         cdRecord,
                         cdOffsetInApk,
                         new MessageDigestSink(mds));
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/V2SchemeVerifier.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/V2SchemeVerifier.java
index 0c303ee..5e1e8fb 100644
--- a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/V2SchemeVerifier.java
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/V2SchemeVerifier.java
@@ -553,6 +553,42 @@
     private static SignatureInfo findSignature(
             DataSource apk, ApkUtils.ZipSections zipSections, Result result)
                     throws IOException, SignatureNotFoundException {
+        // Find the APK Signing Block. The block immediately precedes the Central Directory.
+        ByteBuffer eocd = zipSections.getZipEndOfCentralDirectory();
+        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(apkSigningBlockBuf, result);
+
+        return new SignatureInfo(
+                apkSignatureSchemeV2Block,
+                apkSigningBlockOffset,
+                zipSections.getZipCentralDirectoryOffset(),
+                zipSections.getZipEndOfCentralDirectoryOffset(),
+                eocd);
+    }
+
+    /**
+     * 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)
+        // * @+8  bytes payload
+        // * @-24 bytes uint64:    size in bytes (same as the one above)
+        // * @-16 bytes uint128:   magic
+
         long centralDirStartOffset = zipSections.getZipCentralDirectoryOffset();
         long centralDirEndOffset =
                 centralDirStartOffset + zipSections.getZipCentralDirectorySizeBytes();
@@ -564,43 +600,15 @@
                             + ", 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();
-        long apkSigningBlockOffset = apkSigningBlockAndOffset.getSecond();
-
-        // Find the APK Signature Scheme v2 Block inside the APK Signing Block.
-        ByteBuffer apkSignatureSchemeV2Block =
-                findApkSignatureSchemeV2Block(apkSigningBlock, result);
-
-        return new SignatureInfo(
-                apkSignatureSchemeV2Block,
-                apkSigningBlockOffset,
-                centralDirStartOffset,
-                eocdStartOffset,
-                eocd);
-    }
-
-    private static Pair<ByteBuffer, Long> findApkSigningBlock(
-            DataSource apk, long centralDirOffset) throws IOException, SignatureNotFoundException {
-        // FORMAT:
-        // OFFSET       DATA TYPE  DESCRIPTION
-        // * @+0  bytes uint64:    size in bytes (excluding this field)
-        // * @+8  bytes payload
-        // * @-24 bytes uint64:    size in bytes (same as the one above)
-        // * @-16 bytes uint128:   magic
-
-        if (centralDirOffset < APK_SIG_BLOCK_MIN_SIZE) {
+        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 @@
                     "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 @@
                     "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
index 0000000..2198492
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/util/RandomAccessFileDataSink.java
@@ -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;
+        }
+    }
+}
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/CentralDirectoryRecord.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/CentralDirectoryRecord.java
index 6a5b94c..141d01e 100644
--- a/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/CentralDirectoryRecord.java
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/CentralDirectoryRecord.java
@@ -20,6 +20,7 @@
 
 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 @@
     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 short getCompressionMethod() {
-        return mCompressionMethod;
+    public int getLastModificationTime() {
+        return mLastModificationTime;
+    }
+
+    public int getLastModificationDate() {
+        return mLastModificationDate;
     }
 
     public long getCrc32() {
@@ -114,24 +122,25 @@
                             + " 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 @@
                             + 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(
-                gpFlags,
-                compressionMethod,
+                recordBuf,
+                lastModificationTime,
+                lastModificationDate,
                 crc32,
                 compressedSize,
                 uncompressedSize,
                 localFileHeaderOffset,
-                name);
+                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(
+                result,
+                lastModifiedTime,
+                lastModifiedDate,
+                crc32,
+                compressedSize,
+                uncompressedSize,
+                localFileHeaderOffset,
+                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
index 0000000..8777591
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/EocdRecord.java
@@ -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
index 99a606b..0000000
--- a/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/LocalFileHeader.java
+++ /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
index 0000000..397a450
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/LocalFileRecord.java
@@ -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");
+            }
+        }
+    }
+}
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/ZipUtils.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/ZipUtils.java
index 118585a..6a0c501 100644
--- a/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/ZipUtils.java
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/ZipUtils.java
@@ -16,9 +16,12 @@
 
 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 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 @@
         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
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/util/DataSinks.java b/tools/apksigner/core/src/com/android/apksigner/core/util/DataSinks.java
index 8ee1f13..4aefedb 100644
--- a/tools/apksigner/core/src/com/android/apksigner/core/util/DataSinks.java
+++ b/tools/apksigner/core/src/com/android/apksigner/core/util/DataSinks.java
@@ -17,8 +17,10 @@
 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 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);
+    }
 }
diff --git a/tools/warn.py b/tools/warn.py
index b61505b..6324429 100755
--- a/tools/warn.py
+++ b/tools/warn.py
@@ -68,7 +68,8 @@
                     r".*: warning: ignoring old commands for target .+"] },
     { 'category':'C/C++',   'severity':severity.HIGH,     'members':[], 'option':'-Wimplicit-function-declaration',
         'description':'Implicit function declaration',
-        'patterns':[r".*: warning: implicit declaration of function .+"] },
+        'patterns':[r".*: warning: implicit declaration of function .+",
+                    r".*: warning: implicitly declaring library function" ] },
     { 'category':'C/C++',   'severity':severity.SKIP,     'members':[], 'option':'',
         'description':'',
         'patterns':[r".*: warning: conflicting types for '.+'"] },
@@ -81,6 +82,7 @@
         'description':'Potential leak of memory, bad free, use after free',
         'patterns':[r".*: warning: Potential leak of memory",
                     r".*: warning: Potential memory leak",
+                    r".*: warning: Memory allocated by alloca\(\) should not be deallocated",
                     r".*: warning: Memory allocated by .+ should be deallocated by .+ not .+",
                     r".*: warning: 'delete' applied to a pointer that was allocated",
                     r".*: warning: Use of memory after it is freed",
@@ -88,13 +90,16 @@
                     r".*: warning: Argument to free\(\) is offset by .+ of memory allocated by",
                     r".*: warning: Attempt to .+ released memory"] },
     { 'category':'C/C++',   'severity':severity.HIGH,     'members':[], 'option':'',
+        'description':'Use transient memory for control value',
+        'patterns':[r".*: warning: .+Using such transient memory for the control value is .*dangerous."] },
+    { 'category':'C/C++',   'severity':severity.HIGH,     'members':[], 'option':'',
         'description':'Return address of stack memory',
         'patterns':[r".*: warning: Address of stack memory .+ returned to caller",
                     r".*: warning: Address of stack memory .+ will be a dangling reference"] },
     { 'category':'C/C++',   'severity':severity.HIGH,     'members':[], 'option':'',
         'description':'Problem with vfork',
         'patterns':[r".*: warning: This .+ is prohibited after a successful vfork",
-                    r".*: warning: Call to function 'vfork' is insecure "] },
+                    r".*: warning: Call to function '.+' is insecure "] },
     { 'category':'C/C++',   'severity':severity.HIGH,     'members':[], 'option':'infinite-recursion',
         'description':'Infinite recursion',
         'patterns':[r".*: warning: all paths through this function will call itself"] },
@@ -112,6 +117,9 @@
     { 'category':'C/C++',   'severity':severity.HIGH,     'members':[], 'option':'-fno-builtin',
         'description':'Incompatible declaration of built in function',
         'patterns':[r".*: warning: incompatible implicit declaration of built-in function .+"] },
+    { 'category':'C/C++',   'severity':severity.HIGH,     'members':[], 'option':'',
+        'description':'Null passed as non-null argument',
+        'patterns':[r".*: warning: Null passed to a callee that requires a non-null argument"] },
     { 'category':'C/C++',   'severity':severity.MEDIUM,   'members':[], 'option':'-Wunused-parameter',
         'description':'Unused parameter',
         'patterns':[r".*: warning: unused parameter '.*'"] },
@@ -131,15 +139,22 @@
     { 'category':'C/C++',   'severity':severity.MEDIUM,   'members':[], 'option':'-Wmissing-field-initializers',
         'description':'Missing initializer',
         'patterns':[r".*: warning: missing initializer"] },
+    { 'category':'C/C++',   'severity':severity.MEDIUM,   'members':[], 'option':'-Wdelete-non-virtual-dtor',
+        'description':'Need virtual destructor',
+        'patterns':[r".*: warning: delete called .* has virtual functions but non-virtual destructor"] },
     { 'category':'cont.',   'severity':severity.SKIP,     'members':[], 'option':'',
         'description':'',
         'patterns':[r".*: warning: \(near initialization for '.+'\)"] },
+    { 'category':'C/C++',   'severity':severity.MEDIUM,     'members':[], 'option':'-Wdate-time',
+        'description':'Expansion of data or time macro',
+        'patterns':[r".*: warning: expansion of date or time macro is not reproducible"] },
     { 'category':'C/C++',   'severity':severity.MEDIUM,   'members':[], 'option':'-Wformat',
         'description':'Format string does not match arguments',
         'patterns':[r".*: warning: format '.+' expects type '.+', but argument [0-9]+ has type '.+'",
                     r".*: warning: more '%' conversions than data arguments",
                     r".*: warning: data argument not used by format string",
                     r".*: warning: incomplete format specifier",
+                    r".*: warning: unknown conversion type .* in format",
                     r".*: warning: format .+ expects .+ but argument .+Wformat=",
                     r".*: warning: field precision should have .+ but argument has .+Wformat",
                     r".*: warning: format specifies type .+ but the argument has .*type .+Wformat"] },
@@ -169,28 +184,52 @@
     { 'category':'C/C++',   'severity':severity.HARMLESS, 'members':[], 'option':'',
         'description':'No newline at end of file',
         'patterns':[r".*: warning: no newline at end of file"] },
+    { 'category':'C/C++',   'severity':severity.HARMLESS, 'members':[], 'option':'',
+        'description':'Missing space after macro name',
+        'patterns':[r".*: warning: missing whitespace after the macro name"] },
+    { 'category':'C/C++',   'severity':severity.LOW, 'members':[], 'option':'-Wcast-align',
+        'description':'Cast increases required alignment',
+        'patterns':[r".*: warning: cast from .* to .* increases required alignment .*"] },
     { 'category':'C/C++',   'severity':severity.MEDIUM,   'members':[], 'option':'-Wcast-qual',
         'description':'Qualifier discarded',
         'patterns':[r".*: warning: passing argument [0-9]+ of '.+' discards qualifiers from pointer target type",
                     r".*: warning: assignment discards qualifiers from pointer target type",
                     r".*: warning: passing .+ to parameter of type .+ discards qualifiers",
                     r".*: warning: assigning to .+ from .+ discards qualifiers",
+                    r".*: warning: initializing .+ discards qualifiers .+types-discards-qualifiers",
                     r".*: warning: return discards qualifiers from pointer target type"] },
-    { 'category':'C/C++',   'severity':severity.MEDIUM,   'members':[], 'option':'-Wattributes',
+    { 'category':'C/C++',   'severity':severity.MEDIUM,   'members':[], 'option':'-Wunknown-attributes',
+        'description':'Unknown attribute',
+        'patterns':[r".*: warning: unknown attribute '.+'"] },
+    { 'category':'C/C++',   'severity':severity.MEDIUM,   'members':[], 'option':'-Wignored-attributes',
         'description':'Attribute ignored',
-        'patterns':[r".*: warning: '_*packed_*' attribute ignored"] },
+        'patterns':[r".*: warning: '_*packed_*' attribute ignored",
+                    r".*: warning: attribute declaration must precede definition .+ignored-attributes"] },
+    { 'category':'C/C++',   'severity':severity.MEDIUM,   'members':[], 'option':'-Wvisibility',
+        'description':'Visibility problem',
+        'patterns':[r".*: warning: declaration of '.+' will not be visible outside of this function"] },
     { 'category':'C/C++',   'severity':severity.MEDIUM,   'members':[], 'option':'-Wattributes',
         'description':'Visibility mismatch',
         'patterns':[r".*: warning: '.+' declared with greater visibility than the type of its field '.+'"] },
     { 'category':'C/C++',   'severity':severity.MEDIUM,   'members':[], 'option':'',
         'description':'Shift count greater than width of type',
         'patterns':[r".*: warning: (left|right) shift count >= width of type"] },
-    { 'category':'C/C++',   'severity':severity.MEDIUM,   'members':[], 'option':'',
+    { 'category':'C/C++',   'severity':severity.MEDIUM,   'members':[], 'option':'-Wextern-initializer',
         'description':'extern &lt;foo&gt; is initialized',
-        'patterns':[r".*: warning: '.+' initialized and declared 'extern'"] },
+        'patterns':[r".*: warning: '.+' initialized and declared 'extern'",
+                    r".*: warning: 'extern' variable has an initializer"] },
     { 'category':'C/C++',   'severity':severity.MEDIUM,   'members':[], 'option':'-Wold-style-declaration',
         'description':'Old style declaration',
         'patterns':[r".*: warning: 'static' is not at beginning of declaration"] },
+    { 'category':'C/C++',   'severity':severity.MEDIUM,   'members':[], 'option':'-Wreturn-type',
+        'description':'Missing return value',
+        'patterns':[r".*: warning: control reaches end of non-void function"] },
+    { 'category':'C/C++',   'severity':severity.MEDIUM,   'members':[], 'option':'-Wimplicit-int',
+        'description':'Implicit int type',
+        'patterns':[r".*: warning: type specifier missing, defaults to 'int'"] },
+    { 'category':'C/C++',   'severity':severity.MEDIUM,   'members':[], 'option':'-Wmain-return-type',
+        'description':'Main function should return int',
+        'patterns':[r".*: warning: return type of 'main' is not 'int'"] },
     { 'category':'C/C++',   'severity':severity.MEDIUM,   'members':[], 'option':'-Wuninitialized',
         'description':'Variable may be used uninitialized',
         'patterns':[r".*: warning: '.+' may be used uninitialized in this function"] },
@@ -253,7 +292,8 @@
         'patterns':[r".*: warning: dereferencing .* break strict-aliasing rules"] },
     { 'category':'C/C++',   'severity':severity.MEDIUM,   'members':[], 'option':'-Wpointer-to-int-cast',
         'description':'Cast from pointer to integer of different size',
-        'patterns':[r".*: warning: cast from pointer to integer of different size"] },
+        'patterns':[r".*: warning: cast from pointer to integer of different size",
+                    r".*: warning: initialization makes pointer from integer without a cast"] } ,
     { 'category':'C/C++',   'severity':severity.MEDIUM,   'members':[], 'option':'-Wint-to-pointer-cast',
         'description':'Cast to pointer from integer of different size',
         'patterns':[r".*: warning: cast to pointer from integer of different size"] },
@@ -1326,6 +1366,9 @@
     { 'category':'C/C++',   'severity':severity.MEDIUM,   'members':[], 'option':'',
         'description':'Possible broken line continuation',
         'patterns':[r".*: warning: backslash and newline separated by space"] },
+    { 'category':'C/C++',   'severity':severity.MEDIUM,   'members':[], 'option':'-Wundefined-inline',
+        'description':'Inline function is not defined',
+        'patterns':[r".*: warning: inline function '.*' is not defined"] },
     { 'category':'C/C++',   'severity':severity.MEDIUM,   'members':[], 'option':'-Warray-bounds',
         'description':'Array subscript out of bounds',
         'patterns':[r".*: warning: array subscript is above array bounds",
@@ -1358,6 +1401,9 @@
     { 'category':'C/C++',   'severity':severity.MEDIUM,   'members':[], 'option':'-Woverflow',
         'description':'Constant too large for type, truncated',
         'patterns':[r".*: warning: large integer implicitly truncated to unsigned type"] },
+    { 'category':'C/C++',   'severity':severity.MEDIUM,   'members':[], 'option':'-Winteger-overflow',
+        'description':'Overflow in expression',
+        'patterns':[r".*: warning: overflow in expression; .*Winteger-overflow"] },
     { 'category':'C/C++',   'severity':severity.MEDIUM,   'members':[], 'option':'-Woverflow',
         'description':'Overflow in implicit constant conversion',
         'patterns':[r".*: warning: overflow in implicit constant conversion"] },
@@ -1380,18 +1426,31 @@
     { 'category':'C/C++',   'severity':severity.MEDIUM,   'members':[], 'option':'-Wmissing-parameter-type',
         'description':'Parameter type not specified',
         'patterns':[r".*: warning: type of '.+' defaults to 'int'"] },
+    { 'category':'C/C++',   'severity':severity.MEDIUM,   'members':[], 'option':'-Wmissing-declarations',
+        'description':'Missing declarations',
+        'patterns':[r".*: warning: declaration does not declare anything"] },
+    { 'category':'C/C++',   'severity':severity.MEDIUM,   'members':[], 'option':'-Wmissing-noreturn',
+        'description':'Missing noreturn',
+        'patterns':[r".*: warning: function '.*' could be declared with attribute 'noreturn'"] },
     { 'category':'gcc',     'severity':severity.MEDIUM,   'members':[], 'option':'',
         'description':'Invalid option for C file',
         'patterns':[r".*: warning: command line option "".+"" is valid for C\+\+\/ObjC\+\+ but not for C"] },
     { 'category':'C/C++',   'severity':severity.MEDIUM,   'members':[], 'option':'',
         'description':'User warning',
         'patterns':[r".*: warning: #warning "".+"""] },
+    { 'category':'C/C++',   'severity':severity.MEDIUM,   'members':[], 'option':'-Wvexing-parse',
+        'description':'Vexing parsing problem',
+        'patterns':[r".*: warning: empty parentheses interpreted as a function declaration"] },
     { 'category':'C/C++',   'severity':severity.MEDIUM,   'members':[], 'option':'-Wextra',
         'description':'Dereferencing void*',
         'patterns':[r".*: warning: dereferencing 'void \*' pointer"] },
-    { 'category':'C/C++',   'severity':severity.MEDIUM,   'members':[], 'option':'-Wextra',
-        'description':'Comparison of pointer to zero',
-        'patterns':[r".*: warning: ordered comparison of pointer with integer zero"] },
+    { 'category':'C/C++',   'severity':severity.MEDIUM,   'members':[], 'option':'',
+        'description':'Comparison of pointer and integer',
+        'patterns':[r".*: warning: ordered comparison of pointer with integer zero",
+                    r".*: warning: .*comparison between pointer and integer"] },
+    { 'category':'C/C++',   'severity':severity.MEDIUM,   'members':[], 'option':'',
+        'description':'Use of error-prone unary operator',
+        'patterns':[r".*: warning: use of unary operator that may be intended as compound assignment"] },
     { 'category':'C/C++',   'severity':severity.MEDIUM,   'members':[], 'option':'-Wwrite-strings',
         'description':'Conversion of string constant to non-const char*',
         'patterns':[r".*: warning: deprecated conversion from string constant to '.+'"] },
@@ -1433,8 +1492,21 @@
         'description':'Comparison between different enums',
         'patterns':[r".*: warning: comparison between '.+' and '.+'.+Wenum-compare"] },
     { 'category':'C/C++',   'severity':severity.MEDIUM,   'members':[], 'option':'-Wconversion',
-        'description':'Implicit conversion of negative number to unsigned type',
-        'patterns':[r".*: warning: converting negative value '.+' to '.+'"] },
+        'description':'Conversion may change value',
+        'patterns':[r".*: warning: converting negative value '.+' to '.+'",
+                    r".*: warning: conversion to '.+' .+ may alter its value"] },
+    { 'category':'C/C++',   'severity':severity.MEDIUM,   'members':[], 'option':'-Wconversion-null',
+        'description':'Converting to non-pointer type from NULL',
+        'patterns':[r".*: warning: converting to non-pointer type '.+' from NULL"] },
+    { 'category':'C/C++',   'severity':severity.MEDIUM,   'members':[], 'option':'-Wnull-conversion',
+        'description':'Converting NULL to non-pointer type',
+        'patterns':[r".*: warning: implicit conversion of NULL constant to '.+'"] },
+    { 'category':'C/C++',   'severity':severity.MEDIUM,   'members':[], 'option':'-Wnon-literal-null-conversion',
+        'description':'Zero used as null pointer',
+        'patterns':[r".*: warning: expression .* zero treated as a null pointer constant"] },
+    { 'category':'C/C++',   'severity':severity.MEDIUM,   'members':[], 'option':'-Wliteral-conversion',
+        'description':'Implicit conversion changes value',
+        'patterns':[r".*: warning: implicit conversion .* changes value from .* to .*literal-conversion"] },
     { 'category':'C/C++',   'severity':severity.MEDIUM,   'members':[], 'option':'',
         'description':'Passing NULL as non-pointer argument',
         'patterns':[r".*: warning: passing NULL to non-pointer argument [0-9]+ of '.+'"] },
@@ -1480,6 +1552,9 @@
     { 'category':'C/C++',   'severity':severity.MEDIUM,     'members':[], 'option':'writable-strings',
         'description':'Conversion from string literal to char*',
         'patterns':[r".*: warning: .+ does not allow conversion from string literal to 'char \*'"] },
+    { 'category':'C/C++',   'severity':severity.LOW,     'members':[], 'option':'-Wextra-semi',
+        'description':'Extra \';\'',
+        'patterns':[r".*: warning: extra ';' .+extra-semi"] },
     { 'category':'C/C++',   'severity':severity.LOW,     'members':[], 'option':'',
         'description':'Useless specifier',
         'patterns':[r".*: warning: useless storage class specifier in empty declaration"] },
@@ -1530,6 +1605,12 @@
     { 'category':'logtags',   'severity':severity.LOW,     'members':[], 'option':'asm-operand-widths',
         'description':'ASM value size does not match register size',
         'patterns':[r".*: warning: value size does not match register size specified by the constraint and modifier"] },
+    { 'category':'C/C++',   'severity':severity.LOW,     'members':[], 'option':'tautological-compare',
+        'description':'Comparison of self is always false',
+        'patterns':[r".*: self-comparison always evaluates to false"] },
+    { 'category':'C/C++',   'severity':severity.LOW,     'members':[], 'option':'constant-logical-operand',
+        'description':'Logical op with constant operand',
+        'patterns':[r".*: use of logical '.+' with constant operand"] },
     { 'category':'C/C++',   'severity':severity.LOW,     'members':[], 'option':'literal-suffix',
         'description':'Needs a space between literal and string macro',
         'patterns':[r".*: warning: invalid suffix on literal.+ requires a space .+Wliteral-suffix"] },
@@ -1538,7 +1619,11 @@
         'patterns':[r".*: warning: .+-W#warnings"] },
     { 'category':'C/C++',   'severity':severity.LOW,     'members':[], 'option':'absolute-value',
         'description':'Using float/int absolute value function with int/float argument',
-        'patterns':[r".*: warning: using .+ absolute value function .+ when argument is .+ type .+Wabsolute-value"] },
+        'patterns':[r".*: warning: using .+ absolute value function .+ when argument is .+ type .+Wabsolute-value",
+                    r".*: warning: absolute value function '.+' given .+ which may cause truncation .+Wabsolute-value"] },
+    { 'category':'C/C++',   'severity':severity.LOW,     'members':[], 'option':'-Wc++11-extensions',
+        'description':'Using C++11 extensions',
+        'patterns':[r".*: warning: 'auto' type specifier is a C\+\+11 extension"] },
     { 'category':'C/C++',   'severity':severity.LOW,     'members':[], 'option':'',
         'description':'Refers to implicitly defined namespace',
         'patterns':[r".*: warning: using directive refers to implicitly-defined namespace .+"] },
@@ -1549,9 +1634,10 @@
     { 'category':'C/C++',   'severity':severity.MEDIUM,     'members':[], 'option':'',
         'description':'Operator new returns NULL',
         'patterns':[r".*: warning: 'operator new' must not return NULL unless it is declared 'throw\(\)' .+"] },
-    { 'category':'C/C++',   'severity':severity.MEDIUM,     'members':[], 'option':'',
+    { 'category':'C/C++',   'severity':severity.MEDIUM,     'members':[], 'option':'-Wnull-arithmetic',
         'description':'NULL used in arithmetic',
-        'patterns':[r".*: warning: NULL used in arithmetic"] },
+        'patterns':[r".*: warning: NULL used in arithmetic",
+                    r".*: warning: comparison between NULL and non-pointer"] },
     { 'category':'C/C++',   'severity':severity.MEDIUM,     'members':[], 'option':'header-guard',
         'description':'Misspelled header guard',
         'patterns':[r".*: warning: '.+' is used as a header guard .+ followed by .+ different macro"] },
@@ -1592,6 +1678,12 @@
     { 'category':'C/C++',   'severity':severity.MEDIUM,     'members':[], 'option':'',
         'description':'Result of malloc type incompatible with sizeof operand type',
         'patterns':[r".*: warning: Result of '.+' is converted to .+ incompatible with sizeof operand type"] },
+    { 'category':'C/C++',   'severity':severity.MEDIUM,     'members':[], 'option':'-Wsizeof-array-argument',
+        'description':'Sizeof on array argument',
+        'patterns':[r".*: warning: sizeof on array function parameter will return"] },
+    { 'category':'C/C++',   'severity':severity.MEDIUM,     'members':[], 'option':'-Wsizeof-pointer-memacces',
+        'description':'Bad argument size of memory access functions',
+        'patterns':[r".*: warning: .+\[-Wsizeof-pointer-memaccess\]"] },
     { 'category':'C/C++',   'severity':severity.MEDIUM,     'members':[], 'option':'',
         'description':'Return value not checked',
         'patterns':[r".*: warning: The return value from .+ is not checked"] },