Replace deprecated MoreExecutors.sameThreadExecutor(). am: a5c71db7e0 am: 07b7df8a83
am: 592d1eec01

Change-Id: Id38c59228712fa925e8ae1fa286111c8b37e245e
diff --git a/BUILD b/BUILD
index c334326..0094d45 100644
--- a/BUILD
+++ b/BUILD
@@ -1,5 +1,3 @@
-# Bazel (https://bazel.io/) BUILD file for apkzlib library.
-
 licenses(["notice"])  # Apache License 2.0
 
 java_library(
@@ -7,9 +5,10 @@
     srcs = glob([
         "src/main/java/**/*.java",
     ]),
-    visibility = ["//visibility:public"],
+    visibility = ["//tools/base/build-system/builder:__pkg__"],
     deps = [
-        "//tools/base/annotations",
+        "//tools/apksig",
+        "//tools/base/third_party:com.google.code.findbugs_jsr305",
         "//tools/base/third_party:com.google.guava_guava",
         "//tools/base/third_party:org.bouncycastle_bcpkix-jdk15on",
         "//tools/base/third_party:org.bouncycastle_bcprov-jdk15on",
@@ -18,21 +17,17 @@
 
 java_test(
     name = "apkzlib_tests",
-    srcs = glob([
-        "src/test/java/**/*.java",
-    ]),
+    srcs = glob(["src/test/java/**/*.java"]),
     jvm_flags = ["-Dtest.suite.jar=tests.jar"],
     resources = glob(["src/test/resources/**"]),
-    tags = ["manual"],
     test_class = "com.android.testutils.JarTestSuite",
     deps = [
         ":apkzlib",
-        "//tools/base/annotations",
         "//tools/base/testutils:tools.testutils",
         "//tools/base/third_party:com.google.guava_guava",
         "//tools/base/third_party:junit_junit",
         "//tools/base/third_party:org.bouncycastle_bcpkix-jdk15on",
         "//tools/base/third_party:org.bouncycastle_bcprov-jdk15on",
-        "//tools/base/third_party:org.mockito_mockito-all",
+        "//tools/base/third_party:org.mockito_mockito-core",
     ],
 )
diff --git a/apkzlib.iml b/apkzlib.iml
index 409bf1b..999fffa 100644
--- a/apkzlib.iml
+++ b/apkzlib.iml
@@ -9,10 +9,13 @@
     </content>
     <orderEntry type="inheritedJdk" />
     <orderEntry type="sourceFolder" forTests="false" />
-    <orderEntry type="module" module-name="android-annotations" />
+    <orderEntry type="library" name="jsr305" level="project" />
     <orderEntry type="library" name="guava-tools" level="project" />
     <orderEntry type="library" scope="TEST" name="JUnit4" level="project" />
     <orderEntry type="library" scope="TEST" name="mockito" level="project" />
     <orderEntry type="library" name="bouncy-castle" level="project" />
+    <orderEntry type="module" module-name="testutils" scope="TEST" />
+    <orderEntry type="module" module-name="apksig" />
+    <orderEntry type="library" name="KotlinJavaRuntime" level="project" />
   </component>
 </module>
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 48a1528..771d1b8 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,14 +1,15 @@
-apply plugin: 'java'
+apply from: "$rootDir/buildSrc/base/baseJava.gradle"
 
 dependencies {
-    compile project(':base:annotations')
-
+    compile 'com.google.code.findbugs:jsr305:1.3.9'
     compile 'com.google.guava:guava:18.0'
-    compile 'org.bouncycastle:bcpkix-jdk15on:1.48'
-    compile 'org.bouncycastle:bcprov-jdk15on:1.48'
+    compile 'org.bouncycastle:bcpkix-jdk15on:1.56'
+    compile 'org.bouncycastle:bcprov-jdk15on:1.56'
+    compile project(':apksig')
 
     testCompile 'junit:junit:4.12'
-    testCompile 'org.mockito:mockito-all:1.9.5'
+    testCompile 'org.mockito:mockito-core:2.7.1'
+    testCompile project(':base:testutils')
 }
 
 configurations {
diff --git a/src/main/java/com/android/builder/internal/packaging/sign/DigestAlgorithm.java b/src/main/java/com/android/apkzlib/sign/DigestAlgorithm.java
similarity index 63%
rename from src/main/java/com/android/builder/internal/packaging/sign/DigestAlgorithm.java
rename to src/main/java/com/android/apkzlib/sign/DigestAlgorithm.java
index 13cbf6c..64427ae 100644
--- a/src/main/java/com/android/builder/internal/packaging/sign/DigestAlgorithm.java
+++ b/src/main/java/com/android/apkzlib/sign/DigestAlgorithm.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2015 The Android Open Source Project
+ * 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.
@@ -14,9 +14,9 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.sign;
+package com.android.apkzlib.sign;
 
-import com.android.annotations.NonNull;
+import javax.annotation.Nonnull;
 
 /**
  * Message digest algorithms.
@@ -40,33 +40,34 @@
     SHA256("SHA-256", "SHA-256");
 
     /**
-     * API level which supports {@link #SHA256} with {@link SignatureAlgorithm#RSA}.
+     * API level which supports {@link #SHA256} with {@link SignatureAlgorithm#RSA} and
+     * {@link SignatureAlgorithm#ECDSA}.
      */
-    public static final int API_SHA_256_RSA = 18;
+    public static final int API_SHA_256_RSA_AND_ECDSA = 18;
 
     /**
      * API level which supports {@link #SHA256} for all {@link SignatureAlgorithm}s.
      *
-     * <p>Before that, SHA256 can only be used with RSA.
+     * <p>Before that, SHA256 can only be used with RSA and ECDSA.
      */
     public static final int API_SHA_256_ALL_ALGORITHMS = 21;
 
     /**
      * Name of algorithm for message digest.
      */
-    @NonNull
+    @Nonnull
     public final String messageDigestName;
 
     /**
      * Name of attribute in signature file with the manifest digest.
      */
-    @NonNull
+    @Nonnull
     public final String manifestAttributeName;
 
     /**
      * Name of attribute in entry (both manifest and signature file) with the entry's digest.
      */
-    @NonNull
+    @Nonnull
     public final String entryAttributeName;
 
     /**
@@ -75,31 +76,9 @@
      * @param attributeName attribute name in the signature file
      * @param messageDigestName name of algorithm for message digest
      */
-    DigestAlgorithm(@NonNull String attributeName, @NonNull String messageDigestName) {
+    DigestAlgorithm(@Nonnull String attributeName, @Nonnull String messageDigestName) {
         this.messageDigestName = messageDigestName;
         this.entryAttributeName = attributeName + "-Digest";
         this.manifestAttributeName = attributeName + "-Digest-Manifest";
     }
-
-    /**
-     * Finds the best digest algorithm applicable for a given SDK.
-     *
-     * @param minSdk the minimum SDK
-     * @param signatureAlgorithm signature algorithm used
-     * @return the best algorithm found
-     */
-    @NonNull
-    public static DigestAlgorithm findBest(
-            int minSdk,
-            @NonNull SignatureAlgorithm signatureAlgorithm) {
-        if (signatureAlgorithm == SignatureAlgorithm.RSA) {
-            // PKCS #7 RSA signatures with SHA-256 are
-            // supported only since API Level 18 (JB MR2).
-            return minSdk >= API_SHA_256_RSA ? SHA256 : SHA1;
-        } else {
-            // PKCS #7 ECDSA and DSA signatures with SHA-256
-            // are supported only since API Level 21 (Android L).
-            return minSdk >= API_SHA_256_ALL_ALGORITHMS ? SHA256 : SHA1;
-        }
-    }
 }
diff --git a/src/main/java/com/android/apkzlib/sign/ManifestGenerationExtension.java b/src/main/java/com/android/apkzlib/sign/ManifestGenerationExtension.java
new file mode 100644
index 0000000..33c47c6
--- /dev/null
+++ b/src/main/java/com/android/apkzlib/sign/ManifestGenerationExtension.java
@@ -0,0 +1,244 @@
+/*
+ * 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.apkzlib.sign;
+
+import com.android.apkzlib.utils.CachedSupplier;
+import com.android.apkzlib.utils.IOExceptionRunnable;
+import com.android.apkzlib.zfile.ManifestAttributes;
+import com.android.apkzlib.zip.StoredEntry;
+import com.android.apkzlib.zip.ZFile;
+import com.android.apkzlib.zip.ZFileExtension;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Verify;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.jar.Attributes;
+import java.util.jar.Manifest;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/**
+ * Extension to {@link ZFile} that will generate a manifest. The extension will register
+ * automatically with the {@link ZFile}.
+ *
+ * <p>Creating this extension will ensure a manifest for the zip exists.
+ * This extension will generate a manifest if one does not exist and will update an existing
+ * manifest, if one does exist. The extension will also provide access to the manifest so that
+ * others may update the manifest.
+ *
+ * <p>Apart from standard manifest elements, this extension does not handle any particular manifest
+ * features such as signing or adding custom attributes. It simply generates a plain manifest and
+ * provides infrastructure so that other extensions can add data in the manifest.
+ *
+ * <p>The manifest itself will only be written when the {@link ZFileExtension#beforeUpdate()}
+ * notification is received, meaning all manifest manipulation is done in-memory.
+ */
+public class ManifestGenerationExtension {
+
+    /**
+     * Name of META-INF directory.
+     */
+    private static final String META_INF_DIR = "META-INF";
+
+    /**
+     * Name of the manifest file.
+     */
+    static final String MANIFEST_NAME = META_INF_DIR + "/MANIFEST.MF";
+
+    /**
+     * Who should be reported as the manifest builder.
+     */
+    @Nonnull
+    private final String builtBy;
+
+    /**
+     * Who should be reported as the manifest creator.
+     */
+    @Nonnull
+    private final String createdBy;
+
+    /**
+     * The file this extension is attached to. {@code null} if not yet registered.
+     */
+    @Nullable
+    private ZFile zFile;
+
+    /**
+     * The zip file's manifest.
+     */
+    @Nonnull
+    private final Manifest manifest;
+
+    /**
+     * Byte representation of the manifest. There is no guarantee that two writes of the java's
+     * {@code Manifest} object will yield the same byte array (there is no guaranteed order
+     * of entries in the manifest).
+     *
+     * <p>Because we need the byte representation of the manifest to be stable if there are
+     * no changes to the manifest, we cannot rely on {@code Manifest} to generate the byte
+     * representation every time we need the byte representation.
+     *
+     * <p>This cache will ensure that we will request one byte generation from the {@code Manifest}
+     * and will cache it. All further requests of the manifest's byte representation will
+     * receive the same byte array.
+     */
+    @Nonnull
+    private CachedSupplier<byte[]> manifestBytes;
+
+    /**
+     * Has the current manifest been changed and not yet flushed? If {@link #dirty} is
+     * {@code true}, then {@link #manifestBytes} should not be valid. This means that
+     * marking the manifest as dirty should also invalidate {@link #manifestBytes}. To avoid
+     * breaking the invariant, instead of setting {@link #dirty}, {@link #markDirty()} should
+     * be called.
+     */
+    private boolean dirty;
+
+    /**
+     * The extension to register with the {@link ZFile}. {@code null} if not registered.
+     */
+    @Nullable
+    private ZFileExtension extension;
+
+    /**
+     * Creates a new extension. This will not register the extension with the provided
+     * {@link ZFile}. Until {@link #register(ZFile)} is invoked, this extension is not used.
+     *
+     * @param builtBy who built the manifest?
+     * @param createdBy who created the manifest?
+     */
+    public ManifestGenerationExtension(@Nonnull String builtBy, @Nonnull String createdBy) {
+        this.builtBy = builtBy;
+        this.createdBy = createdBy;
+        manifest = new Manifest();
+        dirty = false;
+        manifestBytes = new CachedSupplier<>(() -> {
+            ByteArrayOutputStream outBytes = new ByteArrayOutputStream();
+            try {
+                manifest.write(outBytes);
+            } catch (IOException e) {
+                throw new UncheckedIOException(e);
+            }
+
+            return outBytes.toByteArray();
+        });
+    }
+
+    /**
+     * Marks the manifest as being dirty, <i>i.e.</i>, its data has changed since it was last
+     * read and/or written.
+     */
+    private void markDirty() {
+        dirty = true;
+        manifestBytes.reset();
+    }
+
+    /**
+     * Registers the extension with the {@link ZFile} provided in the constructor.
+     *
+     * @param zFile the zip file to add the extension to
+     * @throws IOException failed to analyze the zip
+     */
+    public void register(@Nonnull ZFile zFile) throws IOException {
+        Preconditions.checkState(extension == null, "register() has already been invoked.");
+        this.zFile = zFile;
+
+        rebuildManifest();
+
+        extension = new ZFileExtension() {
+            @Nullable
+            @Override
+            public IOExceptionRunnable beforeUpdate() {
+                return ManifestGenerationExtension.this::updateManifest;
+            }
+        };
+
+        this.zFile.addZFileExtension(extension);
+    }
+
+    /**
+     * Rebuilds the zip file's manifest, if it needs changes.
+     */
+    private void rebuildManifest() throws IOException {
+        Verify.verifyNotNull(zFile, "zFile == null");
+
+        StoredEntry manifestEntry = zFile.get(MANIFEST_NAME);
+
+        if (manifestEntry != null) {
+            /*
+             * Read the manifest entry in the zip file. Make sure we store these byte sequence
+             * because writing the manifest may not generate the same byte sequence, which may
+             * trigger an unnecessary re-sign of the jar.
+             */
+            manifest.clear();
+            byte[] manifestBytes = manifestEntry.read();
+            manifest.read(new ByteArrayInputStream(manifestBytes));
+            this.manifestBytes.precomputed(manifestBytes);
+        }
+
+        Attributes mainAttributes = manifest.getMainAttributes();
+        String currentVersion = mainAttributes.getValue(ManifestAttributes.MANIFEST_VERSION);
+        if (currentVersion == null) {
+            setMainAttribute(
+                    ManifestAttributes.MANIFEST_VERSION,
+                    ManifestAttributes.CURRENT_MANIFEST_VERSION);
+        } else {
+            if (!currentVersion.equals(ManifestAttributes.CURRENT_MANIFEST_VERSION)) {
+                throw new IOException("Unsupported manifest version: " + currentVersion + ".");
+            }
+        }
+
+        /*
+         * We "blindly" override all other main attributes.
+         */
+        setMainAttribute(ManifestAttributes.BUILT_BY, builtBy);
+        setMainAttribute(ManifestAttributes.CREATED_BY, createdBy);
+    }
+
+    /**
+     * Sets the value of a main attribute.
+     *
+     * @param attribute the attribute
+     * @param value the value
+     */
+    private void setMainAttribute(@Nonnull String attribute, @Nonnull String value) {
+        Attributes mainAttributes = manifest.getMainAttributes();
+        String current = mainAttributes.getValue(attribute);
+        if (!value.equals(current)) {
+            mainAttributes.putValue(attribute, value);
+            markDirty();
+        }
+    }
+
+    /**
+     * Updates the manifest in the zip file, if it has been changed.
+     *
+     * @throws IOException failed to update the manifest
+     */
+    private void updateManifest() throws IOException {
+        Verify.verifyNotNull(zFile, "zFile == null");
+
+        if (!dirty) {
+            return;
+        }
+
+        zFile.add(MANIFEST_NAME, new ByteArrayInputStream(manifestBytes.get()));
+        dirty = false;
+    }
+}
diff --git a/src/main/java/com/android/builder/internal/packaging/sign/SignatureAlgorithm.java b/src/main/java/com/android/apkzlib/sign/SignatureAlgorithm.java
similarity index 80%
rename from src/main/java/com/android/builder/internal/packaging/sign/SignatureAlgorithm.java
rename to src/main/java/com/android/apkzlib/sign/SignatureAlgorithm.java
index e171fa2..0667252 100644
--- a/src/main/java/com/android/builder/internal/packaging/sign/SignatureAlgorithm.java
+++ b/src/main/java/com/android/apkzlib/sign/SignatureAlgorithm.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2015 The Android Open Source Project
+ * 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.
@@ -14,36 +14,26 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.sign;
-
-import com.android.annotations.NonNull;
+package com.android.apkzlib.sign;
 
 import java.security.NoSuchAlgorithmException;
+import javax.annotation.Nonnull;
 
 /**
  * Signature algorithm.
  */
 public enum SignatureAlgorithm {
-    /**
-     * RSA algorithm.
-     */
-    RSA("RSA", 0, "withRSA"),
+    /** RSA algorithm. */
+    RSA("RSA", 1, "withRSA"),
 
-    /**
-     * ECDSA algorithm.
-     */
+    /** ECDSA algorithm. */
     ECDSA("EC", 18, "withECDSA"),
 
-    /**
-     * DSA algorithm.
-     */
-    DSA("DSA", 0, "withDSA");
+    /** DSA algorithm. */
+    DSA("DSA", 1, "withDSA");
 
-    /**
-     * Name of the private key as reported by {@code PrivateKey}.
-     */
-    @NonNull
-    public final String keyAlgorithm;
+    /** Name of the private key as reported by {@code PrivateKey}. */
+    @Nonnull public final String keyAlgorithm;
 
     /**
      * Minimum SDK version that allows this signature.
@@ -53,7 +43,7 @@
     /**
      * Suffix appended to digest algorithm to obtain signature algorithm.
      */
-    @NonNull
+    @Nonnull
     public final String signatureAlgorithmSuffix;
 
     /**
@@ -64,7 +54,7 @@
      * @param signatureAlgorithmSuffix suffix for signature name with used with a digest
      */
     SignatureAlgorithm(
-            @NonNull String keyAlgorithm, int minSdkVersion, @NonNull String signatureAlgorithmSuffix) {
+            @Nonnull String keyAlgorithm, int minSdkVersion, @Nonnull String signatureAlgorithmSuffix) {
         this.keyAlgorithm = keyAlgorithm;
         this.minSdkVersion = minSdkVersion;
         this.signatureAlgorithmSuffix = signatureAlgorithmSuffix;
@@ -80,8 +70,8 @@
      * @throws NoSuchAlgorithmException if no algorithm was found for the given private key; an
      * algorithm was found but is not applicable to the given SDK version
      */
-    @NonNull
-    public static SignatureAlgorithm fromKeyAlgorithm(@NonNull String keyAlgorithm,
+    @Nonnull
+    public static SignatureAlgorithm fromKeyAlgorithm(@Nonnull String keyAlgorithm,
             int minSdkVersion) throws NoSuchAlgorithmException {
         for (SignatureAlgorithm alg : values()) {
             if (alg.keyAlgorithm.equalsIgnoreCase(keyAlgorithm)) {
@@ -106,8 +96,8 @@
      * @param digestAlgorithm the digest algorithm to use
      * @return the name of the signature algorithm
      */
-    @NonNull
-    public String signatureAlgorithmName(@NonNull DigestAlgorithm digestAlgorithm) {
+    @Nonnull
+    public String signatureAlgorithmName(@Nonnull DigestAlgorithm digestAlgorithm) {
         return digestAlgorithm.messageDigestName.replace("-", "") + signatureAlgorithmSuffix;
     }
 }
diff --git a/src/main/java/com/android/apkzlib/sign/SigningExtension.java b/src/main/java/com/android/apkzlib/sign/SigningExtension.java
new file mode 100644
index 0000000..2685aa1
--- /dev/null
+++ b/src/main/java/com/android/apkzlib/sign/SigningExtension.java
@@ -0,0 +1,392 @@
+/*
+ * 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.apkzlib.sign;
+
+import com.android.apksig.ApkSignerEngine;
+import com.android.apksig.ApkVerifier;
+import com.android.apksig.DefaultApkSignerEngine;
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.util.DataSources;
+import com.android.apkzlib.utils.IOExceptionRunnable;
+import com.android.apkzlib.zip.StoredEntry;
+import com.android.apkzlib.zip.ZFile;
+import com.android.apkzlib.zip.ZFileExtension;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.SignatureException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/**
+ * {@link ZFile} extension which signs the APK.
+ *
+ * <p>
+ * This extension is capable of signing the APK using JAR signing (aka v1 scheme) and APK Signature
+ * Scheme v2 (aka v2 scheme). Which schemes are actually used is specified by parameters to this
+ * extension's constructor.
+ */
+public class SigningExtension {
+    // IMPLEMENTATION NOTE: Most of the heavy lifting is performed by the ApkSignerEngine primitive
+    // from apksig library. This class is an adapter between ZFile extension and ApkSignerEngine.
+    // This class takes care of invoking the right methods on ApkSignerEngine in response to ZFile
+    // extension events/callbacks.
+    //
+    // The main issue leading to additional complexity in this class is that the current build
+    // pipeline does not reuse ApkSignerEngine instances (or ZFile extension instances for that
+    // matter) for incremental builds. Thus:
+    // * ZFile extension receives no events for JAR entries already in the APK whereas
+    //   ApkSignerEngine needs to know about all JAR entries to be covered by signature. Thus, this
+    //   class, during "beforeUpdate" ZFile event, notifies ApkSignerEngine about JAR entries
+    //   already in the APK which ApkSignerEngine hasn't yet been told about -- these are the JAR
+    //   entries which the incremental build session did not touch.
+    // * The build pipeline expects the APK not to change if no JAR entry was added to it or removed
+    //   from it whereas ApkSignerEngine produces no output only if it has already produced a signed
+    //   APK and no changes have since been made to it. This class addresses this issue by checking
+    //   in its "register" method whether the APK is correctly signed and, only if that's the case,
+    //   doesn't modify the APK unless a JAR entry is added to it or removed from it after
+    //   "register".
+
+    /**
+     * Minimum API Level on which this APK is supposed to run.
+     */
+    private final int minSdkVersion;
+
+    /**
+     * Whether JAR signing (aka v1 signing) is enabled.
+     */
+    private final boolean v1SigningEnabled;
+
+    /**
+     * Whether APK Signature Scheme v2 sining (aka v2 signing) is enabled.
+     */
+    private final boolean v2SigningEnabled;
+
+    /**
+     * Certificate of the signer, to be embedded into the APK's signature.
+     */
+    @Nonnull
+    private final X509Certificate certificate;
+
+    /**
+     * APK signer which performs most of the heavy lifting.
+     */
+    @Nonnull
+    private final ApkSignerEngine signer;
+
+    /**
+     * Names of APK entries which have been processed by {@link #signer}.
+     */
+    private final Set<String> signerProcessedOutputEntryNames = new HashSet<>();
+
+    /**
+     * Cached contents of the most recently output APK Signing Block or {@code null} if the block
+     * hasn't yet been output.
+     */
+    @Nullable
+    private byte[] cachedApkSigningBlock;
+
+    /**
+     * {@code true} if signatures may need to be output, {@code false} if there's no need to output
+     * signatures. This is used in an optimization where we don't modify the APK if it's already
+     * signed and if no JAR entries have been added to or removed from the file.
+     */
+    private boolean dirty;
+
+    /**
+     * The extension registered with the {@link ZFile}. {@code null} if not registered.
+     */
+    @Nullable
+    private ZFileExtension extension;
+
+    /**
+     * The file this extension is attached to. {@code null} if not yet registered.
+     */
+    @Nullable
+    private ZFile zFile;
+
+    public SigningExtension(
+            int minSdkVersion,
+            @Nonnull X509Certificate certificate,
+            @Nonnull PrivateKey privateKey,
+            boolean v1SigningEnabled,
+            boolean v2SigningEnabled) throws InvalidKeyException {
+        DefaultApkSignerEngine.SignerConfig signerConfig =
+                new DefaultApkSignerEngine.SignerConfig.Builder(
+                        "CERT", privateKey, ImmutableList.of(certificate)).build();
+        signer =
+                new DefaultApkSignerEngine.Builder(ImmutableList.of(signerConfig), minSdkVersion)
+                        .setOtherSignersSignaturesPreserved(false)
+                        .setV1SigningEnabled(v1SigningEnabled)
+                        .setV2SigningEnabled(v2SigningEnabled)
+                        .setCreatedBy("1.0 (Android)")
+                        .build();
+        this.minSdkVersion = minSdkVersion;
+        this.v1SigningEnabled = v1SigningEnabled;
+        this.v2SigningEnabled = v2SigningEnabled;
+        this.certificate = certificate;
+    }
+
+    public void register(@Nonnull ZFile zFile) throws NoSuchAlgorithmException, IOException {
+        Preconditions.checkState(extension == null, "register() already invoked");
+        this.zFile = zFile;
+        dirty = !isCurrentSignatureAsRequested();
+        extension = new ZFileExtension() {
+            @Override
+            public IOExceptionRunnable added(
+                    @Nonnull StoredEntry entry, @Nullable StoredEntry replaced) {
+                return () -> onZipEntryOutput(entry);
+            }
+
+            @Override
+            public IOExceptionRunnable removed(@Nonnull StoredEntry entry) {
+                String entryName = entry.getCentralDirectoryHeader().getName();
+                return () -> onZipEntryRemovedFromOutput(entryName);
+            }
+
+            @Override
+            public IOExceptionRunnable beforeUpdate() throws IOException {
+                return () -> onOutputZipReadyForUpdate();
+            }
+
+            @Override
+            public void entriesWritten() throws IOException {
+                onOutputZipEntriesWritten();
+            }
+
+            @Override
+            public void closed() {
+                onOutputClosed();
+            }
+        };
+        this.zFile.addZFileExtension(extension);
+    }
+
+    /**
+     * Returns {@code true} if the APK's signatures are as requested by parameters to this signing
+     * extension.
+     */
+    private boolean isCurrentSignatureAsRequested() throws IOException, NoSuchAlgorithmException {
+        ApkVerifier.Result result;
+        try {
+            result =
+                    new ApkVerifier.Builder(new ZFileDataSource(zFile))
+                            .setMinCheckedPlatformVersion(minSdkVersion)
+                            .build()
+                            .verify();
+        } catch (ApkFormatException e) {
+            // Malformed APK
+            return false;
+        }
+
+        if (!result.isVerified()) {
+            // Signature(s) did not verify
+            return false;
+        }
+
+        if ((result.isVerifiedUsingV1Scheme() != v1SigningEnabled)
+                || (result.isVerifiedUsingV2Scheme() != v2SigningEnabled)) {
+            // APK isn't signed with exactly the schemes we want it to be signed
+            return false;
+        }
+
+        List<X509Certificate> verifiedSignerCerts = result.getSignerCertificates();
+        if (verifiedSignerCerts.size() != 1) {
+            // APK is not signed by exactly one signer
+            return false;
+        }
+
+        byte[] expectedEncodedCert;
+        byte[] actualEncodedCert;
+        try {
+            expectedEncodedCert = certificate.getEncoded();
+            actualEncodedCert = verifiedSignerCerts.get(0).getEncoded();
+        } catch (CertificateEncodingException e) {
+            // Failed to encode signing certificates
+            return false;
+        }
+
+        if (!Arrays.equals(expectedEncodedCert, actualEncodedCert)) {
+            // APK is signed by a wrong signer
+            return false;
+        }
+
+        // APK is signed the way we want it to be signed
+        return true;
+    }
+
+    private void onZipEntryOutput(@Nonnull StoredEntry entry) throws IOException {
+        setDirty();
+        String entryName = entry.getCentralDirectoryHeader().getName();
+        // This event may arrive after the entry has already been deleted. In that case, we don't
+        // report the addition of the entry to ApkSignerEngine.
+        if (entry.isDeleted()) {
+            return;
+        }
+        ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
+                signer.outputJarEntry(entryName);
+        signerProcessedOutputEntryNames.add(entryName);
+        if (inspectEntryRequest != null) {
+            byte[] entryContents = entry.read();
+            inspectEntryRequest.getDataSink().consume(entryContents, 0, entryContents.length);
+            inspectEntryRequest.done();
+        }
+    }
+
+    private void onZipEntryRemovedFromOutput(@Nonnull String entryName) {
+        setDirty();
+        signer.outputJarEntryRemoved(entryName);
+        signerProcessedOutputEntryNames.remove(entryName);
+    }
+
+    private void onOutputZipReadyForUpdate() throws IOException {
+        if (!dirty) {
+            return;
+        }
+
+        // Notify signer engine about ZIP entries that have appeared in the output without the
+        // engine knowing. Also identify ZIP entries which disappeared from the output without the
+        // engine knowing.
+        Set<String> unprocessedRemovedEntryNames = new HashSet<>(signerProcessedOutputEntryNames);
+        for (StoredEntry entry : zFile.entries()) {
+            String entryName = entry.getCentralDirectoryHeader().getName();
+            unprocessedRemovedEntryNames.remove(entryName);
+            if (!signerProcessedOutputEntryNames.contains(entryName)) {
+                // Signer engine is not yet aware that this entry is in the output
+                onZipEntryOutput(entry);
+            }
+        }
+
+        // Notify signer engine about entries which disappeared from the output without the engine
+        // knowing
+        for (String entryName : unprocessedRemovedEntryNames) {
+            onZipEntryRemovedFromOutput(entryName);
+        }
+
+        // Check whether we need to output additional JAR entries which comprise the v1 signature
+        ApkSignerEngine.OutputJarSignatureRequest addV1SignatureRequest;
+        try {
+            addV1SignatureRequest = signer.outputJarEntries();
+        } catch (Exception e) {
+            throw new IOException("Failed to generate v1 signature", e);
+        }
+        if (addV1SignatureRequest == null) {
+            return;
+        }
+
+        // We need to output additional JAR entries which comprise the v1 signature
+        List<ApkSignerEngine.OutputJarSignatureRequest.JarEntry> v1SignatureEntries =
+                new ArrayList<>(addV1SignatureRequest.getAdditionalJarEntries());
+
+        // Reorder the JAR entries comprising the v1 signature so that MANIFEST.MF is the first
+        // entry. This ensures that it cleanly overwrites the existing MANIFEST.MF output by
+        // ManifestGenerationExtension.
+        for (int i = 0; i < v1SignatureEntries.size(); i++) {
+            ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry = v1SignatureEntries.get(i);
+            String name = entry.getName();
+            if (!ManifestGenerationExtension.MANIFEST_NAME.equals(name)) {
+                continue;
+            }
+            if (i != 0) {
+                v1SignatureEntries.remove(i);
+                v1SignatureEntries.add(0, entry);
+            }
+            break;
+        }
+
+        // Output the JAR entries comprising the v1 signature
+        for (ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry : v1SignatureEntries) {
+            String name = entry.getName();
+            byte[] data = entry.getData();
+            zFile.add(name, new ByteArrayInputStream(data));
+        }
+
+        addV1SignatureRequest.done();
+    }
+
+    private void onOutputZipEntriesWritten() throws IOException {
+        if (!dirty) {
+            return;
+        }
+
+        // Check whether we should output an APK Signing Block which contains v2 signatures
+        byte[] apkSigningBlock;
+        byte[] centralDirBytes = zFile.getCentralDirectoryBytes();
+        byte[] eocdBytes = zFile.getEocdBytes();
+        ApkSignerEngine.OutputApkSigningBlockRequest addV2SignatureRequest;
+        // This event may arrive a second time -- after we write out the APK Signing Block. Thus, we
+        // cache the block to speed things up. The cached block is invalidated by any changes to the
+        // file (as reported to this extension).
+        if (cachedApkSigningBlock != null) {
+            apkSigningBlock = cachedApkSigningBlock;
+            addV2SignatureRequest = null;
+        } else {
+            DataSource centralDir = DataSources.asDataSource(ByteBuffer.wrap(centralDirBytes));
+            DataSource eocd = DataSources.asDataSource(ByteBuffer.wrap(eocdBytes));
+            long zipEntriesSizeBytes =
+                    zFile.getCentralDirectoryOffset() - zFile.getExtraDirectoryOffset();
+            DataSource zipEntries = new ZFileDataSource(zFile, 0, zipEntriesSizeBytes);
+            try {
+                addV2SignatureRequest = signer.outputZipSections(zipEntries, centralDir, eocd);
+            } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException
+                    | ApkFormatException | IOException e) {
+                throw new IOException("Failed to generate v2 signature", e);
+            }
+            apkSigningBlock =
+                    (addV2SignatureRequest != null)
+                            ? addV2SignatureRequest.getApkSigningBlock() : new byte[0];
+            cachedApkSigningBlock = apkSigningBlock;
+        }
+
+        // Insert the APK Signing Block into the output right before the ZIP Central Directory and
+        // accordingly update the start offset of ZIP Central Directory in ZIP End of Central
+        // Directory.
+        zFile.directWrite(
+                zFile.getCentralDirectoryOffset() - zFile.getExtraDirectoryOffset(),
+                apkSigningBlock);
+        zFile.setExtraDirectoryOffset(apkSigningBlock.length);
+
+        if (addV2SignatureRequest != null) {
+            addV2SignatureRequest.done();
+        }
+    }
+
+    private void onOutputClosed() {
+        if (!dirty) {
+            return;
+        }
+        signer.outputDone();
+        dirty = false;
+    }
+
+    private void setDirty() {
+        dirty = true;
+        cachedApkSigningBlock = null;
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/android/apkzlib/sign/ZFileDataSource.java b/src/main/java/com/android/apkzlib/sign/ZFileDataSource.java
new file mode 100644
index 0000000..049cf35
--- /dev/null
+++ b/src/main/java/com/android/apkzlib/sign/ZFileDataSource.java
@@ -0,0 +1,157 @@
+/*
+ * 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.apkzlib.sign;
+
+import com.android.apksig.util.DataSink;
+import com.android.apksig.util.DataSource;
+import com.android.apkzlib.zip.ZFile;
+import com.google.common.base.Preconditions;
+import java.io.EOFException;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import javax.annotation.Nonnull;
+
+/**
+ * {@link DataSource} backed by contents of {@link ZFile}.
+ */
+class ZFileDataSource implements DataSource {
+
+    private static final int MAX_READ_CHUNK_SIZE = 65536;
+
+    @Nonnull
+    private final ZFile file;
+
+    /**
+     * Offset (in bytes) relative to the start of file where the region visible in this data source
+     * starts.
+     */
+    private final long offset;
+
+    /**
+     * Size (in bytes) of the file region visible in this data source or {@code -1} if the whole
+     * file is visible in this data source and thus its size may change if the file's size changes.
+     */
+    private final long size;
+
+    /**
+     * Constructs a new {@code ZFileDataSource} based on the data contained in the file. Changes to
+     * the contents of the file, including the size of the file, will be visible in this data
+     * source.
+     */
+    public ZFileDataSource(@Nonnull ZFile file) {
+        this.file = file;
+        offset = 0;
+        size = -1;
+    }
+
+    /**
+     * Constructs a new {@code ZFileDataSource} based on the data contained in the specified region
+     * of the provided file. Changes to the contents of this region of the file will be visible in
+     * this data source.
+     */
+    public ZFileDataSource(@Nonnull ZFile file, long offset, long size) {
+        Preconditions.checkArgument(offset >= 0, "offset < 0");
+        Preconditions.checkArgument(size >= 0, "size < 0");
+        this.file = file;
+        this.offset = offset;
+        this.size = size;
+    }
+
+    @Override
+    public long size() {
+        if (size == -1) {
+            // Data source size is the current size of the file
+            try {
+                return file.directSize();
+            } catch (IOException e) {
+                return 0;
+            }
+        } else {
+            // Data source size is fixed
+            return size;
+        }
+    }
+
+    @Override
+    public DataSource slice(long offset, long size) {
+        long sourceSize = size();
+        checkChunkValid(offset, size, sourceSize);
+        if ((offset == 0) && (size == sourceSize)) {
+            return this;
+        }
+
+        return new ZFileDataSource(file, this.offset + offset, size);
+    }
+
+    @Override
+    public void feed(long offset, long size, @Nonnull DataSink sink) throws IOException {
+        long sourceSize = size();
+        checkChunkValid(offset, size, sourceSize);
+        if (size == 0) {
+            return;
+        }
+
+        long chunkOffsetInFile = this.offset + offset;
+        long remaining = size;
+        byte[] buf = new byte[(int) Math.min(remaining, MAX_READ_CHUNK_SIZE)];
+        while (remaining > 0) {
+            int chunkSize = (int) Math.min(remaining, buf.length);
+            int readSize = file.directRead(chunkOffsetInFile, buf, 0, chunkSize);
+            if (readSize == -1) {
+                throw new EOFException("Premature EOF");
+            }
+            if (readSize > 0) {
+                sink.consume(buf, 0, readSize);
+                chunkOffsetInFile += readSize;
+                remaining -= readSize;
+            }
+        }
+    }
+
+    @Override
+    public void copyTo(long offset, int size, @Nonnull ByteBuffer dest) throws IOException {
+        long sourceSize = size();
+        checkChunkValid(offset, size, sourceSize);
+        if (size == 0) {
+            return;
+        }
+
+        int prevLimit = dest.limit();
+        try {
+            file.directFullyRead(this.offset + offset, dest);
+        } finally {
+            dest.limit(prevLimit);
+        }
+    }
+
+    @Override
+    public ByteBuffer getByteBuffer(long offset, int size) throws IOException {
+        ByteBuffer result = ByteBuffer.allocate(size);
+        copyTo(offset, size, result);
+        result.flip();
+        return result;
+    }
+
+    private static void checkChunkValid(long offset, long size, long sourceSize) {
+        Preconditions.checkArgument(offset >= 0, "offset < 0");
+        Preconditions.checkArgument(size >= 0, "size < 0");
+        Preconditions.checkArgument(offset <= sourceSize, "offset > sourceSize");
+        long endOffset = offset + size;
+        Preconditions.checkArgument(offset <= endOffset, "offset > endOffset");
+        Preconditions.checkArgument(endOffset <= sourceSize, "endOffset > sourceSize");
+    }
+}
diff --git a/src/main/java/com/android/builder/internal/packaging/sign/package-info.java b/src/main/java/com/android/apkzlib/sign/package-info.java
similarity index 81%
rename from src/main/java/com/android/builder/internal/packaging/sign/package-info.java
rename to src/main/java/com/android/apkzlib/sign/package-info.java
index e8bf6e1..6bb692c 100644
--- a/src/main/java/com/android/builder/internal/packaging/sign/package-info.java
+++ b/src/main/java/com/android/apkzlib/sign/package-info.java
@@ -28,14 +28,14 @@
 extensions with the {@code zip} package. These extensions are notified in changes made in the zip
 and will change the zip file itself.
 <p>
-The {@link com.android.builder.internal.packaging.sign.ManifestGenerationExtension} extension will
+The {@link com.android.apkzlib.sign.ManifestGenerationExtension} extension will
 ensure the zip has a manifest file and is, therefore, a valid jar.
-The {@link com.android.builder.internal.packaging.sign.SignatureExtension} extension will
+The {@link com.android.apkzlib.sign.SigningExtension} extension will
 ensure the jar is signed.
 <p>
 The extension mechanism used is the one provided in the {@code zip} package (see
-{@link com.android.builder.internal.packaging.zip.ZFile}
-and {@link com.android.builder.internal.packaging.zip.ZFileExtension}. Building the zip and then
+{@link com.android.apkzlib.zip.ZFile}
+and {@link com.android.apkzlib.zip.ZFileExtension}. Building the zip and then
 operating the extensions is not done sequentially, as we don't want to build a zip and then sign it.
 We want to build a zip that is automatically signed. Extension are basically observers that
 register on the zip and are notified when things happen in the zip. They will then modify the zip
@@ -72,7 +72,7 @@
     <li>The zip is finally written with an updated manifest.</li>
 </ol>
 <p>
-To generate a signed apk (v1), we need to add a second extension, the {@code SignatureExtension}.
+To generate a signed apk, we need to add a second extension, the {@code SigningExtension}.
 This extension will also register listeners with the {@code ZFile}.
 <p>
 In this case the flow would be (starting a bit earlier for clarity and assuming a package task
@@ -85,9 +85,9 @@
     <li>Package task registers the {@code ManifestGenerationExtension} with the {@code ZFile}.</li>
     <li>The {@code ManifestGenerationExtension} looks at the {@code ZFile} to see if there is valid
     manifest. No changes are done to the {@code ZFile}.</li>
-    <li>Package task creates a {@code SignatureExtension}.</li>
-    <li>Package task registers the {@code SignatureExtension} with the {@code ZFile}.</li>
-    <li>The {@code SignatureExtension} registers a {@code ZFileExtension} with the {@code ZFile}
+    <li>Package task creates a {@code SigningExtension}.</li>
+    <li>Package task registers the {@code SigningExtension} with the {@code ZFile}.</li>
+    <li>The {@code SigningExtension} registers a {@code ZFileExtension} with the {@code ZFile}
     and look at the {@code ZFile} to see if there is a valid signature file.</li>
     <li>If there are changes to the digital signature file needed, these are marked internally in
     the extension. If there are changes needed to the digests, the manifest is updated (by calling
@@ -100,7 +100,7 @@
     <li>For each file that is added (*), {@code ZFile} calls the added {@code ZFileExtension.added}
     method of all registered extensions.</li>
     <li>The {@code ManifestGenerationExtension} ignores added invocations.</li>
-    <li>The {@code SignatureExtension} computes the digest for the added file and stores them in
+    <li>The {@code SigningExtension} computes the digest for the added file and stores them in
     the manifest.<br>
     <em>(when all files are added to the apk, all digests are computed and the manifest is updated
     but only in memory; the apk file has not been touched; also note that {@code ZFile} has not
@@ -108,15 +108,15 @@
     <li>Package task calls {@code ZFile.update()} to update the apk.</li>
     <li>{@code ZFile} calls {@code before()} for all {@code ZFileExtensions} registered. This is
     done before anything is written. In this case both the {@code ManifestGenerationExtension} and
-    {@code SignatureExtension} are invoked.</li>
+    {@code SigningExtension} are invoked.</li>
     <li>The {@code ManifestGenerationExtension} will update the {@code ZFile} with the new manifest,
     unless nothing has changed, in which case it does nothing.</li>
-    <li>The {@code SignatureExtension} will add the SF file (unless nothing has changed), will
+    <li>The {@code SigningExtension} will add the SF file (unless nothing has changed), will
     compute the digital signature of the SF file and write it to the {@code ZFile}.<br>
     <em>(note that the order by which the {@code ManifestGenerationExtension} and
-    {@code SignatureExtension} are called is non-deterministic; however, this is not a problem
+    {@code SigningExtension} are called is non-deterministic; however, this is not a problem
     because the manifest is already computed by the {@code ManifestGenerationExtension} at this
-    time and the {@code SignatureExtension} will obtain the manifest data from the
+    time and the {@code SigningExtension} will obtain the manifest data from the
     {@code ManifestGenerationExtension} and not from the {@code ZFile}; this means that the
     {@code SF} file may be added to the {@code ZFile} before the {@code MF} file, but that is
     irrelevant.)</em></li>
@@ -124,9 +124,8 @@
     {@code ZFile.update()} method continues.</li>
     <li>{@code ZFile.update()} writes all changes and new entries to the zip file.</li>
     <li>{@code ZFile.update()} calls {@code ZFileExtension.entriesWritten()} for all
-    registered extensions. Both the {@code ManifestGenerationExtension} and
-    {@code SignatureExtension} ignore this notification -- but the {@code FullApkSignExtension} will
-    kick in at this point, if it has been created.</li>
+    registered extensions. {@code SigningExtension} will kick in at this point, if v2 signature
+    has changed.</li>
     <li>{@code ZFile} writes the central directory and EOCD.</li>
     <li>{@code ZFile.update()} returns control to the package task.</li>
     <li>The package task finishes.</li>
@@ -139,18 +138,16 @@
 <p>
 If there are no changes to the {@code ZFile} made by the package task and the file's manifest and v1
 signatures are correct, neither the {@code ManifestGenerationExtension} nor the
-{@code SignatureExtension} will not do anything on the {@code beforeUpdate()} and the
+{@code SigningExtension} will not do anything on the {@code beforeUpdate()} and the
 {@code ZFile} won't even be open for writing.
 <p>
 This implementation provides perfect incremental updates.
 <p>
 Additionally, by adding/removing extensions we can configure what type of apk we want:
 <ul>
-    <li>No SignatureExtension &amp; No FullApkSignExtension ⇒ Aligned, unsigned apk.</li>
-    <li>Signature Extension &amp; No FullApkSignExtension ⇒ Aligned, v1 only signed apk.</li>
-    <li>Signature Extension &amp; FullApkSignExtension ⇒ Aligned, v1 &amp; v2 signed apk.</li>
-    <li>No Signature Extension &amp; FullApkSignExtension ⇒ Aligned, v2 only signed apk.</li>
+    <li>No SigningExtension ⇒ Aligned, unsigned apk.</li>
+    <li>SigningExtension ⇒ Aligned, signed apk.
 </ul>
 So, by configuring which extensions to add, the package task can decide what type of apk we want.
 */
-package com.android.builder.internal.packaging.sign;
\ No newline at end of file
+package com.android.apkzlib.sign;
\ No newline at end of file
diff --git a/src/main/java/com/android/builder/utils/ApkZLibPair.java b/src/main/java/com/android/apkzlib/utils/ApkZLibPair.java
similarity index 95%
rename from src/main/java/com/android/builder/utils/ApkZLibPair.java
rename to src/main/java/com/android/apkzlib/utils/ApkZLibPair.java
index 04c1ecf..8cf79ee 100644
--- a/src/main/java/com/android/builder/utils/ApkZLibPair.java
+++ b/src/main/java/com/android/apkzlib/utils/ApkZLibPair.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.utils;
+package com.android.apkzlib.utils;
 
 /**
  * Pair implementation to use with the {@code apkzlib} library.
diff --git a/src/main/java/com/android/builder/utils/CachedFileContents.java b/src/main/java/com/android/apkzlib/utils/CachedFileContents.java
similarity index 69%
rename from src/main/java/com/android/builder/utils/CachedFileContents.java
rename to src/main/java/com/android/apkzlib/utils/CachedFileContents.java
index 4e75aa4..f2a3331 100644
--- a/src/main/java/com/android/builder/utils/CachedFileContents.java
+++ b/src/main/java/com/android/apkzlib/utils/CachedFileContents.java
@@ -14,38 +14,39 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.utils;
+package com.android.apkzlib.utils;
 
-import com.android.annotations.NonNull;
-import com.android.annotations.Nullable;
 import com.google.common.base.Objects;
 import com.google.common.hash.HashCode;
 import com.google.common.hash.Hashing;
 import com.google.common.io.Files;
-
 import java.io.File;
 import java.io.IOException;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
 /**
- * A cache for file contents. The cache allows closing a file and saving in memory its contents
- * (or some related information). It can then be used to check if the contents are still valid
- * at some later time. Typical usage flow is:
+ * A cache for file contents. The cache allows closing a file and saving in memory its contents (or
+ * some related information). It can then be used to check if the contents are still valid at some
+ * later time. Typical usage flow is:
  *
  * <p>
- * <pre>
- *    Object fileRepresentation = // ...
- *    File toWrite = // ...
- *    // Write file contents and update in memory representation
- *    CachedFileContents<Object> contents = new CachedFileContents<Object>(toWrite);
- *    contents.closed(fileRepresentation);
  *
- *    // Later, when data is needed:
- *    if (contents.isValid()) {
- *        fileRepresentation = contents.getCache();
- *    } else {
- *        // Re-read the file and recreate the file representation
- *    }
- * </pre>
+ * <pre>{@code
+ * Object fileRepresentation = // ...
+ * File toWrite = // ...
+ * // Write file contents and update in memory representation
+ * CachedFileContents<Object> contents = new CachedFileContents<Object>(toWrite);
+ * contents.closed(fileRepresentation);
+ *
+ * // Later, when data is needed:
+ * if (contents.isValid()) {
+ *     fileRepresentation = contents.getCache();
+ * } else {
+ *     // Re-read the file and recreate the file representation
+ * }
+ * }</pre>
+ *
  * @param <T> the type of cached contents
  */
 public class CachedFileContents<T> {
@@ -53,30 +54,30 @@
     /**
      * The file.
      */
-    @NonNull
-    private File mFile;
+    @Nonnull
+    private File file;
 
     /**
      * Time when last closed (time when {@link #closed(Object)} was invoked).
      */
-    private long mLastClosed;
+    private long lastClosed;
 
     /**
      * Size of the file when last closed.
      */
-    private long mSize;
+    private long size;
 
     /**
      * Hash of the file when closed. {@code null} if hashing failed for some reason.
      */
     @Nullable
-    private HashCode mHash;
+    private HashCode hash;
 
     /**
      * Cached data associated with the file.
      */
     @Nullable
-    private T mCache;
+    private T cache;
 
     /**
      * Creates a new contents. When the file is written, {@link #closed(Object)} should be invoked
@@ -84,8 +85,8 @@
      *
      * @param file the file
      */
-    public CachedFileContents(@NonNull File file) {
-        mFile = file;
+    public CachedFileContents(@Nonnull File file) {
+        this.file = file;
     }
 
     /**
@@ -97,10 +98,10 @@
      * @param cache an optional cache to save
      */
     public void closed(@Nullable T cache) {
-        mCache = cache;
-        mLastClosed = mFile.lastModified();
-        mSize = mFile.length();
-        mHash = hashFile();
+        this.cache = cache;
+        lastClosed = file.lastModified();
+        size = file.length();
+        hash = hashFile();
     }
 
     /**
@@ -113,24 +114,24 @@
     public boolean isValid() {
         boolean valid = true;
 
-        if (!mFile.exists()) {
+        if (!file.exists()) {
             valid = false;
         }
 
-        if (valid && mFile.lastModified() != mLastClosed) {
+        if (valid && file.lastModified() != lastClosed) {
             valid = false;
         }
 
-        if (valid && mFile.length() != mSize) {
+        if (valid && file.length() != size) {
             valid = false;
         }
 
-        if (valid && !Objects.equal(mHash, hashFile())) {
+        if (valid && !Objects.equal(hash, hashFile())) {
             valid = false;
         }
 
         if (!valid) {
-            mCache = null;
+            cache = null;
         }
 
         return valid;
@@ -145,7 +146,7 @@
      */
     @Nullable
     public T getCache() {
-        return mCache;
+        return cache;
     }
 
     /**
@@ -156,7 +157,7 @@
     @Nullable
     private HashCode hashFile() {
         try {
-            return Files.hash(mFile, Hashing.crc32());
+            return Files.hash(file, Hashing.crc32());
         } catch (IOException e) {
             return null;
         }
@@ -168,8 +169,8 @@
      * @return the file; this file always exists and contains the old (cached) contents of the
      * file
      */
-    @NonNull
+    @Nonnull
     public File getFile() {
-        return mFile;
+        return file;
     }
 }
diff --git a/src/main/java/com/android/builder/utils/CachedSupplier.java b/src/main/java/com/android/apkzlib/utils/CachedSupplier.java
similarity index 89%
rename from src/main/java/com/android/builder/utils/CachedSupplier.java
rename to src/main/java/com/android/apkzlib/utils/CachedSupplier.java
index ead563a..bc5eb69 100644
--- a/src/main/java/com/android/builder/utils/CachedSupplier.java
+++ b/src/main/java/com/android/apkzlib/utils/CachedSupplier.java
@@ -14,17 +14,17 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.utils;
-
-import com.android.annotations.NonNull;
+package com.android.apkzlib.utils;
 
 import java.util.function.Supplier;
+import javax.annotation.Nonnull;
 
 /**
  * Supplier that will cache a computed value and always supply the same value. It can be used to
  * lazily compute data. For example:
- * <pre>
- * CachedSupplier&lt;Integer&gt; value = new CachedSupplier&lt;&gt;(() -> {
+ *
+ * <pre>{@code
+ * CachedSupplier<Integer> value = new CachedSupplier<>(() -> {
  *     Integer result;
  *     // Do some expensive computation.
  *     return result;
@@ -41,12 +41,13 @@
  * }
  *
  * // If neither a nor b are true, we avoid doing the computation at all.
- * </pre>
+ * }</pre>
  */
 public class CachedSupplier<T> {
 
     /**
-     * The cached data, {@code null} if computation resulted in {@code null}.
+     * The cached data, {@code null} if computation resulted in {@code null}. It is also
+     * {@code null} if the cached data has not yet been computed.
      */
     private T cached;
 
@@ -58,13 +59,13 @@
     /**
      * Actual supplier of data, if computation is needed.
      */
-    @NonNull
+    @Nonnull
     private final Supplier<T> supplier;
 
     /**
      * Creates a new supplier.
      */
-    public CachedSupplier(@NonNull Supplier<T> supplier) {
+    public CachedSupplier(@Nonnull Supplier<T> supplier) {
         valid = false;
         this.supplier = supplier;
     }
diff --git a/src/main/java/com/android/builder/utils/IOExceptionConsumer.java b/src/main/java/com/android/apkzlib/utils/IOExceptionConsumer.java
similarity index 86%
rename from src/main/java/com/android/builder/utils/IOExceptionConsumer.java
rename to src/main/java/com/android/apkzlib/utils/IOExceptionConsumer.java
index 89b430b..fcf3ab0 100644
--- a/src/main/java/com/android/builder/utils/IOExceptionConsumer.java
+++ b/src/main/java/com/android/apkzlib/utils/IOExceptionConsumer.java
@@ -14,13 +14,13 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.utils;
+package com.android.apkzlib.utils;
 
-import com.android.annotations.NonNull;
-import com.android.annotations.Nullable;
 import java.io.IOException;
 import java.io.UncheckedIOException;
 import java.util.function.Consumer;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
 /**
  * Consumer that can throw an {@link IOException}.
@@ -40,8 +40,8 @@
      *
      * @param c the consumer
      */
-    @NonNull
-    static <T> Consumer<T> asConsumer(@NonNull IOExceptionConsumer<T> c)  {
+    @Nonnull
+    static <T> Consumer<T> asConsumer(@Nonnull IOExceptionConsumer<T> c)  {
         return i -> {
             try {
                 c.accept(i);
diff --git a/src/main/java/com/android/builder/utils/IOExceptionFunction.java b/src/main/java/com/android/apkzlib/utils/IOExceptionFunction.java
similarity index 83%
rename from src/main/java/com/android/builder/utils/IOExceptionFunction.java
rename to src/main/java/com/android/apkzlib/utils/IOExceptionFunction.java
index a13cc69..6d84b5b 100644
--- a/src/main/java/com/android/builder/utils/IOExceptionFunction.java
+++ b/src/main/java/com/android/apkzlib/utils/IOExceptionFunction.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2015 The Android Open Source Project
+ * 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.
@@ -14,14 +14,13 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.utils;
-
-import com.android.annotations.NonNull;
-import com.android.annotations.Nullable;
+package com.android.apkzlib.utils;
 
 import java.io.IOException;
 import java.io.UncheckedIOException;
 import java.util.function.Function;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
 /**
  * Function that can throw an I/O Exception
@@ -41,8 +40,8 @@
      *
      * @param f the function
      */
-    @NonNull
-    static <F, T> Function<F, T> asFunction(@NonNull IOExceptionFunction<F, T> f)  {
+    @Nonnull
+    static <F, T> Function<F, T> asFunction(@Nonnull IOExceptionFunction<F, T> f)  {
         return i -> {
             try {
                 return f.apply(i);
diff --git a/src/main/java/com/android/builder/utils/IOExceptionRunnable.java b/src/main/java/com/android/apkzlib/utils/IOExceptionRunnable.java
similarity index 84%
rename from src/main/java/com/android/builder/utils/IOExceptionRunnable.java
rename to src/main/java/com/android/apkzlib/utils/IOExceptionRunnable.java
index 7dbcea3..67ed75c 100644
--- a/src/main/java/com/android/builder/utils/IOExceptionRunnable.java
+++ b/src/main/java/com/android/apkzlib/utils/IOExceptionRunnable.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2015 The Android Open Source Project
+ * 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.
@@ -14,11 +14,11 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.utils;
+package com.android.apkzlib.utils;
 
-import com.android.annotations.NonNull;
 import java.io.IOException;
 import java.io.UncheckedIOException;
+import javax.annotation.Nonnull;
 
 /**
  * Runnable that can throw I/O exceptions.
@@ -28,6 +28,7 @@
 
     /**
      * Runs the runnable.
+     *
      * @throws IOException failed to run
      */
     void run() throws IOException;
@@ -37,8 +38,8 @@
      *
      * @param r the runnable
      */
-    @NonNull
-    public static Runnable asRunnable(@NonNull IOExceptionRunnable r) {
+    @Nonnull
+    public static Runnable asRunnable(@Nonnull IOExceptionRunnable r) {
         return () -> {
             try {
                 r.run();
diff --git a/src/main/java/com/android/builder/utils/IOExceptionWrapper.java b/src/main/java/com/android/apkzlib/utils/IOExceptionWrapper.java
similarity index 87%
rename from src/main/java/com/android/builder/utils/IOExceptionWrapper.java
rename to src/main/java/com/android/apkzlib/utils/IOExceptionWrapper.java
index bb0423e..067b260 100644
--- a/src/main/java/com/android/builder/utils/IOExceptionWrapper.java
+++ b/src/main/java/com/android/apkzlib/utils/IOExceptionWrapper.java
@@ -14,11 +14,10 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.utils;
-
-import com.android.annotations.NonNull;
+package com.android.apkzlib.utils;
 
 import java.io.IOException;
+import javax.annotation.Nonnull;
 
 /**
  * Runtime exception used to encapsulate an IO Exception. This is used to allow throwing I/O
@@ -31,12 +30,12 @@
      *
      * @param e the I/O exception to encapsulate
      */
-    public IOExceptionWrapper(@NonNull IOException e) {
+    public IOExceptionWrapper(@Nonnull IOException e) {
         super(e);
     }
 
     @Override
-    @NonNull
+    @Nonnull
     public IOException getCause() {
         return (IOException) super.getCause();
     }
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/StoredEntryType.java b/src/main/java/com/android/apkzlib/utils/package-info.java
similarity index 67%
copy from src/main/java/com/android/builder/internal/packaging/zip/StoredEntryType.java
copy to src/main/java/com/android/apkzlib/utils/package-info.java
index 22983ab..2f17b60 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/StoredEntryType.java
+++ b/src/main/java/com/android/apkzlib/utils/package-info.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2017 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.
@@ -14,19 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip;
-
 /**
- * Type of stored entry.
+ * Utilities to work with {@code apkzlib}.
  */
-public enum StoredEntryType {
-    /**
-     * Entry is a file.
-     */
-    FILE,
-
-    /**
-     * Entry is a directory.
-     */
-    DIRECTORY
-}
+package com.android.apkzlib.utils;
\ No newline at end of file
diff --git a/src/main/java/com/android/builder/packaging/ApkCreator.java b/src/main/java/com/android/apkzlib/zfile/ApkCreator.java
similarity index 72%
rename from src/main/java/com/android/builder/packaging/ApkCreator.java
rename to src/main/java/com/android/apkzlib/zfile/ApkCreator.java
index d62d730..3cac7dc 100644
--- a/src/main/java/com/android/builder/packaging/ApkCreator.java
+++ b/src/main/java/com/android/apkzlib/zfile/ApkCreator.java
@@ -14,16 +14,15 @@
  * limitations under the License.
  */
 
-package com.android.builder.packaging;
-
-import com.android.annotations.NonNull;
-import com.android.annotations.Nullable;
+package com.android.apkzlib.zfile;
 
 import java.io.Closeable;
 import java.io.File;
 import java.io.IOException;
 import java.util.function.Function;
 import java.util.function.Predicate;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
 /**
  * Creates or updates APKs based on provided entries.
@@ -32,9 +31,9 @@
 
     /**
      * Copies the content of a Jar/Zip archive into the receiver archive.
-     * <p>
-     * An optional {@link ZipEntryFilter} allows to selectively choose which files
-     * to copy over.
+     *
+     * <p>An optional predicate allows to selectively choose which files to copy over and an
+     * option function allows renaming the files as they are copied.
      *
      * @param zip the zip to copy data from
      * @param transform an optional transform to apply to file names before copying them
@@ -43,8 +42,11 @@
      * predicate applies after transformation
      * @throws IOException I/O error
      */
-    void writeZip(@NonNull File zip, @Nullable Function<String, String> transform,
-            @Nullable Predicate<String> isIgnored) throws IOException;
+    void writeZip(
+            @Nonnull File zip,
+            @Nullable Function<String, String> transform,
+            @Nullable Predicate<String> isIgnored)
+            throws IOException;
 
     /**
      * Writes a new {@link File} into the archive. If a file already existed with the given
@@ -54,7 +56,7 @@
      * @param apkPath the filepath inside the archive.
      * @throws IOException I/O error
      */
-    void writeFile(@NonNull File inputFile, @NonNull String apkPath) throws IOException;
+    void writeFile(@Nonnull File inputFile, @Nonnull String apkPath) throws IOException;
 
     /**
      * Deletes a file in a given path.
@@ -62,5 +64,8 @@
      * @param apkPath the path to remove
      * @throws IOException failed to remove the entry
      */
-    void deleteFile(@NonNull String apkPath) throws IOException;
+    void deleteFile(@Nonnull String apkPath) throws IOException;
+
+    /** Returns true if the APK will be rewritten on close. */
+    boolean hasPendingChangesWithWait() throws IOException;
 }
diff --git a/src/main/java/com/android/builder/packaging/ApkCreatorFactory.java b/src/main/java/com/android/apkzlib/zfile/ApkCreatorFactory.java
similarity index 75%
rename from src/main/java/com/android/builder/packaging/ApkCreatorFactory.java
rename to src/main/java/com/android/apkzlib/zfile/ApkCreatorFactory.java
index 3501c33..503eaf1 100644
--- a/src/main/java/com/android/builder/packaging/ApkCreatorFactory.java
+++ b/src/main/java/com/android/apkzlib/zfile/ApkCreatorFactory.java
@@ -14,18 +14,17 @@
  * limitations under the License.
  */
 
-package com.android.builder.packaging;
+package com.android.apkzlib.zfile;
 
 import static com.google.common.base.Preconditions.checkNotNull;
 
-import com.android.annotations.NonNull;
-import com.android.annotations.Nullable;
 import com.google.common.base.Preconditions;
-
 import java.io.File;
 import java.security.PrivateKey;
 import java.security.cert.X509Certificate;
 import java.util.function.Predicate;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
 /**
  * Factory that creates instances of {@link ApkCreator}.
@@ -37,7 +36,7 @@
      *
      * @param creationData the information to create the APK
      */
-    ApkCreator make(@NonNull CreationData creationData);
+    ApkCreator make(@Nonnull CreationData creationData);
 
     /**
      * Data structure with the required information to initiate the creation of an APK. See
@@ -49,54 +48,60 @@
          * The path where the APK should be located. May already exist or not (if it does, then
          * the APK may be updated instead of created).
          */
-        @NonNull
-        private final File mApkPath;
+        @Nonnull
+        private final File apkPath;
 
         /**
          * Key used to sign the APK. May be {@code null}.
          */
         @Nullable
-        private final PrivateKey mKey;
+        private final PrivateKey key;
 
         /**
-         * Certificate used to sign the APK. Is {@code null} if and only if {@link #mKey} is
+         * Certificate used to sign the APK. Is {@code null} if and only if {@link #key} is
          * {@code null}.
          */
         @Nullable
-        private final X509Certificate mCertificate;
+        private final X509Certificate certificate;
 
         /**
          * Whether signing the APK with JAR Signing Scheme (aka v1 signing) is enabled.
          */
-        private final boolean mV1SigningEnabled;
+        private final boolean v1SigningEnabled;
 
         /**
          * Whether signing the APK with APK Signature Scheme v2 (aka v2 signing) is enabled.
          */
-        private final boolean mV2SigningEnabled;
+        private final boolean v2SigningEnabled;
 
         /**
          * Built-by information for the APK, if any.
          */
         @Nullable
-        private final String mBuiltBy;
+        private final String builtBy;
 
         /**
          * Created-by information for the APK, if any.
          */
         @Nullable
-        private final String mCreatedBy;
+        private final String createdBy;
 
         /**
          * Minimum SDk version that will run the APK.
          */
-        private final int mMinSdkVersion;
+        private final int minSdkVersion;
 
-        @NonNull
-        private final NativeLibrariesPackagingMode mNativeLibrariesPackagingMode;
+        /**
+         * How should native libraries be packaged?
+         */
+        @Nonnull
+        private final NativeLibrariesPackagingMode nativeLibrariesPackagingMode;
 
-        @NonNull
-        private final Predicate<String> mNoCompressPredicate;
+        /**
+         * Predicate identifying paths that should not be compressed.
+         */
+        @Nonnull
+        private final Predicate<String> noCompressPredicate;
 
         /**
          *
@@ -115,10 +120,11 @@
          * default should be used
          * @param minSdkVersion minimum SDK version that will run the APK
          * @param nativeLibrariesPackagingMode packaging mode for native libraries
-         * @param noCompressPredicate predicate to decide which file paths should be uncompressed
+         * @param noCompressPredicate predicate to decide which file paths should be uncompressed;
+         * returns {@code true} for files that should not be compressed
          */
         public CreationData(
-                @NonNull File apkPath,
+                @Nonnull File apkPath,
                 @Nullable PrivateKey key,
                 @Nullable X509Certificate certificate,
                 boolean v1SigningEnabled,
@@ -126,22 +132,22 @@
                 @Nullable String builtBy,
                 @Nullable String createdBy,
                 int minSdkVersion,
-                @NonNull NativeLibrariesPackagingMode nativeLibrariesPackagingMode,
-                @NonNull Predicate<String> noCompressPredicate) {
+                @Nonnull NativeLibrariesPackagingMode nativeLibrariesPackagingMode,
+                @Nonnull Predicate<String> noCompressPredicate) {
             Preconditions.checkArgument((key == null) == (certificate == null),
                     "(key == null) != (certificate == null)");
             Preconditions.checkArgument(minSdkVersion >= 0, "minSdkVersion < 0");
 
-            mApkPath = apkPath;
-            mKey = key;
-            mCertificate = certificate;
-            mV1SigningEnabled = v1SigningEnabled;
-            mV2SigningEnabled = v2SigningEnabled;
-            mBuiltBy = builtBy;
-            mCreatedBy = createdBy;
-            mMinSdkVersion = minSdkVersion;
-            mNativeLibrariesPackagingMode = checkNotNull(nativeLibrariesPackagingMode);
-            mNoCompressPredicate = checkNotNull(noCompressPredicate);
+            this.apkPath = apkPath;
+            this.key = key;
+            this.certificate = certificate;
+            this.v1SigningEnabled = v1SigningEnabled;
+            this.v2SigningEnabled = v2SigningEnabled;
+            this.builtBy = builtBy;
+            this.createdBy = createdBy;
+            this.minSdkVersion = minSdkVersion;
+            this.nativeLibrariesPackagingMode = checkNotNull(nativeLibrariesPackagingMode);
+            this.noCompressPredicate = checkNotNull(noCompressPredicate);
         }
 
         /**
@@ -150,9 +156,9 @@
          *
          * @return the path that may already exist or not
          */
-        @NonNull
+        @Nonnull
         public File getApkPath() {
-            return mApkPath;
+            return apkPath;
         }
 
         /**
@@ -162,7 +168,7 @@
          */
         @Nullable
         public PrivateKey getPrivateKey() {
-            return mKey;
+            return key;
         }
 
         /**
@@ -173,7 +179,7 @@
          */
         @Nullable
         public X509Certificate getCertificate() {
-            return mCertificate;
+            return certificate;
         }
 
         /**
@@ -181,7 +187,7 @@
          * scheme).
          */
         public boolean isV1SigningEnabled() {
-            return mV1SigningEnabled;
+            return v1SigningEnabled;
         }
 
         /**
@@ -189,7 +195,7 @@
          * scheme).
          */
         public boolean isV2SigningEnabled() {
-            return mV2SigningEnabled;
+            return v2SigningEnabled;
         }
 
         /**
@@ -199,7 +205,7 @@
          */
         @Nullable
         public String getBuiltBy() {
-            return mBuiltBy;
+            return builtBy;
         }
 
         /**
@@ -209,7 +215,7 @@
          */
         @Nullable
         public String getCreatedBy() {
-            return mCreatedBy;
+            return createdBy;
         }
 
         /**
@@ -218,23 +224,23 @@
          * @return the minimum SDK version
          */
         public int getMinSdkVersion() {
-            return mMinSdkVersion;
+            return minSdkVersion;
         }
 
         /**
          * Returns the packaging policy that the {@link ApkCreator} should use for native libraries.
          */
-        @NonNull
+        @Nonnull
         public NativeLibrariesPackagingMode getNativeLibrariesPackagingMode() {
-            return mNativeLibrariesPackagingMode;
+            return nativeLibrariesPackagingMode;
         }
 
         /**
          * Returns the predicate to decide which file paths should be uncompressed.
          */
-        @NonNull
+        @Nonnull
         public Predicate<String> getNoCompressPredicate() {
-            return mNoCompressPredicate;
+            return noCompressPredicate;
         }
     }
 }
diff --git a/src/main/java/com/android/apkzlib/zfile/ApkZFileCreator.java b/src/main/java/com/android/apkzlib/zfile/ApkZFileCreator.java
new file mode 100644
index 0000000..c85ad44
--- /dev/null
+++ b/src/main/java/com/android/apkzlib/zfile/ApkZFileCreator.java
@@ -0,0 +1,192 @@
+/*
+ * 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.apkzlib.zfile;
+
+import com.android.apkzlib.zip.AlignmentRule;
+import com.android.apkzlib.zip.AlignmentRules;
+import com.android.apkzlib.zip.StoredEntry;
+import com.android.apkzlib.zip.ZFile;
+import com.android.apkzlib.zip.ZFileOptions;
+import com.google.common.base.Preconditions;
+import com.google.common.io.Closer;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/**
+ * {@link ApkCreator} that uses {@link ZFileOptions} to generate the APK.
+ */
+class ApkZFileCreator implements ApkCreator {
+
+    /**
+     * Suffix for native libraries.
+     */
+    private static final String NATIVE_LIBRARIES_SUFFIX = ".so";
+
+    /**
+     * Shared libraries are alignment at 4096 boundaries.
+     */
+    private static final AlignmentRule SO_RULE =
+            AlignmentRules.constantForSuffix(NATIVE_LIBRARIES_SUFFIX, 4096);
+
+    /**
+     * The zip file.
+     */
+    @Nonnull
+    private final ZFile zip;
+
+    /**
+     * Has the zip file been closed?
+     */
+    private boolean closed;
+
+    /**
+     * Predicate defining which files should not be compressed.
+     */
+    @Nonnull
+    private final Predicate<String> noCompressPredicate;
+
+    /**
+     * Creates a new creator.
+     *
+     * @param creationData the data needed to create the APK
+     * @param options zip file options
+     * @throws IOException failed to create the zip
+     */
+    ApkZFileCreator(
+            @Nonnull ApkCreatorFactory.CreationData creationData,
+            @Nonnull ZFileOptions options)
+            throws IOException {
+
+        switch (creationData.getNativeLibrariesPackagingMode()) {
+            case COMPRESSED:
+                noCompressPredicate = creationData.getNoCompressPredicate();
+                break;
+            case UNCOMPRESSED_AND_ALIGNED:
+                noCompressPredicate =
+                        creationData.getNoCompressPredicate().or(
+                                name -> name.endsWith(NATIVE_LIBRARIES_SUFFIX));
+                options.setAlignmentRule(
+                        AlignmentRules.compose(SO_RULE, options.getAlignmentRule()));
+                break;
+            default:
+                throw new AssertionError();
+        }
+
+        zip = ZFiles.apk(
+                creationData.getApkPath(),
+                options,
+                creationData.getPrivateKey(),
+                creationData.getCertificate(),
+                creationData.isV1SigningEnabled(),
+                creationData.isV2SigningEnabled(),
+                creationData.getBuiltBy(),
+                creationData.getCreatedBy(),
+                creationData.getMinSdkVersion());
+        closed = false;
+    }
+
+    @Override
+    public void writeZip(@Nonnull File zip, @Nullable Function<String, String> transform,
+            @Nullable Predicate<String> isIgnored) throws IOException {
+        Preconditions.checkState(!closed, "closed == true");
+        Preconditions.checkArgument(zip.isFile(), "!zip.isFile()");
+
+        Closer closer = Closer.create();
+        try {
+            ZFile toMerge = closer.register(new ZFile(zip));
+
+            Predicate<String> ignorePredicate;
+            if (isIgnored == null) {
+                ignorePredicate = s -> false;
+            } else {
+                ignorePredicate = isIgnored;
+            }
+
+            // Files that *must* be uncompressed in the result should not be merged and should be
+            // added after. This is just very slightly less efficient than ignoring just the ones
+            // that were compressed and must be uncompressed, but it is a lot simpler :)
+            Predicate<String> noMergePredicate = ignorePredicate.or(noCompressPredicate);
+
+            this.zip.mergeFrom(toMerge, noMergePredicate);
+
+            for (StoredEntry toMergeEntry : toMerge.entries()) {
+                String path = toMergeEntry.getCentralDirectoryHeader().getName();
+                if (noCompressPredicate.test(path) && !ignorePredicate.test(path)) {
+                    // This entry *must* be uncompressed so it was ignored in the merge and should
+                    // now be added to the apk.
+                    try (InputStream ignoredData = toMergeEntry.open()) {
+                        this.zip.add(path, ignoredData, false);
+                    }
+                }
+            }
+        } catch (Throwable t) {
+            throw closer.rethrow(t);
+        } finally {
+            closer.close();
+        }
+    }
+
+    @Override
+    public void writeFile(@Nonnull File inputFile, @Nonnull String apkPath) throws IOException {
+        Preconditions.checkState(!closed, "closed == true");
+
+        boolean mayCompress = !noCompressPredicate.test(apkPath);
+
+        Closer closer = Closer.create();
+        try {
+            FileInputStream inputFileStream = closer.register(new FileInputStream(inputFile));
+            zip.add(apkPath, inputFileStream, mayCompress);
+        } catch (IOException e) {
+            throw closer.rethrow(e, IOException.class);
+        } catch (Throwable t) {
+            throw closer.rethrow(t);
+        } finally {
+            closer.close();
+        }
+    }
+
+    @Override
+    public void deleteFile(@Nonnull String apkPath) throws IOException {
+        Preconditions.checkState(!closed, "closed == true");
+
+        StoredEntry entry = zip.get(apkPath);
+        if (entry != null) {
+            entry.delete();
+        }
+    }
+
+    @Override
+    public boolean hasPendingChangesWithWait() throws IOException {
+        return zip.hasPendingChangesWithWait();
+    }
+
+    @Override
+    public void close() throws IOException {
+        if (closed) {
+            return;
+        }
+
+        zip.close();
+        closed = true;
+    }
+}
diff --git a/src/main/java/com/android/builder/internal/packaging/zfile/ApkZFileCreatorFactory.java b/src/main/java/com/android/apkzlib/zfile/ApkZFileCreatorFactory.java
similarity index 67%
rename from src/main/java/com/android/builder/internal/packaging/zfile/ApkZFileCreatorFactory.java
rename to src/main/java/com/android/apkzlib/zfile/ApkZFileCreatorFactory.java
index feb8f25..2e4c7c9 100644
--- a/src/main/java/com/android/builder/internal/packaging/zfile/ApkZFileCreatorFactory.java
+++ b/src/main/java/com/android/apkzlib/zfile/ApkZFileCreatorFactory.java
@@ -14,14 +14,12 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zfile;
+package com.android.apkzlib.zfile;
 
-import com.android.annotations.NonNull;
-import com.android.builder.internal.packaging.zip.ZFileOptions;
-import com.android.builder.packaging.ApkCreator;
-import com.android.builder.packaging.ApkCreatorFactory;
+import com.android.apkzlib.zip.ZFileOptions;
 import java.io.IOException;
 import java.io.UncheckedIOException;
+import javax.annotation.Nonnull;
 
 /**
  * Creates instances of {@link ApkZFileCreator}.
@@ -31,24 +29,24 @@
     /**
      * Options for the {@link ZFileOptions} to use in all APKs.
      */
-    @NonNull
-    private final ZFileOptions mOptions;
+    @Nonnull
+    private final ZFileOptions options;
 
     /**
      * Creates a new factory.
      *
      * @param options the options to use for all instances created
      */
-    public ApkZFileCreatorFactory(@NonNull ZFileOptions options) {
-        mOptions = options;
+    public ApkZFileCreatorFactory(@Nonnull ZFileOptions options) {
+        this.options = options;
     }
 
 
     @Override
-    @NonNull
-    public ApkCreator make(@NonNull CreationData creationData) {
+    @Nonnull
+    public ApkCreator make(@Nonnull CreationData creationData) {
         try {
-            return new ApkZFileCreator(creationData, mOptions);
+            return new ApkZFileCreator(creationData, options);
         } catch (IOException e) {
             throw new UncheckedIOException(e);
         }
diff --git a/src/main/java/com/android/builder/packaging/ManifestAttributes.java b/src/main/java/com/android/apkzlib/zfile/ManifestAttributes.java
similarity index 96%
rename from src/main/java/com/android/builder/packaging/ManifestAttributes.java
rename to src/main/java/com/android/apkzlib/zfile/ManifestAttributes.java
index dc6bdf3..d8a7d2d 100644
--- a/src/main/java/com/android/builder/packaging/ManifestAttributes.java
+++ b/src/main/java/com/android/apkzlib/zfile/ManifestAttributes.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.builder.packaging;
+package com.android.apkzlib.zfile;
 
 /**
  * Java manifest attributes and some default values.
diff --git a/src/main/java/com/android/builder/packaging/NativeLibrariesPackagingMode.java b/src/main/java/com/android/apkzlib/zfile/NativeLibrariesPackagingMode.java
similarity index 96%
rename from src/main/java/com/android/builder/packaging/NativeLibrariesPackagingMode.java
rename to src/main/java/com/android/apkzlib/zfile/NativeLibrariesPackagingMode.java
index df28ef5..8916abe 100644
--- a/src/main/java/com/android/builder/packaging/NativeLibrariesPackagingMode.java
+++ b/src/main/java/com/android/apkzlib/zfile/NativeLibrariesPackagingMode.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.builder.packaging;
+package com.android.apkzlib.zfile;
 
 /**
  * Describes how native libs should be packaged.
diff --git a/src/main/java/com/android/builder/internal/packaging/zfile/ZFiles.java b/src/main/java/com/android/apkzlib/zfile/ZFiles.java
similarity index 63%
rename from src/main/java/com/android/builder/internal/packaging/zfile/ZFiles.java
rename to src/main/java/com/android/apkzlib/zfile/ZFiles.java
index 39fb279..d5102a6 100644
--- a/src/main/java/com/android/builder/internal/packaging/zfile/ZFiles.java
+++ b/src/main/java/com/android/apkzlib/zfile/ZFiles.java
@@ -14,25 +14,22 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zfile;
+package com.android.apkzlib.zfile;
 
-import com.android.annotations.NonNull;
-import com.android.annotations.Nullable;
-import com.android.builder.internal.packaging.sign.FullApkSignExtension;
-import com.android.builder.internal.packaging.sign.ManifestGenerationExtension;
-import com.android.builder.internal.packaging.sign.SignatureExtension;
-import com.android.builder.internal.packaging.zip.AlignmentRule;
-import com.android.builder.internal.packaging.zip.AlignmentRules;
-import com.android.builder.internal.packaging.zip.StoredEntry;
-import com.android.builder.internal.packaging.zip.ZFile;
-import com.android.builder.internal.packaging.zip.ZFileOptions;
-
+import com.android.apkzlib.sign.ManifestGenerationExtension;
+import com.android.apkzlib.sign.SigningExtension;
+import com.android.apkzlib.zip.AlignmentRule;
+import com.android.apkzlib.zip.AlignmentRules;
+import com.android.apkzlib.zip.ZFile;
+import com.android.apkzlib.zip.ZFileOptions;
 import java.io.File;
 import java.io.IOException;
 import java.security.InvalidKeyException;
 import java.security.NoSuchAlgorithmException;
 import java.security.PrivateKey;
 import java.security.cert.X509Certificate;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
 /**
  * Factory for {@link ZFile}s that are specifically configured to be APKs, AARs, ...
@@ -64,8 +61,8 @@
      * @return the zip file
      * @throws IOException failed to create the zip file
      */
-    @NonNull
-    public static ZFile apk(@NonNull File f, @NonNull ZFileOptions options) throws IOException {
+    @Nonnull
+    public static ZFile apk(@Nonnull File f, @Nonnull ZFileOptions options) throws IOException {
         options.setAlignmentRule(
                 AlignmentRules.compose(options.getAlignmentRule(), APK_DEFAULT_RULE));
         return new ZFile(f, options);
@@ -91,10 +88,10 @@
      * @return the zip file
      * @throws IOException failed to create the zip file
      */
-    @NonNull
+    @Nonnull
     public static ZFile apk(
-            @NonNull File f,
-            @NonNull ZFileOptions options,
+            @Nonnull File f,
+            @Nonnull ZFileOptions options,
             @Nullable PrivateKey key,
             @Nullable X509Certificate certificate,
             boolean v1SigningEnabled,
@@ -119,35 +116,12 @@
 
         if (key != null && certificate != null) {
             try {
-                if (v1SigningEnabled) {
-                    String apkSignedHeaderValue =
-                            (v2SigningEnabled)
-                                    ? SignatureExtension
-                                            .SIGNATURE_ANDROID_APK_SIGNER_VALUE_WHEN_V2_SIGNED
-                                    : null;
-                    SignatureExtension jarSignatureSchemeExt = new SignatureExtension(manifestExt,
-                            minSdkVersion, certificate, key,
-                            apkSignedHeaderValue);
-                    jarSignatureSchemeExt.register();
-                }
-                if (v2SigningEnabled) {
-                    FullApkSignExtension apkSignatureSchemeV2Ext =
-                            new FullApkSignExtension(
-                                    zfile,
-                                    minSdkVersion,
-                                    certificate,
-                                    key);
-                    apkSignatureSchemeV2Ext.register();
-                }
-                if (!v1SigningEnabled) {
-                    // Remove v1 signature files from the APK
-                    for (StoredEntry entry : zfile.entries()) {
-                        if (SignatureExtension.isIgnoredFile(
-                                entry.getCentralDirectoryHeader().getName())) {
-                            entry.delete();
-                        }
-                    }
-                }
+                new SigningExtension(
+                        minSdkVersion,
+                        certificate,
+                        key,
+                        v1SigningEnabled,
+                        v2SigningEnabled).register(zfile);
             } catch (NoSuchAlgorithmException | InvalidKeyException e) {
                 throw new IOException("Failed to create signature extensions", e);
             }
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/StoredEntryType.java b/src/main/java/com/android/apkzlib/zfile/package-info.java
similarity index 67%
copy from src/main/java/com/android/builder/internal/packaging/zip/StoredEntryType.java
copy to src/main/java/com/android/apkzlib/zfile/package-info.java
index 22983ab..0c5ab6d 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/StoredEntryType.java
+++ b/src/main/java/com/android/apkzlib/zfile/package-info.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2017 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.
@@ -14,19 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip;
-
 /**
- * Type of stored entry.
+ * The {@code zfile} package contains
  */
-public enum StoredEntryType {
-    /**
-     * Entry is a file.
-     */
-    FILE,
-
-    /**
-     * Entry is a directory.
-     */
-    DIRECTORY
-}
+package com.android.apkzlib.zfile;
\ No newline at end of file
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/AlignmentRule.java b/src/main/java/com/android/apkzlib/zip/AlignmentRule.java
similarity index 89%
rename from src/main/java/com/android/builder/internal/packaging/zip/AlignmentRule.java
rename to src/main/java/com/android/apkzlib/zip/AlignmentRule.java
index 5fcd0a6..4ee6963 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/AlignmentRule.java
+++ b/src/main/java/com/android/apkzlib/zip/AlignmentRule.java
@@ -14,9 +14,9 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
 
-import com.android.annotations.NonNull;
+import javax.annotation.Nonnull;
 
 /**
  * An alignment rule defines how to a file should be aligned in a zip, based on its name.
@@ -35,7 +35,5 @@
      * @return the alignment value, always greater than {@code 0}; if this rule places no
      * restrictions on the provided path, then {@link AlignmentRule#NO_ALIGNMENT} is returned
      */
-    int alignment(@NonNull String path);
+    int alignment(@Nonnull String path);
 }
-
-
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/AlignmentRules.java b/src/main/java/com/android/apkzlib/zip/AlignmentRules.java
similarity index 91%
rename from src/main/java/com/android/builder/internal/packaging/zip/AlignmentRules.java
rename to src/main/java/com/android/apkzlib/zip/AlignmentRules.java
index 01e472f..b06a596 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/AlignmentRules.java
+++ b/src/main/java/com/android/apkzlib/zip/AlignmentRules.java
@@ -14,10 +14,10 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
 
-import com.android.annotations.NonNull;
 import com.google.common.base.Preconditions;
+import javax.annotation.Nonnull;
 
 /**
  * Factory for instances of {@link AlignmentRule}.
@@ -46,7 +46,7 @@
      * @param alignment the alignment for paths that match the provided suffix
      * @return the rule
      */
-    public static AlignmentRule constantForSuffix(@NonNull String suffix, int alignment) {
+    public static AlignmentRule constantForSuffix(@Nonnull String suffix, int alignment) {
         Preconditions.checkArgument(!suffix.isEmpty(), "suffix.isEmpty()");
         Preconditions.checkArgument(alignment > 0, "alignment <= 0");
 
@@ -62,7 +62,7 @@
      * {@link AlignmentRule#NO_ALIGNMENT} is returned
      * @return the composition rule
      */
-    public static AlignmentRule compose(@NonNull AlignmentRule... rules) {
+    public static AlignmentRule compose(@Nonnull AlignmentRule... rules) {
         return (String path) -> {
             for (AlignmentRule r : rules) {
                 int align = r.alignment(path);
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/CentralDirectory.java b/src/main/java/com/android/apkzlib/zip/CentralDirectory.java
similarity index 80%
rename from src/main/java/com/android/builder/internal/packaging/zip/CentralDirectory.java
rename to src/main/java/com/android/apkzlib/zip/CentralDirectory.java
index 92392cf..44389c1 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/CentralDirectory.java
+++ b/src/main/java/com/android/apkzlib/zip/CentralDirectory.java
@@ -14,11 +14,11 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
 
-import com.android.annotations.NonNull;
-import com.android.builder.internal.utils.CachedSupplier;
-import com.android.builder.internal.packaging.zip.utils.MsDosDateTimeUtils;
+import com.android.apkzlib.utils.CachedSupplier;
+import com.android.apkzlib.zip.utils.MsDosDateTimeUtils;
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
@@ -26,14 +26,13 @@
 import com.google.common.primitives.Ints;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
-
 import java.io.IOException;
 import java.io.UncheckedIOException;
 import java.nio.ByteBuffer;
-import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import javax.annotation.Nonnull;
 
 /**
  * Representation of the central directory of a zip archive.
@@ -54,7 +53,8 @@
     /**
      * Field in the central directory with the minimum version required to extract the entry.
      */
-    private static final ZipField.F2 F_VERSION_EXTRACT = new ZipField.F2(F_MADE_BY.endOffset(),
+    @VisibleForTesting
+    static final ZipField.F2 F_VERSION_EXTRACT = new ZipField.F2(F_MADE_BY.endOffset(),
             "Version to extract", new ZipFieldInvariantNonNegative());
 
     /**
@@ -174,30 +174,37 @@
     /**
      * Contains all entries in the directory mapped from their names.
      */
-    @NonNull
-    private final Map<String, StoredEntry> mEntries;
+    @Nonnull
+    private final Map<String, StoredEntry> entries;
 
     /**
      * The file where this directory belongs to.
      */
-    @NonNull
-    private final ZFile mFile;
+    @Nonnull
+    private final ZFile file;
 
     /**
      * Supplier that provides a byte representation of the central directory.
      */
-    @NonNull
-    private final CachedSupplier<byte[]> mBytesSupplier;
+    @Nonnull
+    private final CachedSupplier<byte[]> bytesSupplier;
+
+    /**
+     * Verify log for the central directory.
+     */
+    @Nonnull
+    private final VerifyLog verifyLog;
 
     /**
      * Creates a new, empty, central directory, for a given zip file.
      *
      * @param file the file
      */
-    CentralDirectory(@NonNull ZFile file) {
-        mEntries = Maps.newHashMap();
-        mFile = file;
-        mBytesSupplier = new CachedSupplier<>(this::computeByteRepresentation);
+    CentralDirectory(@Nonnull ZFile file) {
+        entries = Maps.newHashMap();
+        this.file = file;
+        bytesSupplier = new CachedSupplier<>(this::computeByteRepresentation);
+        verifyLog = file.getVerifyLog();
     }
 
     /**
@@ -214,7 +221,7 @@
      * @throws IOException failed to read data from the zip, or the central directory is corrupted
      * or has unsupported features
      */
-    static CentralDirectory makeFromData(@NonNull ByteBuffer bytes, int count, @NonNull ZFile file)
+    static CentralDirectory makeFromData(@Nonnull ByteBuffer bytes, int count, @Nonnull ZFile file)
             throws IOException {
         Preconditions.checkNotNull(bytes, "bytes == null");
         Preconditions.checkArgument(count >= 0, "count < 0");
@@ -223,7 +230,7 @@
 
         for (int i = 0; i < count; i++) {
             try {
-                directory.readEntry(bytes, file);
+                directory.readEntry(bytes);
             } catch (IOException e) {
                 throw new IOException(
                         "Failed to read directory entry index "
@@ -247,52 +254,50 @@
      * @param file the zip file itself
      * @return the created central directory
      */
-    static CentralDirectory makeFromEntries(@NonNull Set<StoredEntry> entries,
-            @NonNull ZFile file) {
+    static CentralDirectory makeFromEntries(
+            @Nonnull Set<StoredEntry> entries,
+            @Nonnull ZFile file) {
         CentralDirectory directory = new CentralDirectory(file);
         for (StoredEntry entry : entries) {
             CentralDirectoryHeader cdr = entry.getCentralDirectoryHeader();
             Preconditions.checkArgument(
-                    !directory.mEntries.containsKey(cdr.getName()),
+                    !directory.entries.containsKey(cdr.getName()),
                     "Duplicate filename");
-            directory.mEntries.put(cdr.getName(), entry);
+            directory.entries.put(cdr.getName(), entry);
         }
 
         return directory;
     }
 
     /**
-     * Reads the next entry from the central directory and adds it to {@link #mEntries}.
+     * Reads the next entry from the central directory and adds it to {@link #entries}.
      *
      * @param bytes the central directory's data, positioned starting at the beginning of the next
      * entry to read; when finished, the buffer's position will be at the first byte after the
      * entry
-     * @param file the file this entry belongs to
      * @throws IOException failed to read the directory entry, either because of an I/O error,
      * because it is corrupt or contains unsupported features
      */
-    private void readEntry(@NonNull ByteBuffer bytes, @NonNull ZFile file) throws IOException {
+    private void readEntry(@Nonnull ByteBuffer bytes) throws IOException {
         F_SIGNATURE.verify(bytes);
         long madeBy = F_MADE_BY.read(bytes);
 
         long versionNeededToExtract = F_VERSION_EXTRACT.read(bytes);
-        if (versionNeededToExtract > MAX_VERSION_TO_EXTRACT) {
-            throw new IOException("Unknown version needed to extract in zip directory entry: "
-                    + versionNeededToExtract + ".");
-        }
+        verifyLog.verify(
+                versionNeededToExtract <= MAX_VERSION_TO_EXTRACT,
+                "Ignored unknown version needed to extract in zip directory entry: %s.",
+                versionNeededToExtract);
 
         long gpBit = F_GP_BIT.read(bytes);
         GPFlags flags = GPFlags.from(gpBit);
 
         long methodCode = F_METHOD.read(bytes);
         CompressionMethod method = CompressionMethod.fromCode(methodCode);
-        if (method == null) {
-            throw new IOException("Unknown method in zip directory entry: " + methodCode + ".");
-        }
+        verifyLog.verify(method != null, "Unknown method in zip directory entry: %s.", methodCode);
 
         long lastModTime;
         long lastModDate;
-        if (mFile.areTimestampsIgnored()) {
+        if (file.areTimestampsIgnored()) {
             lastModTime = 0;
             lastModDate = 0;
             F_LAST_MOD_TIME.skip(bytes);
@@ -309,11 +314,12 @@
         int extraFieldLength = Ints.checkedCast(F_EXTRA_FIELD_LENGTH.read(bytes));
         int fileCommentLength = Ints.checkedCast(F_COMMENT_LENGTH.read(bytes));
 
-        F_DISK_NUMBER_START.verify(bytes);
+        F_DISK_NUMBER_START.verify(bytes, verifyLog);
         long internalAttributes = F_INTERNAL_ATTRIBUTES.read(bytes);
-        if ((internalAttributes & ~ASCII_BIT) != 0) {
-            throw new IOException("Invalid internal attributes: " + internalAttributes + ".");
-        }
+        verifyLog.verify(
+                (internalAttributes & ~ASCII_BIT) == 0,
+                "Ignored invalid internal attributes: %s.",
+                internalAttributes);
 
         long externalAttributes = F_EXTERNAL_ATTRIBUTES.read(bytes);
         long entryOffset = F_OFFSET.read(bytes);
@@ -321,13 +327,23 @@
         long remainingSize = fileNameLength + extraFieldLength + fileCommentLength;
 
         if (bytes.remaining() < fileNameLength + extraFieldLength + fileCommentLength) {
-            throw new IOException("Directory entry should have " + remainingSize
-                    + " bytes remaining (name = " + fileNameLength + ", extra = "
-                    + extraFieldLength + ", comment = " + fileCommentLength + "), but it has "
-                    + bytes.remaining() + ".");
+            throw new IOException(
+                    "Directory entry should have "
+                            + remainingSize
+                            + " bytes remaining (name = "
+                            + fileNameLength
+                            + ", extra = "
+                            + extraFieldLength
+                            + ", comment = "
+                            + fileCommentLength
+                            + "), but it has "
+                            + bytes.remaining()
+                            + ".");
         }
 
-        String fileName = EncodeUtils.decode(bytes, fileNameLength, flags);
+        byte[] encodedFileName = new byte[fileNameLength];
+        bytes.get(encodedFileName);
+        String fileName = EncodeUtils.decode(encodedFileName, flags);
 
         byte[] extraField = new byte[extraFieldLength];
         bytes.get(extraField);
@@ -341,15 +357,14 @@
          * information we need the CentralDirectoryHeader
          */
         ListenableFuture<CentralDirectoryHeaderCompressInfo> compressInfo =
-                Futures.immediateFuture(new CentralDirectoryHeaderCompressInfo(method,
-                        compressedSize, versionNeededToExtract));
+                Futures.immediateFuture(
+                        new CentralDirectoryHeaderCompressInfo(
+                                method,
+                                compressedSize,
+                                versionNeededToExtract));
         CentralDirectoryHeader centralDirectoryHeader =
                 new CentralDirectoryHeader(
-                        fileName,
-                        uncompressedSize,
-                        compressInfo,
-                        flags,
-                        file);
+                        fileName, encodedFileName, uncompressedSize, compressInfo, flags, file);
         centralDirectoryHeader.setMadeBy(madeBy);
         centralDirectoryHeader.setLastModTime(lastModTime);
         centralDirectoryHeader.setLastModDate(lastModDate);
@@ -363,16 +378,16 @@
         StoredEntry entry;
 
         try {
-            entry = new StoredEntry(centralDirectoryHeader, mFile, null);
+            entry = new StoredEntry(centralDirectoryHeader, file, null);
         } catch (IOException e) {
             throw new IOException("Failed to read stored entry '" + fileName + "'.", e);
         }
 
-        if (mEntries.containsKey(fileName)) {
-            throw new IOException("File file contains duplicate file '" + fileName + "'.");
+        if (entries.containsKey(fileName)) {
+            verifyLog.log("File file contains duplicate file '" + fileName + "'.");
         }
 
-        mEntries.put(fileName, entry);
+        entries.put(fileName, entry);
     }
 
     /**
@@ -380,9 +395,9 @@
      *
      * @return all entries on a non-modifiable map
      */
-    @NonNull
+    @Nonnull
     Map<String, StoredEntry> getEntries() {
-        return ImmutableMap.copyOf(mEntries);
+        return ImmutableMap.copyOf(entries);
     }
 
     /**
@@ -392,7 +407,7 @@
      * @throws IOException failed to write the byte array
      */
     byte[] toBytes() throws IOException {
-        return mBytesSupplier.get();
+        return bytesSupplier.get();
     }
 
     /**
@@ -403,15 +418,15 @@
      */
     private byte[] computeByteRepresentation() {
 
-        List<StoredEntry> sorted = Lists.newArrayList(mEntries.values());
-        Collections.sort(sorted, StoredEntry.COMPARE_BY_NAME);
+        List<StoredEntry> sorted = Lists.newArrayList(entries.values());
+        sorted.sort(StoredEntry.COMPARE_BY_NAME);
 
-        CentralDirectoryHeader[] cdhs = new CentralDirectoryHeader[mEntries.size()];
+        CentralDirectoryHeader[] cdhs = new CentralDirectoryHeader[entries.size()];
         CentralDirectoryHeaderCompressInfo[] compressInfos =
-                new CentralDirectoryHeaderCompressInfo[mEntries.size()];
-        byte[][] encodedFileNames = new byte[mEntries.size()][];
-        byte[][] extraFields = new byte[mEntries.size()][];
-        byte[][] comments = new byte[mEntries.size()][];
+                new CentralDirectoryHeaderCompressInfo[entries.size()];
+        byte[][] encodedFileNames = new byte[entries.size()][];
+        byte[][] extraFields = new byte[entries.size()][];
+        byte[][] comments = new byte[entries.size()][];
 
         try {
             /*
@@ -434,14 +449,14 @@
 
             ByteBuffer out = ByteBuffer.allocate(total);
 
-            for (idx = 0; idx < mEntries.size(); idx++) {
+            for (idx = 0; idx < entries.size(); idx++) {
                 F_SIGNATURE.write(out);
                 F_MADE_BY.write(out, cdhs[idx].getMadeBy());
                 F_VERSION_EXTRACT.write(out, compressInfos[idx].getVersionExtract());
                 F_GP_BIT.write(out, cdhs[idx].getGpBit().getValue());
                 F_METHOD.write(out, compressInfos[idx].getMethod().methodCode);
 
-                if (mFile.areTimestampsIgnored()) {
+                if (file.areTimestampsIgnored()) {
                     F_LAST_MOD_TIME.write(out, 0);
                     F_LAST_MOD_DATE.write(out, 0);
                 } else {
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/CentralDirectoryHeader.java b/src/main/java/com/android/apkzlib/zip/CentralDirectoryHeader.java
similarity index 76%
rename from src/main/java/com/android/builder/internal/packaging/zip/CentralDirectoryHeader.java
rename to src/main/java/com/android/apkzlib/zip/CentralDirectoryHeader.java
index f846b9d..f10477f 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/CentralDirectoryHeader.java
+++ b/src/main/java/com/android/apkzlib/zip/CentralDirectoryHeader.java
@@ -14,16 +14,15 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
 
-import com.android.annotations.NonNull;
-import com.android.builder.internal.packaging.zip.utils.MsDosDateTimeUtils;
+import com.android.apkzlib.zip.utils.MsDosDateTimeUtils;
 import com.google.common.base.Verify;
-
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Future;
+import javax.annotation.Nonnull;
 
 /**
  * The Central Directory Header contains information about files stored in the zip. Instances of
@@ -45,121 +44,123 @@
     /**
      * Name of the file.
      */
-    @NonNull
-    private String mName;
+    @Nonnull
+    private String name;
 
     /**
      * CRC32 of the data. 0 if not yet computed.
      */
-    private long mCrc32;
+    private long crc32;
 
     /**
      * Size of the file uncompressed. 0 if the file has no data.
      */
-    private long mUncompressedSize;
+    private long uncompressedSize;
 
     /**
      * Code of the program that made the zip. We actually don't care about this.
      */
-    private long mMadeBy;
+    private long madeBy;
 
     /**
      * General-purpose bit flag.
      */
-    @NonNull
-    private GPFlags mGpBit;
+    @Nonnull
+    private GPFlags gpBit;
 
     /**
      * Last modification time in MS-DOS format (see {@link MsDosDateTimeUtils#packTime(long)}).
      */
-    private long mLastModTime;
+    private long lastModTime;
 
     /**
      * Last modification time in MS-DOS format (see {@link MsDosDateTimeUtils#packDate(long)}).
      */
-    private long mLastModDate;
+    private long lastModDate;
 
     /**
      * Extra data field contents. This field follows a specific structure according to the
      * specification.
      */
-    @NonNull
-    private ExtraField mExtraField;
+    @Nonnull
+    private ExtraField extraField;
 
     /**
      * File comment.
      */
-    @NonNull
-    private byte[] mComment;
+    @Nonnull
+    private byte[] comment;
 
     /**
      * File internal attributes.
      */
-    private long mInternalAttributes;
+    private long internalAttributes;
 
     /**
      * File external attributes.
      */
-    private long mExternalAttributes;
+    private long externalAttributes;
 
     /**
      * Offset in the file where the data is located. This will be -1 if the header corresponds to
      * a new file that is not yet written in the zip and, therefore, has no written data.
      */
-    private long mOffset;
+    private long offset;
 
     /**
      * Encoded file name.
      */
-    private byte[] mEncodedFileName;
+    private byte[] encodedFileName;
 
     /**
      * Compress information that may not have been computed yet due to lazy compression.
      */
-    @NonNull
-    private Future<CentralDirectoryHeaderCompressInfo> mCompressInfo;
+    @Nonnull
+    private Future<CentralDirectoryHeaderCompressInfo> compressInfo;
 
     /**
      * The file this header belongs to.
      */
-    @NonNull
-    private final ZFile mFile;
+    @Nonnull
+    private final ZFile file;
 
     /**
      * Creates data for a file.
      *
      * @param name the file name
+     * @param encodedFileName the encoded file name, this array will be owned by the header
      * @param uncompressedSize the uncompressed file size
      * @param compressInfo computation that defines the compression information
      * @param flags flags used in the entry
      * @param zFile the file this header belongs to
      */
     CentralDirectoryHeader(
-            @NonNull String name,
+            @Nonnull String name,
+            @Nonnull byte[] encodedFileName,
             long uncompressedSize,
-            @NonNull Future<CentralDirectoryHeaderCompressInfo> compressInfo,
-            @NonNull GPFlags flags,
-            @NonNull ZFile zFile) {
-        mName = name;
-        mUncompressedSize = uncompressedSize;
-        mCrc32 = 0;
+            @Nonnull Future<CentralDirectoryHeaderCompressInfo> compressInfo,
+            @Nonnull GPFlags flags,
+            @Nonnull ZFile zFile) {
+        this.name = name;
+        this.uncompressedSize = uncompressedSize;
+        crc32 = 0;
 
         /*
          * Set sensible defaults for the rest.
          */
-        mMadeBy = DEFAULT_VERSION_MADE_BY;
+        madeBy = DEFAULT_VERSION_MADE_BY;
 
-        mGpBit = flags;
-        mLastModTime = MsDosDateTimeUtils.packCurrentTime();
-        mLastModDate = MsDosDateTimeUtils.packCurrentDate();
-        mExtraField = new ExtraField();
-        mComment = new byte[0];
-        mInternalAttributes = 0;
-        mExternalAttributes = 0;
-        mOffset = -1;
-        mEncodedFileName = EncodeUtils.encode(name, mGpBit);
-        mCompressInfo = compressInfo;
-        mFile = zFile;
+        gpBit = flags;
+        lastModTime = MsDosDateTimeUtils.packCurrentTime();
+        lastModDate = MsDosDateTimeUtils.packCurrentDate();
+        extraField = new ExtraField();
+        comment = new byte[0];
+        internalAttributes = 0;
+        externalAttributes = 0;
+        offset = -1;
+        this.encodedFileName = encodedFileName;
+        this.compressInfo = compressInfo;
+        file = zFile;
     }
 
     /**
@@ -167,9 +168,9 @@
      *
      * @return the name
      */
-    @NonNull
+    @Nonnull
     public String getName() {
-        return mName;
+        return name;
     }
 
     /**
@@ -178,7 +179,7 @@
      * @return the size of the file
      */
     public long getUncompressedSize() {
-        return mUncompressedSize;
+        return uncompressedSize;
     }
 
     /**
@@ -187,7 +188,7 @@
      * @return the CRC32, 0 if not yet computed
      */
     public long getCrc32() {
-        return mCrc32;
+        return crc32;
     }
 
     /**
@@ -196,7 +197,7 @@
      * @param crc32 the CRC 32
      */
     void setCrc32(long crc32) {
-        mCrc32 = crc32;
+        this.crc32 = crc32;
     }
 
     /**
@@ -205,7 +206,7 @@
      * @return the code
      */
     public long getMadeBy() {
-        return mMadeBy;
+        return madeBy;
     }
 
     /**
@@ -214,7 +215,7 @@
      * @param madeBy the code
      */
     void setMadeBy(long madeBy) {
-        mMadeBy = madeBy;
+        this.madeBy = madeBy;
     }
 
     /**
@@ -222,9 +223,9 @@
      *
      * @return the bit flag
      */
-    @NonNull
+    @Nonnull
     public GPFlags getGpBit() {
-        return mGpBit;
+        return gpBit;
     }
 
     /**
@@ -234,7 +235,7 @@
      * {@link MsDosDateTimeUtils#packTime(long)})
      */
     public long getLastModTime() {
-        return mLastModTime;
+        return lastModTime;
     }
 
     /**
@@ -244,7 +245,7 @@
      * {@link MsDosDateTimeUtils#packTime(long)})
      */
     void setLastModTime(long lastModTime) {
-        mLastModTime = lastModTime;
+        this.lastModTime = lastModTime;
     }
 
     /**
@@ -254,7 +255,7 @@
      * {@link MsDosDateTimeUtils#packDate(long)})
      */
     public long getLastModDate() {
-        return mLastModDate;
+        return lastModDate;
     }
 
     /**
@@ -264,7 +265,7 @@
      * {@link MsDosDateTimeUtils#packDate(long)})
      */
     void setLastModDate(long lastModDate) {
-        mLastModDate = lastModDate;
+        this.lastModDate = lastModDate;
     }
 
     /**
@@ -272,9 +273,9 @@
      *
      * @return the data (returns an empty array if there is none)
      */
-    @NonNull
+    @Nonnull
     public ExtraField getExtraField() {
-        return mExtraField;
+        return extraField;
     }
 
     /**
@@ -282,9 +283,9 @@
      *
      * @param extraField the data to set
      */
-    public void setExtraField(@NonNull ExtraField extraField) {
+    public void setExtraField(@Nonnull ExtraField extraField) {
         setExtraFieldNoNotify(extraField);
-        mFile.centralDirectoryChanged();
+        file.centralDirectoryChanged();
     }
 
     /**
@@ -293,8 +294,8 @@
      *
      * @param extraField the data to set
      */
-    void setExtraFieldNoNotify(@NonNull ExtraField extraField) {
-        mExtraField = extraField;
+    void setExtraFieldNoNotify(@Nonnull ExtraField extraField) {
+        this.extraField = extraField;
     }
 
     /**
@@ -302,9 +303,9 @@
      *
      * @return the comment (returns an empty array if there is no comment)
      */
-    @NonNull
+    @Nonnull
     public byte[] getComment() {
-        return mComment;
+        return comment;
     }
 
     /**
@@ -312,8 +313,8 @@
      *
      * @param comment the comment
      */
-    void setComment(@NonNull byte[] comment) {
-        mComment = comment;
+    void setComment(@Nonnull byte[] comment) {
+        this.comment = comment;
     }
 
     /**
@@ -322,7 +323,7 @@
      * @return the entry's internal attributes
      */
     public long getInternalAttributes() {
-        return mInternalAttributes;
+        return internalAttributes;
     }
 
     /**
@@ -331,7 +332,7 @@
      * @param internalAttributes the entry's internal attributes
      */
     void setInternalAttributes(long internalAttributes) {
-        mInternalAttributes = internalAttributes;
+        this.internalAttributes = internalAttributes;
     }
 
     /**
@@ -340,7 +341,7 @@
      * @return the entry's external attributes
      */
     public long getExternalAttributes() {
-        return mExternalAttributes;
+        return externalAttributes;
     }
 
     /**
@@ -349,7 +350,7 @@
      * @param externalAttributes the entry's external attributes
      */
     void setExternalAttributes(long externalAttributes) {
-        mExternalAttributes = externalAttributes;
+        this.externalAttributes = externalAttributes;
     }
 
     /**
@@ -359,7 +360,7 @@
      * is stored in memory
      */
     public long getOffset() {
-        return mOffset;
+        return offset;
     }
 
     /**
@@ -368,7 +369,7 @@
      * @param offset the offset or {@code -1} if the file is new and has no data in the zip yet
      */
     void setOffset(long offset) {
-        mOffset = offset;
+        this.offset = offset;
     }
 
     /**
@@ -377,7 +378,7 @@
      * @return the encoded file name
      */
     public byte[] getEncodedFileName() {
-        return mEncodedFileName;
+        return encodedFileName;
     }
 
     /**
@@ -388,15 +389,15 @@
          * We actually create a new set of flags. Since the only information we care about is the
          * UTF-8 encoding, we'll just create a brand new object.
          */
-        mGpBit = GPFlags.make(mGpBit.isUtf8FileName());
+        gpBit = GPFlags.make(gpBit.isUtf8FileName());
     }
 
     @Override
     protected CentralDirectoryHeader clone() throws CloneNotSupportedException {
         CentralDirectoryHeader cdr = (CentralDirectoryHeader) super.clone();
-        cdr.mExtraField = mExtraField;
-        cdr.mComment = Arrays.copyOf(mComment, mComment.length);
-        cdr.mEncodedFileName = Arrays.copyOf(mEncodedFileName, mEncodedFileName.length);
+        cdr.extraField = extraField;
+        cdr.comment = Arrays.copyOf(comment, comment.length);
+        cdr.encodedFileName = Arrays.copyOf(encodedFileName, encodedFileName.length);
         return cdr;
     }
 
@@ -405,9 +406,9 @@
      *
      * @return the information
      */
-    @NonNull
+    @Nonnull
     public Future<CentralDirectoryHeaderCompressInfo> getCompressionInfo() {
-        return mCompressInfo;
+        return compressInfo;
     }
 
     /**
@@ -417,7 +418,7 @@
      * @return the result of the future
      * @throws IOException failed to get the information
      */
-    @NonNull
+    @Nonnull
     public CentralDirectoryHeaderCompressInfo getCompressionInfoWithWait()
             throws IOException {
         try {
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/CentralDirectoryHeaderCompressInfo.java b/src/main/java/com/android/apkzlib/zip/CentralDirectoryHeaderCompressInfo.java
similarity index 80%
rename from src/main/java/com/android/builder/internal/packaging/zip/CentralDirectoryHeaderCompressInfo.java
rename to src/main/java/com/android/apkzlib/zip/CentralDirectoryHeaderCompressInfo.java
index ada9f2b..7c3ad63 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/CentralDirectoryHeaderCompressInfo.java
+++ b/src/main/java/com/android/apkzlib/zip/CentralDirectoryHeaderCompressInfo.java
@@ -14,9 +14,9 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
 
-import com.android.annotations.NonNull;
+import javax.annotation.Nonnull;
 
 /**
  * Information stored in the {@link CentralDirectoryHeader} that is related to compression and may
@@ -37,18 +37,18 @@
     /**
      * The compression method.
      */
-    @NonNull
+    @Nonnull
     private final CompressionMethod mMethod;
 
     /**
      * Size of the file compressed. 0 if the file has no data.
      */
-    private final long mCompressedSize;
+    private final long compressedSize;
 
     /**
      * Version needed to extract the zip.
      */
-    private final long mVersionExtract;
+    private final long versionExtract;
 
     /**
      * Creates new compression information for the central directory header.
@@ -59,12 +59,12 @@
      * {@link #VERSION_WITH_STORE_FILES_ONLY} or {@link #VERSION_WITH_DIRECTORIES_AND_DEFLATE})
      */
     public CentralDirectoryHeaderCompressInfo(
-            @NonNull CompressionMethod method,
+            @Nonnull CompressionMethod method,
             long compressedSize,
             long versionToExtract) {
         mMethod = method;
-        mCompressedSize = compressedSize;
-        mVersionExtract = versionToExtract;
+        this.compressedSize = compressedSize;
+        versionExtract = versionToExtract;
     }
 
     /**
@@ -74,18 +74,18 @@
      * @param method the compression method
      * @param compressedSize the compressed size
      */
-    public CentralDirectoryHeaderCompressInfo(@NonNull CentralDirectoryHeader header,
-            @NonNull CompressionMethod method, long compressedSize) {
+    public CentralDirectoryHeaderCompressInfo(@Nonnull CentralDirectoryHeader header,
+            @Nonnull CompressionMethod method, long compressedSize) {
         mMethod = method;
-        mCompressedSize = compressedSize;
+        this.compressedSize = compressedSize;
 
         if (header.getName().endsWith("/") || method == CompressionMethod.DEFLATE) {
             /*
              * Directories and compressed files only in version 2.0.
              */
-            mVersionExtract = VERSION_WITH_DIRECTORIES_AND_DEFLATE;
+            versionExtract = VERSION_WITH_DIRECTORIES_AND_DEFLATE;
         } else {
-            mVersionExtract = VERSION_WITH_STORE_FILES_ONLY;
+            versionExtract = VERSION_WITH_STORE_FILES_ONLY;
         }
     }
 
@@ -95,7 +95,7 @@
      * @return the compressed data size
      */
     public long getCompressedSize() {
-        return mCompressedSize;
+        return compressedSize;
     }
 
     /**
@@ -103,7 +103,7 @@
      *
      * @return the compression method
      */
-    @NonNull
+    @Nonnull
     public CompressionMethod getMethod() {
         return mMethod;
     }
@@ -114,6 +114,6 @@
      * @return the minimum version
      */
     public long getVersionExtract() {
-        return mVersionExtract;
+        return versionExtract;
     }
 }
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/CompressionMethod.java b/src/main/java/com/android/apkzlib/zip/CompressionMethod.java
similarity index 94%
rename from src/main/java/com/android/builder/internal/packaging/zip/CompressionMethod.java
rename to src/main/java/com/android/apkzlib/zip/CompressionMethod.java
index 9984d3e..dd2ee8d 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/CompressionMethod.java
+++ b/src/main/java/com/android/apkzlib/zip/CompressionMethod.java
@@ -14,9 +14,9 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
 
-import com.android.annotations.Nullable;
+import javax.annotation.Nullable;
 
 /**
  * Enumeration with all known compression methods.
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/CompressionResult.java b/src/main/java/com/android/apkzlib/zip/CompressionResult.java
similarity index 70%
rename from src/main/java/com/android/builder/internal/packaging/zip/CompressionResult.java
rename to src/main/java/com/android/apkzlib/zip/CompressionResult.java
index ab9fc75..34f5d72 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/CompressionResult.java
+++ b/src/main/java/com/android/apkzlib/zip/CompressionResult.java
@@ -14,10 +14,10 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
 
-import com.android.annotations.NonNull;
-import com.android.builder.internal.packaging.zip.utils.CloseableByteSource;
+import com.android.apkzlib.zip.utils.CloseableByteSource;
+import javax.annotation.Nonnull;
 
 /**
  * Result of compressing data.
@@ -27,53 +27,57 @@
     /**
      * The compression method used.
      */
-    @NonNull
-    private final CompressionMethod mCompressionMethod;
+    @Nonnull
+    private final CompressionMethod compressionMethod;
 
     /**
      * The resulting data.
      */
-    @NonNull
-    private final CloseableByteSource mSource;
+    @Nonnull
+    private final CloseableByteSource source;
 
     /**
-     * Size of the compressed source. Kept because {@code mSource.size()} can throw
+     * Size of the compressed source. Kept because {@code source.size()} can throw
      * {@code IOException}.
      */
     private final long mSize;
 
     /**
      * Creates a new compression result.
+     *
      * @param source the data source
      * @param method the compression method
      */
-    public CompressionResult(@NonNull CloseableByteSource source, @NonNull CompressionMethod method,
+    public CompressionResult(@Nonnull CloseableByteSource source, @Nonnull CompressionMethod method,
             long size) {
-        mCompressionMethod = method;
-        mSource = source;
+        compressionMethod = method;
+        this.source = source;
         mSize = size;
     }
 
     /**
      * Obtains the compression method.
+     *
      * @return the compression method
      */
-    @NonNull
+    @Nonnull
     public CompressionMethod getCompressionMethod() {
-        return mCompressionMethod;
+        return compressionMethod;
     }
 
     /**
      * Obtains the compressed data.
+     *
      * @return the data, the resulting array should not be modified
      */
-    @NonNull
+    @Nonnull
     public CloseableByteSource getSource() {
-        return mSource;
+        return source;
     }
 
     /**
      * Obtains the size of the compression result.
+     *
      * @return the size
      */
     public long getSize() {
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/Compressor.java b/src/main/java/com/android/apkzlib/zip/Compressor.java
similarity index 82%
rename from src/main/java/com/android/builder/internal/packaging/zip/Compressor.java
rename to src/main/java/com/android/apkzlib/zip/Compressor.java
index 8084e25..a94242e 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/Compressor.java
+++ b/src/main/java/com/android/apkzlib/zip/Compressor.java
@@ -14,11 +14,11 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
 
-import com.android.annotations.NonNull;
-import com.android.builder.internal.packaging.zip.utils.CloseableByteSource;
+import com.android.apkzlib.zip.utils.CloseableByteSource;
 import com.google.common.util.concurrent.ListenableFuture;
+import javax.annotation.Nonnull;
 
 /**
  * A compressor is capable of, well, compressing data. Data is read from an {@code ByteSource}.
@@ -29,9 +29,10 @@
 
     /**
      * Compresses an entry source.
+     *
      * @param source the source to compress
      * @return a future that will eventually contain the compression result
      */
-    @NonNull
-    ListenableFuture<CompressionResult> compress(@NonNull CloseableByteSource source);
+    @Nonnull
+    ListenableFuture<CompressionResult> compress(@Nonnull CloseableByteSource source);
 }
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/DataDescriptorType.java b/src/main/java/com/android/apkzlib/zip/DataDescriptorType.java
similarity index 96%
rename from src/main/java/com/android/builder/internal/packaging/zip/DataDescriptorType.java
rename to src/main/java/com/android/apkzlib/zip/DataDescriptorType.java
index f762852..9b7425e 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/DataDescriptorType.java
+++ b/src/main/java/com/android/apkzlib/zip/DataDescriptorType.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
 
 /**
  * Type of data descriptor that an entry has. Data descriptors are used if the CRC and sizing data
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/EncodeUtils.java b/src/main/java/com/android/apkzlib/zip/EncodeUtils.java
similarity index 64%
rename from src/main/java/com/android/builder/internal/packaging/zip/EncodeUtils.java
rename to src/main/java/com/android/apkzlib/zip/EncodeUtils.java
index 35ebcf6..259f64e 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/EncodeUtils.java
+++ b/src/main/java/com/android/apkzlib/zip/EncodeUtils.java
@@ -14,14 +14,15 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
 
-import com.android.annotations.NonNull;
 import com.google.common.base.Charsets;
-
 import java.io.IOException;
 import java.nio.ByteBuffer;
+import java.nio.charset.CharacterCodingException;
 import java.nio.charset.Charset;
+import java.nio.charset.CodingErrorAction;
+import javax.annotation.Nonnull;
 
 /**
  * Utilities to encode and decode file names in zips.
@@ -45,18 +46,17 @@
      * @param flags the zip entry flags
      * @return the decode file name
      */
-    @NonNull
-    public static String decode(@NonNull ByteBuffer bytes, int length, @NonNull GPFlags flags)
+    @Nonnull
+    public static String decode(@Nonnull ByteBuffer bytes, int length, @Nonnull GPFlags flags)
             throws IOException {
         if (bytes.remaining() < length) {
             throw new IOException("Only " + bytes.remaining() + " bytes exist in the buffer, but "
                     + "length is " + length + ".");
         }
 
-        Charset charset = flagsCharset(flags);
         byte[] stringBytes = new byte[length];
         bytes.get(stringBytes);
-        return charset.decode(ByteBuffer.wrap(stringBytes)).toString();
+        return decode(stringBytes, flags);
     }
 
     /**
@@ -66,10 +66,34 @@
      * @param flags the zip entry flags
      * @return the decode file name
      */
-    @NonNull
-    public static String decode(@NonNull byte[] data, @NonNull GPFlags flags) {
-        Charset charset = flagsCharset(flags);
-        return charset.decode(ByteBuffer.wrap(data)).toString();
+    @Nonnull
+    public static String decode(@Nonnull byte[] data, @Nonnull GPFlags flags) {
+        return decode(data, flagsCharset(flags));
+    }
+
+    /**
+     * Decodes a file name.
+     *
+     * @param data the raw data
+     * @param charset the charset to use
+     * @return the decode file name
+     */
+    @Nonnull
+    private static String decode(@Nonnull byte[] data, @Nonnull Charset charset) {
+        try {
+            return charset.newDecoder()
+                    .onMalformedInput(CodingErrorAction.REPORT)
+                    .decode(ByteBuffer.wrap(data))
+                    .toString();
+        } catch (CharacterCodingException e) {
+            // If we're trying to decode ASCII, try UTF-8. Otherwise, revert to the default
+            // behavior (usually replacing invalid characters).
+            if (charset.equals(Charsets.US_ASCII)) {
+                return decode(data, Charsets.UTF_8);
+            } else {
+                return charset.decode(ByteBuffer.wrap(data)).toString();
+            }
+        }
     }
 
     /**
@@ -79,8 +103,8 @@
      * @param flags the zip entry flags
      * @return the encoded file name
      */
-    @NonNull
-    public static byte[] encode(@NonNull String name, @NonNull GPFlags flags) {
+    @Nonnull
+    public static byte[] encode(@Nonnull String name, @Nonnull GPFlags flags) {
         Charset charset = flagsCharset(flags);
         ByteBuffer bytes = charset.encode(name);
         byte[] result = new byte[bytes.remaining()];
@@ -94,8 +118,8 @@
      * @param flags the flags
      * @return the charset to use
      */
-    @NonNull
-    private static Charset flagsCharset(@NonNull GPFlags flags) {
+    @Nonnull
+    private static Charset flagsCharset(@Nonnull GPFlags flags) {
         if (flags.isUtf8FileName()) {
             return Charsets.UTF_8;
         } else {
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/Eocd.java b/src/main/java/com/android/apkzlib/zip/Eocd.java
similarity index 79%
rename from src/main/java/com/android/builder/internal/packaging/zip/Eocd.java
rename to src/main/java/com/android/apkzlib/zip/Eocd.java
index 5d3bf33..1568840 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/Eocd.java
+++ b/src/main/java/com/android/apkzlib/zip/Eocd.java
@@ -14,18 +14,17 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
 
-import com.android.annotations.NonNull;
-import com.android.builder.internal.utils.CachedSupplier;
+import com.android.apkzlib.utils.CachedSupplier;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
 import com.google.common.base.Verify;
 import com.google.common.primitives.Ints;
-
 import java.io.IOException;
 import java.io.UncheckedIOException;
 import java.nio.ByteBuffer;
+import javax.annotation.Nonnull;
 
 /**
  * End Of Central Directory record in a zip file.
@@ -90,29 +89,29 @@
     /**
      * Number of entries in the central directory.
      */
-    private final int mTotalRecords;
+    private final int totalRecords;
 
     /**
      * Offset from the beginning of the archive where the Central Directory is located.
      */
-    private final long mDirectoryOffset;
+    private final long directoryOffset;
 
     /**
      * Number of bytes of the Central Directory.
      */
-    private final long mDirectorySize;
+    private final long directorySize;
 
     /**
      * Contents of the EOCD comment.
      */
-    @NonNull
-    private final byte[] mComment;
+    @Nonnull
+    private final byte[] comment;
 
     /**
      * Supplier of the byte representation of the EOCD.
      */
-    @NonNull
-    private final CachedSupplier<byte[]> mByteSupplier;
+    @Nonnull
+    private final CachedSupplier<byte[]> byteSupplier;
 
     /**
      * Creates a new EOCD, reading it from a byte source. This method will parse the byte source
@@ -122,7 +121,7 @@
      * buffer's position will have moved to the end of the EOCD
      * @throws IOException failed to read information or the EOCD data is corrupt or invalid
      */
-    Eocd(@NonNull ByteBuffer bytes) throws IOException {
+    Eocd(@Nonnull ByteBuffer bytes) throws IOException {
 
         /*
          * Read the EOCD record.
@@ -146,18 +145,18 @@
 
         Verify.verify(totalRecords1 <= Integer.MAX_VALUE);
 
-        mTotalRecords = Ints.checkedCast(totalRecords1);
-        mDirectorySize = directorySize;
-        mDirectoryOffset = directoryOffset;
+        totalRecords = Ints.checkedCast(totalRecords1);
+        this.directorySize = directorySize;
+        this.directoryOffset = directoryOffset;
 
         if (bytes.remaining() < commentSize) {
             throw new IOException("Corrupt EOCD record: not enough data for comment (comment "
                     + "size is " + commentSize + ").");
         }
 
-        mComment = new byte[commentSize];
-        bytes.get(mComment);
-        mByteSupplier = new CachedSupplier<>(this::computeByteRepresentation);
+        comment = new byte[commentSize];
+        bytes.get(comment);
+        byteSupplier = new CachedSupplier<>(this::computeByteRepresentation);
     }
 
     /**
@@ -168,17 +167,18 @@
      * @param directoryOffset offset, since beginning of archive, where the Central Directory is
      * located
      * @param directorySize number of bytes of the Central Directory
+     * @param comment the EOCD comment
      */
-    Eocd(int totalRecords, long directoryOffset, long directorySize) {
+    Eocd(int totalRecords, long directoryOffset, long directorySize, @Nonnull byte[] comment) {
         Preconditions.checkArgument(totalRecords >= 0, "totalRecords < 0");
         Preconditions.checkArgument(directoryOffset >= 0, "directoryOffset < 0");
         Preconditions.checkArgument(directorySize >= 0, "directorySize < 0");
 
-        mTotalRecords = totalRecords;
-        mDirectoryOffset = directoryOffset;
-        mDirectorySize = directorySize;
-        mComment = new byte[0];
-        mByteSupplier = new CachedSupplier<byte[]>(this::computeByteRepresentation);
+        this.totalRecords = totalRecords;
+        this.directoryOffset = directoryOffset;
+        this.directorySize = directorySize;
+        this.comment = comment;
+        byteSupplier = new CachedSupplier<>(this::computeByteRepresentation);
     }
 
     /**
@@ -187,7 +187,7 @@
      * @return the number of records
      */
     int getTotalRecords() {
-        return mTotalRecords;
+        return totalRecords;
     }
 
     /**
@@ -197,7 +197,7 @@
      * @return the offset where the Central Directory is located
      */
     long getDirectoryOffset() {
-        return mDirectoryOffset;
+        return directoryOffset;
     }
 
     /**
@@ -206,7 +206,7 @@
      * @return the number of bytes that make up the Central Directory
      */
     long getDirectorySize() {
-        return mDirectorySize;
+        return directorySize;
     }
 
     /**
@@ -215,7 +215,7 @@
      * @return the size, in bytes, of the EOCD
      */
     long getEocdSize() {
-        return F_COMMENT_SIZE.endOffset() + mComment.length;
+        return (long) F_COMMENT_SIZE.endOffset() + comment.length;
     }
 
     /**
@@ -224,9 +224,22 @@
      * @return a byte representation of the EOCD that has exactly {@link #getEocdSize()} bytes
      * @throws IOException failed to generate the EOCD data
      */
-    @NonNull
+    @Nonnull
     byte[] toBytes() throws IOException {
-        return mByteSupplier.get();
+        return byteSupplier.get();
+    }
+
+    /*
+     * Obtains the comment in the EOCD.
+     *
+     * @return the comment exactly as it is represented in the file (no encoding conversion is
+     * done)
+     */
+    @Nonnull
+    byte[] getComment() {
+        byte[] commentCopy = new byte[comment.length];
+        System.arraycopy(comment, 0, commentCopy, 0, comment.length);
+        return commentCopy;
     }
 
     /**
@@ -235,20 +248,20 @@
      * @return a byte representation of the EOCD that has exactly {@link #getEocdSize()} bytes
      * @throws UncheckedIOException failed to generate the EOCD data
      */
-    @NonNull
+    @Nonnull
     private byte[] computeByteRepresentation() {
-        ByteBuffer out = ByteBuffer.allocate(F_COMMENT_SIZE.endOffset() + mComment.length);
+        ByteBuffer out = ByteBuffer.allocate(F_COMMENT_SIZE.endOffset() + comment.length);
 
         try {
             F_SIGNATURE.write(out);
             F_NUMBER_OF_DISK.write(out);
             F_DISK_CD_START.write(out);
-            F_RECORDS_DISK.write(out, mTotalRecords);
-            F_RECORDS_TOTAL.write(out, mTotalRecords);
-            F_CD_SIZE.write(out, mDirectorySize);
-            F_CD_OFFSET.write(out, mDirectoryOffset);
-            F_COMMENT_SIZE.write(out, mComment.length);
-            out.put(mComment);
+            F_RECORDS_DISK.write(out, totalRecords);
+            F_RECORDS_TOTAL.write(out, totalRecords);
+            F_CD_SIZE.write(out, directorySize);
+            F_CD_OFFSET.write(out, directoryOffset);
+            F_COMMENT_SIZE.write(out, comment.length);
+            out.put(comment);
 
             return out.array();
         } catch (IOException e) {
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/ExtraField.java b/src/main/java/com/android/apkzlib/zip/ExtraField.java
similarity index 74%
rename from src/main/java/com/android/builder/internal/packaging/zip/ExtraField.java
rename to src/main/java/com/android/apkzlib/zip/ExtraField.java
index 27721a9..d70fa7f 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/ExtraField.java
+++ b/src/main/java/com/android/apkzlib/zip/ExtraField.java
@@ -14,20 +14,18 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
 
-import com.android.annotations.NonNull;
-import com.android.annotations.Nullable;
-import com.android.builder.internal.packaging.zip.utils.LittleEndianUtils;
+import com.android.apkzlib.zip.utils.LittleEndianUtils;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
-
 import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.stream.Collectors;
-
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
 /**
  * Contains an extra field.
@@ -57,11 +55,11 @@
     static final int ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID = 0xd935;
 
     /**
-     * The field's raw data, if it is known. Either this variable or {@link #mSegments} must be
+     * The field's raw data, if it is known. Either this variable or {@link #segments} must be
      * non-{@code null}.
      */
     @Nullable
-    private final byte[] mRawData;
+    private final byte[] rawData;
 
     /**
      * The list of field's segments. Will be populated if the extra field is created based on a
@@ -69,24 +67,24 @@
      * on the raw bytes.
      */
     @Nullable
-    private ImmutableList<Segment> mSegments;
+    private ImmutableList<Segment> segments;
 
     /**
      * Creates an extra field based on existing raw data.
      *
      * @param rawData the raw data; will not be parsed unless needed
      */
-    public ExtraField(@NonNull byte[] rawData) {
-        mRawData = rawData;
-        mSegments = null;
+    public ExtraField(@Nonnull byte[] rawData) {
+        this.rawData = rawData;
+        segments = null;
     }
 
     /**
      * Creates a new extra field with no segments.
      */
     public ExtraField() {
-        mRawData = null;
-        mSegments = ImmutableList.of();
+        rawData = null;
+        segments = ImmutableList.of();
     }
 
     /**
@@ -94,9 +92,9 @@
      *
      * @param segments the segments
      */
-    public ExtraField(@NonNull ImmutableList<Segment> segments) {
-        mRawData = null;
-        mSegments = segments;
+    public ExtraField(@Nonnull ImmutableList<Segment> segments) {
+        rawData = null;
+        this.segments = segments;
     }
 
     /**
@@ -106,12 +104,12 @@
      * @throws IOException failed to parse the extra field
      */
     public ImmutableList<Segment> getSegments() throws IOException {
-        if (mSegments == null) {
+        if (segments == null) {
             parseSegments();
         }
 
-        Preconditions.checkNotNull(mSegments);
-        return mSegments;
+        Preconditions.checkNotNull(segments);
+        return segments;
     }
 
     /**
@@ -137,16 +135,16 @@
     }
 
     /**
-     * Parses the raw data and generates all segments in {@link #mSegments}.
+     * Parses the raw data and generates all segments in {@link #segments}.
      *
      * @throws IOException failed to parse the data
      */
     private void parseSegments() throws IOException {
-        Preconditions.checkNotNull(mRawData);
-        Preconditions.checkState(mSegments == null);
+        Preconditions.checkNotNull(rawData);
+        Preconditions.checkState(segments == null);
 
         List<Segment> segments = new ArrayList<>();
-        ByteBuffer buffer = ByteBuffer.wrap(mRawData);
+        ByteBuffer buffer = ByteBuffer.wrap(rawData);
 
         while (buffer.remaining() > 0) {
             int headerId = LittleEndianUtils.readUnsigned2Le(buffer);
@@ -160,6 +158,16 @@
             }
 
             byte[] data = new byte[dataSize];
+            if (buffer.remaining() < dataSize) {
+                throw new IOException(
+                        "Invalid data size for extra field segment with header ID "
+                                + headerId
+                                + ": "
+                                + dataSize
+                                + " (only "
+                                + buffer.remaining()
+                                + " bytes are available)");
+            }
             buffer.get(data);
 
             SegmentFactory factory = identifySegmentFactory(headerId);
@@ -167,7 +175,7 @@
             segments.add(seg);
         }
 
-        mSegments = ImmutableList.copyOf(segments);
+        this.segments = ImmutableList.copyOf(segments);
     }
 
     /**
@@ -176,12 +184,12 @@
      * @return the size
      */
     public int size() {
-        if (mRawData != null) {
-            return mRawData.length;
+        if (rawData != null) {
+            return rawData.length;
         } else {
-            Preconditions.checkNotNull(mSegments);
+            Preconditions.checkNotNull(segments);
             int sz = 0;
-            for (Segment s : mSegments) {
+            for (Segment s : segments) {
                 sz += s.size();
             }
 
@@ -196,12 +204,12 @@
      * written
      * @throws IOException failed to write the extra fields
      */
-    public void write(@NonNull ByteBuffer out) throws IOException {
-        if (mRawData != null) {
-            out.put(mRawData);
+    public void write(@Nonnull ByteBuffer out) throws IOException {
+        if (rawData != null) {
+            out.put(rawData);
         } else {
-            Preconditions.checkNotNull(mSegments);
-            for (Segment s : mSegments) {
+            Preconditions.checkNotNull(segments);
+            for (Segment s : segments) {
                 s.write(out);
             }
         }
@@ -213,7 +221,7 @@
      * @param headerId the header ID
      * @return the segmnet factory that creates segments with the given header
      */
-    @NonNull
+    @Nonnull
     private static SegmentFactory identifySegmentFactory(int headerId) {
         if (headerId == ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID) {
             return AlignmentSegment::new;
@@ -249,7 +257,7 @@
          * be written
          * @throws IOException failed to write segment data
          */
-        void write(@NonNull ByteBuffer out) throws IOException;
+        void write(@Nonnull ByteBuffer out) throws IOException;
     }
 
     /**
@@ -266,8 +274,8 @@
          * @return the created segment
          * @throws IOException failed to create the segment from the data
          */
-        @NonNull
-        Segment make(int headerId, @NonNull byte[] data) throws IOException;
+        @Nonnull
+        Segment make(int headerId, @Nonnull byte[] data) throws IOException;
     }
 
     /**
@@ -279,13 +287,13 @@
         /**
          * Header ID.
          */
-        private final int mHeaderId;
+        private final int headerId;
 
         /**
          * Data in the segment.
          */
-        @NonNull
-        private final byte[] mData;
+        @Nonnull
+        private final byte[] data;
 
         /**
          * Creates a new raw data segment.
@@ -293,26 +301,26 @@
          * @param headerId the header ID
          * @param data the segment data
          */
-        RawDataSegment(int headerId, @NonNull byte[] data) {
-            mHeaderId = headerId;
-            mData = data;
+        RawDataSegment(int headerId, @Nonnull byte[] data) {
+            this.headerId = headerId;
+            this.data = data;
         }
 
         @Override
         public int getHeaderId() {
-            return mHeaderId;
+            return headerId;
         }
 
         @Override
-        public void write(@NonNull ByteBuffer out) throws IOException {
-            LittleEndianUtils.writeUnsigned2Le(out, mHeaderId);
-            LittleEndianUtils.writeUnsigned2Le(out, mData.length);
-            out.put(mData);
+        public void write(@Nonnull ByteBuffer out) throws IOException {
+            LittleEndianUtils.writeUnsigned2Le(out, headerId);
+            LittleEndianUtils.writeUnsigned2Le(out, data.length);
+            out.put(data);
         }
 
         @Override
         public int size() {
-            return 4 + mData.length;
+            return 4 + data.length;
         }
     }
 
@@ -326,14 +334,19 @@
     public static class AlignmentSegment implements Segment {
 
         /**
+         * Minimum size for an alignment segment.
+         */
+        public static final int MINIMUM_SIZE = 6;
+
+        /**
          * The alignment value.
          */
-        private int mAlignment;
+        private int alignment;
 
         /**
          * How many bytes of padding are in this segment?
          */
-        private int mPadding;
+        private int padding;
 
         /**
          * Creates a new alignment segment.
@@ -343,14 +356,14 @@
          */
         public AlignmentSegment(int alignment, int totalSize) {
             Preconditions.checkArgument(alignment > 0, "alignment <= 0");
-            Preconditions.checkArgument(totalSize >= 6, "totalSize < 6");
+            Preconditions.checkArgument(totalSize >= MINIMUM_SIZE, "totalSize < MINIMUM_SIZE");
 
             /*
              * We have 6 bytes of fixed data: header ID (2 bytes), data size (2 bytes), alignment
              * value (2 bytes).
              */
-            mAlignment = alignment;
-            mPadding = totalSize - 6;
+            this.alignment = alignment;
+            padding = totalSize - MINIMUM_SIZE;
         }
 
         /**
@@ -360,29 +373,29 @@
          * @param data the segment data
          * @throws IOException failed to create the segment from the data
          */
-        public AlignmentSegment(int headerId, @NonNull byte[] data) throws IOException {
+        public AlignmentSegment(int headerId, @Nonnull byte[] data) throws IOException {
             Preconditions.checkArgument(headerId == ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID);
 
             ByteBuffer dataBuffer = ByteBuffer.wrap(data);
-            mAlignment = LittleEndianUtils.readUnsigned2Le(dataBuffer);
-            if (mAlignment <= 0) {
-                throw new IOException("Invalid alignment in alignment field: " + mAlignment);
+            alignment = LittleEndianUtils.readUnsigned2Le(dataBuffer);
+            if (alignment <= 0) {
+                throw new IOException("Invalid alignment in alignment field: " + alignment);
             }
 
-            mPadding = data.length - 2;
+            padding = data.length - 2;
         }
 
         @Override
-        public void write(@NonNull ByteBuffer out) throws IOException {
+        public void write(@Nonnull ByteBuffer out) throws IOException {
             LittleEndianUtils.writeUnsigned2Le(out, ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID);
-            LittleEndianUtils.writeUnsigned2Le(out, mPadding + 2);
-            LittleEndianUtils.writeUnsigned2Le(out, mAlignment);
-            out.put(new byte[mPadding]);
+            LittleEndianUtils.writeUnsigned2Le(out, padding + 2);
+            LittleEndianUtils.writeUnsigned2Le(out, alignment);
+            out.put(new byte[padding]);
         }
 
         @Override
         public int size() {
-            return mPadding + 6;
+            return padding + 6;
         }
 
         @Override
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/FileUseMap.java b/src/main/java/com/android/apkzlib/zip/FileUseMap.java
similarity index 79%
rename from src/main/java/com/android/builder/internal/packaging/zip/FileUseMap.java
rename to src/main/java/com/android/apkzlib/zip/FileUseMap.java
index ab2df9b..8a76878 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/FileUseMap.java
+++ b/src/main/java/com/android/apkzlib/zip/FileUseMap.java
@@ -14,10 +14,8 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
 
-import com.android.annotations.NonNull;
-import com.android.annotations.Nullable;
 import com.google.common.base.Preconditions;
 import com.google.common.base.Verify;
 import com.google.common.collect.Lists;
@@ -28,6 +26,8 @@
 import java.util.SortedSet;
 import java.util.StringJoiner;
 import java.util.TreeSet;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
 /**
  * The file use map keeps track of which parts of the zip file are used which parts are not.
@@ -50,24 +50,24 @@
 class FileUseMap {
     /**
      * Size of the file according to the map. This should always match the last entry in
-     * {@code #mMap}.
+     * {@code #map}.
      */
-    private long mSize;
+    private long size;
 
     /**
-     * Tree with all intervals ordered by position. Contains coverage from 0 up to {@link #mSize}.
-     * If {@link #mSize} is zero then this set is empty. This is the only situation in which the map
+     * Tree with all intervals ordered by position. Contains coverage from 0 up to {@link #size}.
+     * If {@link #size} is zero then this set is empty. This is the only situation in which the map
      * will be empty.
      */
-    @NonNull
-    private TreeSet<FileUseMapEntry<?>> mMap;
+    @Nonnull
+    private TreeSet<FileUseMapEntry<?>> map;
 
     /**
-     * Tree with all free blocks ordered by size. This is essentially a view over {@link #mMap}
+     * Tree with all free blocks ordered by size. This is essentially a view over {@link #map}
      * containing only the free blocks, but in a different order.
      */
-    @NonNull
-    private TreeSet<FileUseMapEntry<?>> mFree;
+    @Nonnull
+    private TreeSet<FileUseMapEntry<?>> free;
 
     /**
      * If defined, defines the minimum size for a free entry.
@@ -84,9 +84,9 @@
         Preconditions.checkArgument(size >= 0, "size < 0");
         Preconditions.checkArgument(minFreeSize >= 0, "minFreeSize < 0");
 
-        mSize = size;
-        mMap = new TreeSet<>(FileUseMapEntry.COMPARE_BY_START);
-        mFree = new TreeSet<>(FileUseMapEntry.COMPARE_BY_SIZE);
+        this.size = size;
+        map = new TreeSet<>(FileUseMapEntry.COMPARE_BY_START);
+        free = new TreeSet<>(FileUseMapEntry.COMPARE_BY_SIZE);
         mMinFreeSize = minFreeSize;
 
         if (size > 0) {
@@ -99,11 +99,11 @@
      *
      * @param entry the entry to add
      */
-    private void internalAdd(@NonNull FileUseMapEntry<?> entry) {
-        mMap.add(entry);
+    private void internalAdd(@Nonnull FileUseMapEntry<?> entry) {
+        map.add(entry);
 
         if (entry.isFree()) {
-            mFree.add(entry);
+            free.add(entry);
         }
     }
 
@@ -112,12 +112,12 @@
      *
      * @param entry the entry to remove
      */
-    private void internalRemove(@NonNull FileUseMapEntry<?> entry) {
-        boolean wasRemoved = mMap.remove(entry);
-        Preconditions.checkState(wasRemoved, "entry not in mMap");
+    private void internalRemove(@Nonnull FileUseMapEntry<?> entry) {
+        boolean wasRemoved = map.remove(entry);
+        Preconditions.checkState(wasRemoved, "entry not in map");
 
         if (entry.isFree()) {
-            mFree.remove(entry);
+            free.remove(entry);
         }
     }
 
@@ -128,9 +128,9 @@
      *
      * @param entry the entry to add
      */
-    private void add(@NonNull FileUseMapEntry<?> entry) {
-        Preconditions.checkArgument(entry.getStart() < mSize, "entry.getStart() >= mSize");
-        Preconditions.checkArgument(entry.getEnd() <= mSize, "entry.getEnd() > mSize");
+    private void add(@Nonnull FileUseMapEntry<?> entry) {
+        Preconditions.checkArgument(entry.getStart() < size, "entry.getStart() >= size");
+        Preconditions.checkArgument(entry.getEnd() <= size, "entry.getEnd() > size");
         Preconditions.checkArgument(!entry.isFree(), "entry.isFree()");
 
         FileUseMapEntry<?> container = findContainer(entry);
@@ -149,8 +149,8 @@
      *
      * @param entry the entry
      */
-    void remove(@NonNull FileUseMapEntry<?> entry) {
-        Preconditions.checkState(mMap.contains(entry), "!mMap.contains(entry)");
+    void remove(@Nonnull FileUseMapEntry<?> entry) {
+        Preconditions.checkState(map.contains(entry), "!map.contains(entry)");
         Preconditions.checkArgument(!entry.isFree(), "entry.isFree()");
 
         internalRemove(entry);
@@ -174,7 +174,7 @@
      * @param <T> the type of data to store in the entry
      * @return the new entry
      */
-    <T> FileUseMapEntry<T> add(long start, long end, @NonNull T store) {
+    <T> FileUseMapEntry<T> add(long start, long end, @Nonnull T store) {
         Preconditions.checkArgument(start >= 0, "start < 0");
         Preconditions.checkArgument(end > start, "end < start");
 
@@ -189,9 +189,9 @@
      * @param entry the entry whose container we're looking for
      * @return the container
      */
-    @NonNull
-    private FileUseMapEntry<?> findContainer(@NonNull FileUseMapEntry<?> entry) {
-        FileUseMapEntry container = mMap.floor(entry);
+    @Nonnull
+    private FileUseMapEntry<?> findContainer(@Nonnull FileUseMapEntry<?> entry) {
+        FileUseMapEntry container = map.floor(entry);
         Verify.verifyNotNull(container);
         Verify.verify(container.getStart() <= entry.getStart());
         Verify.verify(container.getEnd() >= entry.getEnd());
@@ -203,15 +203,15 @@
      * Splits a container to add an entry, adding new free entries before and after the provided
      * entry if needed.
      *
-     * @param container the container entry, a free entry that is in {@link #mMap} that that
+     * @param container the container entry, a free entry that is in {@link #map} that that
      * encloses {@code entry}
      * @param entry the entry that will be used to split {@code container}
      * @return a set of non-overlapping entries that completely covers {@code container} and that
      * includes {@code entry}
      */
-    @NonNull
-    private static Set<FileUseMapEntry<?>> split(@NonNull FileUseMapEntry<?> container,
-            @NonNull FileUseMapEntry<?> entry) {
+    @Nonnull
+    private static Set<FileUseMapEntry<?>> split(@Nonnull FileUseMapEntry<?> container,
+            @Nonnull FileUseMapEntry<?> entry) {
         Preconditions.checkArgument(container.isFree(), "!container.isFree()");
 
         long farStart = container.getStart();
@@ -243,7 +243,7 @@
      *
      * @param entry the free entry to coalesce with neighbors
      */
-    private void coalesce(@NonNull FileUseMapEntry<?> entry) {
+    private void coalesce(@Nonnull FileUseMapEntry<?> entry) {
         Preconditions.checkArgument(entry.isFree(), "!entry.isFree()");
 
         FileUseMapEntry<?> prevToMerge = null;
@@ -252,7 +252,7 @@
             /*
              * See if we have a previous entry to merge with this one.
              */
-            prevToMerge = mMap.floor(FileUseMapEntry.makeFree(start - 1, start));
+            prevToMerge = map.floor(FileUseMapEntry.makeFree(start - 1, start));
             Verify.verifyNotNull(prevToMerge);
             if (!prevToMerge.isFree()) {
                 prevToMerge = null;
@@ -261,11 +261,11 @@
 
         FileUseMapEntry<?> nextToMerge = null;
         long end = entry.getEnd();
-        if (end < mSize) {
+        if (end < size) {
             /*
              * See if we have a next entry to merge with this one.
              */
-            nextToMerge = mMap.ceiling(FileUseMapEntry.makeFree(end, end + 1));
+            nextToMerge = map.ceiling(FileUseMapEntry.makeFree(end, end + 1));
             Verify.verifyNotNull(nextToMerge);
             if (!nextToMerge.isFree()) {
                 nextToMerge = null;
@@ -296,18 +296,18 @@
      * Truncates map removing the top entry if it is free and reducing the map's size.
      */
     void truncate() {
-        if (mSize == 0) {
+        if (size == 0) {
             return;
         }
 
         /*
          * Find the last entry.
          */
-        FileUseMapEntry<?> last = mMap.last();
+        FileUseMapEntry<?> last = map.last();
         Verify.verifyNotNull(last, "last == null");
         if (last.isFree()) {
             internalRemove(last);
-            mSize = last.getStart();
+            size = last.getStart();
         }
     }
 
@@ -317,7 +317,7 @@
      * @return the size
      */
     long size() {
-        return mSize;
+        return size;
     }
 
     /**
@@ -326,7 +326,7 @@
      * @return the size of the file discounting the last block if it is empty
      */
     long usedSize() {
-        if (mSize == 0) {
+        if (size == 0) {
             return 0;
         }
 
@@ -334,13 +334,13 @@
          * Find the last entry to see if it is an empty entry. If it is, we need to remove its size
          * from the returned value.
          */
-        FileUseMapEntry<?> last = mMap.last();
+        FileUseMapEntry<?> last = map.last();
         Verify.verifyNotNull(last, "last == null");
         if (last.isFree()) {
             return last.getStart();
         } else {
-            Verify.verify(last.getEnd() == mSize);
-            return mSize;
+            Verify.verify(last.getEnd() == size);
+            return size;
         }
     }
 
@@ -351,16 +351,16 @@
      * @param size the new size of the map that cannot be smaller that the current size
      */
     void extend(long size) {
-        Preconditions.checkArgument(size >= mSize, "size < mSize");
+        Preconditions.checkArgument(size >= this.size, "size < size");
 
-        if (mSize == size) {
+        if (this.size == size) {
             return;
         }
 
-        FileUseMapEntry<?> newBlock = FileUseMapEntry.makeFree(mSize, size);
+        FileUseMapEntry<?> newBlock = FileUseMapEntry.makeFree(this.size, size);
         internalAdd(newBlock);
 
-        mSize = size;
+        this.size = size;
 
         coalesce(newBlock);
     }
@@ -381,7 +381,7 @@
      * @param alg which algorithm to use
      * @return the location of the contiguous area; this may be located at the end of the map
      */
-    long locateFree(long size, long alignOffset, long align, @NonNull PositionAlgorithm alg) {
+    long locateFree(long size, long alignOffset, long align, @Nonnull PositionAlgorithm alg) {
         Preconditions.checkArgument(size > 0, "size <= 0");
 
         FileUseMapEntry<?> minimumSizedEntry = FileUseMapEntry.makeFree(0, size);
@@ -389,10 +389,10 @@
 
         switch (alg) {
             case BEST_FIT:
-                matches = mFree.tailSet(minimumSizedEntry);
+                matches = free.tailSet(minimumSizedEntry);
                 break;
             case FIRST_FIT:
-                matches = mMap;
+                matches = map;
                 break;
             default:
                 throw new AssertionError();
@@ -445,7 +445,7 @@
              */
             long emptySpaceLeft = curr.getSize() - (size + extraSize);
             if (emptySpaceLeft > 0 && emptySpaceLeft < mMinFreeSize) {
-                FileUseMapEntry<?> next = mMap.higher(curr);
+                FileUseMapEntry<?> next = map.higher(curr);
                 if (next != null && !next.isFree()) {
                     continue;
                 }
@@ -473,9 +473,9 @@
         /*
          * If no entry that could hold size is found, get the first free byte.
          */
-        long firstFree = mSize;
-        if (best == null && !mMap.isEmpty()) {
-            FileUseMapEntry<?> last = mMap.last();
+        long firstFree = this.size;
+        if (best == null && !map.isEmpty()) {
+            FileUseMapEntry<?> last = map.last();
             if (last.isFree()) {
                 firstFree = last.getStart();
             }
@@ -511,12 +511,12 @@
      * in file order, that is, if area {@code x} starts before area {@code y}, then area {@code x}
      * will be stored before area {@code y} in the list
      */
-    @NonNull
+    @Nonnull
     List<FileUseMapEntry<?>> getFreeAreas() {
         List<FileUseMapEntry<?>> freeAreas = Lists.newArrayList();
 
-        for (FileUseMapEntry<?> area : mMap) {
-            if (area.isFree() && area.getEnd() != mSize) {
+        for (FileUseMapEntry<?> area : map) {
+            if (area.isFree() && area.getEnd() != size) {
                 freeAreas.add(area);
             }
         }
@@ -532,16 +532,53 @@
      * in the map
      */
     @Nullable
-    FileUseMapEntry<?> before(@NonNull FileUseMapEntry<?> entry) {
+    FileUseMapEntry<?> before(@Nonnull FileUseMapEntry<?> entry) {
         Preconditions.checkNotNull(entry, "entry == null");
 
-        return mMap.lower(entry);
+        return map.lower(entry);
+    }
+
+    /**
+     * Obtains the entry that is located after the one provided.
+     *
+     * @param entry the map entry to get the next one for; must belong to the map
+     * @return the entry after the provided one, {@code null} if {@code entry} is the last entry in
+     *     the map
+     */
+    @Nullable
+    FileUseMapEntry<?> after(@Nonnull FileUseMapEntry<?> entry) {
+        Preconditions.checkNotNull(entry, "entry == null");
+
+        return map.higher(entry);
+    }
+
+    /**
+     * Obtains the entry at the given offset.
+     *
+     * @param offset the offset to look for
+     * @return the entry found or {@code null} if there is no entry (not even a free one) at the
+     *     given offset
+     */
+    @Nullable
+    FileUseMapEntry<?> at(long offset) {
+        Preconditions.checkArgument(offset >= 0, "offset < 0");
+        Preconditions.checkArgument(offset < size, "offset >= size");
+
+        FileUseMapEntry<?> entry = map.floor(FileUseMapEntry.makeFree(offset, offset + 1));
+        if (entry == null) {
+            return null;
+        }
+
+        Verify.verify(entry.getStart() <= offset);
+        Verify.verify(entry.getEnd() > offset);
+
+        return entry;
     }
 
     @Override
     public String toString() {
         StringJoiner j = new StringJoiner(", ");
-        mMap.stream()
+        map.stream()
                 .map(e -> e.getStart() + " - " + e.getEnd() + ": " + e.getStore())
                 .forEach(j::add);
         return "FileUseMap[" + j.toString() + "]";
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/FileUseMapEntry.java b/src/main/java/com/android/apkzlib/zip/FileUseMapEntry.java
similarity index 87%
rename from src/main/java/com/android/builder/internal/packaging/zip/FileUseMapEntry.java
rename to src/main/java/com/android/apkzlib/zip/FileUseMapEntry.java
index c0529c8..3e5aaba 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/FileUseMapEntry.java
+++ b/src/main/java/com/android/apkzlib/zip/FileUseMapEntry.java
@@ -14,15 +14,14 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
 
-import com.android.annotations.NonNull;
-import com.android.annotations.Nullable;
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Preconditions;
 import com.google.common.primitives.Ints;
-
 import java.util.Comparator;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
 /**
  * Represents an entry in the {@link FileUseMap}. Each entry contains an interval of bytes. The
@@ -51,18 +50,18 @@
     /**
      * The first byte in the entry.
      */
-    private final long mStart;
+    private final long start;
 
     /**
      * The first byte no longer in the entry.
      */
-    private final long mEnd;
+    private final long end;
 
     /**
      * The stored data. If {@code null} then this entry represents a free entry.
      */
     @Nullable
-    private final T mStore;
+    private final T store;
 
     /**
      * Creates a new map entry.
@@ -75,9 +74,9 @@
         Preconditions.checkArgument(start >= 0, "start < 0");
         Preconditions.checkArgument(end > start, "end <= start");
 
-        mStart = start;
-        mEnd = end;
-        mStore = store;
+        this.start = start;
+        this.end = end;
+        this.store = store;
     }
 
     /**
@@ -100,7 +99,7 @@
      * @param <T> the type of data to store in the entry
      * @return the entry
      */
-    public static <T> FileUseMapEntry<T> makeUsed(long start, long end, @NonNull T store) {
+    public static <T> FileUseMapEntry<T> makeUsed(long start, long end, @Nonnull T store) {
         Preconditions.checkNotNull(store, "store == null");
         return new FileUseMapEntry<>(start, end, store);
     }
@@ -112,7 +111,7 @@
      * is empty and contains no data)
      */
     long getStart() {
-        return mStart;
+        return start;
     }
 
     /**
@@ -121,7 +120,7 @@
      * @return the first byte no longer in the entry
      */
     long getEnd() {
-        return mEnd;
+        return end;
     }
 
     /**
@@ -130,7 +129,7 @@
      * @return the number of bytes contained in the entry
      */
     long getSize() {
-        return mEnd - mStart;
+        return end - start;
     }
 
     /**
@@ -139,7 +138,7 @@
      * @return is this entry free?
      */
     boolean isFree() {
-        return mStore == null;
+        return store == null;
     }
 
     /**
@@ -149,15 +148,15 @@
      */
     @Nullable
     T getStore() {
-        return mStore;
+        return store;
     }
 
     @Override
     public String toString() {
         return MoreObjects.toStringHelper(this)
-                .add("start", mStart)
-                .add("end", mEnd)
-                .add("store", mStore)
+                .add("start", start)
+                .add("end", end)
+                .add("store", store)
                 .toString();
     }
 }
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/GPFlags.java b/src/main/java/com/android/apkzlib/zip/GPFlags.java
similarity index 90%
rename from src/main/java/com/android/builder/internal/packaging/zip/GPFlags.java
rename to src/main/java/com/android/apkzlib/zip/GPFlags.java
index 440718a..fc27c5d 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/GPFlags.java
+++ b/src/main/java/com/android/apkzlib/zip/GPFlags.java
@@ -14,11 +14,10 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip;
-
-import com.android.annotations.NonNull;
+package com.android.apkzlib.zip;
 
 import java.io.IOException;
+import javax.annotation.Nonnull;
 
 /**
  * General purpose bit flags. Contains the encoding of the zip's general purpose bits.
@@ -68,20 +67,21 @@
      */
     private static final int BIT_UNUSED = (1 << 7) | (1 << 8) | (1 << 9) | (1 << 10)
             | (1 << 14) | (1 << 15);
+
     /**
      * Bit flag value.
      */
-    private final long mValue;
+    private final long value;
 
     /**
      * Has the CRC computation beeen deferred?
      */
-    private boolean mDeferredCrc;
+    private boolean deferredCrc;
 
     /**
      * Is the file name encoded in UTF-8?
      */
-    private boolean mUtf8FileName;
+    private boolean utf8FileName;
 
     /**
      * Creates a new flags object.
@@ -89,10 +89,10 @@
      * @param value the value of the bit mask
      */
     private GPFlags(long value) {
-        mValue = value;
+        this.value = value;
 
-        mDeferredCrc = ((value & BIT_DEFERRED_CRC) != 0);
-        mUtf8FileName = ((value & BIT_EFS) != 0);
+        deferredCrc = ((value & BIT_DEFERRED_CRC) != 0);
+        utf8FileName = ((value & BIT_EFS) != 0);
     }
 
     /**
@@ -101,7 +101,7 @@
      * @return the value of the bit mask
      */
     public long getValue() {
-        return mValue;
+        return value;
     }
 
     /**
@@ -110,7 +110,7 @@
      * @return is the CRC computation deferred?
      */
     public boolean isDeferredCrc() {
-        return mDeferredCrc;
+        return deferredCrc;
     }
 
     /**
@@ -119,7 +119,7 @@
      * @return is the file name encoded in UTF-8?
      */
     public boolean isUtf8FileName() {
-        return mUtf8FileName;
+        return utf8FileName;
     }
 
     /**
@@ -128,7 +128,7 @@
      * @param utf8Encoding should UTF-8 encoding be used?
      * @return the new bit mask
      */
-    @NonNull
+    @Nonnull
     static GPFlags make(boolean utf8Encoding) {
         long flags = 0;
 
@@ -147,7 +147,7 @@
      * @return the created flag information
      * @throws IOException unsupported options are used in the bit mask
      */
-    @NonNull
+    @Nonnull
     static GPFlags from(long bits) throws IOException {
         if ((bits & BIT_ENCRYPTION) != 0) {
             throw new IOException("Zip files with encrypted of entries not supported.");
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/InflaterByteSource.java b/src/main/java/com/android/apkzlib/zip/InflaterByteSource.java
similarity index 78%
rename from src/main/java/com/android/builder/internal/packaging/zip/InflaterByteSource.java
rename to src/main/java/com/android/apkzlib/zip/InflaterByteSource.java
index 45154cc..974d4ac 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/InflaterByteSource.java
+++ b/src/main/java/com/android/apkzlib/zip/InflaterByteSource.java
@@ -14,17 +14,16 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
 
-import com.android.annotations.NonNull;
-import com.android.builder.internal.packaging.zip.utils.CloseableByteSource;
-
+import com.android.apkzlib.zip.utils.CloseableByteSource;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.SequenceInputStream;
 import java.util.zip.Inflater;
 import java.util.zip.InflaterInputStream;
+import javax.annotation.Nonnull;
 
 /**
  * Byte source that inflates another byte source. It assumed the inner byte source has deflated
@@ -35,15 +34,15 @@
     /**
      * The stream factory for the deflated data.
      */
-    @NonNull
-    private final CloseableByteSource mDeflatedSource;
+    @Nonnull
+    private final CloseableByteSource deflatedSource;
 
     /**
      * Creates a new source.
      * @param byteSource the factory for deflated data
      */
-    public InflaterByteSource(@NonNull CloseableByteSource byteSource) {
-        mDeflatedSource = byteSource;
+    public InflaterByteSource(@Nonnull CloseableByteSource byteSource) {
+        deflatedSource = byteSource;
     }
 
     @Override
@@ -54,12 +53,12 @@
          * "Oh, I need an extra dummy byte to allow for some... err... optimizations..."
          */
         ByteArrayInputStream hackByte = new ByteArrayInputStream(new byte[] { 0 });
-        return new InflaterInputStream(new SequenceInputStream(mDeflatedSource.openStream(),
+        return new InflaterInputStream(new SequenceInputStream(deflatedSource.openStream(),
                 hackByte), new Inflater(true));
     }
 
     @Override
     public void innerClose() throws IOException {
-        mDeflatedSource.close();
+        deflatedSource.close();
     }
 }
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/LazyDelegateByteSource.java b/src/main/java/com/android/apkzlib/zip/LazyDelegateByteSource.java
similarity index 84%
rename from src/main/java/com/android/builder/internal/packaging/zip/LazyDelegateByteSource.java
rename to src/main/java/com/android/apkzlib/zip/LazyDelegateByteSource.java
index 1620d10..bdd3e4c 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/LazyDelegateByteSource.java
+++ b/src/main/java/com/android/apkzlib/zip/LazyDelegateByteSource.java
@@ -14,11 +14,10 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
 
 
-import com.android.annotations.NonNull;
-import com.android.builder.internal.packaging.zip.utils.CloseableByteSource;
+import com.android.apkzlib.zip.utils.CloseableByteSource;
 import com.google.common.hash.HashCode;
 import com.google.common.hash.HashFunction;
 import com.google.common.io.ByteProcessor;
@@ -26,12 +25,12 @@
 import com.google.common.io.ByteSource;
 import com.google.common.io.CharSource;
 import com.google.common.util.concurrent.ListenableFuture;
-
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.nio.charset.Charset;
 import java.util.concurrent.ExecutionException;
+import javax.annotation.Nonnull;
 
 /**
  * {@code ByteSource} that delegates all operations to another {@code ByteSource}. The other
@@ -42,24 +41,24 @@
     /**
      * Byte source where we delegate operations to.
      */
-    @NonNull
-    private final ListenableFuture<CloseableByteSource> mDelegate;
+    @Nonnull
+    private final ListenableFuture<CloseableByteSource> delegate;
 
     /**
      * Creates a new byte source that delegates operations to the provided source.
      * @param delegate the source that will receive all operations
      */
-    public LazyDelegateByteSource(@NonNull ListenableFuture<CloseableByteSource> delegate) {
-        mDelegate = delegate;
+    public LazyDelegateByteSource(@Nonnull ListenableFuture<CloseableByteSource> delegate) {
+        this.delegate = delegate;
     }
 
     /**
      * Obtains the delegate future.
      * @return the delegate future, that may be computed or not
      */
-    @NonNull
+    @Nonnull
     public ListenableFuture<CloseableByteSource> getDelegate() {
-        return mDelegate;
+        return delegate;
     }
 
     /**
@@ -67,10 +66,10 @@
      * @return the byte source
      * @throws IOException failed to compute the future :)
      */
-    @NonNull
+    @Nonnull
     private CloseableByteSource get() throws IOException {
         try {
-            CloseableByteSource r = mDelegate.get();
+            CloseableByteSource r = delegate.get();
             if (r == null) {
                 throw new IOException("Delegate byte source computation resulted in null.");
             }
@@ -117,12 +116,12 @@
     }
 
     @Override
-    public long copyTo(@NonNull OutputStream output) throws IOException {
+    public long copyTo(@Nonnull OutputStream output) throws IOException {
         return get().copyTo(output);
     }
 
     @Override
-    public long copyTo(@NonNull ByteSink sink) throws IOException {
+    public long copyTo(@Nonnull ByteSink sink) throws IOException {
         return get().copyTo(sink);
     }
 
@@ -132,7 +131,7 @@
     }
 
     @Override
-    public <T> T read(@NonNull ByteProcessor<T> processor) throws IOException {
+    public <T> T read(@Nonnull ByteProcessor<T> processor) throws IOException {
         return get().read(processor);
     }
 
@@ -142,7 +141,7 @@
     }
 
     @Override
-    public boolean contentEquals(@NonNull ByteSource other) throws IOException {
+    public boolean contentEquals(@Nonnull ByteSource other) throws IOException {
         return get().contentEquals(other);
     }
 
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/ProcessedAndRawByteSources.java b/src/main/java/com/android/apkzlib/zip/ProcessedAndRawByteSources.java
similarity index 75%
rename from src/main/java/com/android/builder/internal/packaging/zip/ProcessedAndRawByteSources.java
rename to src/main/java/com/android/apkzlib/zip/ProcessedAndRawByteSources.java
index 1bda379..86c6382 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/ProcessedAndRawByteSources.java
+++ b/src/main/java/com/android/apkzlib/zip/ProcessedAndRawByteSources.java
@@ -14,14 +14,13 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
 
-import com.android.annotations.NonNull;
-import com.android.builder.internal.packaging.zip.utils.CloseableByteSource;
+import com.android.apkzlib.zip.utils.CloseableByteSource;
 import com.google.common.io.Closer;
-
 import java.io.Closeable;
 import java.io.IOException;
+import javax.annotation.Nonnull;
 
 /**
  * Container that has two bytes sources: one representing raw data and another processed data.
@@ -34,51 +33,54 @@
     /**
      * The processed byte source.
      */
-    @NonNull
-    private final CloseableByteSource mProcessedSource;
+    @Nonnull
+    private final CloseableByteSource processedSource;
 
     /**
      * The processed raw source.
      */
-    @NonNull
-    private final CloseableByteSource mRawSource;
+    @Nonnull
+    private final CloseableByteSource rawSource;
 
     /**
      * Creates a new container.
+     *
      * @param processedSource the processed source
      * @param rawSource the raw source
      */
-    public ProcessedAndRawByteSources(@NonNull CloseableByteSource processedSource,
-            @NonNull CloseableByteSource rawSource) {
-        mProcessedSource = processedSource;
-        mRawSource = rawSource;
+    public ProcessedAndRawByteSources(@Nonnull CloseableByteSource processedSource,
+            @Nonnull CloseableByteSource rawSource) {
+        this.processedSource = processedSource;
+        this.rawSource = rawSource;
     }
 
     /**
      * Obtains a byte source that read the processed contents of the entry.
+     *
      * @return a byte source
      */
-    @NonNull
+    @Nonnull
     public CloseableByteSource getProcessedByteSource() {
-        return mProcessedSource;
+        return processedSource;
     }
 
     /**
      * Obtains a byte source that reads the raw contents of an entry. This is the data that is
      * ultimately stored in the file and, in the case of compressed files, is the same data in the
      * source returned by {@link #getProcessedByteSource()}.
+     * 
      * @return a byte source
      */
-    @NonNull
+    @Nonnull
     public CloseableByteSource getRawByteSource() {
-        return mRawSource;
+        return rawSource;
     }
 
     @Override
     public void close() throws IOException {
         Closer closer = Closer.create();
-        closer.register(mProcessedSource);
-        closer.register(mRawSource);
+        closer.register(processedSource);
+        closer.register(rawSource);
         closer.close();
     }
 }
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/StoredEntry.java b/src/main/java/com/android/apkzlib/zip/StoredEntry.java
similarity index 68%
rename from src/main/java/com/android/builder/internal/packaging/zip/StoredEntry.java
rename to src/main/java/com/android/apkzlib/zip/StoredEntry.java
index a67c6b2..854bf3a 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/StoredEntry.java
+++ b/src/main/java/com/android/apkzlib/zip/StoredEntry.java
@@ -14,22 +14,23 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
 
-import com.android.annotations.NonNull;
-import com.android.annotations.Nullable;
-import com.android.builder.internal.packaging.zip.utils.CloseableByteSource;
-import com.android.builder.internal.packaging.zip.utils.CloseableDelegateByteSource;
+import com.android.apkzlib.zip.utils.CloseableByteSource;
+import com.android.apkzlib.zip.utils.CloseableDelegateByteSource;
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
 import com.google.common.base.Verify;
 import com.google.common.io.ByteSource;
 import com.google.common.io.ByteStreams;
 import com.google.common.primitives.Ints;
-
+import java.io.BufferedInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.nio.ByteBuffer;
 import java.util.Comparator;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
 /**
  * A stored entry represents a file in the zip. The entry may or may not be written to the zip
@@ -85,7 +86,8 @@
     /**
      * Local header field: version to extract, should match the CDH's.
      */
-    private static final ZipField.F2 F_VERSION_EXTRACT = new ZipField.F2(
+    @VisibleForTesting
+    static final ZipField.F2 F_VERSION_EXTRACT = new ZipField.F2(
             F_LOCAL_SIGNATURE.endOffset(), "Version to extract",
             new ZipFieldInvariantNonNegative());
 
@@ -152,44 +154,50 @@
     /**
      * Type of entry.
      */
-    @NonNull
-    private StoredEntryType mType;
+    @Nonnull
+    private StoredEntryType type;
 
     /**
      * The central directory header with information about the file.
      */
-    @NonNull
-    private CentralDirectoryHeader mCdh;
+    @Nonnull
+    private CentralDirectoryHeader cdh;
 
     /**
      * The file this entry is associated with
      */
-    @NonNull
-    private ZFile mFile;
+    @Nonnull
+    private ZFile file;
 
     /**
      * Has this entry been deleted?
      */
-    private boolean mDeleted;
+    private boolean deleted;
 
     /**
      * Extra field specified in the local directory.
      */
-    @NonNull
-    private ExtraField mLocalExtra;
+    @Nonnull
+    private ExtraField localExtra;
 
     /**
      * Type of data descriptor associated with the entry.
      */
-    @NonNull
-    private DataDescriptorType mDataDescriptorType;
+    @Nonnull
+    private DataDescriptorType dataDescriptorType;
 
     /**
      * Source for this entry's data. If this entry is a directory, this source has to have zero
      * size.
      */
-    @NonNull
-    private ProcessedAndRawByteSources mSource;
+    @Nonnull
+    private ProcessedAndRawByteSources source;
+
+    /**
+     * Verify log for the entry.
+     */
+    @Nonnull
+    private final VerifyLog verifyLog;
 
     /**
      * Creates a new stored entry.
@@ -201,39 +209,45 @@
      * read from the zip file, that is, if {@code header.getOffset()} is non-negative
      * @throws IOException failed to create the entry
      */
-    StoredEntry(@NonNull CentralDirectoryHeader header, @NonNull ZFile file,
-            @Nullable ProcessedAndRawByteSources source) throws IOException {
-        mCdh = header;
-        mFile = file;
-        mDeleted = false;
+    StoredEntry(
+            @Nonnull CentralDirectoryHeader header,
+            @Nonnull ZFile file,
+            @Nullable ProcessedAndRawByteSources source)
+            throws IOException {
+        cdh = header;
+        this.file = file;
+        deleted = false;
+        verifyLog = file.makeVerifyLog();
 
         if (header.getOffset() >= 0) {
             /*
              * This will be overwritten during readLocalHeader. However, IJ complains if we don't
-             * assign a value to mLocalExtra because of the @NonNull annotation.
+             * assign a value to localExtra because of the @Nonnull annotation.
              */
-            mLocalExtra = new ExtraField();
+            localExtra = new ExtraField();
 
             readLocalHeader();
 
-            Preconditions.checkArgument(source == null, "Source was defined but contents already "
-                    + "exist on file.");
+            Preconditions.checkArgument(
+                    source == null,
+                    "Source was defined but contents already exist on file.");
 
             /*
              * Since the file is already in the zip, dynamically create a source that will read
              * the file from the zip when needed. The assignment is not really needed, but we
              * would get a warning because of the @NotNull otherwise.
              */
-            mSource = createSourceFromZip(mCdh.getOffset());
+            this.source = createSourceFromZip(cdh.getOffset());
         } else {
             /*
              * There is no local extra data for new files.
              */
-            mLocalExtra = new ExtraField();
+            localExtra = new ExtraField();
 
-            Preconditions.checkNotNull(source, "Source was not defined, but contents are not "
-                    + "on file.");
-            mSource = source;
+            Preconditions.checkNotNull(
+                    source,
+                    "Source was not defined, but contents are not on file.");
+            this.source = source;
         }
 
         /*
@@ -241,31 +255,35 @@
          * This seems to be respected by all zip utilities although I could not find there anywhere
          * in the specification.
          */
-        if (mCdh.getName().endsWith(Character.toString(ZFile.SEPARATOR))) {
-            mType = StoredEntryType.DIRECTORY;
-            Verify.verify(mSource.getProcessedByteSource().isEmpty(),
+        if (cdh.getName().endsWith(Character.toString(ZFile.SEPARATOR))) {
+            type = StoredEntryType.DIRECTORY;
+            verifyLog.verify(
+                    this.source.getProcessedByteSource().isEmpty(),
                     "Directory source is not empty.");
-            Verify.verify(mCdh.getCrc32() == 0, "Directory has CRC32 = %s.", mCdh.getCrc32());
-            Verify.verify(mCdh.getUncompressedSize() == 0, "Directory has uncompressed size = %s.",
-                    mCdh.getUncompressedSize());
+            verifyLog.verify(cdh.getCrc32() == 0, "Directory has CRC32 = %s.", cdh.getCrc32());
+            verifyLog.verify(
+                    cdh.getUncompressedSize() == 0,
+                    "Directory has uncompressed size = %s.",
+                    cdh.getUncompressedSize());
 
             /*
              * Some clever (OMG!) tools, like jar will actually try to compress the directory
              * contents and generate a 2 byte compressed data. Of course, the uncompressed size is
              * zero and we're just wasting space.
              */
-            long compressedSize = mCdh.getCompressionInfoWithWait().getCompressedSize();
-            Verify.verify(compressedSize == 0 || compressedSize == 2,
+            long compressedSize = cdh.getCompressionInfoWithWait().getCompressedSize();
+            verifyLog.verify(
+                    compressedSize == 0 || compressedSize == 2,
                     "Directory has compressed size = %s.", compressedSize);
         } else {
-            mType = StoredEntryType.FILE;
+            type = StoredEntryType.FILE;
         }
 
         /*
          * By default we assume there is no data descriptor unless the CRC is marked as deferred
          * in the header's GP Bit.
          */
-        mDataDescriptorType = DataDescriptorType.NO_DATA_DESCRIPTOR;
+        dataDescriptorType = DataDescriptorType.NO_DATA_DESCRIPTOR;
         if (header.getGpBit().isDeferredCrc()) {
             /*
              * If the deferred CRC bit exists, then we have an extra descriptor field. This extra
@@ -288,8 +306,8 @@
      * @return the local header size in bytes
      */
     public int getLocalHeaderSize() {
-        Preconditions.checkState(!mDeleted, "mDeleted");
-        return FIXED_LOCAL_FILE_HEADER_SIZE + mCdh.getEncodedFileName().length + mLocalExtra.size();
+        Preconditions.checkState(!deleted, "deleted");
+        return FIXED_LOCAL_FILE_HEADER_SIZE + cdh.getEncodedFileName().length + localExtra.size();
     }
 
     /**
@@ -300,9 +318,9 @@
      * @throws IOException failed to get compression information
      */
     long getInFileSize() throws IOException {
-        Preconditions.checkState(!mDeleted, "mDeleted");
-        return mCdh.getCompressionInfoWithWait().getCompressedSize() + getLocalHeaderSize()
-                + mDataDescriptorType.size;
+        Preconditions.checkState(!deleted, "deleted");
+        return cdh.getCompressionInfoWithWait().getCompressedSize() + getLocalHeaderSize()
+                + dataDescriptorType.size;
     }
 
     /**
@@ -311,9 +329,9 @@
      * @return a stream that will return as many bytes as the uncompressed entry size
      * @throws IOException failed to open the stream
      */
-    @NonNull
+    @Nonnull
     public InputStream open() throws IOException {
-        return mSource.getProcessedByteSource().openStream();
+        return source.getProcessedByteSource().openStream();
     }
 
     /**
@@ -322,7 +340,7 @@
      * @return a byte array with the contents of the file (uncompressed if the file was compressed)
      * @throws IOException failed to read the file
      */
-    @NonNull
+    @Nonnull
     public byte[] read() throws IOException {
         try (InputStream is = open()) {
             return ByteStreams.toByteArray(is);
@@ -330,14 +348,31 @@
     }
 
     /**
+     * Obtains the contents of the file in an existing buffer.
+     *
+     * @param bytes buffer to read the file contents in.
+     * @return the number of bytes read
+     * @throws IOException failed to read the file.
+     */
+    public int read(byte[] bytes) throws IOException {
+        if (bytes.length < getCentralDirectoryHeader().getUncompressedSize()) {
+            throw new RuntimeException(
+                    "Buffer to small while reading {}" + getCentralDirectoryHeader().getName());
+        }
+        try (InputStream is = new BufferedInputStream(open())) {
+            return ByteStreams.read(is, bytes, 0, bytes.length);
+        }
+    }
+
+    /**
      * Obtains the type of entry.
      *
      * @return the type of entry
      */
-    @NonNull
+    @Nonnull
     public StoredEntryType getType() {
-        Preconditions.checkState(!mDeleted, "mDeleted");
-        return mType;
+        Preconditions.checkState(!deleted, "deleted");
+        return type;
     }
 
     /**
@@ -345,6 +380,7 @@
      * To eventually write updates to disk, {@link ZFile#update()} must be called.
      *
      * @throws IOException failed to delete the entry
+     * @throws IllegalStateException if the zip file was open in read-only mode
      */
     public void delete() throws IOException {
         delete(true);
@@ -357,12 +393,20 @@
      * @param notify should listeners be notified of the deletion? This will only be
      * {@code false} if the entry is being removed as part of a replacement
      * @throws IOException failed to delete the entry
+     * @throws IllegalStateException if the zip file was open in read-only mode
      */
     void delete(boolean notify) throws IOException {
-        Preconditions.checkState(!mDeleted, "mDeleted");
-        mFile.delete(this, notify);
-        mDeleted = true;
-        mSource.close();
+        Preconditions.checkState(!deleted, "deleted");
+        file.delete(this, notify);
+        deleted = true;
+        source.close();
+    }
+
+    /**
+     * Returns {@code true} if this entry has been deleted/replaced.
+     */
+    public boolean isDeleted() {
+        return deleted;
     }
 
     /**
@@ -370,9 +414,9 @@
      *
      * @return the CDH
      */
-    @NonNull
+    @Nonnull
     public CentralDirectoryHeader getCentralDirectoryHeader() {
-        return mCdh;
+        return cdh;
     }
 
     /**
@@ -380,29 +424,29 @@
      * Header provided in the constructor. This method should only be called if the entry already
      * exists on disk; new entries do not have local headers.
      * <p>
-     * This method will define the {@link #mLocalExtra} field that is only defined in the
+     * This method will define the {@link #localExtra} field that is only defined in the
      * local descriptor.
      *
      * @throws IOException failed to read the local header
      */
     private void readLocalHeader() throws IOException {
         byte[] localHeader = new byte[FIXED_LOCAL_FILE_HEADER_SIZE];
-        mFile.directFullyRead(mCdh.getOffset(), localHeader);
+        file.directFullyRead(cdh.getOffset(), localHeader);
 
-        CentralDirectoryHeaderCompressInfo compressInfo = mCdh.getCompressionInfoWithWait();
+        CentralDirectoryHeaderCompressInfo compressInfo = cdh.getCompressionInfoWithWait();
 
         ByteBuffer bytes = ByteBuffer.wrap(localHeader);
         F_LOCAL_SIGNATURE.verify(bytes);
-        F_VERSION_EXTRACT.verify(bytes, compressInfo.getVersionExtract());
-        F_GP_BIT.verify(bytes, mCdh.getGpBit().getValue());
-        F_METHOD.verify(bytes, compressInfo.getMethod().methodCode);
+        F_VERSION_EXTRACT.verify(bytes, compressInfo.getVersionExtract(), verifyLog);
+        F_GP_BIT.verify(bytes, cdh.getGpBit().getValue(), verifyLog);
+        F_METHOD.verify(bytes, compressInfo.getMethod().methodCode, verifyLog);
 
-        if (mFile.areTimestampsIgnored()) {
+        if (file.areTimestampsIgnored()) {
             F_LAST_MOD_TIME.skip(bytes);
             F_LAST_MOD_DATE.skip(bytes);
         } else {
-            F_LAST_MOD_TIME.verify(bytes, mCdh.getLastModTime());
-            F_LAST_MOD_DATE.verify(bytes, mCdh.getLastModDate());
+            F_LAST_MOD_TIME.verify(bytes, cdh.getLastModTime(), verifyLog);
+            F_LAST_MOD_DATE.verify(bytes, cdh.getLastModDate(), verifyLog);
         }
 
         /*
@@ -410,32 +454,36 @@
          * File Header must be ignored and their actual values must be read from the Data
          * Descriptor following the contents of this entry. See readDataDescriptorRecord().
          */
-        if (mCdh.getGpBit().isDeferredCrc()) {
+        if (cdh.getGpBit().isDeferredCrc()) {
             F_CRC32.skip(bytes);
             F_COMPRESSED_SIZE.skip(bytes);
             F_UNCOMPRESSED_SIZE.skip(bytes);
         } else {
-            F_CRC32.verify(bytes, mCdh.getCrc32());
-            F_COMPRESSED_SIZE.verify(bytes, compressInfo.getCompressedSize());
-            F_UNCOMPRESSED_SIZE.verify(bytes, mCdh.getUncompressedSize());
+            F_CRC32.verify(bytes, cdh.getCrc32(), verifyLog);
+            F_COMPRESSED_SIZE.verify(bytes, compressInfo.getCompressedSize(), verifyLog);
+            F_UNCOMPRESSED_SIZE.verify(bytes, cdh.getUncompressedSize(), verifyLog);
         }
 
-        F_FILE_NAME_LENGTH.verify(bytes, mCdh.getEncodedFileName().length);
+        F_FILE_NAME_LENGTH.verify(bytes, cdh.getEncodedFileName().length);
         long extraLength = F_EXTRA_LENGTH.read(bytes);
-        long fileNameStart = mCdh.getOffset() + F_EXTRA_LENGTH.endOffset();
-        byte[] fileNameData = new byte[mCdh.getEncodedFileName().length];
-        mFile.directFullyRead(fileNameStart, fileNameData);
+        long fileNameStart = cdh.getOffset() + F_EXTRA_LENGTH.endOffset();
+        byte[] fileNameData = new byte[cdh.getEncodedFileName().length];
+        file.directFullyRead(fileNameStart, fileNameData);
 
-        String fileName = EncodeUtils.decode(fileNameData, mCdh.getGpBit());
-        if (!fileName.equals(mCdh.getName())) {
-            throw new IOException("Central directory reports file as being named '" + mCdh.getName()
-                    + "' but local header reports file being named '" + fileName + "'.");
+        String fileName = EncodeUtils.decode(fileNameData, cdh.getGpBit());
+        if (!fileName.equals(cdh.getName())) {
+            verifyLog.log(
+                    String.format(
+                            "Central directory reports file as being named '%s' but local header"
+                                    + "reports file being named '%s'.",
+                    cdh.getName(),
+                    fileName));
         }
 
-        long localExtraStart = fileNameStart + mCdh.getEncodedFileName().length;
+        long localExtraStart = fileNameStart + cdh.getEncodedFileName().length;
         byte[] localExtraRaw = new byte[Ints.checkedCast(extraLength)];
-        mFile.directFullyRead(localExtraStart, localExtraRaw);
-        mLocalExtra = new ExtraField(localExtraRaw);
+        file.directFullyRead(localExtraStart, localExtraRaw);
+        localExtra = new ExtraField(localExtraRaw);
     }
 
     /**
@@ -443,18 +491,18 @@
      * that a data descriptor does exist. It will read the data descriptor and check that the data
      * described there matches the data provided in the Central Directory.
      * <p>
-     * This method will set the {@link #mDataDescriptorType} field to the appropriate type of
+     * This method will set the {@link #dataDescriptorType} field to the appropriate type of
      * data descriptor record.
      *
      * @throws IOException failed to read the data descriptor record
      */
     private void readDataDescriptorRecord() throws IOException {
-        CentralDirectoryHeaderCompressInfo compressInfo = mCdh.getCompressionInfoWithWait();
+        CentralDirectoryHeaderCompressInfo compressInfo = cdh.getCompressionInfoWithWait();
 
-        long ddStart = mCdh.getOffset() + FIXED_LOCAL_FILE_HEADER_SIZE
-                + mCdh.getName().length() + mLocalExtra.size() + compressInfo.getCompressedSize();
-        byte ddData[] = new byte[DataDescriptorType.DATA_DESCRIPTOR_WITH_SIGNATURE.size];
-        mFile.directFullyRead(ddStart, ddData);
+        long ddStart = cdh.getOffset() + FIXED_LOCAL_FILE_HEADER_SIZE
+                + cdh.getName().length() + localExtra.size() + compressInfo.getCompressedSize();
+        byte[] ddData = new byte[DataDescriptorType.DATA_DESCRIPTOR_WITH_SIGNATURE.size];
+        file.directFullyRead(ddStart, ddData);
 
         ByteBuffer ddBytes = ByteBuffer.wrap(ddData);
 
@@ -462,9 +510,9 @@
         int cpos = ddBytes.position();
         long sig = signatureField.read(ddBytes);
         if (sig == DATA_DESC_SIGNATURE) {
-            mDataDescriptorType = DataDescriptorType.DATA_DESCRIPTOR_WITH_SIGNATURE;
+            dataDescriptorType = DataDescriptorType.DATA_DESCRIPTOR_WITH_SIGNATURE;
         } else {
-            mDataDescriptorType = DataDescriptorType.DATA_DESCRIPTOR_WITHOUT_SIGNATURE;
+            dataDescriptorType = DataDescriptorType.DATA_DESCRIPTOR_WITHOUT_SIGNATURE;
             ddBytes.position(cpos);
         }
 
@@ -473,11 +521,9 @@
         ZipField.F4 uncompressedField = new ZipField.F4(compressedField.endOffset(),
                 "Uncompressed size");
 
-        if (!mFile.getSkipDataDescriptorVerification()) {
-            crc32Field.verify(ddBytes, mCdh.getCrc32());
-            compressedField.verify(ddBytes, compressInfo.getCompressedSize());
-            uncompressedField.verify(ddBytes, mCdh.getUncompressedSize());
-        }
+        crc32Field.verify(ddBytes, cdh.getCrc32(), verifyLog);
+        compressedField.verify(ddBytes, compressInfo.getCompressedSize(), verifyLog);
+        uncompressedField.verify(ddBytes, cdh.getUncompressedSize(), verifyLog);
     }
 
     /**
@@ -487,14 +533,14 @@
      * @throws IOException failed to close the old source
      * @return the created source
      */
-    @NonNull
+    @Nonnull
     private ProcessedAndRawByteSources createSourceFromZip(final long zipOffset)
             throws IOException {
         Preconditions.checkArgument(zipOffset >= 0, "zipOffset < 0");
 
         final CentralDirectoryHeaderCompressInfo compressInfo;
         try {
-            compressInfo = mCdh.getCompressionInfoWithWait();
+            compressInfo = cdh.getCompressionInfoWithWait();
         } catch (IOException e) {
             throw new RuntimeException("IOException should never occur here because compression "
                     + "information should be immediately available if reading from zip.", e);
@@ -509,16 +555,16 @@
                 return compressInfo.getCompressedSize();
             }
 
-            @NonNull
+            @Nonnull
             @Override
             public InputStream openStream() throws IOException {
-                Preconditions.checkState(!mDeleted, "mDeleted");
+                Preconditions.checkState(!deleted, "deleted");
 
                 long dataStart = zipOffset + getLocalHeaderSize();
                 long dataEnd = dataStart + compressInfo.getCompressedSize();
 
-                mFile.openReadOnly();
-                return mFile.directOpen(dataStart, dataEnd);
+                file.openReadOnly();
+                return file.directOpen(dataStart, dataEnd);
             }
 
             @Override
@@ -540,12 +586,12 @@
      * @param rawContents the raw data to create the source from
      * @return the sources for this entry
      */
-    @NonNull
+    @Nonnull
     private ProcessedAndRawByteSources createSourcesFromRawContents(
-            @NonNull CloseableByteSource rawContents) {
+            @Nonnull CloseableByteSource rawContents) {
         CentralDirectoryHeaderCompressInfo compressInfo;
         try {
-            compressInfo = mCdh.getCompressionInfoWithWait();
+            compressInfo = cdh.getCompressionInfoWithWait();
         } catch (IOException e) {
             throw new RuntimeException("IOException should never occur here because compression "
                     + "information should be immediately available if creating from raw "
@@ -568,7 +614,7 @@
     }
 
     /**
-     * Replaces {@link #mSource} with one that reads file data from the zip file.
+     * Replaces {@link #source} with one that reads file data from the zip file.
      *
      * @param zipFileOffset the offset in the zip file where data is written; must be non-negative
      * @throws IOException failed to replace the source
@@ -576,14 +622,14 @@
     void replaceSourceFromZip(long zipFileOffset) throws IOException {
         Preconditions.checkArgument(zipFileOffset >= 0, "zipFileOffset < 0");
 
-        ProcessedAndRawByteSources oldSource = mSource;
-        mSource = createSourceFromZip(zipFileOffset);
-        mCdh.setOffset(zipFileOffset);
+        ProcessedAndRawByteSources oldSource = source;
+        source = createSourceFromZip(zipFileOffset);
+        cdh.setOffset(zipFileOffset);
         oldSource.close();
     }
 
     /**
-     * Loads all data in memory and replaces {@link #mSource} with one that contains all the data
+     * Loads all data in memory and replaces {@link #source} with one that contains all the data
      * in memory.
      *
      * <p>If the entry's contents are already in memory, this call does nothing.
@@ -591,7 +637,7 @@
      * @throws IOException failed to replace the source
      */
     void loadSourceIntoMemory() throws IOException {
-        if (mCdh.getOffset() == -1) {
+        if (cdh.getOffset() == -1) {
             /*
              * No offset in the CDR means data has not been written to disk which, in turn,
              * means data is already loaded into memory.
@@ -599,11 +645,11 @@
             return;
         }
 
-        ProcessedAndRawByteSources oldSource = mSource;
+        ProcessedAndRawByteSources oldSource = source;
         byte[] rawContents = oldSource.getRawByteSource().read();
-        mSource = createSourcesFromRawContents(new CloseableDelegateByteSource(
+        source = createSourcesFromRawContents(new CloseableDelegateByteSource(
                 ByteSource.wrap(rawContents), rawContents.length));
-        mCdh.setOffset(-1);
+        cdh.setOffset(-1);
         oldSource.close();
     }
 
@@ -613,9 +659,9 @@
      *
      * @return the entry source
      */
-    @NonNull
+    @Nonnull
     ProcessedAndRawByteSources getSource() {
-        return mSource;
+        return source;
     }
 
     /**
@@ -623,9 +669,25 @@
      *
      * @return the type of data descriptor
      */
-    @NonNull
+    @Nonnull
     public DataDescriptorType getDataDescriptorType() {
-        return mDataDescriptorType;
+        return dataDescriptorType;
+    }
+
+    /**
+     * Removes the data descriptor, if it has one and resets the data descriptor bit in the
+     * central directory header.
+     *
+     * @return was the data descriptor remove?
+     */
+    boolean removeDataDescriptor() {
+        if (dataDescriptorType == DataDescriptorType.NO_DATA_DESCRIPTOR) {
+            return false;
+        }
+
+        dataDescriptorType = DataDescriptorType.NO_DATA_DESCRIPTOR;
+        cdh.resetDeferredCrc();
+        return true;
     }
 
     /**
@@ -634,38 +696,38 @@
      * @return the header data
      * @throws IOException failed to get header byte data
      */
-    @NonNull
+    @Nonnull
     byte[] toHeaderData() throws IOException {
 
-        byte[] encodedFileName = mCdh.getEncodedFileName();
+        byte[] encodedFileName = cdh.getEncodedFileName();
 
         ByteBuffer out =
                 ByteBuffer.allocate(
-                        F_EXTRA_LENGTH.endOffset() + encodedFileName.length + mLocalExtra.size());
+                        F_EXTRA_LENGTH.endOffset() + encodedFileName.length + localExtra.size());
 
-        CentralDirectoryHeaderCompressInfo compressInfo = mCdh.getCompressionInfoWithWait();
+        CentralDirectoryHeaderCompressInfo compressInfo = cdh.getCompressionInfoWithWait();
 
         F_LOCAL_SIGNATURE.write(out);
         F_VERSION_EXTRACT.write(out, compressInfo.getVersionExtract());
-        F_GP_BIT.write(out, mCdh.getGpBit().getValue());
+        F_GP_BIT.write(out, cdh.getGpBit().getValue());
         F_METHOD.write(out, compressInfo.getMethod().methodCode);
 
-        if (mFile.areTimestampsIgnored()) {
+        if (file.areTimestampsIgnored()) {
             F_LAST_MOD_TIME.write(out, 0);
             F_LAST_MOD_DATE.write(out, 0);
         } else {
-            F_LAST_MOD_TIME.write(out, mCdh.getLastModTime());
-            F_LAST_MOD_DATE.write(out, mCdh.getLastModDate());
+            F_LAST_MOD_TIME.write(out, cdh.getLastModTime());
+            F_LAST_MOD_DATE.write(out, cdh.getLastModDate());
         }
 
-        F_CRC32.write(out, mCdh.getCrc32());
+        F_CRC32.write(out, cdh.getCrc32());
         F_COMPRESSED_SIZE.write(out, compressInfo.getCompressedSize());
-        F_UNCOMPRESSED_SIZE.write(out, mCdh.getUncompressedSize());
-        F_FILE_NAME_LENGTH.write(out, mCdh.getEncodedFileName().length);
-        F_EXTRA_LENGTH.write(out, mLocalExtra.size());
+        F_UNCOMPRESSED_SIZE.write(out, cdh.getUncompressedSize());
+        F_FILE_NAME_LENGTH.write(out, cdh.getEncodedFileName().length);
+        F_EXTRA_LENGTH.write(out, localExtra.size());
 
-        out.put(mCdh.getEncodedFileName());
-        mLocalExtra.write(out);
+        out.put(cdh.getEncodedFileName());
+        localExtra.write(out);
 
         return out.array();
     }
@@ -683,9 +745,9 @@
      * file
      */
     public boolean realign() throws IOException {
-        Preconditions.checkState(!mDeleted, "Entry has been deleted.");
+        Preconditions.checkState(!deleted, "Entry has been deleted.");
 
-        return mFile.realign(this);
+        return file.realign(this);
     }
 
     /**
@@ -693,9 +755,9 @@
      *
      * @return the contents of the local extra field
      */
-    @NonNull
+    @Nonnull
     public ExtraField getLocalExtra() {
-        return mLocalExtra;
+        return localExtra;
     }
 
     /**
@@ -704,9 +766,9 @@
      * @param localExtra the contents of the local extra field
      * @throws IOException failed to update the zip file
      */
-    public void setLocalExtra(@NonNull ExtraField localExtra) throws IOException {
+    public void setLocalExtra(@Nonnull ExtraField localExtra) throws IOException {
         boolean resized = setLocalExtraNoNotify(localExtra);
-        mFile.localHeaderChanged(this, resized);
+        file.localHeaderChanged(this, resized);
     }
 
     /**
@@ -718,7 +780,7 @@
      * @return has the local header size changed?
      * @throws IOException failed to load the file
      */
-    boolean setLocalExtraNoNotify(@NonNull ExtraField localExtra) throws IOException {
+    boolean setLocalExtraNoNotify(@Nonnull ExtraField localExtra) throws IOException {
         boolean sizeChanged;
 
         /*
@@ -734,13 +796,23 @@
          */
         loadSourceIntoMemory();
 
-        if (mLocalExtra.size() != localExtra.size()) {
+        if (this.localExtra.size() != localExtra.size()) {
             sizeChanged = true;
         } else {
             sizeChanged = false;
         }
 
-        mLocalExtra = localExtra;
+        this.localExtra = localExtra;
         return sizeChanged;
     }
+
+    /**
+     * Obtains the verify log for the entry.
+     *
+     * @return the verify log
+     */
+    @Nonnull
+    public VerifyLog getVerifyLog() {
+        return verifyLog;
+    }
 }
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/StoredEntryType.java b/src/main/java/com/android/apkzlib/zip/StoredEntryType.java
similarity index 93%
rename from src/main/java/com/android/builder/internal/packaging/zip/StoredEntryType.java
rename to src/main/java/com/android/apkzlib/zip/StoredEntryType.java
index 22983ab..9ce9252 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/StoredEntryType.java
+++ b/src/main/java/com/android/apkzlib/zip/StoredEntryType.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
 
 /**
  * Type of stored entry.
diff --git a/src/main/java/com/android/apkzlib/zip/VerifyLog.java b/src/main/java/com/android/apkzlib/zip/VerifyLog.java
new file mode 100644
index 0000000..2a7db7c
--- /dev/null
+++ b/src/main/java/com/android/apkzlib/zip/VerifyLog.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2017 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.apkzlib.zip;
+
+import com.google.common.collect.ImmutableList;
+import javax.annotation.Nonnull;
+
+/**
+ * The verify log contains verification messages. It is used to capture validation issues with a
+ * zip file or with parts of a zip file.
+ */
+public interface VerifyLog {
+
+    /**
+     * Logs a message.
+     *
+     * @param message the message to verify
+     */
+    void log(@Nonnull String message);
+
+    /**
+     * Obtains all save logged messages.
+     *
+     * @return the logged messages
+     */
+    @Nonnull
+    ImmutableList<String> getLogs();
+
+    /**
+     * Performs verification of a non-critical condition, logging a message if the condition is
+     * not verified.
+     *
+     * @param condition the condition
+     * @param message the message to write if {@code condition} is {@code false}.
+     * @param args arguments for formatting {@code message} using {@code String.format}
+     */
+    default void verify(boolean condition, @Nonnull String message, @Nonnull Object... args) {
+        if (!condition) {
+            log(String.format(message, args));
+        }
+    }
+}
diff --git a/src/main/java/com/android/apkzlib/zip/VerifyLogs.java b/src/main/java/com/android/apkzlib/zip/VerifyLogs.java
new file mode 100644
index 0000000..b7acb83
--- /dev/null
+++ b/src/main/java/com/android/apkzlib/zip/VerifyLogs.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2017 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.apkzlib.zip;
+
+import com.google.common.collect.ImmutableList;
+import java.util.ArrayList;
+import java.util.List;
+import javax.annotation.Nonnull;
+
+/**
+ * Factory for verification logs.
+ */
+final class VerifyLogs {
+
+    private VerifyLogs() {}
+
+    /**
+     * Creates a {@link VerifyLog} that ignores all messages logged.
+     *
+     * @return the log
+     */
+    @Nonnull
+    static VerifyLog devNull() {
+        return new VerifyLog() {
+            @Override
+            public void log(@Nonnull String message) {}
+
+            @Nonnull
+            @Override
+            public ImmutableList<String> getLogs() {
+                return ImmutableList.of();
+            }
+        };
+    }
+
+    /**
+     * Creates a {@link VerifyLog} that stores all log messages.
+     *
+     * @return the log
+     */
+    @Nonnull
+    static VerifyLog unlimited() {
+        return new VerifyLog() {
+
+            /**
+             * All saved messages.
+             */
+            @Nonnull
+            private final List<String> messages = new ArrayList<>();
+
+            @Override
+            public void log(@Nonnull String message) {
+                messages.add(message);
+            }
+
+            @Nonnull
+            @Override
+            public ImmutableList<String> getLogs() {
+                return ImmutableList.copyOf(messages);
+            }
+        };
+    }
+}
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/ZFile.java b/src/main/java/com/android/apkzlib/zip/ZFile.java
similarity index 67%
rename from src/main/java/com/android/builder/internal/packaging/zip/ZFile.java
rename to src/main/java/com/android/apkzlib/zip/ZFile.java
index 7048e46..9034f4c 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/ZFile.java
+++ b/src/main/java/com/android/apkzlib/zip/ZFile.java
@@ -14,19 +14,18 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
 
-import com.android.annotations.NonNull;
-import com.android.annotations.Nullable;
-import com.android.builder.internal.packaging.zip.utils.ByteTracker;
-import com.android.builder.internal.packaging.zip.utils.CloseableByteSource;
-import com.android.builder.internal.packaging.zip.utils.LittleEndianUtils;
-import com.android.builder.internal.packaging.zip.utils.RandomAccessFileUtils;
-import com.android.builder.internal.utils.CachedFileContents;
-import com.android.builder.internal.utils.IOExceptionFunction;
-import com.android.builder.internal.utils.IOExceptionRunnable;
+import com.android.apkzlib.utils.CachedFileContents;
+import com.android.apkzlib.utils.IOExceptionFunction;
+import com.android.apkzlib.utils.IOExceptionRunnable;
+import com.android.apkzlib.zip.compress.Zip64NotSupportedException;
+import com.android.apkzlib.zip.utils.ByteTracker;
+import com.android.apkzlib.zip.utils.CloseableByteSource;
+import com.android.apkzlib.zip.utils.LittleEndianUtils;
 import com.google.common.base.Preconditions;
 import com.google.common.base.Verify;
+import com.google.common.base.VerifyException;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
@@ -39,15 +38,18 @@
 import com.google.common.util.concurrent.FutureCallback;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
 import com.google.common.util.concurrent.SettableFuture;
 import java.io.ByteArrayInputStream;
 import java.io.Closeable;
+import java.io.EOFException;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.RandomAccessFile;
 import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
@@ -60,6 +62,9 @@
 import java.util.concurrent.Future;
 import java.util.function.Function;
 import java.util.function.Predicate;
+import java.util.function.Supplier;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
 /**
  * The {@code ZFile} provides the main interface for interacting with zip files. A {@code ZFile}
@@ -196,9 +201,14 @@
     private static final int ZIP64_EOCD_LOCATOR_SIZE = 20;
 
     /**
+     * Maximum size for the EOCD.
+     */
+    private static final int MAX_EOCD_COMMENT_SIZE = 65535;
+
+    /**
      * How many bytes to look back from the end of the file to look for the EOCD signature.
      */
-    private static final int LAST_BYTES_TO_READ = 65535 + MIN_EOCD_SIZE;
+    private static final int LAST_BYTES_TO_READ = MIN_EOCD_SIZE + MAX_EOCD_COMMENT_SIZE;
 
     /**
      * Signature of the Zip64 EOCD locator record.
@@ -206,6 +216,11 @@
     private static final int ZIP64_EOCD_LOCATOR_SIGNATURE = 0x07064b50;
 
     /**
+     * Signature of the EOCD record.
+     */
+    private static final byte[] EOCD_SIGNATURE = new byte[] { 0x06, 0x05, 0x4b, 0x50 };
+
+    /**
      * Size of buffer for I/O operations.
      */
     private static final int IO_BUFFER_SIZE = 1024 * 1024;
@@ -217,9 +232,11 @@
     private static final int MAXIMUM_EXTENSION_CYCLE_COUNT = 10;
 
     /**
-     * Minimum size for the extra field.
+     * Minimum size for the extra field when we have to add one. We rely on the alignment segment
+     * to do that so the minimum size for the extra field is the minimum size of an alignment
+     * segment.
      */
-    private static final int MINIMUM_EXTRA_FIELD_SIZE = 6;
+    private static final int MINIMUM_EXTRA_FIELD_SIZE = ExtraField.AlignmentSegment.MINIMUM_SIZE;
 
     /**
      * Maximum size of the extra field.
@@ -232,29 +249,32 @@
     /**
      * File zip file.
      */
-    @NonNull
-    private final File mFile;
+    @Nonnull
+    private final File file;
 
     /**
      * The random access file used to access the zip file. This will be {@code null} if and only
-     * if {@link #mState} is {@link ZipFileState#CLOSED}.
+     * if {@link #state} is {@link ZipFileState#CLOSED}.
      */
     @Nullable
-    private RandomAccessFile mRaf;
+    private RandomAccessFile raf;
 
     /**
      * The map containing the in-memory contents of the zip file. It keeps track of which parts of
      * the zip file are used and which are not.
      */
-    @NonNull
-    private final FileUseMap mMap;
+    @Nonnull
+    private final FileUseMap map;
 
     /**
      * The EOCD entry. Will be {@code null} if there is no EOCD (because the zip is new) or the
      * one that exists on disk is no longer valid (because the zip has been changed).
+     *
+     * <p>If the EOCD is deleted because the zip has been changed and the old EOCD was no longer
+     * valid, then {@link #eocdComment} will contain the comment saved from the EOCD.
      */
     @Nullable
-    private FileUseMapEntry<Eocd> mEocdEntry;
+    private FileUseMapEntry<Eocd> eocdEntry;
 
     /**
      * The Central Directory entry. Will be {@code null} if there is no Central Directory (because
@@ -262,42 +282,45 @@
      * has been changed).
      */
     @Nullable
-    private FileUseMapEntry<CentralDirectory> mDirectoryEntry;
+    private FileUseMapEntry<CentralDirectory> directoryEntry;
 
     /**
      * All entries in the zip file. It includes in-memory changes and may not reflect what is
      * written on disk. Only entries that have been compressed are in this list.
      */
-    @NonNull
-    private final Map<String, FileUseMapEntry<StoredEntry>> mEntries;
+    @Nonnull
+    private final Map<String, FileUseMapEntry<StoredEntry>> entries;
 
     /**
      * Entries added to the zip file, but that are not yet compressed. When compression is done,
-     * these entries are eventually moved to {@link #mEntries}. mUncompressedEntries is a list
+     * these entries are eventually moved to {@link #entries}. uncompressedEntries is a list
      * because entries need to be kept in the order by which they were added. It allows adding
      * multiple files with the same name and getting the right notifications on which files replaced
      * which.
      *
      * <p>Files are placed in this list in {@link #add(StoredEntry)} method. This method will
-     * keep files here temporarily and move then to {@link #mEntries} when the data is
+     * keep files here temporarily and move then to {@link #entries} when the data is
      * available.
      *
-     * <p>Moving files out of this list to {@link #mEntries} is done by
+     * <p>Moving files out of this list to {@link #entries} is done by
      * {@link #processAllReadyEntries()}.
      */
-    @NonNull
-    private final List<StoredEntry> mUncompressedEntries;
+    @Nonnull
+    private final List<StoredEntry> uncompressedEntries;
 
     /**
      * Current state of the zip file.
      */
-    @NonNull
-    private ZipFileState mState;
+    @Nonnull
+    private ZipFileState state;
 
     /**
      * Are the in-memory changes that have not been written to the zip file?
+     *
+     * <p>This might be false, but will become true after {@link #processAllReadyEntriesWithWait()}
+     * is called if there are {@link #uncompressedEntries} compressing in the background.
      */
-    private boolean mDirty;
+    private boolean dirty;
 
     /**
      * Non-{@code null} only if the file is currently closed. Used to detect if the zip is
@@ -305,72 +328,96 @@
      * be {@code null} even if it is closed.
      */
     @Nullable
-    private CachedFileContents<Object> mClosedControl;
+    private CachedFileContents<Object> closedControl;
 
     /**
      * The alignment rule.
      */
-    @NonNull
-    private final AlignmentRule mAlignmentRule;
+    @Nonnull
+    private final AlignmentRule alignmentRule;
 
     /**
      * Extensions registered with the file.
      */
-    @NonNull
-    private final List<ZFileExtension> mExtensions;
+    @Nonnull
+    private final List<ZFileExtension> extensions;
 
     /**
      * When notifying extensions, extensions may request that some runnables are executed. This
      * list collects all runnables by the order they were requested. Together with
-     * {@link #mIsNotifying}, it is used to avoid reordering notifications.
+     * {@link #isNotifying}, it is used to avoid reordering notifications.
      */
-    @NonNull
-    private final List<IOExceptionRunnable> mToRun;
+    @Nonnull
+    private final List<IOExceptionRunnable> toRun;
 
     /**
-     * {@code true} when {@link #notify(IOExceptionFunction)} is notifying extensions. Used
-     * to avoid reordering notifications.
+     * {@code true} when {@link #notify(com.android.apkzlib.utils.IOExceptionFunction)} is
+     * notifying extensions. Used to avoid reordering notifications.
      */
-    private boolean mIsNotifying;
+    private boolean isNotifying;
 
     /**
      * An extra offset for the central directory location. {@code 0} if the central directory
      * should be written in its standard location.
      */
-    private long mExtraDirectoryOffset;
+    private long extraDirectoryOffset;
 
     /**
      * Should all timestamps be zeroed when reading / writing the zip?
      */
-    private boolean mNoTimestamps;
+    private boolean noTimestamps;
 
     /**
      * Compressor to use.
      */
-    @NonNull
-    private Compressor mCompressor;
+    @Nonnull
+    private Compressor compressor;
 
     /**
      * Byte tracker to use.
      */
-    @NonNull
-    private final ByteTracker mTracker;
+    @Nonnull
+    private final ByteTracker tracker;
 
     /**
      * Use the zip entry's "extra field" field to cover empty space in the zip file?
      */
-    private boolean mCoverEmptySpaceUsingExtraField;
+    private boolean coverEmptySpaceUsingExtraField;
 
     /**
      * Should files be automatically sorted when updating?
      */
-    private boolean mAutoSortFiles;
+    private boolean autoSortFiles;
 
     /**
-     * Should data descriptor verification be skipped? See
-     * {@link ZFileOptions#getSkipDataDescriptorValidation()}.
+     * Verify log factory to use.
      */
-    private boolean mSkipDataDescriptorVerification;
+    @Nonnull
+    private final Supplier<VerifyLog> verifyLogFactory;
+
+    /**
+     * Verify log to use.
+     */
+    @Nonnull
+    private final VerifyLog verifyLog;
+
+    /**
+     * This field contains the comment in the zip's EOCD if there is no in-memory EOCD structure.
+     * This may happen, for example, if the zip has been changed and the Central Directory and
+     * EOCD have been deleted (in-memory). In that case, this field will save the comment to place
+     * on the EOCD once it is created.
+     *
+     * <p>This field will only be non-{@code null} if there is no in-memory EOCD structure
+     * (<i>i.e.</i>, {@link #eocdEntry} is {@code null}). If there is an {@link #eocdEntry}, then
+     * the comment will be there instead of being in this field.
+     */
+    @Nullable
+    private byte[] eocdComment;
+
+    /**
+     * Is the file in read-only mode? In read-only mode no changes are allowed.
+     */
+    private boolean readOnly;
 
 
     /**
@@ -382,7 +429,7 @@
      * @param file the zip file
      * @throws IOException some file exists but could not be read
      */
-    public ZFile(@NonNull File file) throws IOException {
+    public ZFile(@Nonnull File file) throws IOException {
         this(file, new ZFileOptions());
     }
 
@@ -396,55 +443,90 @@
      * @param options configuration options
      * @throws IOException some file exists but could not be read
      */
-    public ZFile(@NonNull File file, @NonNull ZFileOptions options) throws IOException {
-        mFile = file;
-        mMap = new FileUseMap(
+    public ZFile(@Nonnull File file, @Nonnull ZFileOptions options) throws IOException {
+        this(file, options, false);
+    }
+
+    /**
+     * Creates a new zip file. If the zip file does not exist, then no file is created at this
+     * point and {@code ZFile} will contain an empty structure. However, an (empty) zip file will
+     * be created if either {@link #update()} or {@link #close()} are used. If a zip file exists,
+     * it will be parsed and read.
+     *
+     * @param file the zip file
+     * @param options configuration options
+     * @param readOnly should the file be open in read-only mode? If {@code true} then the file must
+     * exist and no methods can be invoked that could potentially change the file
+     * @throws IOException some file exists but could not be read
+     */
+    public ZFile(@Nonnull File file, @Nonnull ZFileOptions options, boolean readOnly)
+            throws IOException {
+        this.file = file;
+        map = new FileUseMap(
                 0,
                 options.getCoverEmptySpaceUsingExtraField()
                         ? MINIMUM_EXTRA_FIELD_SIZE
                         : 0);
-        mDirty = false;
-        mClosedControl = null;
-        mAlignmentRule = options.getAlignmentRule();
-        mExtensions = Lists.newArrayList();
-        mToRun = Lists.newArrayList();
-        mNoTimestamps = options.getNoTimestamps();
-        mTracker = options.getTracker();
-        mCompressor = options.getCompressor();
-        mCoverEmptySpaceUsingExtraField = options.getCoverEmptySpaceUsingExtraField();
-        mAutoSortFiles = options.getAutoSortFiles();
-        mSkipDataDescriptorVerification = options.getSkipDataDescriptorValidation();
+        this.readOnly = readOnly;
+        dirty = false;
+        closedControl = null;
+        alignmentRule = options.getAlignmentRule();
+        extensions = Lists.newArrayList();
+        toRun = Lists.newArrayList();
+        noTimestamps = options.getNoTimestamps();
+        tracker = options.getTracker();
+        compressor = options.getCompressor();
+        coverEmptySpaceUsingExtraField = options.getCoverEmptySpaceUsingExtraField();
+        autoSortFiles = options.getAutoSortFiles();
+        verifyLogFactory = options.getVerifyLogFactory();
+        verifyLog = verifyLogFactory.get();
 
         /*
          * These two values will be overwritten by openReadOnly() below if the file exists.
          */
-        mState = ZipFileState.CLOSED;
-        mRaf = null;
+        state = ZipFileState.CLOSED;
+        raf = null;
 
         if (file.exists()) {
             openReadOnly();
+        } else if (readOnly) {
+            throw new IOException("File does not exist but read-only mode requested");
         } else {
-            mDirty = true;
+            dirty = true;
         }
 
-        mEntries = Maps.newHashMap();
-        mUncompressedEntries = Lists.newArrayList();
-        mExtraDirectoryOffset = 0;
+        entries = Maps.newHashMap();
+        uncompressedEntries = Lists.newArrayList();
+        extraDirectoryOffset = 0;
 
         try {
-            if (mState != ZipFileState.CLOSED) {
-                long rafSize = mRaf.length();
+            if (state != ZipFileState.CLOSED) {
+                long rafSize = raf.length();
                 if (rafSize > Integer.MAX_VALUE) {
                     throw new IOException("File exceeds size limit of " + Integer.MAX_VALUE + ".");
                 }
 
-                mMap.extend(Ints.checkedCast(rafSize));
+                map.extend(Ints.checkedCast(rafSize));
                 readData();
+            }
 
+            // If we don't have an EOCD entry, set the comment to empty.
+            if (eocdEntry == null) {
+                eocdComment = new byte[0];
+            }
+
+            // Notify the extensions if the zip file has been open.
+            if (state != ZipFileState.CLOSED) {
                 notify(ZFileExtension::open);
             }
+        } catch (Zip64NotSupportedException e) {
+            throw e;
         } catch (IOException e) {
             throw new IOException("Failed to read zip file '" + file.getAbsolutePath() + "'.", e);
+        } catch (IllegalStateException | IllegalArgumentException | VerifyException e) {
+            throw new RuntimeException(
+                    "Internal error when trying to read zip file '" + file.getAbsolutePath() + "'.",
+                    e);
         }
     }
 
@@ -454,11 +536,11 @@
      *
      * @return all entries in the zip
      */
-    @NonNull
+    @Nonnull
     public Set<StoredEntry> entries() {
         Map<String, StoredEntry> entries = Maps.newHashMap();
 
-        for (FileUseMapEntry<StoredEntry> mapEntry : mEntries.values()) {
+        for (FileUseMapEntry<StoredEntry> mapEntry : this.entries.values()) {
             StoredEntry entry = mapEntry.getStore();
             assert entry != null;
             entries.put(entry.getCentralDirectoryHeader().getName(), entry);
@@ -468,7 +550,7 @@
          * mUncompressed may override mEntriesReady as we may not have yet processed all
          * entries.
          */
-        for (StoredEntry uncompressed : mUncompressedEntries) {
+        for (StoredEntry uncompressed : uncompressedEntries) {
             entries.put(uncompressed.getCentralDirectoryHeader().getName(), uncompressed);
         }
 
@@ -482,18 +564,18 @@
      * @return the entry at the path or {@code null} if none exists
      */
     @Nullable
-    public StoredEntry get(@NonNull String path) {
+    public StoredEntry get(@Nonnull String path) {
         /*
          * The latest entries are the last ones in uncompressed and they may eventually override
-         * files in mEntries.
+         * files in entries.
          */
-        for (StoredEntry stillUncompressed : Lists.reverse(mUncompressedEntries)) {
+        for (StoredEntry stillUncompressed : Lists.reverse(uncompressedEntries)) {
             if (stillUncompressed.getCentralDirectoryHeader().getName().equals(path)) {
                 return stillUncompressed;
             }
         }
 
-        FileUseMapEntry<StoredEntry> found = mEntries.get(path);
+        FileUseMapEntry<StoredEntry> found = entries.get(path);
         if (found == null) {
             return null;
         }
@@ -508,20 +590,20 @@
      * @throws IOException failed to read the zip file
      */
     private void readData() throws IOException {
-        Preconditions.checkState(mState != ZipFileState.CLOSED, "mState == ZipFileState.CLOSED");
-        Preconditions.checkState(mRaf != null, "mRaf == null");
+        Preconditions.checkState(state != ZipFileState.CLOSED, "state == ZipFileState.CLOSED");
+        Preconditions.checkState(raf != null, "raf == null");
 
         readEocd();
         readCentralDirectory();
 
         /*
-         * Compute where the last file ends. We will need this to compute thee extra offset.
+         * Go over all files and create the usage map, verifying there is no overlap in the files.
          */
         long entryEndOffset;
         long directoryStartOffset;
 
-        if (mDirectoryEntry != null) {
-            CentralDirectory directory = mDirectoryEntry.getStore();
+        if (directoryEntry != null) {
+            CentralDirectory directory = directoryEntry.getStore();
             assert directory != null;
 
             entryEndOffset = 0;
@@ -543,53 +625,101 @@
                  * file.
                  */
 
-                FileUseMapEntry<StoredEntry> mapEntry = mMap.add(start, end, entry);
-                mEntries.put(entry.getCentralDirectoryHeader().getName(), mapEntry);
+                Verify.verify(start >= 0, "start < 0");
+                Verify.verify(end < map.size(), "end >= map.size()");
+
+                FileUseMapEntry<?> found = map.at(start);
+                Verify.verifyNotNull(found);
+
+                // We've got a problem if the found entry is not free or is a free entry but
+                // doesn't cover the whole file.
+                if (!found.isFree() || found.getEnd() < end) {
+                    if (found.isFree()) {
+                        found = map.after(found);
+                        Verify.verify(found != null && !found.isFree());
+                    }
+
+                    Object foundEntry = found.getStore();
+                    Verify.verify(foundEntry != null);
+
+                    // Obtains a custom description of an entry.
+                    IOExceptionFunction<StoredEntry, String> describe =
+                            e ->
+                                    String.format(
+                                            "'%s' (offset: %d, size: %d)",
+                                            e.getCentralDirectoryHeader().getName(),
+                                            e.getCentralDirectoryHeader().getOffset(),
+                                            e.getInFileSize());
+
+                    String overlappingEntryDescription;
+                    if (foundEntry instanceof StoredEntry) {
+                        StoredEntry foundStored = (StoredEntry) foundEntry;
+                        overlappingEntryDescription = describe.apply((StoredEntry) foundEntry);
+                    } else {
+                        overlappingEntryDescription =
+                                "Central Directory / EOCD: "
+                                        + found.getStart()
+                                        + " - "
+                                        + found.getEnd();
+                    }
+
+                    throw new IOException(
+                            "Cannot read entry "
+                                    + describe.apply(entry)
+                                    + " because it overlaps with "
+                                    + overlappingEntryDescription);
+                }
+
+                FileUseMapEntry<StoredEntry> mapEntry = map.add(start, end, entry);
+                entries.put(entry.getCentralDirectoryHeader().getName(), mapEntry);
 
                 if (end > entryEndOffset) {
                     entryEndOffset = end;
                 }
             }
 
-            directoryStartOffset = mDirectoryEntry.getStart();
+            directoryStartOffset = directoryEntry.getStart();
         } else {
             /*
              * No directory means an empty zip file. Use the start of the EOCD to compute
              * an existing offset.
              */
-            Verify.verifyNotNull(mEocdEntry);
-            assert mEocdEntry != null;
-            directoryStartOffset = mEocdEntry.getStart();
+            Verify.verifyNotNull(eocdEntry);
+            assert eocdEntry != null;
+            directoryStartOffset = eocdEntry.getStart();
             entryEndOffset = 0;
         }
 
+        /*
+         * Check if there is an extra central directory offset. If there is, save it. Note that
+         * we can't call extraDirectoryOffset() because that would mark the file as dirty.
+         */
         long extraOffset = directoryStartOffset - entryEndOffset;
         Verify.verify(extraOffset >= 0, "extraOffset (%s) < 0", extraOffset);
-        setExtraDirectoryOffset(extraOffset);
+        extraDirectoryOffset = extraOffset;
     }
 
     /**
-     * Finds the EOCD marker and reads it. It will populate the {@link #mEocdEntry} variable.
+     * Finds the EOCD marker and reads it. It will populate the {@link #eocdEntry} variable.
      *
      * @throws IOException failed to read the EOCD
      */
     private void readEocd() throws IOException {
-        Preconditions.checkState(mState != ZipFileState.CLOSED, "mState == ZipFileState.CLOSED");
-        Preconditions.checkState(mRaf != null, "mRaf == null");
+        Preconditions.checkState(state != ZipFileState.CLOSED, "state == ZipFileState.CLOSED");
+        Preconditions.checkState(raf != null, "raf == null");
 
         /*
          * Read the last part of the zip into memory. If we don't find the EOCD signature by then,
          * the file is corrupt.
          */
         int lastToRead = LAST_BYTES_TO_READ;
-        if (lastToRead > mRaf.length()) {
-            lastToRead = Ints.checkedCast(mRaf.length());
+        if (lastToRead > raf.length()) {
+            lastToRead = Ints.checkedCast(raf.length());
         }
 
         byte[] last = new byte[lastToRead];
-        directFullyRead(mRaf.length() - lastToRead, last);
+        directFullyRead(raf.length() - lastToRead, last);
 
-        byte[] eocdSignature = new byte[] { 0x06, 0x05, 0x4b, 0x50 };
 
         /*
          * Start endIdx at the first possible location where the signature can be located and then
@@ -610,10 +740,10 @@
             /*
              * Remember: little endian...
              */
-            if (last[endIdx] == eocdSignature[3]
-                    && last[endIdx + 1] == eocdSignature[2]
-                    && last[endIdx + 2] == eocdSignature[1]
-                    && last[endIdx + 3] == eocdSignature[0]) {
+            if (last[endIdx] == EOCD_SIGNATURE[3]
+                    && last[endIdx + 1] == EOCD_SIGNATURE[2]
+                    && last[endIdx + 2] == EOCD_SIGNATURE[1]
+                    && last[endIdx + 3] == EOCD_SIGNATURE[0]) {
 
                 /*
                  * We found a signature. Try to read the EOCD record.
@@ -625,15 +755,20 @@
 
                 try {
                     eocd = new Eocd(eocdBytes);
-                    eocdStart = Ints.checkedCast(mRaf.length() - lastToRead + foundEocdSignature);
+                    eocdStart = Ints.checkedCast(raf.length() - lastToRead + foundEocdSignature);
 
                     /*
-                     * Make sure the EOCD takes the whole file up to the end.
+                     * Make sure the EOCD takes the whole file up to the end. Log an error if it
+                     * doesn't.
                      */
-                    if (eocdStart + eocd.getEocdSize() != mRaf.length()) {
-                        throw new IOException("EOCD starts at " + eocdStart + " and has "
-                                + eocd.getEocdSize() + " bytes, but file ends at " + mRaf.length()
-                                + ".");
+                    if (eocdStart + eocd.getEocdSize() != raf.length()) {
+                        verifyLog.log("EOCD starts at "
+                                        + eocdStart
+                                        + " and has "
+                                        + eocd.getEocdSize()
+                                        + " bytes, but file ends at "
+                                        + raf.length()
+                                        + ".");
                     }
                 } catch (IOException e) {
                     if (errorFindingSignature != null) {
@@ -660,44 +795,60 @@
          */
         int zip64LocatorStart = eocdStart - ZIP64_EOCD_LOCATOR_SIZE;
         if (zip64LocatorStart >= 0) {
-            byte possibleZip64Locator[] = new byte[4];
+            byte[] possibleZip64Locator = new byte[4];
             directFullyRead(zip64LocatorStart, possibleZip64Locator);
             if (LittleEndianUtils.readUnsigned4Le(ByteBuffer.wrap(possibleZip64Locator)) ==
                     ZIP64_EOCD_LOCATOR_SIGNATURE) {
-                throw new IOException("Zip64 EOCD locator found but Zip64 format is not "
-                        + "supported.");
+                throw new Zip64NotSupportedException(
+                        "Zip64 EOCD locator found but Zip64 format is not supported.");
             }
         }
 
-        mEocdEntry = mMap.add(eocdStart, eocdStart + eocd.getEocdSize(), eocd);
+        eocdEntry = map.add(eocdStart, eocdStart + eocd.getEocdSize(), eocd);
     }
 
     /**
-     * Reads the zip's central directory and populates the {@link #mDirectoryEntry} variable. This
+     * Reads the zip's central directory and populates the {@link #directoryEntry} variable. This
      * method can only be called after the EOCD has been read. If the central directory is empty
-     * (if there are no files on the zip archive), then {@link #mDirectoryEntry} will be set to
+     * (if there are no files on the zip archive), then {@link #directoryEntry} will be set to
      * {@code null}.
      *
      * @throws IOException failed to read the central directory
      */
     private void readCentralDirectory() throws IOException {
-        Preconditions.checkNotNull(mEocdEntry, "mEocdEntry == null");
-        Preconditions.checkNotNull(mEocdEntry.getStore(), "mEocdEntry.getStore() == null");
-        Preconditions.checkState(mState != ZipFileState.CLOSED, "mState == ZipFileState.CLOSED");
-        Preconditions.checkState(mRaf != null, "mRaf == null");
-        Preconditions.checkState(mDirectoryEntry == null, "mDirectoryEntry != null");
+        Preconditions.checkNotNull(eocdEntry, "eocdEntry == null");
+        Preconditions.checkNotNull(eocdEntry.getStore(), "eocdEntry.getStore() == null");
+        Preconditions.checkState(state != ZipFileState.CLOSED, "state == ZipFileState.CLOSED");
+        Preconditions.checkState(raf != null, "raf == null");
+        Preconditions.checkState(directoryEntry == null, "directoryEntry != null");
 
-        Eocd eocd = mEocdEntry.getStore();
+        Eocd eocd = eocdEntry.getStore();
 
         long dirSize = eocd.getDirectorySize();
         if (dirSize > Integer.MAX_VALUE) {
             throw new IOException("Cannot read central directory with size " + dirSize + ".");
         }
 
-        if (eocd.getDirectoryOffset() + dirSize != mEocdEntry.getStart()) {
-            throw new IOException("Central directory is stored in [" + eocd.getDirectoryOffset()
-                    + " - " + (eocd.getDirectoryOffset() + dirSize) + "] and EOCD starts at "
-                    + mEocdEntry.getStart() + ".");
+        long centralDirectoryEnd = eocd.getDirectoryOffset() + dirSize;
+        if (centralDirectoryEnd != eocdEntry.getStart()) {
+            String msg = "Central directory is stored in ["
+                    + eocd.getDirectoryOffset()
+                    + " - "
+                    + (centralDirectoryEnd - 1)
+                    + "] and EOCD starts at "
+                    + eocdEntry.getStart()
+                    + ".";
+
+            /*
+             * If there is an empty space between the central directory and the EOCD, we proceed
+             * logging an error. If the central directory ends after the start of the EOCD (and
+             * therefore, they overlap), throw an exception.
+             */
+            if (centralDirectoryEnd > eocdEntry.getSize()) {
+                throw new IOException(msg);
+            } else {
+                verifyLog.log(msg);
+            }
         }
 
         byte[] directoryData = new byte[Ints.checkedCast(dirSize)];
@@ -709,8 +860,10 @@
                         eocd.getTotalRecords(),
                         this);
         if (eocd.getDirectorySize() > 0) {
-            mDirectoryEntry = mMap.add(eocd.getDirectoryOffset(), eocd.getDirectoryOffset()
-                    + eocd.getDirectorySize(), directory);
+            directoryEntry = map.add(
+                    eocd.getDirectoryOffset(),
+                    eocd.getDirectoryOffset() + eocd.getDirectorySize(),
+                    directory);
         }
     }
 
@@ -726,13 +879,13 @@
      * returned <em>as is</em>
      * @throws IOException failed to open the zip file
      */
-    @NonNull
+    @Nonnull
     public InputStream directOpen(final long start, final long end) throws IOException {
-        Preconditions.checkState(mState != ZipFileState.CLOSED, "mState == ZipFileState.CLOSED");
-        Preconditions.checkState(mRaf != null, "mRaf == null");
+        Preconditions.checkState(state != ZipFileState.CLOSED, "state == ZipFileState.CLOSED");
+        Preconditions.checkState(raf != null, "raf == null");
         Preconditions.checkArgument(start >= 0, "start < 0");
         Preconditions.checkArgument(end >= start, "end < start");
-        Preconditions.checkArgument(end <= mRaf.length(), "end > mRaf.length()");
+        Preconditions.checkArgument(end <= raf.length(), "end > raf.length()");
 
         return new InputStream() {
             private long mCurr = start;
@@ -743,7 +896,7 @@
                     return -1;
                 }
 
-                byte b[] = new byte[1];
+                byte[] b = new byte[1];
                 int r = directRead(mCurr, b);
                 if (r > 0) {
                     mCurr++;
@@ -754,7 +907,7 @@
             }
 
             @Override
-            public int read(@NonNull byte[] b, int off, int len) throws IOException {
+            public int read(@Nonnull byte[] b, int off, int len) throws IOException {
                 Preconditions.checkNotNull(b, "b == null");
                 Preconditions.checkArgument(off >= 0, "off < 0");
                 Preconditions.checkArgument(off <= b.length, "off > b.length");
@@ -790,17 +943,20 @@
      * @param notify should listeners be notified of the deletion? This will only be
      * {@code false} if the entry is being removed as part of a replacement
      * @throws IOException failed to delete the entry
+     * @throws IllegalStateException if open in read-only mode
      */
-    void delete(@NonNull final StoredEntry entry, boolean notify) throws IOException {
+    void delete(@Nonnull final StoredEntry entry, boolean notify) throws IOException {
+        checkNotInReadOnlyMode();
+
         String path = entry.getCentralDirectoryHeader().getName();
-        FileUseMapEntry<StoredEntry> mapEntry = mEntries.get(path);
+        FileUseMapEntry<StoredEntry> mapEntry = entries.get(path);
         Preconditions.checkNotNull(mapEntry, "mapEntry == null");
         Preconditions.checkArgument(entry == mapEntry.getStore(), "entry != mapEntry.getStore()");
 
-        mDirty = true;
+        dirty = true;
 
-        mMap.remove(mapEntry);
-        mEntries.remove(path);
+        map.remove(mapEntry);
+        entries.remove(path);
 
         if (notify) {
             notify(ext -> ext.removed(entry));
@@ -808,13 +964,26 @@
     }
 
     /**
+     * Checks that the file is not in read-only mode.
+     *
+     * @throws IllegalStateException if the file is in read-only mode
+     */
+    private void checkNotInReadOnlyMode() {
+        if (readOnly) {
+            throw new IllegalStateException("Illegal operation in read only model");
+        }
+    }
+
+    /**
      * Updates the file writing new entries and removing deleted entries. This will force
      * reopening the file as read/write if the file wasn't open in read/write mode.
-     * 
+     *
      * @throws IOException failed to update the file; this exception may have been thrown by
      * the compressor but only reported here
      */
     public void update() throws IOException {
+        checkNotInReadOnlyMode();
+
         /*
          * Process all background stuff before calling in the extensions.
          */
@@ -828,7 +997,7 @@
         processAllReadyEntriesWithWait();
 
 
-        if (!mDirty) {
+        if (!dirty) {
             return;
         }
 
@@ -839,7 +1008,7 @@
          * empty spaces or sort. If we sort, we don't need to repack as sorting forces the
          * zip file to be as compact as possible.
          */
-        if (mAutoSortFiles) {
+        if (autoSortFiles) {
             sortZipContents();
         } else {
             packIfNecessary();
@@ -850,22 +1019,22 @@
          * will have to be rewritten.
          */
         deleteDirectoryAndEocd();
-        mMap.truncate();
+        map.truncate();
 
         /*
          * If we need to use the extra field to cover empty spaces, we do the processing here.
          */
-        if (mCoverEmptySpaceUsingExtraField) {
+        if (coverEmptySpaceUsingExtraField) {
 
             /* We will go over all files in the zip and check whether there is empty space before
              * them. If there is, then we will move the entry to the beginning of the empty space
              * (covering it) and extend the extra field with the size of the empty space.
              */
-            for (FileUseMapEntry<StoredEntry> entry : new HashSet<>(mEntries.values())) {
+            for (FileUseMapEntry<StoredEntry> entry : new HashSet<>(entries.values())) {
                 StoredEntry storedEntry = entry.getStore();
                 assert storedEntry != null;
 
-                FileUseMapEntry<?> before = mMap.before(entry);
+                FileUseMapEntry<?> before = map.before(entry);
                 if (before == null || !before.isFree()) {
                     continue;
                 }
@@ -891,17 +1060,27 @@
                  * Remove the entry.
                  */
                 String name = storedEntry.getCentralDirectoryHeader().getName();
-                mMap.remove(entry);
-                Verify.verify(entry == mEntries.remove(name));
+                map.remove(entry);
+                Verify.verify(entry == entries.remove(name));
 
                 /*
                  * Make a list will all existing segments in the entry's extra field, but remove
                  * the alignment field, if it exists. Also, sum the size of all kept extra field
                  * segments.
                  */
+                ImmutableList<ExtraField.Segment> currentSegments;
+                try {
+                    currentSegments = storedEntry.getLocalExtra().getSegments();
+                } catch (IOException e) {
+                    /*
+                     * Parsing current segments has failed. This means the contents of the extra
+                     * field are not valid. We'll continue discarding the existing segments.
+                     */
+                    currentSegments = ImmutableList.of();
+                }
+
                 List<ExtraField.Segment> extraFieldSegments = new ArrayList<>();
-                int newExtraFieldSize =
-                        storedEntry.getLocalExtra().getSegments().stream()
+                int newExtraFieldSize = currentSegments.stream()
                         .filter(s -> s.getHeaderId()
                                 != ExtraField.ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID)
                         .peek(extraFieldSegments::add)
@@ -919,7 +1098,7 @@
 
                 storedEntry.setLocalExtraNoNotify(
                         new ExtraField(ImmutableList.copyOf(extraFieldSegments)));
-                mEntries.put(name, mMap.add(newStart, newStart + newSize, storedEntry));
+                entries.put(name, map.add(newStart, newStart + newSize, storedEntry));
 
                 /*
                  * Reset the offset to force the file to be rewritten.
@@ -941,7 +1120,7 @@
         TreeMap<FileUseMapEntry<?>, StoredEntry> toWriteToStore =
                 new TreeMap<>(FileUseMapEntry.COMPARE_BY_START);
 
-        for (FileUseMapEntry<StoredEntry> entry : mEntries.values()) {
+        for (FileUseMapEntry<StoredEntry> entry : entries.values()) {
             StoredEntry entryStore = entry.getStore();
             assert entryStore != null;
             if (entryStore.getCentralDirectoryHeader().getOffset() == -1) {
@@ -952,7 +1131,7 @@
         /*
          * Add all free entries to the set.
          */
-        for(FileUseMapEntry<?> freeArea : mMap.getFreeAreas()) {
+        for(FileUseMapEntry<?> freeArea : map.getFreeAreas()) {
             toWriteToStore.put(freeArea, null);
         }
 
@@ -975,7 +1154,7 @@
             computeCentralDirectory();
             computeEocd();
 
-            hasCentralDirectory = (mDirectoryEntry != null);
+            hasCentralDirectory = (directoryEntry != null);
 
             notify(ext -> {
                 ext.entriesWritten();
@@ -986,15 +1165,15 @@
                 throw new IOException("Extensions keep resetting the central directory. This is "
                         + "probably a bug.");
             }
-        } while (hasCentralDirectory && mDirectoryEntry == null);
+        } while (hasCentralDirectory && directoryEntry == null);
 
         appendCentralDirectory();
         appendEocd();
 
-        Verify.verifyNotNull(mRaf);
-        mRaf.setLength(mMap.size());
+        Verify.verifyNotNull(raf);
+        raf.setLength(map.size());
 
-        mDirty = false;
+        dirty = false;
 
         notify(ext -> {
            ext.updated();
@@ -1004,7 +1183,7 @@
 
     /**
      * Reorganizes the zip so that there are no gaps between files bigger than
-     * {@link #MAX_LOCAL_EXTRA_FIELD_CONTENTS_SIZE} if {@link #mCoverEmptySpaceUsingExtraField}
+     * {@link #MAX_LOCAL_EXTRA_FIELD_CONTENTS_SIZE} if {@link #coverEmptySpaceUsingExtraField}
      * is set to {@code true}.
      *
      * <p>Essentially, this makes sure we can cover any empty space with the extra field, given
@@ -1014,19 +1193,19 @@
      * @throws IOException failed to repack
      */
     private void packIfNecessary() throws IOException {
-        if (!mCoverEmptySpaceUsingExtraField) {
+        if (!coverEmptySpaceUsingExtraField) {
             return;
         }
 
         SortedSet<FileUseMapEntry<StoredEntry>> entriesByLocation =
                 new TreeSet<>(FileUseMapEntry.COMPARE_BY_START);
-        entriesByLocation.addAll(mEntries.values());
+        entriesByLocation.addAll(entries.values());
 
         for (FileUseMapEntry<StoredEntry> entry : entriesByLocation) {
             StoredEntry storedEntry = entry.getStore();
             assert storedEntry != null;
 
-            FileUseMapEntry<?> before = mMap.before(entry);
+            FileUseMapEntry<?> before = map.before(entry);
             if (before == null || !before.isFree()) {
                 continue;
             }
@@ -1052,20 +1231,20 @@
      * @param positionHint hint to where the file should be positioned when re-adding
      * @throws IOException failed to load the entry into memory
      */
-    private void reAdd(@NonNull StoredEntry entry, @NonNull PositionHint positionHint)
+    private void reAdd(@Nonnull StoredEntry entry, @Nonnull PositionHint positionHint)
             throws IOException {
         String name = entry.getCentralDirectoryHeader().getName();
-        FileUseMapEntry<StoredEntry> mapEntry = mEntries.get(name);
+        FileUseMapEntry<StoredEntry> mapEntry = entries.get(name);
         Preconditions.checkNotNull(mapEntry);
         Preconditions.checkState(mapEntry.getStore() == entry);
 
         entry.loadSourceIntoMemory();
 
-        mMap.remove(mapEntry);
-        mEntries.remove(name);
+        map.remove(mapEntry);
+        entries.remove(name);
         FileUseMapEntry<StoredEntry> positioned = positionInFile(entry, positionHint);
-        mEntries.put(name, positioned);
-        mDirty = true;
+        entries.put(name, positioned);
+        dirty = true;
     }
 
     /**
@@ -1076,8 +1255,8 @@
      * @param resized was the local header resized?
      * @throws IOException failed to load the entry into memory
      */
-    void localHeaderChanged(@NonNull StoredEntry entry, boolean resized) throws IOException {
-        mDirty = true;
+    void localHeaderChanged(@Nonnull StoredEntry entry, boolean resized) throws IOException {
+        dirty = true;
 
         if (resized) {
             reAdd(entry, PositionHint.ANYWHERE);
@@ -1088,7 +1267,7 @@
      * Invoked when the central directory has changed and needs to be rewritten.
      */
     void centralDirectoryChanged() {
-        mDirty = true;
+        dirty = true;
         deleteDirectoryAndEocd();
     }
 
@@ -1097,10 +1276,12 @@
      */
     @Override
     public void close() throws IOException {
-        // We need to make sure to release mRaf, otherwise we end up locking the file on
+        // We need to make sure to release raf, otherwise we end up locking the file on
         // Windows. Use try-with-resources to handle exception suppressing.
         try (Closeable ignored = this::innerClose) {
-            update();
+            if (!readOnly) {
+                update();
+            }
         }
 
         notify(ext -> {
@@ -1112,16 +1293,22 @@
     /**
      * Removes the Central Directory and EOCD from the file. This will free space for new entries
      * as well as allowing the zip file to be truncated if files have been removed.
+     *
+     * <p>This method does not mark the zip as dirty.
      */
     private void deleteDirectoryAndEocd() {
-        if (mDirectoryEntry != null) {
-            mMap.remove(mDirectoryEntry);
-            mDirectoryEntry = null;
+        if (directoryEntry != null) {
+            map.remove(directoryEntry);
+            directoryEntry = null;
         }
 
-        if (mEocdEntry != null) {
-            mMap.remove(mEocdEntry);
-            mEocdEntry = null;
+        if (eocdEntry != null) {
+            map.remove(eocdEntry);
+
+            Eocd eocd = eocdEntry.getStore();
+            Verify.verify(eocd != null);
+            eocdComment = eocd.getComment();
+            eocdEntry = null;
         }
     }
 
@@ -1134,12 +1321,12 @@
      * @param offset the offset at which the entry should be written
      * @throws IOException failed to write the entry
      */
-    private void writeEntry(@NonNull StoredEntry entry, long offset) throws IOException {
+    private void writeEntry(@Nonnull StoredEntry entry, long offset) throws IOException {
         Preconditions.checkArgument(entry.getDataDescriptorType()
                 == DataDescriptorType. NO_DATA_DESCRIPTOR, "Cannot write entries with a data "
                 + "descriptor.");
-        Preconditions.checkNotNull(mRaf, "mRaf == null");
-        Preconditions.checkState(mState == ZipFileState.OPEN_RW, "mState != ZipFileState.OPEN_RW");
+        Preconditions.checkNotNull(raf, "raf == null");
+        Preconditions.checkState(state == ZipFileState.OPEN_RW, "state != ZipFileState.OPEN_RW");
 
         /*
          * Place the cursor and write the local header.
@@ -1175,19 +1362,19 @@
 
     /**
      * Computes the central directory. The central directory must not have been computed yet. When
-     * this method finishes, the central directory has been computed {@link #mDirectoryEntry},
-     * unless the directory is empty in which case {@link #mDirectoryEntry}
+     * this method finishes, the central directory has been computed {@link #directoryEntry},
+     * unless the directory is empty in which case {@link #directoryEntry}
      * is left as {@code null}. Nothing is written to disk as a result of this method's invocation.
      *
      * @throws IOException failed to append the central directory
      */
     private void computeCentralDirectory() throws IOException {
-        Preconditions.checkState(mState == ZipFileState.OPEN_RW, "mState != ZipFileState.OPEN_RW");
-        Preconditions.checkNotNull(mRaf, "mRaf == null");
-        Preconditions.checkState(mDirectoryEntry == null, "mDirectoryEntry == null");
+        Preconditions.checkState(state == ZipFileState.OPEN_RW, "state != ZipFileState.OPEN_RW");
+        Preconditions.checkNotNull(raf, "raf == null");
+        Preconditions.checkState(directoryEntry == null, "directoryEntry == null");
 
         Set<StoredEntry> newStored = Sets.newHashSet();
-        for (FileUseMapEntry<StoredEntry> mapEntry : mEntries.values()) {
+        for (FileUseMapEntry<StoredEntry> mapEntry : entries.values()) {
             newStored.add(mapEntry.getStore());
         }
 
@@ -1195,47 +1382,47 @@
          * Make sure we truncate the map before computing the central directory's location since
          * the central directory is the last part of the file.
          */
-        mMap.truncate();
+        map.truncate();
 
         CentralDirectory newDirectory = CentralDirectory.makeFromEntries(newStored, this);
         byte[] newDirectoryBytes = newDirectory.toBytes();
-        long directoryOffset = mMap.size() + mExtraDirectoryOffset;
+        long directoryOffset = map.size() + extraDirectoryOffset;
 
-        mMap.extend(directoryOffset + newDirectoryBytes.length);
+        map.extend(directoryOffset + newDirectoryBytes.length);
 
         if (newDirectoryBytes.length > 0) {
-            mDirectoryEntry = mMap.add(directoryOffset, directoryOffset + newDirectoryBytes.length,
+            directoryEntry = map.add(directoryOffset, directoryOffset + newDirectoryBytes.length,
                     newDirectory);
         }
     }
 
     /**
-     * Writes the central directory to the end of the zip file. {@link #mDirectoryEntry} may be
+     * Writes the central directory to the end of the zip file. {@link #directoryEntry} may be
      * {@code null} only if there are no files in the archive.
      *
      * @throws IOException failed to append the central directory
      */
     private void appendCentralDirectory() throws IOException {
-        Preconditions.checkState(mState == ZipFileState.OPEN_RW, "mState != ZipFileState.OPEN_RW");
-        Preconditions.checkNotNull(mRaf, "mRaf == null");
+        Preconditions.checkState(state == ZipFileState.OPEN_RW, "state != ZipFileState.OPEN_RW");
+        Preconditions.checkNotNull(raf, "raf == null");
 
-        if (mEntries.isEmpty()) {
-            Preconditions.checkState(mDirectoryEntry == null, "mDirectoryEntry != null");
+        if (entries.isEmpty()) {
+            Preconditions.checkState(directoryEntry == null, "directoryEntry != null");
             return;
         }
 
-        Preconditions.checkNotNull(mDirectoryEntry, "mDirectoryEntry != null");
+        Preconditions.checkNotNull(directoryEntry, "directoryEntry != null");
 
-        CentralDirectory newDirectory = mDirectoryEntry.getStore();
+        CentralDirectory newDirectory = directoryEntry.getStore();
         Preconditions.checkNotNull(newDirectory, "newDirectory != null");
 
         byte[] newDirectoryBytes = newDirectory.toBytes();
-        long directoryOffset = mDirectoryEntry.getStart();
+        long directoryOffset = directoryEntry.getStart();
 
         /*
          * It is fine to seek beyond the end of file. Seeking beyond the end of file will not extend
          * the file. Even if we do not have any directory data to write, the extend() call below
-         * will force the file to be extended leaving exactly mExtraDirectoryOffset bytes empty at
+         * will force the file to be extended leaving exactly extraDirectoryOffset bytes empty at
          * the beginning.
          */
         directWrite(directoryOffset, newDirectoryBytes);
@@ -1249,79 +1436,81 @@
      * @return the byte representation, or an empty array if there are no entries in the zip
      * @throws IOException failed to compute the central directory byte representation
      */
-    @NonNull
+    @Nonnull
     public byte[] getCentralDirectoryBytes() throws IOException {
-        if (mEntries.isEmpty()) {
-            Preconditions.checkState(mDirectoryEntry == null, "mDirectoryEntry != null");
+        if (entries.isEmpty()) {
+            Preconditions.checkState(directoryEntry == null, "directoryEntry != null");
             return new byte[0];
         }
 
-        Preconditions.checkNotNull(mDirectoryEntry, "mDirectoryEntry == null");
+        Preconditions.checkNotNull(directoryEntry, "directoryEntry == null");
 
-        CentralDirectory cd = mDirectoryEntry.getStore();
+        CentralDirectory cd = directoryEntry.getStore();
         Preconditions.checkNotNull(cd, "cd == null");
         return cd.toBytes();
     }
 
     /**
-     * Computes the EOCD. This creates a new {@link #mEocdEntry}. The
-     * central directory must already be written. If {@link #mDirectoryEntry} is {@code null}, then
+     * Computes the EOCD. This creates a new {@link #eocdEntry}. The
+     * central directory must already be written. If {@link #directoryEntry} is {@code null}, then
      * the zip file must not have any entries.
      *
      * @throws IOException failed to write the EOCD
      */
     private void computeEocd() throws IOException {
-        Preconditions.checkState(mState == ZipFileState.OPEN_RW, "mState != ZipFileState.OPEN_RW");
-        Preconditions.checkNotNull(mRaf, "mRaf == null");
-        if (mDirectoryEntry == null) {
-            Preconditions.checkState(mEntries.isEmpty(),
-                    "mDirectoryEntry == null && !mEntries.isEmpty()");
+        Preconditions.checkState(state == ZipFileState.OPEN_RW, "state != ZipFileState.OPEN_RW");
+        Preconditions.checkNotNull(raf, "raf == null");
+        if (directoryEntry == null) {
+            Preconditions.checkState(entries.isEmpty(),
+                    "directoryEntry == null && !entries.isEmpty()");
         }
 
         long dirStart;
         long dirSize = 0;
 
-        if (mDirectoryEntry != null) {
-            CentralDirectory directory = mDirectoryEntry.getStore();
+        if (directoryEntry != null) {
+            CentralDirectory directory = directoryEntry.getStore();
             assert directory != null;
 
-            dirStart = mDirectoryEntry.getStart();
-            dirSize = mDirectoryEntry.getSize();
-            Verify.verify(directory.getEntries().size() == mEntries.size());
+            dirStart = directoryEntry.getStart();
+            dirSize = directoryEntry.getSize();
+            Verify.verify(directory.getEntries().size() == entries.size());
         } else {
             /*
              * If we do not have a directory, then we must leave any requested offset empty.
              */
-            dirStart = mExtraDirectoryOffset;
+            dirStart = extraDirectoryOffset;
         }
 
-        Eocd eocd = new Eocd(mEntries.size(), dirStart, dirSize);
+        Verify.verify(eocdComment != null);
+        Eocd eocd = new Eocd(entries.size(), dirStart, dirSize, eocdComment);
+        eocdComment = null;
 
         byte[] eocdBytes = eocd.toBytes();
-        long eocdOffset = mMap.size();
+        long eocdOffset = map.size();
 
-        mMap.extend(eocdOffset + eocdBytes.length);
+        map.extend(eocdOffset + eocdBytes.length);
 
-        mEocdEntry = mMap.add(eocdOffset, eocdOffset + eocdBytes.length, eocd);
+        eocdEntry = map.add(eocdOffset, eocdOffset + eocdBytes.length, eocd);
     }
 
     /**
-     * Writes the EOCD to the end of the zip file. This creates a new {@link #mEocdEntry}. The
-     * central directory must already be written. If {@link #mDirectoryEntry} is {@code null}, then
+     * Writes the EOCD to the end of the zip file. This creates a new {@link #eocdEntry}. The
+     * central directory must already be written. If {@link #directoryEntry} is {@code null}, then
      * the zip file must not have any entries.
      *
      * @throws IOException failed to write the EOCD
      */
     private void appendEocd() throws IOException {
-        Preconditions.checkState(mState == ZipFileState.OPEN_RW, "mState != ZipFileState.OPEN_RW");
-        Preconditions.checkNotNull(mRaf, "mRaf == null");
-        Preconditions.checkNotNull(mEocdEntry, "mEocdEntry == null");
+        Preconditions.checkState(state == ZipFileState.OPEN_RW, "state != ZipFileState.OPEN_RW");
+        Preconditions.checkNotNull(raf, "raf == null");
+        Preconditions.checkNotNull(eocdEntry, "eocdEntry == null");
 
-        Eocd eocd = mEocdEntry.getStore();
+        Eocd eocd = eocdEntry.getStore();
         Preconditions.checkNotNull(eocd, "eocd == null");
 
         byte[] eocdBytes = eocd.toBytes();
-        long eocdOffset = mEocdEntry.getStart();
+        long eocdOffset = eocdEntry.getStart();
 
         directWrite(eocdOffset, eocdBytes);
     }
@@ -1333,11 +1522,11 @@
      * @return the byte representation of the EOCD
      * @throws IOException failed to obtain the byte representation of the EOCD
      */
-    @NonNull
+    @Nonnull
     public byte[] getEocdBytes() throws IOException {
-        Preconditions.checkNotNull(mEocdEntry, "mEocdEntry == null");
+        Preconditions.checkNotNull(eocdEntry, "eocdEntry == null");
 
-        Eocd eocd = mEocdEntry.getStore();
+        Eocd eocd = eocdEntry.getStore();
         Preconditions.checkNotNull(eocd, "eocd == null");
         return eocd.toBytes();
     }
@@ -1348,20 +1537,20 @@
      * @throws IOException failed to close the file
      */
     private void innerClose() throws IOException {
-        if (mState == ZipFileState.CLOSED) {
+        if (state == ZipFileState.CLOSED) {
             return;
         }
 
-        Verify.verifyNotNull(mRaf, "mRaf == null");
+        Verify.verifyNotNull(raf, "raf == null");
 
-        mRaf.close();
-        mRaf = null;
-        mState = ZipFileState.CLOSED;
-        if (mClosedControl == null) {
-            mClosedControl = new CachedFileContents<>(mFile);
+        raf.close();
+        raf = null;
+        state = ZipFileState.CLOSED;
+        if (closedControl == null) {
+            closedControl = new CachedFileContents<>(file);
         }
 
-        mClosedControl.closed(null);
+        closedControl.closed(null);
     }
 
     /**
@@ -1372,28 +1561,31 @@
      * @throws IOException failed to open the file
      */
     public void openReadOnly() throws IOException {
-        if (mState != ZipFileState.CLOSED) {
+        if (state != ZipFileState.CLOSED) {
             return;
         }
 
-        mState = ZipFileState.OPEN_RO;
-        mRaf = new RandomAccessFile(mFile, "r");
+        state = ZipFileState.OPEN_RO;
+        raf = new RandomAccessFile(file, "r");
     }
 
     /**
      * Opens (or reopens) the zip file as read-write. This method will ensure that
-     * {@link #mRaf} is not null and open for writing.
+     * {@link #raf} is not null and open for writing.
      *
      * @throws IOException failed to open the file, failed to close it or the file was closed and
      * has been modified outside the control of this object
      */
     private void reopenRw() throws IOException {
-        if (mState == ZipFileState.OPEN_RW) {
+        // We an never open a file RW in read-only mode. We should never get this far, though.
+        Verify.verify(!readOnly);
+
+        if (state == ZipFileState.OPEN_RW) {
             return;
         }
 
         boolean wasClosed;
-        if (mState == ZipFileState.OPEN_RO) {
+        if (state == ZipFileState.OPEN_RO) {
             /*
              * ReadAccessFile does not have a way to reopen as RW so we have to close it and
              * open it again.
@@ -1404,16 +1596,24 @@
             wasClosed = true;
         }
 
-        Verify.verify(mState == ZipFileState.CLOSED, "mState != ZpiFileState.CLOSED");
-        Verify.verify(mRaf == null, "mRaf != null");
+        Verify.verify(state == ZipFileState.CLOSED, "state != ZpiFileState.CLOSED");
+        Verify.verify(raf == null, "raf != null");
 
-        if (mClosedControl != null && !mClosedControl.isValid()) {
-            throw new IOException("File '" + mFile.getAbsolutePath() + "' has been modified "
+        if (closedControl != null && !closedControl.isValid()) {
+            throw new IOException("File '" + file.getAbsolutePath() + "' has been modified "
                     + "by an external application.");
         }
 
-        mRaf = new RandomAccessFile(mFile, "rw");
-        mState = ZipFileState.OPEN_RW;
+        raf = new RandomAccessFile(file, "rw");
+        state = ZipFileState.OPEN_RW;
+
+        /*
+         * Now that we've open the zip and are ready to write, clear out any data descriptors
+         * in the zip since we don't need them and they take space in the archive.
+         */
+        for (StoredEntry entry : entries()) {
+            dirty |= entry.removeDataDescriptor();
+        }
 
         if (wasClosed) {
             notify(ZFileExtension::open);
@@ -1428,8 +1628,10 @@
      * and the name should not end in slash
      * @param stream the source for the file's data
      * @throws IOException failed to read the source data
+     * @throws IllegalStateException if the file is in read-only mode
      */
-    public void add(@NonNull String name, @NonNull InputStream stream) throws IOException {
+    public void add(@Nonnull String name, @Nonnull InputStream stream) throws IOException {
+        checkNotInReadOnlyMode();
         add(name, stream, true);
     }
 
@@ -1443,25 +1645,27 @@
      * @return the created entry
      * @throws IOException failed to create the entry
      */
-    @NonNull
+    @Nonnull
     private StoredEntry makeStoredEntry(
-            @NonNull String name,
-            @NonNull InputStream stream,
+            @Nonnull String name,
+            @Nonnull InputStream stream,
             boolean mayCompress)
             throws IOException {
-        CloseableByteSource source = mTracker.fromStream(stream);
+        CloseableByteSource source = tracker.fromStream(stream);
         long crc32 = source.hash(Hashing.crc32()).padToLong();
 
         boolean encodeWithUtf8 = !EncodeUtils.canAsciiEncode(name);
 
         SettableFuture<CentralDirectoryHeaderCompressInfo> compressInfo =
                 SettableFuture.create();
+        GPFlags flags = GPFlags.make(encodeWithUtf8);
         CentralDirectoryHeader newFileData =
                 new CentralDirectoryHeader(
                         name,
+                        EncodeUtils.encode(name, flags),
                         source.size(),
                         compressInfo,
-                        GPFlags.make(encodeWithUtf8),
+                        flags,
                         this);
         newFileData.setCrc32(crc32);
 
@@ -1489,30 +1693,37 @@
      * @return the sources whose data may or may not be already defined
      * @throws IOException failed to create the raw sources
      */
-    @NonNull
+    @Nonnull
     private ProcessedAndRawByteSources createSources(
             boolean mayCompress,
-            @NonNull CloseableByteSource source,
-            @NonNull SettableFuture<CentralDirectoryHeaderCompressInfo> compressInfo,
-            @NonNull CentralDirectoryHeader newFileData)
+            @Nonnull CloseableByteSource source,
+            @Nonnull SettableFuture<CentralDirectoryHeaderCompressInfo> compressInfo,
+            @Nonnull CentralDirectoryHeader newFileData)
             throws IOException {
         if (mayCompress) {
-            ListenableFuture<CompressionResult> result = mCompressor.compress(source);
-            Futures.addCallback(result, new FutureCallback<CompressionResult>() {
-                @Override
-                public void onSuccess(CompressionResult result) {
-                    compressInfo.set(new CentralDirectoryHeaderCompressInfo(newFileData,
-                            result.getCompressionMethod(), result.getSize()));
-                }
+            ListenableFuture<CompressionResult> result = compressor.compress(source);
+            Futures.addCallback(
+                    result,
+                    new FutureCallback<CompressionResult>() {
+                        @Override
+                        public void onSuccess(CompressionResult result) {
+                            compressInfo.set(
+                                    new CentralDirectoryHeaderCompressInfo(
+                                            newFileData,
+                                            result.getCompressionMethod(),
+                                            result.getSize()));
+                        }
 
-                @Override
-                public void onFailure(@NonNull Throwable t) {
-                    compressInfo.setException(t);
-                }
-            });
+                        @Override
+                        public void onFailure(@Nonnull Throwable t) {
+                            compressInfo.setException(t);
+                        }
+                    },
+                    MoreExecutors.directExecutor());
 
             ListenableFuture<CloseableByteSource> compressedByteSourceFuture =
-                    Futures.transform(result, CompressionResult::getSource);
+                    Futures.transform(
+                            result, CompressionResult::getSource, MoreExecutors.directExecutor());
             LazyDelegateByteSource compressedByteSource = new LazyDelegateByteSource(
                     compressedByteSourceFuture);
             return new ProcessedAndRawByteSources(source, compressedByteSource);
@@ -1538,9 +1749,11 @@
      * @param mayCompress can the file be compressed? This flag will be ignored if the alignment
      * rules force the file to be aligned, in which case the file will not be compressed.
      * @throws IOException failed to read the source data
+     * @throws IllegalStateException if the file is in read-only mode
      */
-    public void add(@NonNull String name, @NonNull InputStream stream, boolean mayCompress)
+    public void add(@Nonnull String name, @Nonnull InputStream stream, boolean mayCompress)
             throws IOException {
+        checkNotInReadOnlyMode();
 
         /*
          * Clean pending background work, if needed.
@@ -1552,25 +1765,25 @@
 
     /**
      * Adds a {@link StoredEntry} to the zip. The entry is not immediately added to
-     * {@link #mEntries} because data may not yet be available. Instead, it is placed under
-     * {@link #mUncompressedEntries} and later moved to {@link #processAllReadyEntries()} when
+     * {@link #entries} because data may not yet be available. Instead, it is placed under
+     * {@link #uncompressedEntries} and later moved to {@link #processAllReadyEntries()} when
      * done.
      *
      * <p>This method invokes {@link #processAllReadyEntries()} to move the entry if it has already
      * been computed so, if there is no delay in compression, and no more files are in waiting
-     * queue, then the entry is added to {@link #mEntries} immediately.
+     * queue, then the entry is added to {@link #entries} immediately.
      *
      * @param newEntry the entry to add
      * @throws IOException failed to process this entry (or a previous one whose future only
      * completed now)
      */
-    private void add(@NonNull final StoredEntry newEntry) throws IOException {
-        mUncompressedEntries.add(newEntry);
+    private void add(@Nonnull final StoredEntry newEntry) throws IOException {
+        uncompressedEntries.add(newEntry);
         processAllReadyEntries();
     }
 
     /**
-     * Moves all ready entries from {@link #mUncompressedEntries} to {@link #mEntries}. It will
+     * Moves all ready entries from {@link #uncompressedEntries} to {@link #entries}. It will
      * stop as soon as entry whose future has not been completed is found.
      *
      * @throws IOException the exception reported in the future computation, if any, or failed
@@ -1580,15 +1793,15 @@
         /*
          * Many things can happen during addToEntries(). Because addToEntries() fires
          * notifications to extensions, other files can be added, removed, etc. Ee are *not*
-         * guaranteed that new stuff does not get into mUncompressedEntries: add() will still work
+         * guaranteed that new stuff does not get into uncompressedEntries: add() will still work
          * and will add new entries in there.
          *
          * However -- important -- processReadyEntries() may be invoked during addToEntries()
          * because of the extension mechanism. This means that stuff *can* be removed from
-         * mUncompressedEntries and moved to mEntries during addToEntries().
+         * uncompressedEntries and moved to entries during addToEntries().
          */
-        while (!mUncompressedEntries.isEmpty()) {
-            StoredEntry next = mUncompressedEntries.get(0);
+        while (!uncompressedEntries.isEmpty()) {
+            StoredEntry next = uncompressedEntries.get(0);
             CentralDirectoryHeader cdh = next.getCentralDirectoryHeader();
             Future<CentralDirectoryHeaderCompressInfo> compressionInfo = cdh.getCompressionInfo();
             if (!compressionInfo.isDone()) {
@@ -1598,7 +1811,7 @@
                 return;
             }
 
-            mUncompressedEntries.remove(0);
+            uncompressedEntries.remove(0);
 
             try {
                 compressionInfo.get();
@@ -1614,19 +1827,19 @@
     }
 
     /**
-     * Waits until {@link #mUncompressedEntries} is empty.
+     * Waits until {@link #uncompressedEntries} is empty.
      *
      * @throws IOException the exception reported in the future computation, if any, or failed
      * to add a file to the archive
      */
     private void processAllReadyEntriesWithWait() throws IOException {
         processAllReadyEntries();
-        while (!mUncompressedEntries.isEmpty()) {
+        while (!uncompressedEntries.isEmpty()) {
             /*
              * Wait for the first future to complete and then try again. Keep looping until we're
              * done.
              */
-            StoredEntry first = mUncompressedEntries.get(0);
+            StoredEntry first = uncompressedEntries.get(0);
             CentralDirectoryHeader cdh = first.getCentralDirectoryHeader();
             cdh.getCompressionInfoWithWait();
 
@@ -1635,13 +1848,13 @@
     }
 
     /**
-     * Adds a new file to {@link #mEntries}. This is actually added to the zip and its space
-     * allocated in the {@link #mMap}.
+     * Adds a new file to {@link #entries}. This is actually added to the zip and its space
+     * allocated in the {@link #map}.
      *
      * @param newEntry the new entry to add
      * @throws IOException failed to add the file
      */
-    private void addToEntries(@NonNull final StoredEntry newEntry) throws IOException {
+    private void addToEntries(@Nonnull final StoredEntry newEntry) throws IOException {
         Preconditions.checkArgument(newEntry.getDataDescriptorType() ==
                 DataDescriptorType.NO_DATA_DESCRIPTOR, "newEntry has data descriptor");
 
@@ -1651,7 +1864,7 @@
          * StoredEntry.delete() will call {@link ZFile#delete(StoredEntry, boolean)}  to perform
          * data structure cleanup.
          */
-        FileUseMapEntry<StoredEntry> toReplace = mEntries.get(
+        FileUseMapEntry<StoredEntry> toReplace = entries.get(
                 newEntry.getCentralDirectoryHeader().getName());
         final StoredEntry replaceStore;
         if (toReplace != null) {
@@ -1664,9 +1877,9 @@
 
         FileUseMapEntry<StoredEntry> fileUseMapEntry =
                 positionInFile(newEntry, PositionHint.ANYWHERE);
-        mEntries.put(newEntry.getCentralDirectoryHeader().getName(), fileUseMapEntry);
+        entries.put(newEntry.getCentralDirectoryHeader().getName(), fileUseMapEntry);
 
-        mDirty = true;
+        dirty = true;
 
         notify(ext -> ext.added(newEntry, replaceStore));
     }
@@ -1685,10 +1898,10 @@
      * @param positionHint hint to where the file should be positioned
      * @return the position in the file where the entry should be placed
      */
-    @NonNull
+    @Nonnull
     private FileUseMapEntry<StoredEntry> positionInFile(
-            @NonNull StoredEntry entry,
-            @NonNull PositionHint positionHint)
+            @Nonnull StoredEntry entry,
+            @Nonnull PositionHint positionHint)
             throws IOException {
         deleteDirectoryAndEocd();
         long size = entry.getInFileSize();
@@ -1708,13 +1921,13 @@
                 throw new AssertionError();
         }
 
-        long newOffset = mMap.locateFree(size, localHeaderSize, alignment, algorithm);
+        long newOffset = map.locateFree(size, localHeaderSize, alignment, algorithm);
         long newEnd = newOffset + entry.getInFileSize();
-        if (newEnd > mMap.size()) {
-            mMap.extend(newEnd);
+        if (newEnd > map.size()) {
+            map.extend(newEnd);
         }
 
-        return mMap.add(newOffset, newEnd, entry);
+        return map.add(newOffset, newEnd, entry);
     }
 
     /**
@@ -1725,7 +1938,7 @@
      * required for the entry
      * @throws IOException failed to determine the alignment
      */
-    private int chooseAlignment(@NonNull StoredEntry entry) throws IOException {
+    private int chooseAlignment(@Nonnull StoredEntry entry) throws IOException {
         CentralDirectoryHeader cdh = entry.getCentralDirectoryHeader();
         CentralDirectoryHeaderCompressInfo compressionInfo = cdh.getCompressionInfoWithWait();
 
@@ -1733,7 +1946,7 @@
         if (isCompressed) {
             return AlignmentRule.NO_ALIGNMENT;
         } else {
-            return mAlignmentRule.alignment(cdh.getName());
+            return alignmentRule.alignment(cdh.getName());
         }
     }
 
@@ -1750,9 +1963,12 @@
      * @param ignoreFilter predicate that, if {@code true}, identifies files in <em>src</em> that
      * should be ignored by merging; merging will behave as if these files were not there
      * @throws IOException failed to read from <em>src</em> or write on the output
+     * @throws IllegalStateException if the file is in read-only mode
      */
-    public void mergeFrom(@NonNull ZFile src, @NonNull Predicate<String> ignoreFilter)
+    public void mergeFrom(@Nonnull ZFile src, @Nonnull Predicate<String> ignoreFilter)
             throws IOException {
+        checkNotInReadOnlyMode();
+
         for (StoredEntry fromEntry : src.entries()) {
             if (ignoreFilter.test(fromEntry.getCentralDirectoryHeader().getName())) {
                 continue;
@@ -1760,7 +1976,7 @@
 
             boolean replaceCurrent = true;
             String path = fromEntry.getCentralDirectoryHeader().getName();
-            FileUseMapEntry<StoredEntry> currentEntry = mEntries.get(path);
+            FileUseMapEntry<StoredEntry> currentEntry = entries.get(path);
 
             if (currentEntry != null) {
                 long fromSize = fromEntry.getCentralDirectoryHeader().getUncompressedSize();
@@ -1806,7 +2022,7 @@
                     throw new IOException("Cannot read source with " + sourceSize + " bytes.");
                 }
 
-                byte data[] = new byte[Ints.checkedCast(sourceSize)];
+                byte[] data = new byte[Ints.checkedCast(sourceSize)];
                 int read = 0;
                 while (read < data.length) {
                     int r = fromInput.read(data, read, data.length - read);
@@ -1818,7 +2034,7 @@
                  * Build the new source and wrap it around an inflater source if data came from
                  * a compressed source.
                  */
-                CloseableByteSource rawContents = mTracker.fromSource(fromSource.getRawByteSource());
+                CloseableByteSource rawContents = tracker.fromSource(fromSource.getRawByteSource());
                 CloseableByteSource processedContents;
                 if (fromCompressInfo.getMethod() == CompressionMethod.DEFLATE) {
                     //noinspection IOResourceOpenedButNotSafelyClosed
@@ -1842,9 +2058,12 @@
     /**
      * Forcibly marks this zip file as touched, forcing it to be updated when {@link #update()}
      * or {@link #close()} are invoked.
+     *
+     * @throws IllegalStateException if the file is in read-only mode
      */
     public void touch() {
-        mDirty = true;
+        checkNotInReadOnlyMode();
+        dirty = true;
     }
 
     /**
@@ -1870,13 +2089,20 @@
      * of {@code ZFile} may refer to {@link StoredEntry}s that are no longer valid
      * @throws IOException failed to realign the zip; some entries in the zip may have been lost
      * due to the I/O error
+     * @throws IllegalStateException if the file is in read-only mode
      */
     public boolean realign() throws IOException {
+        checkNotInReadOnlyMode();
+
         boolean anyChanges = false;
         for (StoredEntry entry : entries()) {
             anyChanges |= entry.realign();
         }
 
+        if (anyChanges) {
+            dirty = true;
+        }
+
         return anyChanges;
     }
 
@@ -1890,9 +2116,9 @@
      * @throws IOException failed to read/write an entry; the entry may no longer exist in the
      * file
      */
-    boolean realign(@NonNull StoredEntry entry) throws IOException {
+    boolean realign(@Nonnull StoredEntry entry) throws IOException {
         FileUseMapEntry<StoredEntry> mapEntry =
-                mEntries.get(entry.getCentralDirectoryHeader().getName());
+                entries.get(entry.getCentralDirectoryHeader().getName());
         Verify.verify(entry == mapEntry.getStore());
         long currentDataOffset = mapEntry.getStart() + entry.getLocalHeaderSize();
 
@@ -1910,21 +2136,21 @@
              * File is not aligned but it is not written. We do not really need to do much other
              * than find another place in the map.
              */
-            mMap.remove(mapEntry);
+            map.remove(mapEntry);
             long newStart =
-                    mMap.locateFree(
+                    map.locateFree(
                             mapEntry.getSize(),
                             entry.getLocalHeaderSize(),
                             expectedAlignment,
                             FileUseMap.PositionAlgorithm.BEST_FIT);
-            mapEntry = mMap.add(newStart, newStart + entry.getInFileSize(), entry);
-            mEntries.put(entry.getCentralDirectoryHeader().getName(), mapEntry);
+            mapEntry = map.add(newStart, newStart + entry.getInFileSize(), entry);
+            entries.put(entry.getCentralDirectoryHeader().getName(), mapEntry);
 
             /*
              * Just for safety. We're modifying the in-memory structures but the file should
              * already be marked as dirty.
              */
-            Verify.verify(mDirty);
+            Verify.verify(dirty);
 
             return false;
 
@@ -1956,7 +2182,7 @@
         clonedCdh.setOffset(-1);
         clonedCdh.resetDeferredCrc();
 
-        CloseableByteSource rawContents = mTracker.fromSource(source.getRawByteSource());
+        CloseableByteSource rawContents = tracker.fromSource(source.getRawByteSource());
         CloseableByteSource processedContents;
 
         if (compressInfo.getMethod() == CompressionMethod.DEFLATE) {
@@ -1981,18 +2207,22 @@
      * Adds an extension to this zip file.
      *
      * @param extension the listener to add
+     * @throws IllegalStateException if the file is in read-only mode
      */
-    public void addZFileExtension(@NonNull ZFileExtension extension) {
-        mExtensions.add(extension);
+    public void addZFileExtension(@Nonnull ZFileExtension extension) {
+        checkNotInReadOnlyMode();
+        extensions.add(extension);
     }
 
     /**
      * Removes an extension from this zip file.
      *
      * @param extension the listener to remove
+     * @throws IllegalStateException if the file is in read-only mode
      */
-    public void removeZFileExtension(@NonNull ZFileExtension extension) {
-        mExtensions.remove(extension);
+    public void removeZFileExtension(@Nonnull ZFileExtension extension) {
+        checkNotInReadOnlyMode();
+        extensions.remove(extension);
     }
 
     /**
@@ -2002,25 +2232,25 @@
      * notification method on the listener and return the result of that invocation
      * @throws IOException failed to process some extensions
      */
-    private void notify(@NonNull IOExceptionFunction<ZFileExtension, IOExceptionRunnable> function)
+    private void notify(@Nonnull IOExceptionFunction<ZFileExtension, IOExceptionRunnable> function)
             throws IOException {
-        for (ZFileExtension fl : Lists.newArrayList(mExtensions)) {
+        for (ZFileExtension fl : Lists.newArrayList(extensions)) {
             IOExceptionRunnable r = function.apply(fl);
             if (r != null) {
-                mToRun.add(r);
+                toRun.add(r);
             }
         }
 
-        if (!mIsNotifying) {
-            mIsNotifying = true;
+        if (!isNotifying) {
+            isNotifying = true;
 
             try {
-                while (!mToRun.isEmpty()) {
-                    IOExceptionRunnable r = mToRun.remove(0);
+                while (!toRun.isEmpty()) {
+                    IOExceptionRunnable r = toRun.remove(0);
                     r.run();
                 }
             } finally {
-                mIsNotifying = false;
+                isNotifying = false;
             }
         }
     }
@@ -2035,9 +2265,12 @@
      * @param start start offset in  {@code data} where data to write is located
      * @param count number of bytes of data to write
      * @throws IOException failed to write the data
+     * @throws IllegalStateException if the file is in read-only mode
      */
-    public void directWrite(long offset, @NonNull byte[] data, int start, int count)
+    public void directWrite(long offset, @Nonnull byte[] data, int start, int count)
             throws IOException {
+        checkNotInReadOnlyMode();
+
         Preconditions.checkArgument(offset >= 0, "offset < 0");
         Preconditions.checkArgument(start >= 0, "start >= 0");
         Preconditions.checkArgument(count >= 0, "count >= 0");
@@ -2050,10 +2283,10 @@
         Preconditions.checkArgument(start + count <= data.length, "start + count > data.length");
 
         reopenRw();
-        assert mRaf != null;
+        assert raf != null;
 
-        mRaf.seek(offset);
-        mRaf.write(data, start, count);
+        raf.seek(offset);
+        raf.write(data, start, count);
     }
 
     /**
@@ -2062,12 +2295,30 @@
      * @param offset the offset at which data should be written
      * @param data the data to write, may be an empty array
      * @throws IOException failed to write the data
+     * @throws IllegalStateException if the file is in read-only mode
      */
-    public void directWrite(long offset, @NonNull byte[] data) throws IOException {
+    public void directWrite(long offset, @Nonnull byte[] data) throws IOException {
+        checkNotInReadOnlyMode();
         directWrite(offset, data, 0, data.length);
     }
 
     /**
+     * Returns the current size (in bytes) of the underlying file.
+     *
+     * @throws IOException if an I/O error occurs
+     */
+    public long directSize() throws IOException {
+        /*
+         * Only force a reopen if the file is closed.
+         */
+        if (raf == null) {
+            reopenRw();
+            assert raf != null;
+        }
+        return raf.length();
+    }
+
+    /**
      * Directly reads data from the zip file. Invoking this method may force the zip to be reopened
      * in read/write mode.
      *
@@ -2079,29 +2330,42 @@
      * to be read
      * @throws IOException failed to write the data
      */
-    public int directRead(long offset, @NonNull byte[] data, int start, int count)
+    public int directRead(long offset, @Nonnull byte[] data, int start, int count)
             throws IOException {
-        Preconditions.checkArgument(offset >= 0, "offset < 0");
         Preconditions.checkArgument(start >= 0, "start >= 0");
         Preconditions.checkArgument(count >= 0, "count >= 0");
-
-        if (data.length == 0) {
-            return 0;
-        }
-
         Preconditions.checkArgument(start <= data.length, "start > data.length");
         Preconditions.checkArgument(start + count <= data.length, "start + count > data.length");
+        return directRead(offset, ByteBuffer.wrap(data, start, count));
+    }
+
+    /**
+     * Directly reads data from the zip file. Invoking this method may force the zip to be reopened
+     * in read/write mode.
+     *
+     * @param offset the offset from which data should be read
+     * @param dest the output buffer to fill with data from the {@code offset}.
+     * @return how many bytes of data have been written or {@code -1} if there are no more bytes
+     * to be read
+     * @throws IOException failed to write the data
+     */
+    public int directRead(long offset, @Nonnull ByteBuffer dest) throws IOException {
+        Preconditions.checkArgument(offset >= 0, "offset < 0");
+
+        if (!dest.hasRemaining()) {
+            return 0;
+        }
 
         /*
          * Only force a reopen if the file is closed.
          */
-        if (mRaf == null) {
+        if (raf == null) {
             reopenRw();
-            assert mRaf != null;
+            assert raf != null;
         }
 
-        mRaf.seek(offset);
-        return mRaf.read(data, start, count);
+        raf.seek(offset);
+        return raf.getChannel().read(dest);
     }
 
     /**
@@ -2111,24 +2375,55 @@
      * @param data receives the read data, may be an empty array
      * @throws IOException failed to read the data
      */
-    public int directRead(long offset, @NonNull byte[] data) throws IOException {
+    public int directRead(long offset, @Nonnull byte[] data) throws IOException {
         return directRead(offset, data, 0, data.length);
     }
 
     /**
-     * Reads exactly @code data.length} bytes of data, failing if it was not possible to read all
+     * Reads exactly {@code data.length} bytes of data, failing if it was not possible to read all
      * the requested data.
      *
      * @param offset the offset at which to start reading
      * @param data the array that receives the data read
      * @throws IOException failed to read some data or there is not enough data to read
      */
-    public void directFullyRead(long offset, @NonNull byte[] data) throws IOException {
-        Preconditions.checkArgument(offset >= 0, "offset < 0");
-        Preconditions.checkNotNull(mRaf, "File is closed");
+    public void directFullyRead(long offset, @Nonnull byte[] data) throws IOException {
+        directFullyRead(offset, ByteBuffer.wrap(data));
+    }
 
-        mRaf.seek(offset);
-        RandomAccessFileUtils.fullyRead(mRaf, data);
+    /**
+     * Reads exactly {@code dest.remaining()} bytes of data, failing if it was not possible to read
+     * all the requested data.
+     *
+     * @param offset the offset at which to start reading
+     * @param dest the output buffer to fill with data
+     * @throws IOException failed to read some data or there is not enough data to read
+     */
+    public void directFullyRead(long offset, @Nonnull ByteBuffer dest) throws IOException {
+        Preconditions.checkArgument(offset >= 0, "offset < 0");
+
+        if (!dest.hasRemaining()) {
+            return;
+        }
+
+        /*
+         * Only force a reopen if the file is closed.
+         */
+        if (raf == null) {
+            reopenRw();
+            assert raf != null;
+        }
+
+        FileChannel fileChannel = raf.getChannel();
+        while (dest.hasRemaining()) {
+            fileChannel.position(offset);
+            int chunkSize = fileChannel.read(dest);
+            if (chunkSize == -1) {
+                throw new EOFException(
+                        "Failed to read " + dest.remaining() + " more bytes: premature EOF");
+            }
+            offset += chunkSize;
+        }
     }
 
     /**
@@ -2140,8 +2435,10 @@
      * @param file a file or directory; if it is a directory, all files and directories will be
      * added recursively
      * @throws IOException failed to some (or all ) of the files
+     * @throws IllegalStateException if the file is in read-only mode
      */
-    public void addAllRecursively(@NonNull File file) throws IOException {
+    public void addAllRecursively(@Nonnull File file) throws IOException {
+        checkNotInReadOnlyMode();
         addAllRecursively(file, f -> true);
     }
 
@@ -2152,9 +2449,13 @@
      * added recursively
      * @param mayCompress a function that decides whether files may be compressed
      * @throws IOException failed to some (or all ) of the files
+     * @throws IllegalStateException if the file is in read-only mode
      */
-    public void addAllRecursively(@NonNull File file,
-            @NonNull Function<? super File, Boolean> mayCompress) throws IOException {
+    public void addAllRecursively(
+            @Nonnull File file,
+            @Nonnull Function<? super File, Boolean> mayCompress) throws IOException {
+        checkNotInReadOnlyMode();
+
         /*
          * The case of file.isFile() is different because if file.isFile() we will add it to the
          * zip in the root. However, if file.isDirectory() we won't add it and add its children.
@@ -2199,22 +2500,22 @@
      * includes any extra offset for the central directory
      */
     public long getCentralDirectoryOffset() {
-        if (mDirectoryEntry != null) {
-            return mDirectoryEntry.getStart();
+        if (directoryEntry != null) {
+            return directoryEntry.getStart();
         }
 
         /*
          * If there are no entries, the central directory is written at the start of the file.
          */
-        if (mEntries.isEmpty()) {
-            return mExtraDirectoryOffset;
+        if (entries.isEmpty()) {
+            return extraDirectoryOffset;
         }
 
         /*
          * The Central Directory is written after all entries. This will be at the end of the file
          * if the
          */
-        return mMap.usedSize() + mExtraDirectoryOffset;
+        return map.usedSize() + extraDirectoryOffset;
     }
 
     /**
@@ -2225,11 +2526,11 @@
      * been computed
      */
     public long getCentralDirectorySize() {
-        if (mDirectoryEntry != null) {
-            return mDirectoryEntry.getSize();
+        if (directoryEntry != null) {
+            return directoryEntry.getSize();
         }
 
-        if (mEntries.isEmpty()) {
+        if (entries.isEmpty()) {
             return 0;
         }
 
@@ -2242,11 +2543,11 @@
      * @return the offset of the EOCD or {@code -1} if none exists yet
      */
     public long getEocdOffset() {
-        if (mEocdEntry == null) {
+        if (eocdEntry == null) {
             return -1;
         }
 
-        return mEocdEntry.getStart();
+        return eocdEntry.getStart();
     }
 
     /**
@@ -2255,11 +2556,77 @@
      * @return the size of the EOCD of {@code -1} it none exists yet
      */
     public long getEocdSize() {
-        if (mEocdEntry == null) {
+        if (eocdEntry == null) {
             return -1;
         }
 
-        return mEocdEntry.getSize();
+        return eocdEntry.getSize();
+    }
+
+    /**
+     * Obtains the comment in the EOCD.
+     *
+     * @return the comment exactly as it was encoded in the EOCD, no encoding conversion is done
+     */
+    @Nonnull
+    public byte[] getEocdComment() {
+        if (eocdEntry == null) {
+            Verify.verify(eocdComment != null);
+            byte[] eocdCommentCopy = new byte[eocdComment.length];
+            System.arraycopy(eocdComment, 0, eocdCommentCopy, 0, eocdComment.length);
+            return eocdCommentCopy;
+        }
+
+        Eocd eocd = eocdEntry.getStore();
+        Verify.verify(eocd != null);
+        return eocd.getComment();
+    }
+
+    /**
+     * Sets the comment in the EOCD.
+     *
+     * @param comment the new comment; no conversion is done, these exact bytes will be placed in
+     * the EOCD comment
+     * @throws IllegalStateException if file is in read-only mode
+     */
+    public void setEocdComment(@Nonnull byte[] comment) {
+        checkNotInReadOnlyMode();
+
+        if (comment.length > MAX_EOCD_COMMENT_SIZE) {
+            throw new IllegalArgumentException(
+                    "EOCD comment size ("
+                            + comment.length
+                            + ") is larger than the maximum allowed ("
+                            + MAX_EOCD_COMMENT_SIZE
+                            + ")");
+        }
+
+        // Check if the EOCD signature appears anywhere in the comment we need to check if it
+        // is valid.
+        for (int i = 0; i < comment.length - MIN_EOCD_SIZE; i++) {
+            // Remember: little endian...
+            if (comment[i] == EOCD_SIGNATURE[3]
+                    && comment[i + 1] == EOCD_SIGNATURE[2]
+                    && comment[i + 2] == EOCD_SIGNATURE[1]
+                    && comment[i + 3] == EOCD_SIGNATURE[0]) {
+                // We found a possible EOCD signature at position i. Try to read it.
+                ByteBuffer bytes = ByteBuffer.wrap(comment, i, comment.length - i);
+                try {
+                    new Eocd(bytes);
+                    throw new IllegalArgumentException(
+                            "Position "
+                                    + i
+                                    + " of the comment contains a valid EOCD record.");
+                } catch (IOException e) {
+                    // Fine, this is an invalid record. Move along...
+                }
+            }
+        }
+
+        deleteDirectoryAndEocd();
+        eocdComment = new byte[comment.length];
+        System.arraycopy(comment, 0, eocdComment, 0, comment.length);
+        dirty = true;
     }
 
     /**
@@ -2268,14 +2635,16 @@
      * updated.
      *
      * @param offset the offset or {@code 0} to write the central directory at its current location
+     * @throws IllegalStateException if file is in read-only mode
      */
     public void setExtraDirectoryOffset(long offset) {
+        checkNotInReadOnlyMode();
         Preconditions.checkArgument(offset >= 0, "offset < 0");
 
-        if (mExtraDirectoryOffset != offset) {
-            mExtraDirectoryOffset = offset;
+        if (extraDirectoryOffset != offset) {
+            extraDirectoryOffset = offset;
             deleteDirectoryAndEocd();
-            mDirty = true;
+            dirty = true;
         }
     }
 
@@ -2285,7 +2654,7 @@
      * @return the offset or {@code 0} if no offset is set
      */
     public long getExtraDirectoryOffset() {
-        return mExtraDirectoryOffset;
+        return extraDirectoryOffset;
     }
 
     /**
@@ -2294,7 +2663,7 @@
      * @return are the timestamps being ignored?
      */
     public boolean areTimestampsIgnored() {
-        return mNoTimestamps;
+        return noTimestamps;
     }
 
     /**
@@ -2305,33 +2674,36 @@
      * written to disk.
      *
      * @throws IOException failed to load or move a file in the zip
+     * @throws IllegalStateException if file is in read-only mode
      */
     public void sortZipContents() throws IOException {
+        checkNotInReadOnlyMode();
         reopenRw();
 
         processAllReadyEntriesWithWait();
 
-        Verify.verify(mUncompressedEntries.isEmpty());
+        Verify.verify(uncompressedEntries.isEmpty());
 
         SortedSet<StoredEntry> sortedEntries = Sets.newTreeSet(StoredEntry.COMPARE_BY_NAME);
-        for (FileUseMapEntry<StoredEntry> fmEntry : mEntries.values()) {
+        for (FileUseMapEntry<StoredEntry> fmEntry : entries.values()) {
             StoredEntry entry = fmEntry.getStore();
             Preconditions.checkNotNull(entry);
             sortedEntries.add(entry);
             entry.loadSourceIntoMemory();
 
-            mMap.remove(fmEntry);
+            map.remove(fmEntry);
         }
 
-        mEntries.clear();
+        entries.clear();
         for (StoredEntry entry : sortedEntries) {
             String name = entry.getCentralDirectoryHeader().getName();
             FileUseMapEntry<StoredEntry> positioned =
                     positionInFile(entry, PositionHint.LOWEST_OFFSET);
-            mEntries.put(name, positioned);
+
+            entries.put(name, positioned);
         }
 
-        mDirty = true;
+        dirty = true;
     }
 
     /**
@@ -2340,23 +2712,44 @@
      * @return the file that may or may not exist (depending on whether something existed there
      * before the zip was created and on whether the zip has been updated or not)
      */
-    @NonNull
+    @Nonnull
     public File getFile() {
-        return mFile;
+        return file;
     }
 
     /**
-     * Checks whether data description verification should be skipped.
+     * Creates a new verify log.
      *
-     * @return should it be skipped?
+     * @return the new verify log
      */
-    boolean getSkipDataDescriptorVerification() {
-        return mSkipDataDescriptorVerification;
+    @Nonnull
+    VerifyLog makeVerifyLog() {
+        VerifyLog log = verifyLogFactory.get();
+        assert log != null;
+        return log;
     }
 
     /**
-     * Hint to where files should be positioned.
+     * Obtains the zip file's verify log.
+     *
+     * @return the verify log
      */
+    @Nonnull
+    VerifyLog getVerifyLog() {
+        return verifyLog;
+    }
+
+    /**
+     * Are there in-memory changes that have not been written to the zip file?
+     *
+     * <p>Waits for all pending processing which may make changes.
+     */
+    public boolean hasPendingChangesWithWait() throws IOException {
+        processAllReadyEntriesWithWait();
+        return dirty;
+    }
+
+    /** Hint to where files should be positioned. */
     enum PositionHint {
         /**
          * File may be positioned anywhere, caller doesn't care.
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/ZFileExtension.java b/src/main/java/com/android/apkzlib/zip/ZFileExtension.java
similarity index 94%
rename from src/main/java/com/android/builder/internal/packaging/zip/ZFileExtension.java
rename to src/main/java/com/android/apkzlib/zip/ZFileExtension.java
index 2a26caf..fdb6ca4 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/ZFileExtension.java
+++ b/src/main/java/com/android/apkzlib/zip/ZFileExtension.java
@@ -14,13 +14,12 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
 
-import com.android.annotations.NonNull;
-import com.android.annotations.Nullable;
-import com.android.builder.internal.utils.IOExceptionRunnable;
-
+import com.android.apkzlib.utils.IOExceptionRunnable;
 import java.io.IOException;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
 /**
  * An extension of a {@link ZFile}. Extensions are notified when files are open, updated, closed and
@@ -127,7 +126,7 @@
      * @return an optional runnable to run when notification of all listeners has ended
      */
     @Nullable
-    public IOExceptionRunnable added(@NonNull StoredEntry entry, @Nullable StoredEntry replaced) {
+    public IOExceptionRunnable added(@Nonnull StoredEntry entry, @Nullable StoredEntry replaced) {
         return null;
     }
 
@@ -141,7 +140,7 @@
      * @return an optional runnable to run when notification of all listeners has ended
      */
     @Nullable
-    public IOExceptionRunnable removed(@NonNull StoredEntry entry) {
+    public IOExceptionRunnable removed(@Nonnull StoredEntry entry) {
         return null;
     }
 }
diff --git a/src/main/java/com/android/apkzlib/zip/ZFileOptions.java b/src/main/java/com/android/apkzlib/zip/ZFileOptions.java
new file mode 100644
index 0000000..08a1d83
--- /dev/null
+++ b/src/main/java/com/android/apkzlib/zip/ZFileOptions.java
@@ -0,0 +1,214 @@
+/*
+ * 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.apkzlib.zip;
+
+import com.android.apkzlib.zip.compress.DeflateExecutionCompressor;
+import com.android.apkzlib.zip.utils.ByteTracker;
+import java.util.function.Supplier;
+import java.util.zip.Deflater;
+import javax.annotation.Nonnull;
+
+/**
+ * Options to create a {@link ZFile}.
+ */
+public class ZFileOptions {
+
+    /**
+     * The byte tracker.
+     */
+    @Nonnull
+    private ByteTracker tracker;
+
+    /**
+     * The compressor to use.
+     */
+    @Nonnull
+    private Compressor compressor;
+
+    /**
+     * Should timestamps be zeroed?
+     */
+    private boolean noTimestamps;
+
+    /**
+     * The alignment rule to use.
+     */
+    @Nonnull
+    private AlignmentRule alignmentRule;
+
+    /**
+     * Should the extra field be used to cover empty space?
+     */
+    private boolean coverEmptySpaceUsingExtraField;
+
+    /**
+     * Should files be automatically sorted before update?
+     */
+    private boolean autoSortFiles;
+
+    /**
+     * Factory creating verification logs to use.
+     */
+    @Nonnull
+    private Supplier<VerifyLog> verifyLogFactory;
+
+    /**
+     * Creates a new options object. All options are set to their defaults.
+     */
+    public ZFileOptions() {
+        tracker = new ByteTracker();
+        compressor =
+                new DeflateExecutionCompressor(
+                        Runnable::run,
+                        tracker,
+                        Deflater.DEFAULT_COMPRESSION);
+        alignmentRule = AlignmentRules.compose();
+        verifyLogFactory = VerifyLogs::devNull;
+    }
+
+    /**
+     * Obtains the ZFile's byte tracker.
+     *
+     * @return the byte tracker
+     */
+    @Nonnull
+    public ByteTracker getTracker() {
+        return tracker;
+    }
+
+    /**
+     * Obtains the compressor to use.
+     *
+     * @return the compressor
+     */
+    @Nonnull
+    public Compressor getCompressor() {
+        return compressor;
+    }
+
+    /**
+     * Sets the compressor to use.
+     *
+     * @param compressor the compressor
+     */
+    public ZFileOptions setCompressor(@Nonnull Compressor compressor) {
+        this.compressor = compressor;
+        return this;
+    }
+
+    /**
+     * Obtains whether timestamps should be zeroed.
+     *
+     * @return should timestamps be zeroed?
+     */
+    public boolean getNoTimestamps() {
+        return noTimestamps;
+    }
+
+    /**
+     * Sets whether timestamps should be zeroed.
+     *
+     * @param noTimestamps should timestamps be zeroed?
+     */
+    public ZFileOptions setNoTimestamps(boolean noTimestamps) {
+        this.noTimestamps = noTimestamps;
+        return this;
+    }
+
+    /**
+     * Obtains the alignment rule.
+     *
+     * @return the alignment rule
+     */
+    @Nonnull
+    public AlignmentRule getAlignmentRule() {
+        return alignmentRule;
+    }
+
+    /**
+     * Sets the alignment rule.
+     *
+     * @param alignmentRule the alignment rule
+     */
+    public ZFileOptions setAlignmentRule(@Nonnull AlignmentRule alignmentRule) {
+        this.alignmentRule = alignmentRule;
+        return this;
+    }
+
+    /**
+     * Obtains whether the extra field should be used to cover empty spaces. See {@link ZFile} for
+     * an explanation on using the extra field for covering empty spaces.
+     *
+     * @return should the extra field be used to cover empty spaces?
+     */
+    public boolean getCoverEmptySpaceUsingExtraField() {
+        return coverEmptySpaceUsingExtraField;
+    }
+
+    /**
+     * Sets whether the extra field should be used to cover empty spaces. See {@link ZFile} for an
+     * explanation on using the extra field for covering empty spaces.
+     *
+     * @param coverEmptySpaceUsingExtraField should the extra field be used to cover empty spaces?
+     */
+    public ZFileOptions setCoverEmptySpaceUsingExtraField(boolean coverEmptySpaceUsingExtraField) {
+        this.coverEmptySpaceUsingExtraField = coverEmptySpaceUsingExtraField;
+        return this;
+    }
+
+    /**
+     * Obtains whether files should be automatically sorted before updating the zip file. See
+     * {@link ZFile} for an explanation on automatic sorting.
+     *
+     * @return should the file be automatically sorted?
+     */
+    public boolean getAutoSortFiles() {
+        return autoSortFiles;
+    }
+
+    /**
+     * Sets whether files should be automatically sorted before updating the zip file. See {@link
+     * ZFile} for an explanation on automatic sorting.
+     *
+     * @param autoSortFiles should the file be automatically sorted?
+     */
+    public ZFileOptions setAutoSortFiles(boolean autoSortFiles) {
+        this.autoSortFiles = autoSortFiles;
+        return this;
+    }
+
+    /**
+     * Sets the verification log factory.
+     *
+     * @param verifyLogFactory verification log factory
+     */
+    public ZFileOptions setVerifyLogFactory(@Nonnull Supplier<VerifyLog> verifyLogFactory) {
+        this.verifyLogFactory = verifyLogFactory;
+        return this;
+    }
+
+    /**
+     * Obtains the verification log factory. By default, the verification log doesn't store
+     * anything and will always return an empty log.
+     *
+     * @return the verification log factory
+     */
+    @Nonnull
+    public Supplier<VerifyLog> getVerifyLogFactory() {
+        return verifyLogFactory;
+    }
+}
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/ZipField.java b/src/main/java/com/android/apkzlib/zip/ZipField.java
similarity index 68%
rename from src/main/java/com/android/builder/internal/packaging/zip/ZipField.java
rename to src/main/java/com/android/apkzlib/zip/ZipField.java
index 1169f69..4b0b675 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/ZipField.java
+++ b/src/main/java/com/android/apkzlib/zip/ZipField.java
@@ -14,20 +14,19 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
 
-import com.android.annotations.NonNull;
-import com.android.annotations.Nullable;
-import com.android.builder.internal.packaging.zip.utils.LittleEndianUtils;
+import com.android.apkzlib.zip.utils.LittleEndianUtils;
 import com.google.common.base.Preconditions;
 import com.google.common.base.Verify;
 import com.google.common.collect.Sets;
 import com.google.common.primitives.Ints;
-
 import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
 import java.util.Set;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
 /**
  * The ZipField class represents a field in a record in a zip file. Zip files are made with records
@@ -58,30 +57,30 @@
     /**
      * Field name. Used for providing (more) useful error messages.
      */
-    @NonNull
-    private final String mName;
+    @Nonnull
+    private final String name;
 
     /**
      * Offset of the file in the record.
      */
-    protected final int mOffset;
+    protected final int offset;
 
     /**
      * Size of the field. Only 2 or 4 allowed.
      */
-    private final int mSize;
+    private final int size;
 
     /**
      * If a fixed value exists for the field, then this attribute will contain that value.
      */
     @Nullable
-    private final Long mExpected;
+    private final Long expected;
 
     /**
      * All invariants that this field must verify.
      */
-    @NonNull
-    private Set<ZipFieldInvariant> mInvariants;
+    @Nonnull
+    private Set<ZipFieldInvariant> invariants;
 
     /**
      * Creates a new field that does not contain a fixed value.
@@ -91,15 +90,15 @@
      * @param name the field's name
      * @param invariants the invariants that must be verified by the field
      */
-    ZipField(int offset, int size, @NonNull String name, ZipFieldInvariant... invariants) {
+    ZipField(int offset, int size, @Nonnull String name, ZipFieldInvariant... invariants) {
         Preconditions.checkArgument(offset >= 0, "offset >= 0");
         Preconditions.checkArgument(size == 2 || size == 4, "size != 2 && size != 4");
 
-        mName = name;
-        mOffset = offset;
-        mSize = size;
-        mExpected = null;
-        mInvariants = Sets.newHashSet(invariants);
+        this.name = name;
+        this.offset = offset;
+        this.size = size;
+        expected = null;
+        this.invariants = Sets.newHashSet(invariants);
     }
 
     /**
@@ -110,15 +109,15 @@
      * @param expected the expected field value
      * @param name the field's name
      */
-    ZipField(int offset, int size, long expected, @NonNull String name) {
+    ZipField(int offset, int size, long expected, @Nonnull String name) {
         Preconditions.checkArgument(offset >= 0, "offset >= 0");
         Preconditions.checkArgument(size == 2 || size == 4, "size != 2 && size != 4");
 
-        mName = name;
-        mOffset = offset;
-        mSize = size;
-        mExpected = expected;
-        mInvariants = Sets.newHashSet();
+        this.name = name;
+        this.offset = offset;
+        this.size = size;
+        this.expected = expected;
+        invariants = Sets.newHashSet();
     }
 
     /**
@@ -129,9 +128,9 @@
      * @throws IOException the invariants are not verified
      */
     private void checkVerifiesInvariants(long value) throws IOException {
-        for (ZipFieldInvariant invariant : mInvariants) {
+        for (ZipFieldInvariant invariant : invariants) {
             if (!invariant.isValid(value)) {
-                throw new IOException("Value " + value + " of field " + mName + " is invalid "
+                throw new IOException("Value " + value + " of field " + name + " is invalid "
                         + "(fails '" + invariant.getName() + "').");
             }
         }
@@ -144,13 +143,13 @@
      * the size of this field
      * @throws IOException failed to advance the buffer
      */
-    void skip(@NonNull ByteBuffer bytes) throws IOException {
-        if (bytes.remaining() < mSize) {
-            throw new IOException("Cannot skip field " + mName + " because only "
+    void skip(@Nonnull ByteBuffer bytes) throws IOException {
+        if (bytes.remaining() < size) {
+            throw new IOException("Cannot skip field " + name + " because only "
                     + bytes.remaining() + " remain in the buffer.");
         }
 
-        bytes.position(bytes.position() + mSize);
+        bytes.position(bytes.position() + size);
     }
 
     /**
@@ -161,16 +160,16 @@
      * @return the value of the field
      * @throws IOException failed to read the field
      */
-    long read(@NonNull ByteBuffer bytes) throws IOException {
-        if (bytes.remaining() < mSize) {
-            throw new IOException("Cannot skip field " + mName + " because only "
+    long read(@Nonnull ByteBuffer bytes) throws IOException {
+        if (bytes.remaining() < size) {
+            throw new IOException("Cannot skip field " + name + " because only "
                     + bytes.remaining() + " remain in the buffer.");
         }
 
         bytes.order(ByteOrder.LITTLE_ENDIAN);
 
         long r;
-        if (mSize == 2) {
+        if (size == 2) {
             r = LittleEndianUtils.readUnsigned2Le(bytes);
         } else {
             r =  LittleEndianUtils.readUnsigned4Le(bytes);
@@ -188,9 +187,23 @@
      * will be positioned at the first byte after the field
      * @throws IOException failed to read the field or the field does not have the expected value
      */
-    void verify(@NonNull ByteBuffer bytes) throws IOException {
-        Preconditions.checkState(mExpected != null, "mExpected == null");
-        verify(bytes, mExpected);
+    void verify(@Nonnull ByteBuffer bytes) throws IOException {
+        verify(bytes, null);
+    }
+
+    /**
+     * Verifies that the field at the current buffer position has the expected value. The field
+     * must have been created with the constructor that defines the expected value.
+     *
+     * @param bytes the byte buffer with the record data; after this method finishes, the buffer
+     * will be positioned at the first byte after the field
+     * @param verifyLog if non-{@code null}, will log the verification error
+     * @throws IOException failed to read the data or the field does not have the expected value;
+     * only thrown if {@code verifyLog} is {@code null}
+     */
+    void verify(@Nonnull ByteBuffer bytes, @Nullable VerifyLog verifyLog) throws IOException {
+        Preconditions.checkState(expected != null, "expected == null");
+        verify(bytes, expected, verifyLog);
     }
 
     /**
@@ -202,12 +215,40 @@
      * value must verify them
      * @throws IOException failed to read the data or the field does not have the expected value
      */
-    void verify(@NonNull ByteBuffer bytes, long expected) throws IOException {
+    void verify(@Nonnull ByteBuffer bytes, long expected) throws IOException {
+        verify(bytes, expected, null);
+    }
+
+    /**
+     * Verifies that the field has an expected value.
+     *
+     * @param bytes the byte buffer with the record data; after this method finishes, the buffer
+     * will be positioned at the first byte after the field
+     * @param expected the value we expect the field to have; if this field has invariants, the
+     * value must verify them
+     * @param verifyLog if non-{@code null}, will log the verification error
+     * @throws IOException failed to read the data or the field does not have the expected value;
+     * only thrown if {@code verifyLog} is {@code null}
+     */
+    void verify(
+            @Nonnull ByteBuffer bytes,
+            long expected,
+            @Nullable VerifyLog verifyLog) throws IOException {
         checkVerifiesInvariants(expected);
         long r = read(bytes);
         if (r != expected) {
-            throw new IOException("Incorrect value for field '" + mName + "': value is " +
-                    r + " but " + expected + " expected.");
+            String error =
+                    String.format(
+                            "Incorrect value for field '%s': value is %s but %s expected.",
+                            name,
+                            r,
+                            expected);
+
+            if (verifyLog == null) {
+                throw new IOException(error);
+            } else {
+                verifyLog.log(error);
+            }
         }
     }
 
@@ -219,16 +260,16 @@
      * @param value the value to write
      * @throws IOException failed to write the value in the stream
      */
-    void write(@NonNull ByteBuffer output, long value) throws IOException {
+    void write(@Nonnull ByteBuffer output, long value) throws IOException {
         checkVerifiesInvariants(value);
 
         Preconditions.checkArgument(value >= 0, "value (%s) < 0", value);
 
-        if (mSize == 2) {
+        if (size == 2) {
             Preconditions.checkArgument(value <= 0x0000ffff, "value (%s) > 0x0000ffff", value);
             LittleEndianUtils.writeUnsigned2Le(output, Ints.checkedCast(value));
         } else {
-            Verify.verify(mSize == 4);
+            Verify.verify(size == 4);
             Preconditions.checkArgument(value <= 0x00000000ffffffffL,
                     "value (%s) > 0x00000000ffffffffL", value);
             LittleEndianUtils.writeUnsigned4Le(output, value);
@@ -242,9 +283,18 @@
      * of the buffer
      * @throws IOException failed to write the value in the stream
      */
-    void write(@NonNull ByteBuffer output) throws IOException {
-        Preconditions.checkState(mExpected != null, "mExpected == null");
-        write(output, mExpected);
+    void write(@Nonnull ByteBuffer output) throws IOException {
+        Preconditions.checkState(expected != null, "expected == null");
+        write(output, expected);
+    }
+
+    /**
+     * Obtains the offset at which the field starts.
+     *
+     * @return the start offset
+     */
+    int offset() {
+        return offset;
     }
 
     /**
@@ -254,7 +304,7 @@
      * @return the end offset
      */
     int endOffset() {
-        return mOffset + mSize;
+        return offset + size;
     }
 
     /**
@@ -269,7 +319,7 @@
          * @param name the field's name
          * @param invariants the invariants that must be verified by the field
          */
-        F2(int offset, @NonNull String name, ZipFieldInvariant... invariants) {
+        F2(int offset, @Nonnull String name, ZipFieldInvariant... invariants) {
             super(offset, 2, name, invariants);
         }
 
@@ -280,7 +330,7 @@
          * @param expected the expected field value
          * @param name the field's name
          */
-        F2(int offset, long expected, @NonNull String name) {
+        F2(int offset, long expected, @Nonnull String name) {
             super(offset, 2, expected, name);
         }
     }
@@ -296,7 +346,7 @@
          * @param name the field's name
          * @param invariants the invariants that must be verified by the field
          */
-        F4(int offset, @NonNull String name, ZipFieldInvariant... invariants) {
+        F4(int offset, @Nonnull String name, ZipFieldInvariant... invariants) {
             super(offset, 4, name, invariants);
         }
 
@@ -307,7 +357,7 @@
          * @param expected the expected field value
          * @param name the field's name
          */
-        F4(int offset, long expected, @NonNull String name) {
+        F4(int offset, long expected, @Nonnull String name) {
             super(offset, 4, expected, name);
         }
     }
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/ZipFieldInvariant.java b/src/main/java/com/android/apkzlib/zip/ZipFieldInvariant.java
similarity index 95%
rename from src/main/java/com/android/builder/internal/packaging/zip/ZipFieldInvariant.java
rename to src/main/java/com/android/apkzlib/zip/ZipFieldInvariant.java
index 0ae76cd..87fc46c 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/ZipFieldInvariant.java
+++ b/src/main/java/com/android/apkzlib/zip/ZipFieldInvariant.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
 
 /**
  * A field rule defines an invariant (<em>i.e.</em>, a constraint) that has to be verified by a
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/ZipFieldInvariantMaxValue.java b/src/main/java/com/android/apkzlib/zip/ZipFieldInvariantMaxValue.java
similarity index 86%
rename from src/main/java/com/android/builder/internal/packaging/zip/ZipFieldInvariantMaxValue.java
rename to src/main/java/com/android/apkzlib/zip/ZipFieldInvariantMaxValue.java
index 437e2bb..5905e1a 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/ZipFieldInvariantMaxValue.java
+++ b/src/main/java/com/android/apkzlib/zip/ZipFieldInvariantMaxValue.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
 
 /**
  * Invariant checking a zip field does not exceed a threshold.
@@ -24,7 +24,7 @@
     /**
      * The maximum value allowed.
      */
-    private long mMax;
+    private long max;
 
     /**
      * Creates a new invariant.
@@ -32,16 +32,16 @@
      * @param max the maximum value allowed for the field
      */
     ZipFieldInvariantMaxValue(int max) {
-        mMax = max;
+        this.max = max;
     }
 
     @Override
     public boolean isValid(long value) {
-        return value <= mMax;
+        return value <= max;
     }
 
     @Override
     public String getName() {
-        return "Maximum value " + mMax;
+        return "Maximum value " + max;
     }
 }
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/ZipFieldInvariantNonNegative.java b/src/main/java/com/android/apkzlib/zip/ZipFieldInvariantNonNegative.java
similarity index 94%
rename from src/main/java/com/android/builder/internal/packaging/zip/ZipFieldInvariantNonNegative.java
rename to src/main/java/com/android/apkzlib/zip/ZipFieldInvariantNonNegative.java
index e4d68d8..4d1770b 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/ZipFieldInvariantNonNegative.java
+++ b/src/main/java/com/android/apkzlib/zip/ZipFieldInvariantNonNegative.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
 
 /**
  * Invariant that verifies a field's value is not negative.
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/ZipFileState.java b/src/main/java/com/android/apkzlib/zip/ZipFileState.java
similarity index 94%
rename from src/main/java/com/android/builder/internal/packaging/zip/ZipFileState.java
rename to src/main/java/com/android/apkzlib/zip/ZipFileState.java
index b3b9990..7ecf2d5 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/ZipFileState.java
+++ b/src/main/java/com/android/apkzlib/zip/ZipFileState.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
 
 /**
  * The {@code ZipFileState} enumeration holds the state of a {@link ZFile}.
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/compress/BestAndDefaultDeflateExecutorCompressor.java b/src/main/java/com/android/apkzlib/zip/compress/BestAndDefaultDeflateExecutorCompressor.java
similarity index 65%
rename from src/main/java/com/android/builder/internal/packaging/zip/compress/BestAndDefaultDeflateExecutorCompressor.java
rename to src/main/java/com/android/apkzlib/zip/compress/BestAndDefaultDeflateExecutorCompressor.java
index 5e132d2..8948a1c 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/compress/BestAndDefaultDeflateExecutorCompressor.java
+++ b/src/main/java/com/android/apkzlib/zip/compress/BestAndDefaultDeflateExecutorCompressor.java
@@ -14,16 +14,15 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip.compress;
+package com.android.apkzlib.zip.compress;
 
-import com.android.annotations.NonNull;
-import com.android.builder.internal.packaging.zip.CompressionResult;
-import com.android.builder.internal.packaging.zip.utils.ByteTracker;
-import com.android.builder.internal.packaging.zip.utils.CloseableByteSource;
+import com.android.apkzlib.zip.CompressionResult;
+import com.android.apkzlib.zip.utils.ByteTracker;
+import com.android.apkzlib.zip.utils.CloseableByteSource;
 import com.google.common.base.Preconditions;
-
 import java.util.concurrent.Executor;
 import java.util.zip.Deflater;
+import javax.annotation.Nonnull;
 
 /**
  * Compressor that tries both the best and default compression algorithms and picks the default
@@ -34,20 +33,20 @@
     /**
      * Deflater using the default compression level.
      */
-    @NonNull
-    private final DeflateExecutionCompressor mDefaultDeflater;
+    @Nonnull
+    private final DeflateExecutionCompressor defaultDeflater;
 
     /**
      * Deflater using the best compression level.
      */
-    @NonNull
-    private final DeflateExecutionCompressor mBestDeflater;
+    @Nonnull
+    private final DeflateExecutionCompressor bestDeflater;
 
     /**
      * Minimum best compression size / default compression size ratio needed to pick the default
      * compression size.
      */
-    private final double mMinRatio;
+    private final double minRatio;
 
     /**
      * Creates a new compressor.
@@ -59,29 +58,29 @@
      * if {@code 1.0} then the best compression is always picked unless it produces the exact same
      * size as the default compression.
      */
-    public BestAndDefaultDeflateExecutorCompressor(@NonNull Executor executor,
-            @NonNull ByteTracker tracker, double minRatio) {
+    public BestAndDefaultDeflateExecutorCompressor(@Nonnull Executor executor,
+            @Nonnull ByteTracker tracker, double minRatio) {
         super(executor);
 
         Preconditions.checkArgument(minRatio >= 0.0, "minRatio < 0.0");
         Preconditions.checkArgument(minRatio <= 1.0, "minRatio > 1.0");
 
-        mDefaultDeflater = new DeflateExecutionCompressor(executor, tracker,
-                Deflater.DEFAULT_COMPRESSION);
-        mBestDeflater = new DeflateExecutionCompressor(executor, tracker,
-                Deflater.BEST_COMPRESSION);
-        mMinRatio = minRatio;
+        defaultDeflater =
+                new DeflateExecutionCompressor(executor, tracker, Deflater.DEFAULT_COMPRESSION);
+        bestDeflater =
+                new DeflateExecutionCompressor(executor, tracker, Deflater.BEST_COMPRESSION);
+        this.minRatio = minRatio;
     }
 
-    @NonNull
+    @Nonnull
     @Override
-    protected CompressionResult immediateCompress(@NonNull CloseableByteSource source)
+    protected CompressionResult immediateCompress(@Nonnull CloseableByteSource source)
             throws Exception {
-        CompressionResult defaultResult = mDefaultDeflater.immediateCompress(source);
-        CompressionResult bestResult = mBestDeflater.immediateCompress(source);
+        CompressionResult defaultResult = defaultDeflater.immediateCompress(source);
+        CompressionResult bestResult = bestDeflater.immediateCompress(source);
 
         double sizeRatio = bestResult.getSize() / (double) defaultResult.getSize();
-        if (sizeRatio >= mMinRatio) {
+        if (sizeRatio >= minRatio) {
             return defaultResult;
         } else {
             return bestResult;
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/compress/DeflateExecutionCompressor.java b/src/main/java/com/android/apkzlib/zip/compress/DeflateExecutionCompressor.java
similarity index 69%
rename from src/main/java/com/android/builder/internal/packaging/zip/compress/DeflateExecutionCompressor.java
rename to src/main/java/com/android/apkzlib/zip/compress/DeflateExecutionCompressor.java
index d8c6ab9..309e356 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/compress/DeflateExecutionCompressor.java
+++ b/src/main/java/com/android/apkzlib/zip/compress/DeflateExecutionCompressor.java
@@ -14,18 +14,17 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip.compress;
+package com.android.apkzlib.zip.compress;
 
-import com.android.annotations.NonNull;
-import com.android.builder.internal.packaging.zip.CompressionMethod;
-import com.android.builder.internal.packaging.zip.CompressionResult;
-import com.android.builder.internal.packaging.zip.utils.ByteTracker;
-import com.android.builder.internal.packaging.zip.utils.CloseableByteSource;
-
+import com.android.apkzlib.zip.CompressionMethod;
+import com.android.apkzlib.zip.CompressionResult;
+import com.android.apkzlib.zip.utils.ByteTracker;
+import com.android.apkzlib.zip.utils.CloseableByteSource;
 import java.io.ByteArrayOutputStream;
 import java.util.concurrent.Executor;
 import java.util.zip.Deflater;
 import java.util.zip.DeflaterOutputStream;
+import javax.annotation.Nonnull;
 
 /**
  * Compressor that uses deflate with an executor.
@@ -36,13 +35,13 @@
     /**
      * Deflate compression level.
      */
-    private final int mLevel;
+    private final int level;
 
     /**
      * Byte tracker to use to create byte sources.
      */
-    @NonNull
-    private final ByteTracker mTracker;
+    @Nonnull
+    private final ByteTracker tracker;
 
     /**
      * Creates a new compressor.
@@ -51,26 +50,28 @@
      * @param tracker the byte tracker to use to keep track of memory usage
      * @param level the compression level
      */
-    public DeflateExecutionCompressor(@NonNull Executor executor, @NonNull ByteTracker tracker,
+    public DeflateExecutionCompressor(
+            @Nonnull Executor executor,
+            @Nonnull ByteTracker tracker,
             int level) {
         super(executor);
 
-        mLevel = level;
-        mTracker = tracker;
+        this.level = level;
+        this.tracker = tracker;
     }
 
-    @NonNull
+    @Nonnull
     @Override
-    protected CompressionResult immediateCompress(@NonNull CloseableByteSource source)
+    protected CompressionResult immediateCompress(@Nonnull CloseableByteSource source)
             throws Exception {
         ByteArrayOutputStream output = new ByteArrayOutputStream();
-        Deflater deflater = new Deflater(mLevel, true);
+        Deflater deflater = new Deflater(level, true);
 
         try (DeflaterOutputStream dos = new DeflaterOutputStream(output, deflater)) {
             dos.write(source.read());
         }
 
-        CloseableByteSource result = mTracker.fromStream(output);
+        CloseableByteSource result = tracker.fromStream(output);
         if (result.size() >= source.size()) {
             return new CompressionResult(source, CompressionMethod.STORE, source.size());
         } else {
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/compress/ExecutorCompressor.java b/src/main/java/com/android/apkzlib/zip/compress/ExecutorCompressor.java
similarity index 72%
rename from src/main/java/com/android/builder/internal/packaging/zip/compress/ExecutorCompressor.java
rename to src/main/java/com/android/apkzlib/zip/compress/ExecutorCompressor.java
index f52c542..54be20c 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/compress/ExecutorCompressor.java
+++ b/src/main/java/com/android/apkzlib/zip/compress/ExecutorCompressor.java
@@ -14,16 +14,15 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip.compress;
+package com.android.apkzlib.zip.compress;
 
-import com.android.annotations.NonNull;
-import com.android.builder.internal.packaging.zip.CompressionResult;
-import com.android.builder.internal.packaging.zip.Compressor;
-import com.android.builder.internal.packaging.zip.utils.CloseableByteSource;
+import com.android.apkzlib.zip.CompressionResult;
+import com.android.apkzlib.zip.Compressor;
+import com.android.apkzlib.zip.utils.CloseableByteSource;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.SettableFuture;
-
 import java.util.concurrent.Executor;
+import javax.annotation.Nonnull;
 
 /**
  * A synchronous compressor is a compressor that computes the result of compression immediately
@@ -34,26 +33,26 @@
     /**
      * The executor that does the work.
      */
-    @NonNull
-    private final Executor mExecutor;
+    @Nonnull
+    private final Executor executor;
 
     /**
      * Compressor that delegates execution into the given executor.
      * @param executor the executor that will do the compress
      */
-    public ExecutorCompressor(@NonNull Executor executor) {
-        mExecutor = executor;
+    public ExecutorCompressor(@Nonnull Executor executor) {
+        this.executor = executor;
     }
 
-    @NonNull
+    @Nonnull
     @Override
     public ListenableFuture<CompressionResult> compress(
-            @NonNull final CloseableByteSource source) {
+            @Nonnull final CloseableByteSource source) {
         final SettableFuture<CompressionResult> future = SettableFuture.create();
-        mExecutor.execute(() -> {
+        executor.execute(() -> {
             try {
                 future.set(immediateCompress(source));
-            } catch (Exception e) {
+            } catch (Throwable e) {
                 future.setException(e);
             }
         });
@@ -67,7 +66,7 @@
      * @return the result of compression
      * @throws Exception failed to compress
      */
-    @NonNull
-    protected abstract CompressionResult immediateCompress(@NonNull CloseableByteSource source)
+    @Nonnull
+    protected abstract CompressionResult immediateCompress(@Nonnull CloseableByteSource source)
             throws Exception;
 }
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/StoredEntryType.java b/src/main/java/com/android/apkzlib/zip/compress/Zip64NotSupportedException.java
similarity index 60%
copy from src/main/java/com/android/builder/internal/packaging/zip/StoredEntryType.java
copy to src/main/java/com/android/apkzlib/zip/compress/Zip64NotSupportedException.java
index 22983ab..3b7411e 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/StoredEntryType.java
+++ b/src/main/java/com/android/apkzlib/zip/compress/Zip64NotSupportedException.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2017 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.
@@ -14,19 +14,14 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip.compress;
 
-/**
- * Type of stored entry.
- */
-public enum StoredEntryType {
-    /**
-     * Entry is a file.
-     */
-    FILE,
+import java.io.IOException;
 
-    /**
-     * Entry is a directory.
-     */
-    DIRECTORY
+/** Exception raised by ZFile when encountering unsupported Zip64 format jar files. */
+public class Zip64NotSupportedException extends IOException {
+
+    public Zip64NotSupportedException(String message) {
+        super(message);
+    }
 }
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/StoredEntryType.java b/src/main/java/com/android/apkzlib/zip/compress/package-info.java
similarity index 67%
copy from src/main/java/com/android/builder/internal/packaging/zip/StoredEntryType.java
copy to src/main/java/com/android/apkzlib/zip/compress/package-info.java
index 22983ab..e2fcbd6 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/StoredEntryType.java
+++ b/src/main/java/com/android/apkzlib/zip/compress/package-info.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2017 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.
@@ -14,19 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip;
-
 /**
- * Type of stored entry.
+ * Compressors to use with the {@code zip} package.
  */
-public enum StoredEntryType {
-    /**
-     * Entry is a file.
-     */
-    FILE,
-
-    /**
-     * Entry is a directory.
-     */
-    DIRECTORY
-}
+package com.android.apkzlib.zip.compress;
\ No newline at end of file
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/utils/ByteTracker.java b/src/main/java/com/android/apkzlib/zip/utils/ByteTracker.java
similarity index 82%
rename from src/main/java/com/android/builder/internal/packaging/zip/utils/ByteTracker.java
rename to src/main/java/com/android/apkzlib/zip/utils/ByteTracker.java
index 9c7a719..88ed939 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/utils/ByteTracker.java
+++ b/src/main/java/com/android/apkzlib/zip/utils/ByteTracker.java
@@ -14,35 +14,38 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip.utils;
+package com.android.apkzlib.zip.utils;
 
-import com.android.annotations.NonNull;
 import com.google.common.io.ByteSource;
 import com.google.common.io.ByteStreams;
-
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import javax.annotation.Nonnull;
 
+/**
+ * Keeps track of used bytes allowing gauging memory usage.
+ */
 public class ByteTracker {
 
     /**
      * Number of bytes currently in use.
      */
-    private long mBytesUsed;
+    private long bytesUsed;
 
     /**
      * Maximum number of bytes used.
      */
-    private long mMaxBytesUsed;
+    private long maxBytesUsed;
 
     /**
      * Creates a new byte source by fully reading an input stream.
+     *
      * @param stream the input stream
      * @return a byte source containing the cached data from the given stream
      * @throws IOException failed to read the stream
      */
-    public CloseableDelegateByteSource fromStream(@NonNull InputStream stream) throws IOException {
+    public CloseableDelegateByteSource fromStream(@Nonnull InputStream stream) throws IOException {
         byte[] data = ByteStreams.toByteArray(stream);
         updateUsage(data.length);
         return new CloseableDelegateByteSource(ByteSource.wrap(data), data.length) {
@@ -56,11 +59,13 @@
 
     /**
      * Creates a new byte source by snapshotting the provided stream.
+     *
      * @param stream the stream with the data
      * @return a byte source containing the cached data from the given stream
      * @throws IOException failed to read the stream
      */
-    public CloseableDelegateByteSource fromStream(@NonNull ByteArrayOutputStream stream) throws IOException {
+    public CloseableDelegateByteSource fromStream(@Nonnull ByteArrayOutputStream stream)
+            throws IOException {
         byte[] data = stream.toByteArray();
         updateUsage(data.length);
         return new CloseableDelegateByteSource(ByteSource.wrap(data), data.length) {
@@ -74,38 +79,42 @@
 
     /**
      * Creates a new byte source from another byte source.
+     *
      * @param source the byte source to copy data from
      * @return the tracked byte source
      * @throws IOException failed to read data from the byte source
      */
-    public CloseableDelegateByteSource fromSource(@NonNull ByteSource source) throws IOException {
+    public CloseableDelegateByteSource fromSource(@Nonnull ByteSource source) throws IOException {
         return fromStream(source.openStream());
     }
 
     /**
      * Updates the memory used by this tracker.
+     *
      * @param delta the number of bytes to add or remove, if negative
      */
     private synchronized void updateUsage(long delta) {
-        mBytesUsed += delta;
-        if (mMaxBytesUsed < mBytesUsed) {
-            mMaxBytesUsed = mBytesUsed;
+        bytesUsed += delta;
+        if (maxBytesUsed < bytesUsed) {
+            maxBytesUsed = bytesUsed;
         }
     }
 
     /**
      * Obtains the number of bytes currently used.
+     *
      * @return the number of bytes
      */
     public synchronized long getBytesUsed() {
-        return mBytesUsed;
+        return bytesUsed;
     }
 
     /**
      * Obtains the maximum number of bytes ever used by this tracker.
+     *
      * @return the number of bytes
      */
     public synchronized long getMaxBytesUsed() {
-        return mMaxBytesUsed;
+        return maxBytesUsed;
     }
 }
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/utils/CloseableByteSource.java b/src/main/java/com/android/apkzlib/zip/utils/CloseableByteSource.java
similarity index 89%
rename from src/main/java/com/android/builder/internal/packaging/zip/utils/CloseableByteSource.java
rename to src/main/java/com/android/apkzlib/zip/utils/CloseableByteSource.java
index 28afd71..1479a03 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/utils/CloseableByteSource.java
+++ b/src/main/java/com/android/apkzlib/zip/utils/CloseableByteSource.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip.utils;
+package com.android.apkzlib.zip.utils;
 
 import com.google.common.io.ByteSource;
 
@@ -32,31 +32,32 @@
     /**
      * Has the source been closed?
      */
-    private boolean mClosed;
+    private boolean closed;
 
     /**
      * Creates a new byte source.
      */
     public CloseableByteSource() {
-        mClosed = false;
+        closed = false;
     }
 
     @Override
     public final synchronized void close() throws IOException {
-        if (mClosed) {
+        if (closed) {
             return;
         }
 
         try {
             innerClose();
         } finally {
-            mClosed = true;
+            closed = true;
         }
     }
 
     /**
      * Closes the by source. This method is only invoked once, even if {@link #close()} is
-     * called multiple times
+     * called multiple times.
+     *
      * @throws IOException failed to close
      */
     protected abstract void innerClose() throws IOException;
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/utils/CloseableDelegateByteSource.java b/src/main/java/com/android/apkzlib/zip/utils/CloseableDelegateByteSource.java
similarity index 83%
rename from src/main/java/com/android/builder/internal/packaging/zip/utils/CloseableDelegateByteSource.java
rename to src/main/java/com/android/apkzlib/zip/utils/CloseableDelegateByteSource.java
index 7788b9d..aebb29a 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/utils/CloseableDelegateByteSource.java
+++ b/src/main/java/com/android/apkzlib/zip/utils/CloseableDelegateByteSource.java
@@ -14,21 +14,20 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip.utils;
+package com.android.apkzlib.zip.utils;
 
-import com.android.annotations.NonNull;
-import com.android.annotations.Nullable;
 import com.google.common.hash.HashCode;
 import com.google.common.hash.HashFunction;
 import com.google.common.io.ByteProcessor;
 import com.google.common.io.ByteSink;
 import com.google.common.io.ByteSource;
 import com.google.common.io.CharSource;
-
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.nio.charset.Charset;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
 /**
  * Closeable byte source that delegates to another byte source.
@@ -39,11 +38,11 @@
      * The byte source we delegate all operations to. {@code null} if disposed.
      */
     @Nullable
-    private ByteSource mInner;
+    private ByteSource inner;
 
     /**
-     * Size of the byte source. This is the same as {@code mInner.size()} (when {@code mInner}
-     * is not {@code null}), but we keep it separate to avoid calling {@code mInner.size()}
+     * Size of the byte source. This is the same as {@code inner.size()} (when {@code inner}
+     * is not {@code null}), but we keep it separate to avoid calling {@code inner.size()}
      * because it might throw {@code IOException}.
      */
     private final long mSize;
@@ -54,8 +53,8 @@
      * @param inner the inner byte source
      * @param size the size of the source
      */
-    public CloseableDelegateByteSource(@NonNull ByteSource inner, long size) {
-        mInner = inner;
+    public CloseableDelegateByteSource(@Nonnull ByteSource inner, long size) {
+        this.inner = inner;
         mSize = size;
     }
 
@@ -65,13 +64,13 @@
      *
      * @return the inner byte source
      */
-    @NonNull
+    @Nonnull
     private synchronized ByteSource get() {
-        if (mInner == null) {
+        if (inner == null) {
             throw new ByteSourceDisposedException();
         }
 
-        return mInner;
+        return inner;
     }
 
     /**
@@ -79,11 +78,11 @@
      */
     @Override
     protected synchronized void innerClose() throws IOException {
-        if (mInner == null) {
+        if (inner == null) {
             return;
         }
 
-        mInner = null;
+        inner = null;
     }
 
     /**
@@ -122,12 +121,12 @@
     }
 
     @Override
-    public long copyTo(@NonNull OutputStream output) throws IOException {
+    public long copyTo(@Nonnull OutputStream output) throws IOException {
         return get().copyTo(output);
     }
 
     @Override
-    public long copyTo(@NonNull ByteSink sink) throws IOException {
+    public long copyTo(@Nonnull ByteSink sink) throws IOException {
         return get().copyTo(sink);
     }
 
@@ -137,7 +136,7 @@
     }
 
     @Override
-    public <T> T read(@NonNull ByteProcessor<T> processor) throws IOException {
+    public <T> T read(@Nonnull ByteProcessor<T> processor) throws IOException {
         return get().read(processor);
     }
 
@@ -147,7 +146,7 @@
     }
 
     @Override
-    public boolean contentEquals(@NonNull ByteSource other) throws IOException {
+    public boolean contentEquals(@Nonnull ByteSource other) throws IOException {
         return get().contentEquals(other);
     }
 
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/utils/LittleEndianUtils.java b/src/main/java/com/android/apkzlib/zip/utils/LittleEndianUtils.java
similarity index 85%
rename from src/main/java/com/android/builder/internal/packaging/zip/utils/LittleEndianUtils.java
rename to src/main/java/com/android/apkzlib/zip/utils/LittleEndianUtils.java
index 51885e2..c257d39 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/utils/LittleEndianUtils.java
+++ b/src/main/java/com/android/apkzlib/zip/utils/LittleEndianUtils.java
@@ -14,17 +14,14 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip.utils;
+package com.android.apkzlib.zip.utils;
 
-import com.android.annotations.NonNull;
 import com.google.common.base.Preconditions;
 import com.google.common.base.Verify;
-import com.google.common.io.ByteSource;
-
 import java.io.EOFException;
 import java.io.IOException;
-import java.io.OutputStream;
 import java.nio.ByteBuffer;
+import javax.annotation.Nonnull;
 
 /**
  * Utilities to read and write 16 and 32 bit integers with support for little-endian
@@ -46,7 +43,7 @@
      * @return the 32-bit value
      * @throws IOException failed to read the value
      */
-    public static long readUnsigned4Le(@NonNull ByteBuffer bytes) throws IOException {
+    public static long readUnsigned4Le(@Nonnull ByteBuffer bytes) throws IOException {
         Preconditions.checkNotNull(bytes, "bytes == null");
 
         if (bytes.remaining() < 4) {
@@ -72,12 +69,14 @@
      * @return the 16-bit value
      * @throws IOException failed to read the value
      */
-    public static int readUnsigned2Le(@NonNull ByteBuffer bytes) throws IOException {
+    public static int readUnsigned2Le(@Nonnull ByteBuffer bytes) throws IOException {
         Preconditions.checkNotNull(bytes, "bytes == null");
 
         if (bytes.remaining() < 2) {
-            throw new EOFException("Not enough data: 2 bytes expected, " + bytes.remaining()
-                    + " available.");
+            throw new EOFException(
+                    "Not enough data: 2 bytes expected, "
+                            + bytes.remaining()
+                            + " available.");
         }
 
         byte b0 = bytes.get();
@@ -96,12 +95,14 @@
      * @param value the 32-bit value to convert
      * @throws IOException failed to write the value data
      */
-    public static void writeUnsigned4Le(@NonNull ByteBuffer output, long value)
+    public static void writeUnsigned4Le(@Nonnull ByteBuffer output, long value)
             throws IOException {
         Preconditions.checkNotNull(output, "output == null");
         Preconditions.checkArgument(value >= 0, "value (%s) < 0", value);
-        Preconditions.checkArgument(value <= 0x00000000ffffffffL,
-                "value (%s) > 0x00000000ffffffffL", value);
+        Preconditions.checkArgument(
+                value <= 0x00000000ffffffffL,
+                "value (%s) > 0x00000000ffffffffL",
+                value);
 
         output.put((byte) (value & 0xff));
         output.put((byte) ((value >> 8) & 0xff));
@@ -116,7 +117,7 @@
      * @param value the 16-bit value to convert
      * @throws IOException failed to write the value data
      */
-    public static void writeUnsigned2Le(@NonNull ByteBuffer output, int value)
+    public static void writeUnsigned2Le(@Nonnull ByteBuffer output, int value)
             throws IOException {
         Preconditions.checkNotNull(output, "output == null");
         Preconditions.checkArgument(value >= 0, "value (%s) < 0", value);
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/utils/MsDosDateTimeUtils.java b/src/main/java/com/android/apkzlib/zip/utils/MsDosDateTimeUtils.java
similarity index 97%
rename from src/main/java/com/android/builder/internal/packaging/zip/utils/MsDosDateTimeUtils.java
rename to src/main/java/com/android/apkzlib/zip/utils/MsDosDateTimeUtils.java
index 43c5793..2b9f365 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/utils/MsDosDateTimeUtils.java
+++ b/src/main/java/com/android/apkzlib/zip/utils/MsDosDateTimeUtils.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip.utils;
+package com.android.apkzlib.zip.utils;
 
 import com.google.common.base.Verify;
 
@@ -33,6 +33,7 @@
 
     /**
      * Packs java time value into an MS-DOS time value.
+     *
      * @param time the time value
      * @return the MS-DOS packed time
      */
@@ -57,6 +58,7 @@
 
     /**
      * Packs the current time value into an MS-DOS time value.
+     *
      * @return the MS-DOS packed time
      */
     public static int packCurrentTime() {
@@ -65,6 +67,7 @@
 
     /**
      * Packs java time value into an MS-DOS date value.
+     *
      * @param time the time value
      * @return the MS-DOS packed date
      */
@@ -99,6 +102,7 @@
 
     /**
      * Packs the current time value into an MS-DOS date value.
+     *
      * @return the MS-DOS packed date
      */
     public static int packCurrentDate() {
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/utils/RandomAccessFileUtils.java b/src/main/java/com/android/apkzlib/zip/utils/RandomAccessFileUtils.java
similarity index 70%
rename from src/main/java/com/android/builder/internal/packaging/zip/utils/RandomAccessFileUtils.java
rename to src/main/java/com/android/apkzlib/zip/utils/RandomAccessFileUtils.java
index c1ee5b6..3bfb55c 100644
--- a/src/main/java/com/android/builder/internal/packaging/zip/utils/RandomAccessFileUtils.java
+++ b/src/main/java/com/android/apkzlib/zip/utils/RandomAccessFileUtils.java
@@ -14,22 +14,18 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip.utils;
-
-import com.android.annotations.NonNull;
+package com.android.apkzlib.zip.utils;
 
 import java.io.IOException;
 import java.io.RandomAccessFile;
+import javax.annotation.Nonnull;
 
 /**
  * Utility class with utility methods for random access files.
  */
-public class RandomAccessFileUtils {
-    /**
-     * Utility class: no constructor.
-     */
-    private RandomAccessFileUtils() {
-    }
+public final class RandomAccessFileUtils {
+
+    private RandomAccessFileUtils() {}
 
     /**
      * Reads from an random access file until the provided array is filled. Data is read from the
@@ -37,9 +33,9 @@
      *
      * @param raf the file to read data from
      * @param data the array that will receive the data
-     * @throws IOException
+     * @throws IOException failed to read the data
      */
-    public static void fullyRead(@NonNull RandomAccessFile raf, @NonNull byte[] data)
+    public static void fullyRead(@Nonnull RandomAccessFile raf, @Nonnull byte[] data)
             throws IOException {
         int r;
         int p = 0;
@@ -52,8 +48,12 @@
         }
 
         if (p < data.length) {
-            throw new IOException("Failed to read " + data.length + " bytes from file. Only "
-                    + p + " bytes could be read.");
+            throw new IOException(
+                    "Failed to read "
+                            + data.length
+                            + " bytes from file. Only "
+                            + p
+                            + " bytes could be read.");
         }
     }
 }
diff --git a/src/main/java/com/android/builder/internal/packaging/sign/FullApkSignExtension.java b/src/main/java/com/android/builder/internal/packaging/sign/FullApkSignExtension.java
deleted file mode 100644
index 8f9dd6a..0000000
--- a/src/main/java/com/android/builder/internal/packaging/sign/FullApkSignExtension.java
+++ /dev/null
@@ -1,213 +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.builder.internal.packaging.sign;
-
-import com.android.annotations.NonNull;
-import com.android.annotations.Nullable;
-import com.android.builder.internal.packaging.sign.v2.ApkSignerV2;
-import com.android.builder.internal.packaging.sign.v2.ByteArrayDigestSource;
-import com.android.builder.internal.packaging.sign.v2.DigestSource;
-import com.android.builder.internal.packaging.sign.v2.SignatureAlgorithm;
-import com.android.builder.internal.packaging.sign.v2.ZFileDigestSource;
-import com.android.builder.internal.packaging.zip.StoredEntry;
-import com.android.builder.internal.packaging.zip.ZFile;
-import com.android.builder.internal.packaging.zip.ZFileExtension;
-import com.android.builder.internal.utils.IOExceptionRunnable;
-import com.google.common.base.Preconditions;
-import com.google.common.base.Verify;
-import com.google.common.collect.ImmutableList;
-
-import java.io.IOException;
-import java.security.InvalidKeyException;
-import java.security.PrivateKey;
-import java.security.SignatureException;
-import java.security.cert.X509Certificate;
-import java.util.List;
-
-/**
- * Extension that adds full APK signing. This extension will:
- * <ul>
- *     <li>Generate a new signature if the zip is modified after the extension is added.</li>
- *     <li>Generate a new signature even if the zip is not modified after the extension is
- *     added, but a valid signature is not found.</li>
- * </ul>
- * <p>
- * The extension computes the signature, if needed, at the {@link ZFileExtension#entriesWritten()}
- * event, after all zip entries have been written, but before the central directory or EOCD have.
- * This allows the extension to set the central directory offset parameter in the zip file
- * using {@link ZFile#setExtraDirectoryOffset(long)} adding enough space in the zip file for the
- * signature block.
- * <p>
- * The signature block is written before the central directory, allowing the zip file to be written
- * in sequential order.
- */
-public class FullApkSignExtension {
-
-    /**
-     * The zip file this extension is registered with.
-     */
-    @NonNull
-    private final ZFile mFile;
-
-    /**
-     * Signer certificate.
-     */
-    @NonNull
-    private final X509Certificate mCertificate;
-
-    /**
-     * Signer private key.
-     */
-    @NonNull
-    private final PrivateKey mPrivateKey;
-
-    /**
-     * APK Signature Scheme v2 algorithms to use for signing the APK.
-     */
-    private final List<SignatureAlgorithm> mV2SignatureAlgorithms;
-
-    /**
-     * {@code true} if the zip needs its signature to be updated.
-     */
-    private boolean mNeedsSignatureUpdate = true;
-
-    /**
-     * The extension to register with the {@link ZFile}. {@code null} if not registered.
-     */
-    @Nullable
-    private ZFileExtension mExtension;
-
-    /**
-     * Creates a new extension. This will not register the extension with the provided
-     * {@link ZFile}. Until {@link #register()} is invoked, this extension is not used.
-     *
-     * @param file the zip file to register the extension with
-     * @param minSdkVersion minSdkVersion of the package
-     * @param certificate sign certificate
-     * @param privateKey the private key to sign the jar
-     *
-     * @throws InvalidKeyException if the signing key is not suitable for signing this APK.
-     */
-    public FullApkSignExtension(@NonNull ZFile file,
-            int minSdkVersion,
-            @NonNull X509Certificate certificate,
-            @NonNull PrivateKey privateKey) throws InvalidKeyException {
-        mFile = file;
-        mCertificate = certificate;
-        mPrivateKey = privateKey;
-        mV2SignatureAlgorithms =
-                ApkSignerV2.getSuggestedSignatureAlgorithms(
-                        certificate.getPublicKey(), minSdkVersion);
-    }
-
-    /**
-     * Registers the extension with the {@link ZFile} provided in the constructor.
-     */
-    public void register() {
-        Preconditions.checkState(mExtension == null, "register() has already been invoked.");
-
-        mExtension = new ZFileExtension() {
-            @Nullable
-            @Override
-            public IOExceptionRunnable beforeUpdate() throws IOException {
-                mFile.setExtraDirectoryOffset(0);
-                return null;
-            }
-
-            @Nullable
-            @Override
-            public IOExceptionRunnable added(@NonNull StoredEntry entry,
-                    @Nullable StoredEntry replaced) {
-                onZipChanged();
-                return null;
-            }
-
-            @Nullable
-            @Override
-            public IOExceptionRunnable removed(@NonNull StoredEntry entry) {
-                onZipChanged();
-                return null;
-            }
-
-            @Override
-            public void entriesWritten() throws IOException {
-                onEntriesWritten();
-            }
-        };
-
-        mFile.addZFileExtension(mExtension);
-    }
-
-    /**
-     * Invoked when the zip file has been changed.
-     */
-    private void onZipChanged() {
-        mNeedsSignatureUpdate = true;
-    }
-
-    /**
-     * Invoked before the zip file has been updated.
-     *
-     * @throws IOException failed to perform the update
-     */
-    private void onEntriesWritten() throws IOException {
-        if (!mNeedsSignatureUpdate) {
-            return;
-        }
-        mNeedsSignatureUpdate = false;
-
-        byte[] apkSigningBlock = generateApkSigningBlock();
-        Verify.verify(apkSigningBlock.length > 0, "apkSigningBlock.length == 0");
-        mFile.setExtraDirectoryOffset(apkSigningBlock.length);
-        long apkSigningBlockOffset =
-                mFile.getCentralDirectoryOffset() - mFile.getExtraDirectoryOffset();
-        mFile.directWrite(apkSigningBlockOffset, apkSigningBlock);
-    }
-
-    /**
-     * Generates a signature for the APK.
-     *
-     * @return the signature data block
-     * @throws IOException failed to generate a signature
-     */
-    @NonNull
-    private byte[] generateApkSigningBlock() throws IOException {
-        byte[] centralDirectoryData = mFile.getCentralDirectoryBytes();
-        byte[] eocdData = mFile.getEocdBytes();
-
-        ApkSignerV2.SignerConfig signerConfig = new ApkSignerV2.SignerConfig();
-        signerConfig.privateKey = mPrivateKey;
-        signerConfig.certificates = ImmutableList.of(mCertificate);
-        signerConfig.signatureAlgorithms = mV2SignatureAlgorithms;
-        DigestSource centralDir = new ByteArrayDigestSource(centralDirectoryData);
-        DigestSource eocd = new ByteArrayDigestSource(eocdData);
-        DigestSource zipEntries =
-                new ZFileDigestSource(
-                        mFile,
-                        0,
-                        mFile.getCentralDirectoryOffset() - mFile.getExtraDirectoryOffset());
-        try {
-            return ApkSignerV2.generateApkSigningBlock(
-                    zipEntries,
-                    centralDir,
-                    eocd,
-                    ImmutableList.of(signerConfig));
-        } catch (InvalidKeyException | SignatureException e) {
-            throw new IOException("Failed to sign APK using APK Signature Scheme v2", e);
-        }
-    }
-}
diff --git a/src/main/java/com/android/builder/internal/packaging/sign/ManifestGenerationExtension.java b/src/main/java/com/android/builder/internal/packaging/sign/ManifestGenerationExtension.java
deleted file mode 100644
index 8db3d6c..0000000
--- a/src/main/java/com/android/builder/internal/packaging/sign/ManifestGenerationExtension.java
+++ /dev/null
@@ -1,347 +0,0 @@
-/*
- * Copyright (C) 2015 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.builder.internal.packaging.sign;
-
-import com.android.annotations.NonNull;
-import com.android.annotations.Nullable;
-import com.android.builder.internal.packaging.zip.StoredEntry;
-import com.android.builder.internal.packaging.zip.ZFile;
-import com.android.builder.internal.packaging.zip.ZFileExtension;
-import com.android.builder.internal.utils.CachedSupplier;
-import com.android.builder.internal.utils.IOExceptionRunnable;
-import com.android.builder.packaging.ManifestAttributes;
-import com.google.common.base.Preconditions;
-import com.google.common.base.Verify;
-import com.google.common.collect.Maps;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.util.Map;
-import java.util.jar.Attributes;
-import java.util.jar.Manifest;
-
-/**
- * Extension to {@link ZFile} that will generate a manifest. The extension will register
- * automatically with the {@link ZFile}.
- *
- * <p>Creating this extension will ensure a manifest for the zip exists.
- * This extension will generate a manifest if one does not exist and will update an existing
- * manifest, if one does exist. The extension will also provide access to the manifest so that
- * others may update the manifest.
- *
- * <p>Apart from standard manifest elements, this extension does not handle any particular manifest
- * features such as signing or adding custom attributes. It simply generates a plain manifest and
- * provides infrastructure so that other extensions can add data in the manifest.
- *
- * <p>The manifest itself will only be written when the {@link ZFileExtension#beforeUpdate()}
- * notification is received, meaning all manifest manipulation is done in-memory.
- */
-public class ManifestGenerationExtension {
-
-    /**
-     * Name of META-INF directory.
-     */
-    public static final String META_INF_DIR = "META-INF";
-
-    /**
-     * Name of the manifest file.
-     */
-    public static final String MANIFEST_NAME = META_INF_DIR + "/MANIFEST.MF";
-
-    /**
-     * Who should be reported as the manifest builder.
-     */
-    @NonNull
-    private final String mBuiltBy;
-
-    /**
-     * Who should be reported as the manifest creator.
-     */
-    @NonNull
-    private final String mCreatedBy;
-
-    /**
-     * The file this extension is attached to. {@code null} if not yet registered.
-     */
-    @Nullable
-    private ZFile mZFile;
-
-    /**
-     * The zip file's manifest.
-     */
-    @NonNull
-    private final Manifest mManifest;
-
-    /**
-     * Byte representation of the manifest. There is no guarantee that two writes of the java's
-     * {@code Manifest} object will yield the same byte array (there is no guaranteed order
-     * of entries in the manifest).
-     *
-     * <p>Because we need the byte representation of the manifest to be stable if there are
-     * no changes to the manifest, we cannot rely on {@code Manifest} to generate the byte
-     * representation every time we need the byte representation.
-     *
-     * <p>This cache will ensure that we will request one byte generation from the {@code Manifest}
-     * and will cache it. All further requests of the manifest's byte representation will
-     * receive the same byte array.
-     */
-    @NonNull
-    private CachedSupplier<byte[]> mManifestBytes;
-
-    /**
-     * Has the current manifest been changed and not yet flushed? If {@link #mDirty} is
-     * {@code true}, then {@link #mManifestBytes} should not be valid. This means that
-     * marking the manifest as dirty should also invalidate {@link #mManifestBytes}. To avoid
-     * breaking the invariant, instead of setting {@link #mDirty}, {@link #markDirty()} should
-     * be called.
-     */
-    private boolean mDirty;
-
-    /**
-     * The extension to register with the {@link ZFile}. {@code null} if not registered.
-     */
-    @Nullable
-    private ZFileExtension mExtension;
-
-    /**
-     * Creates a new extension. This will not register the extension with the provided
-     * {@link ZFile}. Until {@link #register(ZFile)} is invoked, this extension is not used.
-     *
-     * @param builtBy who built the manifest?
-     * @param createdBy who created the manifest?
-     */
-    public ManifestGenerationExtension(@NonNull String builtBy, @NonNull String createdBy) {
-        mBuiltBy = builtBy;
-        mCreatedBy = createdBy;
-        mManifest = new Manifest();
-        mDirty = false;
-        mManifestBytes = new CachedSupplier<>(() -> {
-            ByteArrayOutputStream outBytes = new ByteArrayOutputStream();
-            try {
-                mManifest.write(outBytes);
-            } catch (IOException e) {
-                throw new UncheckedIOException(e);
-            }
-
-            return outBytes.toByteArray();
-        });
-    }
-
-    /**
-     * Marks the manifest as being dirty, <i>i.e.</i>, its data has changed since it was last
-     * read and/or written.
-     */
-    private void markDirty() {
-        mDirty = true;
-        mManifestBytes.reset();
-    }
-
-    /**
-     * Registers the extension with the {@link ZFile} provided in the constructor.
-     *
-     * @param zFile the zip file to add the extension to
-     * @throws IOException failed to analyze the zip
-     */
-    public void register(@NonNull ZFile zFile) throws IOException {
-        Preconditions.checkState(mExtension == null, "register() has already been invoked.");
-        mZFile = zFile;
-
-        rebuildManifest();
-
-        mExtension = new ZFileExtension() {
-            @Nullable
-            @Override
-            public IOExceptionRunnable beforeUpdate() {
-                return ManifestGenerationExtension.this::updateManifest;
-            }
-        };
-
-        mZFile.addZFileExtension(mExtension);
-    }
-
-    /**
-     * Rebuilds the zip file's manifest, if it needs changes.
-     */
-    private void rebuildManifest() throws IOException {
-        Verify.verifyNotNull(mZFile, "mZFile == null");
-
-        StoredEntry manifestEntry = mZFile.get(MANIFEST_NAME);
-
-        if (manifestEntry != null) {
-            /*
-             * Read the manifest entry in the zip file. Make sure we store these byte sequence
-             * because writing the manifest may not generate the same byte sequence, which may
-             * trigger an unnecessary re-sign of the jar.
-             */
-            mManifest.clear();
-            byte[] manifestBytes = manifestEntry.read();
-            mManifest.read(new ByteArrayInputStream(manifestBytes));
-            mManifestBytes.precomputed(manifestBytes);
-        }
-
-        Attributes mainAttributes = mManifest.getMainAttributes();
-        String currentVersion = mainAttributes.getValue(ManifestAttributes.MANIFEST_VERSION);
-        if (currentVersion == null) {
-            setMainAttribute(
-                    ManifestAttributes.MANIFEST_VERSION,
-                    ManifestAttributes.CURRENT_MANIFEST_VERSION);
-        } else {
-            if (!currentVersion.equals(ManifestAttributes.CURRENT_MANIFEST_VERSION)) {
-                throw new IOException("Unsupported manifest version: " + currentVersion + ".");
-            }
-        }
-
-        /*
-         * We "blindly" override all other main attributes.
-         */
-        setMainAttribute(ManifestAttributes.BUILT_BY, mBuiltBy);
-        setMainAttribute(ManifestAttributes.CREATED_BY, mCreatedBy);
-    }
-
-    /**
-     * Sets the value of a main attribute.
-     *
-     * @param attribute the attribute
-     * @param value the value
-     */
-    private void setMainAttribute(@NonNull String attribute, @NonNull String value) {
-        Attributes mainAttributes = mManifest.getMainAttributes();
-        String current = mainAttributes.getValue(attribute);
-        if (!value.equals(current)) {
-            mainAttributes.putValue(attribute, value);
-            markDirty();
-        }
-    }
-
-    /**
-     * Updates the manifest in the zip file, if it has been changed.
-     *
-     * @throws IOException failed to update the manifest
-     */
-    private void updateManifest() throws IOException {
-        Verify.verifyNotNull(mZFile, "mZFile == null");
-
-        if (!mDirty) {
-            return;
-        }
-
-        mZFile.add(MANIFEST_NAME, new ByteArrayInputStream(mManifestBytes.get()));
-        mDirty = false;
-    }
-
-    /**
-     * Obtains the {@link ZFile} this extension is associated with. This method can only be invoked
-     * after {@link #register(ZFile)} has been invoked.
-     *
-     * @return the {@link ZFile}
-     */
-    @NonNull
-    public ZFile zFile() {
-        Preconditions.checkNotNull(mZFile, "mZFile == null");
-        return mZFile;
-    }
-
-    /**
-     * Obtains the stored entry in the {@link ZFile} that contains the manifest. This method can
-     * only be invoked after {@link #register(ZFile)} has been invoked.
-     *
-     * @return the entry, {@code null} if none
-     */
-    @Nullable
-    public StoredEntry manifestEntry() {
-        Preconditions.checkNotNull(mZFile, "mZFile == null");
-        return mZFile.get(MANIFEST_NAME);
-    }
-
-    /**
-     * Obtains an attribute of an entry.
-     *
-     * @param entryName the name of the entry
-     * @param attr the name of the attribute
-     * @return the attribute value or {@code null} if the entry does not have any attributes or
-     * if it doesn't have the specified attribute
-     */
-    @Nullable
-    public String getAttribute(@NonNull String entryName, @NonNull String attr) {
-        Attributes attrs = mManifest.getAttributes(entryName);
-        if (attrs == null) {
-            return null;
-        }
-
-        return attrs.getValue(attr);
-    }
-
-    /**
-     * Sets the value of an attribute of an entry. If this entry's attribute already has the given
-     * value, this method does nothing.
-     *
-     * @param entryName the name of the entry
-     * @param attr the name of the attribute
-     * @param value the attribute value
-     */
-    public void setAttribute(@NonNull String entryName, @NonNull String attr,
-            @NonNull String value) {
-        Attributes attrs = mManifest.getAttributes(entryName);
-        if (attrs == null) {
-            attrs = new Attributes();
-            markDirty();
-            mManifest.getEntries().put(entryName, attrs);
-        }
-
-        String current = attrs.getValue(attr);
-        if (!value.equals(current)) {
-            attrs.putValue(attr, value);
-            markDirty();
-        }
-    }
-
-    /**
-     * Obtains the current manifest.
-     *
-     * @return a byte sequence representation of the manifest that is guaranteed not to change if
-     * the manifest is not modified
-     * @throws IOException failed to compute the manifest's byte representation
-     */
-    @NonNull
-    public byte[] getManifestBytes() throws IOException {
-        return mManifestBytes.get();
-    }
-
-    /**
-     * Obtains all entries and all attributes they have in the manifest.
-     *
-     * @return a map that relates entry names to entry attributes
-     */
-    @NonNull
-    public Map<String, Attributes> allEntries() {
-        return Maps.newHashMap(mManifest.getEntries());
-    }
-
-    /**
-     * Removes an entry from the manifest. If no entry exists with the given name, this operation
-     * does nothing.
-     *
-     * @param name the entry's name
-     */
-    public void removeEntry(@NonNull String name) {
-        if (mManifest.getEntries().remove(name) != null) {
-            markDirty();
-        }
-    }
-}
diff --git a/src/main/java/com/android/builder/internal/packaging/sign/SignatureExtension.java b/src/main/java/com/android/builder/internal/packaging/sign/SignatureExtension.java
deleted file mode 100644
index b8d526b..0000000
--- a/src/main/java/com/android/builder/internal/packaging/sign/SignatureExtension.java
+++ /dev/null
@@ -1,630 +0,0 @@
-/*
- * Copyright (C) 2015 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.builder.internal.packaging.sign;
-
-import com.android.annotations.NonNull;
-import com.android.annotations.Nullable;
-import com.android.builder.internal.packaging.zip.StoredEntry;
-import com.android.builder.internal.packaging.zip.ZFile;
-import com.android.builder.internal.packaging.zip.ZFileExtension;
-import com.android.builder.internal.utils.IOExceptionRunnable;
-import com.google.common.base.Objects;
-import com.google.common.base.Preconditions;
-import com.google.common.collect.Sets;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.security.PrivateKey;
-import java.security.cert.CertificateEncodingException;
-import java.security.cert.X509Certificate;
-import java.util.ArrayList;
-import java.util.Base64;
-import java.util.Locale;
-import java.util.Set;
-import java.util.jar.Attributes;
-import java.util.jar.Manifest;
-import java.util.stream.Collectors;
-import org.bouncycastle.asn1.ASN1InputStream;
-import org.bouncycastle.asn1.DEROutputStream;
-import org.bouncycastle.cert.jcajce.JcaCertStore;
-import org.bouncycastle.cms.CMSException;
-import org.bouncycastle.cms.CMSProcessableByteArray;
-import org.bouncycastle.cms.CMSSignedData;
-import org.bouncycastle.cms.CMSSignedDataGenerator;
-import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
-import org.bouncycastle.operator.ContentSigner;
-import org.bouncycastle.operator.OperatorCreationException;
-import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
-import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
-
-/**
- * {@link ZFile} extension that signs all files in the APK and generates a signature file and a
- * digital signature of the signature file. The extension registers itself automatically with the
- * {@link ZFile} upon creation.
- * <p>
- * The signature extension will recompute signatures of files already in the zip file but won't
- * update the manifest if these signatures match the ones in the manifest.
- * <p>
- * This extension does 4 main tasks: maintaining the digests of all files in the zip in the manifest
- * file, maintaining the digests of all files in the zip in the signature file, maintaining the
- * digest of the manifest in the signature file and maintaining the digital signature file. For
- * performance, the digests and signatures are only computed when needed.
- * <p>
- * These tasks are done at three different moments: when the extension
- * is created, when files are added to the zip and before the zip is updated.
- * When the extension is created: (Note that the manifest's digest is <em>not</em> checked when
- * the extension is created.)
- * <ul>
- *     <li>The signature file is read, if one exists.
- *     <li>The signature "administrative" info is read and updated if not up-to-date.
- *     <li>The digests for entries in the manifest and signature file that do not correspond to
- *     any file in the zip are removed.
- *     <li>The digests for all entries in the zip are recomputed and updated in the signature file
- *     and in the manifest, if needed.
- * </ul>
- * <p>
- * When files are added or removed:
- * <ul>
- *     <li>The signature file and manifest are updated to reflect the changes.
- *     <li>If the file was added, its digest is computed.
- * </ul>
- * <p>
- * Before updating the zip file:
- * <ul>
- *     <li>If a signature file already exists, checks the digest of the manifest and updates the
- *     signature file if needed.
- *     <li>Creates the signature file if it did not already exist.
- *     <li>Recreates the digital signature of the signature file if the signature file was created
- *     or updated.
- * </ul>
- */
-public class SignatureExtension {
-
-    /**
-     * Base of signature files.
-     */
-    private static final String SIGNATURE_BASE = ManifestGenerationExtension.META_INF_DIR + "/CERT";
-
-    /**
-     * Path of the signature file.
-     */
-    private static final String SIGNATURE_FILE = SIGNATURE_BASE + ".SF";
-
-    /**
-     * Name of attribute with the signature version.
-     */
-    private static final String SIGNATURE_VERSION_NAME = "Signature-Version";
-
-    /**
-     * Version of the signature version.
-     */
-    private static final String SIGNATURE_VERSION_VALUE = "1.0";
-
-    /**
-     * Name of attribute with the "created by" attribute.
-     */
-    private static final String SIGNATURE_CREATED_BY_NAME = "Created-By";
-
-    /**
-     * Value of the "created by" attribute.
-     */
-    private static final String SIGNATURE_CREATED_BY_VALUE = "1.0 (Android)";
-
-    /**
-     * Name of the {@code X-Android-APK-Signer} attribute.
-     */
-    private static final String SIGNATURE_ANDROID_APK_SIGNED_NAME = "X-Android-APK-Signed";
-
-    /**
-     * Value of the {@code X-Android-APK-Signer} attribute when the APK is signed with the v2
-     * scheme.
-     */
-    public static final String SIGNATURE_ANDROID_APK_SIGNER_VALUE_WHEN_V2_SIGNED = "2";
-
-    /**
-     * Files to ignore when signing. See
-     * https://docs.oracle.com/javase/7/docs/technotes/guides/jar/jar.html
-     */
-    private static final Set<String> IGNORED_FILES = Sets.newHashSet(
-            ManifestGenerationExtension.MANIFEST_NAME, SIGNATURE_FILE);
-
-    /**
-     * Same as {@link #IGNORED_FILES} but with all names in lower case.
-     */
-    private static final Set<String> IGNORED_FILES_LC = Sets.newHashSet(
-            IGNORED_FILES.stream()
-                    .map(i -> i.toLowerCase(Locale.US))
-                    .collect(Collectors.toSet()));
-
-
-    /**
-     * Prefix of files in META-INF to ignore when signing. See
-     * https://docs.oracle.com/javase/7/docs/technotes/guides/jar/jar.html
-     */
-    private static final Set<String> IGNORED_PREFIXES = Sets.newHashSet(
-            "SIG-");
-
-    /**
-     * Same as {@link #IGNORED_PREFIXES} but with all names in lower case.
-     */
-    private static final Set<String> IGNORED_PREFIXES_LC = Sets.newHashSet(
-            IGNORED_PREFIXES.stream()
-                    .map(i -> i.toLowerCase(Locale.US))
-                    .collect(Collectors.toSet()));
-
-    /**
-     * Suffixes of files in META-INF to ignore when signing. See
-     * https://docs.oracle.com/javase/7/docs/technotes/guides/jar/jar.html
-     */
-    private static final Set<String> IGNORED_SUFFIXES = Sets.newHashSet(
-            ".SF", ".DSA", ".RSA", ".EC");
-
-    /**
-     * Same as {@link #IGNORED_SUFFIXES} but with all names in lower case.
-     */
-    private static final Set<String> IGNORED_SUFFIXES_LC = Sets.newHashSet(
-            IGNORED_SUFFIXES.stream()
-                    .map(i -> i.toLowerCase(Locale.US))
-                    .collect(Collectors.toSet()));
-
-    /**
-     * Extension maintaining the manifest.
-     */
-    @NonNull
-    private final ManifestGenerationExtension mManifestExtension;
-
-    /**
-     * Message digest to use.
-     */
-    @NonNull
-    private final MessageDigest mMessageDigest;
-
-    /**
-     * Signature file. Note that the signature file is itself a manifest file but it is
-     * a different one from the "standard" MANIFEST.MF.
-     */
-    @NonNull
-    private final Manifest mSignatureFile;
-
-    /**
-     * Has the signature manifest been changed?
-     */
-    private boolean mDirty;
-
-    /**
-     * Signer certificate.
-     */
-    @NonNull
-    private final X509Certificate mCertificate;
-
-    /**
-     * The private key used to sign the jar.
-     */
-    @NonNull
-    private final PrivateKey mPrivateKey;
-
-    /**
-     * Algorithm with which .SF file is signed.
-     */
-    @NonNull
-    private final SignatureAlgorithm mSignatureAlgorithm;
-
-    /**
-     * Digest algorithm to use for MANIFEST.MF and contents of APK entries.
-     */
-    @NonNull
-    private final DigestAlgorithm mDigestAlgorithm;
-
-    /**
-     * Value to output for the {@code X-Android-APK-Signed} header or {@code null} if the header
-     * should not be output.
-     */
-    @Nullable
-    private final String mApkSignedHeaderValue;
-
-    /**
-     * The extension registered with the {@link ZFile}. {@code null} if not registered.
-     */
-    @Nullable
-    private ZFileExtension mExtension;
-
-    /**
-     * Creates a new signature extension.
-     *
-     * @param manifestExtension the extension maintaining the manifest
-     * @param minSdkVersion minSdkVersion of the package
-     * @param certificate sign certificate
-     * @param privateKey the private key to sign the jar
-     * @param apkSignedHeaderValue value of the {@code X-Android-APK-Signed} header to output into
-     * the {@code .SF} file or {@code null} if the header should not be output.
-     *
-     * @throws NoSuchAlgorithmException failed to obtain the digest algorithm.
-     */
-    public SignatureExtension(@NonNull ManifestGenerationExtension manifestExtension,
-            int minSdkVersion, @NonNull X509Certificate certificate, @NonNull PrivateKey privateKey,
-            @Nullable String apkSignedHeaderValue)
-            throws NoSuchAlgorithmException {
-        mManifestExtension = manifestExtension;
-        mSignatureFile = new Manifest();
-        mDirty = false;
-        mCertificate = certificate;
-        mPrivateKey = privateKey;
-        mApkSignedHeaderValue = apkSignedHeaderValue;
-
-        mSignatureAlgorithm =
-                SignatureAlgorithm.fromKeyAlgorithm(privateKey.getAlgorithm(), minSdkVersion);
-        mDigestAlgorithm = DigestAlgorithm.findBest(minSdkVersion, mSignatureAlgorithm);
-        mMessageDigest = MessageDigest.getInstance(mDigestAlgorithm.messageDigestName);
-    }
-
-    /**
-     * Registers the extension with the {@link ZFile} provided in the
-     * {@link ManifestGenerationExtension}. Note that the {@code ManifestGenerationExtension}
-     * needs to be registered as a precondition for this method.
-     *
-     * @throws IOException failed to analyze the zip
-     */
-    public void register() throws IOException {
-        Preconditions.checkState(mExtension == null, "register() already invoked");
-
-        mExtension = new ZFileExtension() {
-            @Nullable
-            @Override
-            public IOExceptionRunnable beforeUpdate() {
-                return SignatureExtension.this::updateSignatureIfNeeded;
-            }
-
-            @Nullable
-            @Override
-            public IOExceptionRunnable added(@NonNull final StoredEntry entry,
-                    @Nullable final StoredEntry replaced) {
-                if (replaced != null) {
-                    Preconditions.checkArgument(entry.getCentralDirectoryHeader().getName().equals(
-                            replaced.getCentralDirectoryHeader().getName()));
-                }
-
-                if (isIgnoredFile(entry.getCentralDirectoryHeader().getName())) {
-                    return null;
-                }
-
-                return () -> {
-                    if (replaced != null) {
-                        SignatureExtension.this.removed(replaced);
-                    }
-
-                    SignatureExtension.this.added(entry);
-                };
-            }
-
-            @Nullable
-            @Override
-            public IOExceptionRunnable removed(@NonNull final StoredEntry entry) {
-                if (isIgnoredFile(entry.getCentralDirectoryHeader().getName())) {
-                    return null;
-                }
-
-                return () -> SignatureExtension.this.removed(entry);
-            }
-        };
-
-        mManifestExtension.zFile().addZFileExtension(mExtension);
-        readSignatureFile();
-    }
-
-    /**
-     * Reads the signature file (if any) on the zip file.
-     * <p>
-     * When this method terminates, we have the following guarantees:
-     * <ul>
-     *      <li>An internal signature manifest exists.</li>
-     *      <li>All entries in the in-memory signature file exist in the zip file.</li>
-     *      <li>All entries in the zip file (with the exception of the signature-related files,
-     *      as specified by https://docs.oracle.com/javase/7/docs/technotes/guides/jar/jar.html)
-     *      exist in the in-memory signature file.</li>
-     *      <li>All entries in the in-memory signature file have digests that match their
-     *      contents in the zip.</li>
-     *      <li>All entries in the in-memory signature manifest exist also in the manifest file
-     *      and the digests are the same.</li>
-     *      <li>The main attributes of the in-memory signature manifest are valid. The manifest's
-     *      digest has not been verified and may not even exist.</li>
-     *      <li>If the internal in-memory signature manifest differs in any way from the one
-     *      written in the file, {@link #mDirty} will be set to {@code true}. Otherwise,
-     *      {@link #mDirty} will be set to {@code false}.</li>
-     * </ul>
-     *
-     * @throws IOException failed to read the signature file
-     */
-    private void readSignatureFile() throws IOException {
-        boolean needsNewSignature = false;
-
-        StoredEntry signatureEntry = mManifestExtension.zFile().get(SIGNATURE_FILE);
-        if (signatureEntry != null) {
-            byte[] signatureData = signatureEntry.read();
-            mSignatureFile.read(new ByteArrayInputStream(signatureData));
-
-            Attributes mainAttrs = mSignatureFile.getMainAttributes();
-            String versionName = mainAttrs.getValue(SIGNATURE_VERSION_NAME);
-            String createdBy = mainAttrs.getValue(SIGNATURE_CREATED_BY_NAME);
-            String apkSigned = mainAttrs.getValue(SIGNATURE_ANDROID_APK_SIGNED_NAME);
-
-            if (!SIGNATURE_VERSION_VALUE.equals(versionName)
-                    || !SIGNATURE_CREATED_BY_VALUE.equals(createdBy)
-                    || mainAttrs.getValue(mDigestAlgorithm.manifestAttributeName) == null
-                    || !Objects.equal(mApkSignedHeaderValue, apkSigned)) {
-                needsNewSignature = true;
-            }
-        } else {
-            needsNewSignature = true;
-        }
-
-        if (needsNewSignature) {
-            Attributes mainAttrs = mSignatureFile.getMainAttributes();
-
-            mainAttrs.putValue(SIGNATURE_CREATED_BY_NAME, SIGNATURE_CREATED_BY_VALUE);
-            mainAttrs.putValue(SIGNATURE_VERSION_NAME, SIGNATURE_VERSION_VALUE);
-            if (mApkSignedHeaderValue != null) {
-                mainAttrs.putValue(SIGNATURE_ANDROID_APK_SIGNED_NAME, mApkSignedHeaderValue);
-            } else {
-                mainAttrs.remove(SIGNATURE_ANDROID_APK_SIGNED_NAME);
-            }
-
-            mDirty = true;
-        }
-
-        /*
-         * At this point we have a valid in-memory signature file with a valid header. mDirty
-         * states whether this is the same as the file-based signature file.
-         *
-         * Now, check we have the same files in the zip as in the signature file and that all
-         * digests match. While we do this, make sure the manifest is also up-do-date.
-         *
-         * We ignore all signature-related files that exist in the zip that are signature-related.
-         * This are defined in the jar format specification.
-         */
-        Set<StoredEntry> allEntries =
-                mManifestExtension.zFile().entries().stream()
-                        .filter(se -> !isIgnoredFile(se.getCentralDirectoryHeader().getName()))
-                        .collect(Collectors.toSet());
-
-        Set<String> sigEntriesToRemove = Sets.newHashSet(mSignatureFile.getEntries().keySet());
-        Set<String> manEntriesToRemove = Sets.newHashSet(mManifestExtension.allEntries().keySet());
-        for (StoredEntry se : allEntries) {
-            /*
-             * Update the entry's digest, if needed.
-             */
-            setDigestForEntry(se);
-
-            /*
-             * This entry exists in the file, so remove it from the list of entries to remove
-             * from the manifest and signature file.
-             */
-            sigEntriesToRemove.remove(se.getCentralDirectoryHeader().getName());
-            manEntriesToRemove.remove(se.getCentralDirectoryHeader().getName());
-        }
-
-        for (String toRemoveInSignature : sigEntriesToRemove) {
-            mSignatureFile.getEntries().remove(toRemoveInSignature);
-            mDirty = true;
-        }
-
-        for (String toRemoveInManifest : manEntriesToRemove) {
-            mManifestExtension.removeEntry(toRemoveInManifest);
-        }
-    }
-
-    /**
-     * This method will recompute the manifest's digest and will update the signature file if the
-     * manifest has changed. It then writes the signature file, if dirty for any reason (including
-     * from recomputing the manifest's digest).
-     *
-     * @throws IOException failed to read / write zip data
-     */
-    private void updateSignatureIfNeeded() throws IOException {
-        byte[] manifestData = mManifestExtension.getManifestBytes();
-        byte[] manifestDataDigest = mMessageDigest.digest(manifestData);
-
-
-        String manifestDataDigestTxt = Base64.getEncoder().encodeToString(manifestDataDigest);
-
-        if (!manifestDataDigestTxt.equals(mSignatureFile.getMainAttributes().getValue(
-                mDigestAlgorithm.manifestAttributeName))) {
-            mSignatureFile
-                    .getMainAttributes()
-                    .putValue(mDigestAlgorithm.manifestAttributeName, manifestDataDigestTxt);
-            mDirty = true;
-        }
-
-        if (!mDirty) {
-            return;
-        }
-
-        ByteArrayOutputStream signatureBytes = new ByteArrayOutputStream();
-        mSignatureFile.write(signatureBytes);
-
-        mManifestExtension.zFile().add(
-                SIGNATURE_FILE,
-                new ByteArrayInputStream(signatureBytes.toByteArray()));
-
-        String digitalSignatureFile = SIGNATURE_BASE + "." + mPrivateKey.getAlgorithm();
-        try {
-            mManifestExtension.zFile().add(
-                    digitalSignatureFile,
-                    new ByteArrayInputStream(computePkcs7Signature(signatureBytes.toByteArray())));
-        } catch (CertificateEncodingException | OperatorCreationException | CMSException e) {
-            throw new IOException("Failed to digitally sign signature file.", e);
-        }
-
-        mDirty = false;
-    }
-
-    /**
-     * A new file has been added.
-     *
-     * @param entry the entry added
-     * @throws IOException failed to add the entry to the signature file (or failed to compute the
-     * entry's signature)
-     */
-    private void added(@NonNull StoredEntry entry) throws IOException {
-        setDigestForEntry(entry);
-    }
-
-    /**
-     * Adds / updates the signature for an entry. If this entry has no signature, or its digest
-     * doesn't match the one in the signature file (or manifest), it will be updated.
-     *
-     * @param entry the entry
-     * @throws IOException failed to compute the entry's digest
-     */
-    private void setDigestForEntry(@NonNull StoredEntry entry) throws IOException {
-        String entryName = entry.getCentralDirectoryHeader().getName();
-        byte[] entryDigestArray = mMessageDigest.digest(entry.read());
-        String entryDigest = Base64.getEncoder().encodeToString(entryDigestArray);
-
-        Attributes signatureAttributes = mSignatureFile.getEntries().get(entryName);
-        if (signatureAttributes == null) {
-            signatureAttributes = new Attributes();
-            mSignatureFile.getEntries().put(entryName, signatureAttributes);
-            mDirty = true;
-        }
-
-        if (!entryDigest.equals(signatureAttributes.getValue(
-                mDigestAlgorithm.entryAttributeName))) {
-            signatureAttributes.putValue(mDigestAlgorithm.entryAttributeName, entryDigest);
-            mDirty = true;
-        }
-
-        /*
-         * setAttribute will not mark the manifest as changed if the attribute is already there
-         * and with the same value.
-         */
-        mManifestExtension.setAttribute(entryName, mDigestAlgorithm.entryAttributeName,
-                entryDigest);
-    }
-
-    /**
-     * File has been removed.
-     *
-     * @param entry the entry removed
-     */
-    private void removed(@NonNull StoredEntry entry) {
-        mSignatureFile.getEntries().remove(entry.getCentralDirectoryHeader().getName());
-        mManifestExtension.removeEntry(entry.getCentralDirectoryHeader().getName());
-        mDirty = true;
-    }
-
-    /**
-     * Checks if a file should be ignored when signing.
-     *
-     * @param name the file name
-     * @return should it be ignored
-     */
-    public static boolean isIgnoredFile(@NonNull String name) {
-        String metaInfPfx = ManifestGenerationExtension.META_INF_DIR + "/";
-        boolean inMetaInf = name.startsWith(metaInfPfx)
-                && !name.substring(metaInfPfx.length()).contains("/");
-
-        /*
-         * Only files in META-INF can be ignored. Files in sub-directories of META-INF are not
-          * ignored.
-         */
-        if (!inMetaInf) {
-            return false;
-        }
-
-        String nameLc = name.toLowerCase(Locale.US);
-
-        /*
-         * All files with names that match (case insensitive) the ignored list are ignored.
-         */
-        if (IGNORED_FILES_LC.contains(nameLc)) {
-            return true;
-        }
-
-        for (String pfx : IGNORED_PREFIXES_LC) {
-            if (nameLc.startsWith(pfx)) {
-                return true;
-            }
-        }
-
-        for (String sfx : IGNORED_SUFFIXES_LC) {
-            if (nameLc.endsWith(sfx)) {
-                return true;
-            }
-        }
-
-        return false;
-    }
-
-    /**
-     * Computes the digital signature of an array of data.
-     *
-     * @param data the data
-     * @return the digital signature
-     * @throws IOException failed to read/write signature data
-     * @throws CertificateEncodingException failed to sign the data
-     * @throws OperatorCreationException failed to sign the data
-     * @throws CMSException failed to sign the data
-     */
-    private byte[] computePkcs7Signature(@NonNull byte[] data) throws IOException,
-            CertificateEncodingException, OperatorCreationException, CMSException {
-        CMSProcessableByteArray cmsData = new CMSProcessableByteArray(data);
-
-        ArrayList<X509Certificate> certList = new ArrayList<>();
-        certList.add(mCertificate);
-        JcaCertStore certs = new JcaCertStore(certList);
-
-        CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
-        String signatureAlgName = mSignatureAlgorithm.signatureAlgorithmName(mDigestAlgorithm);
-        ContentSigner shaSigner =
-                new JcaContentSignerBuilder(signatureAlgName).build(mPrivateKey);
-        gen.addSignerInfoGenerator(
-                new JcaSignerInfoGeneratorBuilder(
-                        new JcaDigestCalculatorProviderBuilder()
-                                .build())
-                                .setDirectSignature(true)
-                                .build(shaSigner, mCertificate));
-        gen.addCertificates(certs);
-        CMSSignedData sigData = gen.generate(cmsData, false);
-
-        ByteArrayOutputStream outputBytes = new ByteArrayOutputStream();
-
-        /*
-         * DEROutputStream is not closeable! OMG!
-         */
-        DEROutputStream dos = null;
-        try (ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded())) {
-            dos = new DEROutputStream(outputBytes);
-            dos.writeObject(asn1.readObject());
-
-            DEROutputStream toClose = dos;
-            dos = null;
-            toClose.close();
-        } catch (IOException e) {
-            if (dos != null) {
-                try {
-                    dos.close();
-                } catch (IOException ee) {
-                    e.addSuppressed(ee);
-                }
-            }
-        }
-
-        return outputBytes.toByteArray();
-    }
-}
diff --git a/src/main/java/com/android/builder/internal/packaging/sign/v2/ApkSignerV2.java b/src/main/java/com/android/builder/internal/packaging/sign/v2/ApkSignerV2.java
deleted file mode 100644
index 66b1518..0000000
--- a/src/main/java/com/android/builder/internal/packaging/sign/v2/ApkSignerV2.java
+++ /dev/null
@@ -1,596 +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.builder.internal.packaging.sign.v2;
-
-import com.android.annotations.NonNull;
-import com.android.builder.internal.utils.ApkZLibPair;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
-import com.google.common.collect.Sets;
-import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.security.DigestException;
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.security.KeyFactory;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.security.PrivateKey;
-import java.security.PublicKey;
-import java.security.Signature;
-import java.security.SignatureException;
-import java.security.cert.CertificateEncodingException;
-import java.security.cert.X509Certificate;
-import java.security.interfaces.ECKey;
-import java.security.interfaces.RSAKey;
-import java.security.spec.AlgorithmParameterSpec;
-import java.security.spec.InvalidKeySpecException;
-import java.security.spec.X509EncodedKeySpec;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-/**
- * APK Signature Scheme v2 signer.
- *
- * <p>APK Signature Scheme v2 is a whole-file signature scheme which aims to protect every single
- * bit of the APK, as opposed to the JAR Signature Scheme which protects only the names and
- * uncompressed contents of ZIP entries.
- *
- * <p>TODO: Link to APK Signature Scheme v2 documentation once it's available.
- */
-public abstract class ApkSignerV2 {
-    /*
-     * The two main goals of APK Signature Scheme v2 are:
-     * 1. Detect any unauthorized modifications to the APK. This is achieved by making the signature
-     *    cover every byte of the APK being signed.
-     * 2. Enable much faster signature and integrity verification. This is achieved by requiring
-     *    only a minimal amount of APK parsing before the signature is verified, thus completely
-     *    bypassing ZIP entry decompression and by making integrity verification parallelizable by
-     *    employing a hash tree.
-     *
-     * The generated signature block is wrapped into an APK Signing Block and inserted into the
-     * original APK immediately before the start of ZIP Central Directory. This is to ensure that
-     * JAR and ZIP parsers continue to work on the signed APK. The APK Signing Block is designed for
-     * extensibility. For example, a future signature scheme could insert its signatures there as
-     * well. The contract of the APK Signing Block is that all contents outside of the block must be
-     * protected by signatures inside the block.
-     */
-
-    private static final int CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES = 1024 * 1024;
-
-    private static final byte[] APK_SIGNING_BLOCK_MAGIC =
-          new byte[] {
-              0x41, 0x50, 0x4b, 0x20, 0x53, 0x69, 0x67, 0x20,
-              0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x20, 0x34, 0x32,
-          };
-    private static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a;
-
-    private ApkSignerV2() {}
-
-    /**
-     * Signer configuration.
-     */
-    public static final class SignerConfig {
-        /**
-         * Private key.
-         */
-        @NonNull
-        public PrivateKey privateKey;
-
-        /**
-         * Certificates, with the first certificate containing the public key corresponding to
-         * {@link #privateKey}.
-         */
-        @NonNull
-        public List<X509Certificate> certificates;
-
-        /**
-         * List of signature algorithms with which to sign. At least one algorithm must be
-         * provided.
-         */
-        @NonNull
-        public List<SignatureAlgorithm> signatureAlgorithms;
-    }
-
-    /**
-     * Gets the APK Signature Scheme v2 signature algorithms to be used for signing an APK using the
-     * provided key.
-     *
-     * @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see
-     *        AndroidManifest.xml minSdkVersion attribute)
-     *
-     * @throws InvalidKeyException if the provided key is not suitable for signing APKs using
-     *         APK Signature Scheme v2
-     */
-    public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(
-            @NonNull PublicKey signingKey, int minSdkVersion) throws InvalidKeyException {
-        String keyAlgorithm = signingKey.getAlgorithm();
-        if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
-            // Use RSASSA-PKCS1-v1_5 signature scheme instead of RSASSA-PSS to guarantee
-            // deterministic signatures which make life easier for OTA updates (fewer files
-            // changed when deterministic signature schemes are used).
-
-            // Pick a digest which is no weaker than the key.
-            int modulusLengthBits = ((RSAKey) signingKey).getModulus().bitLength();
-            if (modulusLengthBits <= 3072) {
-                // 3072-bit RSA is roughly 128-bit strong, meaning SHA-256 is a good fit.
-                return ImmutableList.of(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA256);
-            } else {
-                // Keys longer than 3072 bit need to be paired with a stronger digest to avoid the
-                // digest being the weak link. SHA-512 is the next strongest supported digest.
-                return ImmutableList.of(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA512);
-            }
-        } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
-            // DSA is supported only with SHA-256.
-            return ImmutableList.of(SignatureAlgorithm.DSA_WITH_SHA256);
-        } else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
-            // Pick a digest which is no weaker than the key.
-            int keySizeBits = ((ECKey) signingKey).getParams().getOrder().bitLength();
-            if (keySizeBits <= 256) {
-                // 256-bit Elliptic Curve is roughly 128-bit strong, meaning SHA-256 is a good fit.
-                return ImmutableList.of(SignatureAlgorithm.ECDSA_WITH_SHA256);
-            } else {
-                // Keys longer than 256 bit need to be paired with a stronger digest to avoid the
-                // digest being the weak link. SHA-512 is the next strongest supported digest.
-                return ImmutableList.of(SignatureAlgorithm.ECDSA_WITH_SHA512);
-            }
-        } else {
-            throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
-        }
-    }
-
-    /**
-     * Signs the provided APK using APK Signature Scheme v2 and returns the APK Signing Block
-     * containing the signature.
-     *
-     * <p>NOTE: To enable APK signature verifier to detect v2 signature stripping, header sections
-     * of META-INF/*.SF files of APK being signed must contain the
-     * {@code X-Android-APK-Signed: 2} attribute.
-     *
-     * @param signerConfigs signer configurations, one for each signer. At least one configuration
-     *        must be provided.
-     *
-     * @throws InvalidKeyException if a signing key is not suitable for this signature scheme or
-     *         cannot be used in general
-     * @throws SignatureException if an error occurs when computing digests of generating
-     *         signatures
-     */
-    @NonNull
-    public static byte[] generateApkSigningBlock(
-            @NonNull DigestSource beforeCentralDir,
-            @NonNull DigestSource centralDir,
-            @NonNull DigestSource eocd,
-            @NonNull List<SignerConfig> signerConfigs)
-                    throws InvalidKeyException, SignatureException {
-        if (signerConfigs.isEmpty()) {
-            throw new IllegalArgumentException(
-                    "No signer configs provided. At least one is required");
-        }
-
-        // Figure out which digest(s) to use for APK contents.
-        Set<ContentDigestAlgorithm> contentDigestAlgorithms = Sets.newHashSetWithExpectedSize(1);
-        for (SignerConfig signerConfig : signerConfigs) {
-            for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) {
-                contentDigestAlgorithms.add(signatureAlgorithm.getContentDigestAlgorithm());
-            }
-        }
-
-        // Compute digests of APK contents.
-        Map<ContentDigestAlgorithm, byte[]> contentDigests; // digest algorithm ID -> digest
-        try {
-            contentDigests =
-                    computeContentDigests(
-                            contentDigestAlgorithms,
-                            new DigestSource[] {beforeCentralDir, centralDir, eocd});
-        } catch (DigestException e) {
-            throw new SignatureException("Failed to compute digests of APK", e);
-        }
-
-        // Sign the digests and wrap the signatures and signer info into an APK Signing Block.
-        return generateApkSigningBlock(signerConfigs, contentDigests);
-    }
-
-    @NonNull
-    private static Map<ContentDigestAlgorithm, byte[]> computeContentDigests(
-            @NonNull Set<ContentDigestAlgorithm> digestAlgorithms,
-            @NonNull DigestSource[] contents) throws DigestException {
-        // For each digest algorithm the result is computed as follows:
-        // 1. Each segment of contents is split into consecutive chunks of 1 MB in size.
-        //    The final chunk will be shorter iff the length of segment is not a multiple of 1 MB.
-        //    No chunks are produced for empty (zero length) segments.
-        // 2. The digest of each chunk is computed over the concatenation of byte 0xa5, the chunk's
-        //    length in bytes (uint32 little-endian) and the chunk's contents.
-        // 3. The output digest is computed over the concatenation of the byte 0x5a, the number of
-        //    chunks (uint32 little-endian) and the concatenation of digests of chunks of all
-        //    segments in-order.
-
-        long chunkCountLong = 0;
-        for (DigestSource input : contents) {
-            chunkCountLong += getChunkCount(input.size(), CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
-        }
-        if (chunkCountLong >= Integer.MAX_VALUE) {
-            throw new DigestException("Input too long: " + chunkCountLong + " chunks");
-        }
-        int chunkCount = (int) chunkCountLong;
-
-        ContentDigestAlgorithm[] digestAlgorithmsArray =
-                digestAlgorithms.toArray(new ContentDigestAlgorithm[digestAlgorithms.size()]);
-        MessageDigest[] mds = new MessageDigest[digestAlgorithmsArray.length];
-        byte[][] digestsOfChunks = new byte[digestAlgorithmsArray.length][];
-        int[] digestOutputSizes = new int[digestAlgorithmsArray.length];
-        for (int i = 0; i < digestAlgorithmsArray.length; i++) {
-            ContentDigestAlgorithm digestAlgorithm = digestAlgorithmsArray[i];
-            int digestOutputSizeBytes = digestAlgorithm.getChunkDigestOutputSizeBytes();
-            digestOutputSizes[i] = digestOutputSizeBytes;
-            byte[] concatenationOfChunkCountAndChunkDigests =
-                    new byte[5 + chunkCount * digestOutputSizeBytes];
-            concatenationOfChunkCountAndChunkDigests[0] = 0x5a;
-            setUnsignedInt32LittleEndian(
-                    chunkCount, concatenationOfChunkCountAndChunkDigests, 1);
-            digestsOfChunks[i] = concatenationOfChunkCountAndChunkDigests;
-            String jcaAlgorithmName = digestAlgorithm.getJcaMessageDigestAlgorithmName();
-            try {
-                mds[i] = MessageDigest.getInstance(jcaAlgorithmName);
-            } catch (NoSuchAlgorithmException e) {
-                throw new RuntimeException(jcaAlgorithmName + " MessageDigest not supported", e);
-            }
-        }
-
-        byte[] chunkContentPrefix = new byte[5];
-        chunkContentPrefix[0] = (byte) 0xa5;
-        int chunkIndex = 0;
-        // Optimization opportunity: digests of chunks can be computed in parallel. However,
-        // determining the number of computations to be performed in parallel is non-trivial. This
-        // depends on a wide range of factors, such as data source type (e.g., in-memory or fetched
-        // from file), CPU/memory/disk cache bandwidth and latency, interconnect architecture of CPU
-        // cores, load on the system from other threads of execution and other processes, size of
-        // input.
-        // For now, we compute these digests sequentially and thus have the luxury of improving
-        // performance by writing the digest of each chunk into a pre-allocated buffer at exactly
-        // the right position. This avoids unnecessary allocations, copying, and enables the final
-        // digest to be more efficient because it's presented with all of its input in one go.
-        for (DigestSource input : contents) {
-            long inputOffset = 0;
-            long inputRemaining = input.size();
-            while (inputRemaining > 0) {
-                int chunkSize =
-                        (int) Math.min(inputRemaining, CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
-                setUnsignedInt32LittleEndian(chunkSize, chunkContentPrefix, 1);
-                for (int i = 0; i < mds.length; i++) {
-                    mds[i].update(chunkContentPrefix);
-                }
-                try {
-                    input.feedDigests(inputOffset, chunkSize, mds);
-                } catch (IOException e) {
-                    throw new DigestException("Failed to digest chunk #" + chunkIndex, e);
-                }
-                for (int i = 0; i < digestAlgorithmsArray.length; i++) {
-                    MessageDigest md = mds[i];
-                    byte[] concatenationOfChunkCountAndChunkDigests = digestsOfChunks[i];
-                    int expectedDigestSizeBytes = digestOutputSizes[i];
-                    int actualDigestSizeBytes =
-                            md.digest(
-                                    concatenationOfChunkCountAndChunkDigests,
-                                    5 + chunkIndex * expectedDigestSizeBytes,
-                                    expectedDigestSizeBytes);
-                    if (actualDigestSizeBytes != expectedDigestSizeBytes) {
-                        throw new RuntimeException(
-                                "Unexpected output size of " + md.getAlgorithm()
-                                        + " digest: " + actualDigestSizeBytes);
-                    }
-                }
-                inputOffset += chunkSize;
-                inputRemaining -= chunkSize;
-                chunkIndex++;
-            }
-        }
-
-        Map<ContentDigestAlgorithm, byte[]> result =
-                Maps.newHashMapWithExpectedSize(digestAlgorithmsArray.length);
-        for (int i = 0; i < digestAlgorithmsArray.length; i++) {
-            ContentDigestAlgorithm digestAlgorithm = digestAlgorithmsArray[i];
-            byte[] concatenationOfChunkCountAndChunkDigests = digestsOfChunks[i];
-            MessageDigest md = mds[i];
-            byte[] digest = md.digest(concatenationOfChunkCountAndChunkDigests);
-            result.put(digestAlgorithm, digest);
-        }
-        return result;
-    }
-
-    private static final long getChunkCount(long inputSize, int chunkSize) {
-        return (inputSize + chunkSize - 1) / chunkSize;
-    }
-
-    private static void setUnsignedInt32LittleEndian(int value, byte[] result, int offset) {
-        result[offset] = (byte) (value & 0xff);
-        result[offset + 1] = (byte) ((value >> 8) & 0xff);
-        result[offset + 2] = (byte) ((value >> 16) & 0xff);
-        result[offset + 3] = (byte) ((value >> 24) & 0xff);
-    }
-
-    private static byte[] generateApkSigningBlock(
-            List<SignerConfig> signerConfigs,
-            Map<ContentDigestAlgorithm, byte[]> contentDigests)
-                    throws InvalidKeyException, SignatureException {
-        byte[] apkSignatureSchemeV2Block =
-                generateApkSignatureSchemeV2Block(signerConfigs, contentDigests);
-        return generateApkSigningBlock(apkSignatureSchemeV2Block);
-    }
-
-    private static byte[] generateApkSigningBlock(byte[] apkSignatureSchemeV2Block) {
-        // FORMAT:
-        // uint64:  size (excluding this field)
-        // repeated ID-value pairs:
-        //     uint64:           size (excluding this field)
-        //     uint32:           ID
-        //     (size - 4) bytes: value
-        // uint64:  size (same as the one above)
-        // uint128: magic
-
-        int resultSize =
-                8 // size
-                + 8 + 4 + apkSignatureSchemeV2Block.length // v2Block as ID-value pair
-                + 8 // size
-                + 16 // magic
-                ;
-        ByteBuffer result = ByteBuffer.allocate(resultSize);
-        result.order(ByteOrder.LITTLE_ENDIAN);
-        long blockSizeFieldValue = resultSize - 8;
-        result.putLong(blockSizeFieldValue);
-
-        long pairSizeFieldValue = 4 + apkSignatureSchemeV2Block.length;
-        result.putLong(pairSizeFieldValue);
-        result.putInt(APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
-        result.put(apkSignatureSchemeV2Block);
-
-        result.putLong(blockSizeFieldValue);
-        result.put(APK_SIGNING_BLOCK_MAGIC);
-
-        return result.array();
-    }
-
-    private static byte[] generateApkSignatureSchemeV2Block(
-            List<SignerConfig> signerConfigs,
-            Map<ContentDigestAlgorithm, byte[]> contentDigests)
-                    throws InvalidKeyException, SignatureException {
-        // FORMAT:
-        // * length-prefixed sequence of length-prefixed signer blocks.
-
-        List<byte[]> signerBlocks = Lists.newArrayListWithExpectedSize(signerConfigs.size());
-        int signerNumber = 0;
-        for (SignerConfig signerConfig : signerConfigs) {
-            signerNumber++;
-            byte[] signerBlock;
-            try {
-                signerBlock = generateSignerBlock(signerConfig, contentDigests);
-            } catch (InvalidKeyException e) {
-                throw new InvalidKeyException("Signer #" + signerNumber + " failed", e);
-            } catch (SignatureException e) {
-                throw new SignatureException("Signer #" + signerNumber + " failed", e);
-            }
-            signerBlocks.add(signerBlock);
-        }
-
-        return encodeAsSequenceOfLengthPrefixedElements(
-                new byte[][] {
-                    encodeAsSequenceOfLengthPrefixedElements(signerBlocks),
-                });
-    }
-
-    private static byte[] generateSignerBlock(
-            SignerConfig signerConfig,
-            Map<ContentDigestAlgorithm, byte[]> contentDigests)
-                    throws InvalidKeyException, SignatureException {
-        if (signerConfig.certificates.isEmpty()) {
-            throw new SignatureException("No certificates configured for signer");
-        }
-        PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
-
-        byte[] encodedPublicKey = encodePublicKey(publicKey);
-
-        V2SignatureSchemeBlock.SignedData signedData = new V2SignatureSchemeBlock.SignedData();
-        try {
-            signedData.certificates = encodeCertificates(signerConfig.certificates);
-        } catch (CertificateEncodingException e) {
-            throw new SignatureException("Failed to encode certificates", e);
-        }
-
-        List<ApkZLibPair<Integer, byte[]>> digests =
-                Lists.newArrayListWithExpectedSize(signerConfig.signatureAlgorithms.size());
-        for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) {
-            ContentDigestAlgorithm contentDigestAlgorithm =
-                    signatureAlgorithm.getContentDigestAlgorithm();
-            byte[] contentDigest = contentDigests.get(contentDigestAlgorithm);
-            if (contentDigest == null) {
-                throw new RuntimeException(
-                        contentDigestAlgorithm + " content digest for " + signatureAlgorithm
-                                + " not computed");
-            }
-            digests.add(new ApkZLibPair<>(signatureAlgorithm.getId(), contentDigest));
-        }
-        signedData.digests = digests;
-
-        V2SignatureSchemeBlock.Signer signer = new V2SignatureSchemeBlock.Signer();
-        // FORMAT:
-        // * length-prefixed sequence of length-prefixed digests:
-        //   * uint32: signature algorithm ID
-        //   * length-prefixed bytes: digest of contents
-        // * length-prefixed sequence of certificates:
-        //   * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded).
-        // * length-prefixed sequence of length-prefixed additional attributes:
-        //   * uint32: ID
-        //   * (length - 4) bytes: value
-        signer.signedData = encodeAsSequenceOfLengthPrefixedElements(new byte[][] {
-            encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(signedData.digests),
-            encodeAsSequenceOfLengthPrefixedElements(signedData.certificates),
-            // additional attributes
-            new byte[0],
-        });
-        signer.publicKey = encodedPublicKey;
-        signer.signatures =
-                Lists.newArrayListWithExpectedSize(signerConfig.signatureAlgorithms.size());
-        for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) {
-            ApkZLibPair<String, ? extends AlgorithmParameterSpec> sigAlgAndParams =
-                    signatureAlgorithm.getJcaSignatureAlgorithmAndParams();
-            String jcaSignatureAlgorithm = sigAlgAndParams.v1;
-            AlgorithmParameterSpec jcaSignatureAlgorithmParams = sigAlgAndParams.v2;
-            byte[] signatureBytes;
-            try {
-                Signature signature = Signature.getInstance(jcaSignatureAlgorithm);
-                signature.initSign(signerConfig.privateKey);
-                if (jcaSignatureAlgorithmParams != null) {
-                    signature.setParameter(jcaSignatureAlgorithmParams);
-                }
-                signature.update(signer.signedData);
-                signatureBytes = signature.sign();
-            } catch (InvalidKeyException e) {
-                throw new InvalidKeyException("Failed sign using " + jcaSignatureAlgorithm, e);
-            } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException
-                    | SignatureException e) {
-                throw new SignatureException("Failed sign using " + jcaSignatureAlgorithm, e);
-            }
-
-            try {
-                Signature signature = Signature.getInstance(jcaSignatureAlgorithm);
-                signature.initVerify(publicKey);
-                if (jcaSignatureAlgorithmParams != null) {
-                    signature.setParameter(jcaSignatureAlgorithmParams);
-                }
-                signature.update(signer.signedData);
-                if (!signature.verify(signatureBytes)) {
-                    throw new SignatureException("Signature did not verify");
-                }
-            } catch (InvalidKeyException e) {
-                throw new InvalidKeyException("Failed to verify generated " + jcaSignatureAlgorithm
-                        + " signature using public key from certificate", e);
-            } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException
-                    | SignatureException e) {
-                throw new SignatureException("Failed to verify generated " + jcaSignatureAlgorithm
-                        + " signature using public key from certificate", e);
-            }
-
-            signer.signatures.add(new ApkZLibPair<>(signatureAlgorithm.getId(), signatureBytes));
-        }
-
-        // FORMAT:
-        // * length-prefixed signed data
-        // * length-prefixed sequence of length-prefixed signatures:
-        //   * uint32: signature algorithm ID
-        //   * length-prefixed bytes: signature of signed data
-        // * length-prefixed bytes: public key (X.509 SubjectPublicKeyInfo, ASN.1 DER encoded)
-        return encodeAsSequenceOfLengthPrefixedElements(
-                new byte[][] {
-                    signer.signedData,
-                    encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
-                            signer.signatures),
-                    signer.publicKey,
-                });
-    }
-
-    private static final class V2SignatureSchemeBlock {
-        private static final class Signer {
-            public byte[] signedData;
-            public List<ApkZLibPair<Integer, byte[]>> signatures;
-            public byte[] publicKey;
-        }
-
-        private static final class SignedData {
-            public List<ApkZLibPair<Integer, byte[]>> digests;
-            public List<byte[]> certificates;
-        }
-    }
-
-    private static byte[] encodePublicKey(PublicKey publicKey) throws InvalidKeyException {
-        byte[] encodedPublicKey = null;
-        if ("X.509".equals(publicKey.getFormat())) {
-            encodedPublicKey = publicKey.getEncoded();
-        }
-        if (encodedPublicKey == null) {
-            try {
-                encodedPublicKey =
-                        KeyFactory.getInstance(publicKey.getAlgorithm())
-                                .getKeySpec(publicKey, X509EncodedKeySpec.class)
-                                .getEncoded();
-            } catch (NoSuchAlgorithmException e) {
-                throw new InvalidKeyException(
-                        "Failed to obtain X.509 encoded form of public key " + publicKey
-                                + " of class " + publicKey.getClass().getName(),
-                        e);
-            } catch (InvalidKeySpecException e) {
-                throw new InvalidKeyException(
-                        "Failed to obtain X.509 encoded form of public key " + publicKey
-                                + " of class " + publicKey.getClass().getName(),
-                        e);
-            }
-        }
-        if ((encodedPublicKey == null) || (encodedPublicKey.length == 0)) {
-            throw new InvalidKeyException(
-                    "Failed to obtain X.509 encoded form of public key " + publicKey
-                            + " of class " + publicKey.getClass().getName());
-        }
-        return encodedPublicKey;
-    }
-
-    private static List<byte[]> encodeCertificates(List<X509Certificate> certificates)
-            throws CertificateEncodingException {
-        List<byte[]> result = Lists.newArrayListWithExpectedSize(certificates.size());
-        for (X509Certificate certificate : certificates) {
-            result.add(certificate.getEncoded());
-        }
-        return result;
-    }
-
-    private static byte[] encodeAsSequenceOfLengthPrefixedElements(List<byte[]> sequence) {
-        return encodeAsSequenceOfLengthPrefixedElements(
-                sequence.toArray(new byte[sequence.size()][]));
-    }
-
-    private static byte[] encodeAsSequenceOfLengthPrefixedElements(byte[][] sequence) {
-        int payloadSize = 0;
-        for (byte[] element : sequence) {
-            payloadSize += 4 + element.length;
-        }
-        ByteBuffer result = ByteBuffer.allocate(payloadSize);
-        result.order(ByteOrder.LITTLE_ENDIAN);
-        for (byte[] element : sequence) {
-            result.putInt(element.length);
-            result.put(element);
-        }
-        return result.array();
-      }
-
-    private static byte[] encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
-            List<ApkZLibPair<Integer, byte[]>> sequence) {
-        int resultSize = 0;
-        for (ApkZLibPair<Integer, byte[]> element : sequence) {
-            resultSize += 12 + element.v2.length;
-        }
-        ByteBuffer result = ByteBuffer.allocate(resultSize);
-        result.order(ByteOrder.LITTLE_ENDIAN);
-        for (ApkZLibPair<Integer, byte[]> element : sequence) {
-            byte[] second = element.v2;
-            result.putInt(8 + second.length);
-            result.putInt(element.v1);
-            result.putInt(second.length);
-            result.put(second);
-        }
-        return result.array();
-    }
-}
diff --git a/src/main/java/com/android/builder/internal/packaging/sign/v2/ByteArrayDigestSource.java b/src/main/java/com/android/builder/internal/packaging/sign/v2/ByteArrayDigestSource.java
deleted file mode 100644
index 8aafa95..0000000
--- a/src/main/java/com/android/builder/internal/packaging/sign/v2/ByteArrayDigestSource.java
+++ /dev/null
@@ -1,60 +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.builder.internal.packaging.sign.v2;
-
-import java.security.MessageDigest;
-
-import com.android.annotations.NonNull;
-import com.google.common.base.Preconditions;
-
-/**
- * {@code byte[]} which is fed into {@link MessageDigest} instances.
- */
-public class ByteArrayDigestSource implements DigestSource {
-    private final byte[] mBuf;
-
-    /**
-     * Constructs a new {@code ByteArrayDigestSource} instance which obtains its data from the
-     * provided byte array. Changes to the byte array's contents are reflected visible in this
-     * source.
-     */
-    public ByteArrayDigestSource(@NonNull byte[] buf) {
-        mBuf = buf;
-    }
-
-    @Override
-    public long size() {
-        return mBuf.length;
-    }
-
-    @Override
-    public void feedDigests(long offset, int size, @NonNull MessageDigest[] digests) {
-        Preconditions.checkArgument(offset >= 0, "offset: %s", offset);
-        Preconditions.checkArgument(size >= 0, "size: %s", size);
-        Preconditions.checkArgument(offset <= mBuf.length, "offset too large: %s", offset);
-        int offsetInBuf = (int) offset;
-        Preconditions.checkPositionIndex(offsetInBuf, mBuf.length, "offset out of range");
-        int availableSize = mBuf.length - offsetInBuf;
-        Preconditions.checkArgument(
-                size <= availableSize,
-                "offset (%s) + size (%s) > array length (%s)", offset, size, mBuf.length);
-
-        for (MessageDigest md : digests) {
-            md.update(mBuf, offsetInBuf, size);
-        }
-    }
-}
diff --git a/src/main/java/com/android/builder/internal/packaging/sign/v2/ContentDigestAlgorithm.java b/src/main/java/com/android/builder/internal/packaging/sign/v2/ContentDigestAlgorithm.java
deleted file mode 100644
index d17b3b2..0000000
--- a/src/main/java/com/android/builder/internal/packaging/sign/v2/ContentDigestAlgorithm.java
+++ /dev/null
@@ -1,52 +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.builder.internal.packaging.sign.v2;
-
-/**
- * APK Signature Scheme v2 content digest algorithm.
- */
-enum ContentDigestAlgorithm {
-    /** SHA2-256 over 1 MB chunks. */
-    CHUNKED_SHA256("SHA-256", 256 / 8),
-
-    /** SHA2-512 over 1 MB chunks. */
-    CHUNKED_SHA512("SHA-512", 512 / 8);
-
-    private final String mJcaMessageDigestAlgorithmName;
-    private final int mChunkDigestOutputSizeBytes;
-
-    private ContentDigestAlgorithm(
-            String jcaMessageDigestAlgorithmName, int chunkDigestOutputSizeBytes) {
-        mJcaMessageDigestAlgorithmName = jcaMessageDigestAlgorithmName;
-        mChunkDigestOutputSizeBytes = chunkDigestOutputSizeBytes;
-    }
-
-    /**
-     * Returns the {@link java.security.MessageDigest} algorithm used for computing digests of
-     * chunks by this content digest algorithm.
-     */
-    String getJcaMessageDigestAlgorithmName() {
-        return mJcaMessageDigestAlgorithmName;
-    }
-
-    /**
-     * Returns the size (in bytes) of the digest of a chunk of content.
-     */
-    int getChunkDigestOutputSizeBytes() {
-        return mChunkDigestOutputSizeBytes;
-    }
-}
\ No newline at end of file
diff --git a/src/main/java/com/android/builder/internal/packaging/sign/v2/DigestSource.java b/src/main/java/com/android/builder/internal/packaging/sign/v2/DigestSource.java
deleted file mode 100644
index b2afd2f..0000000
--- a/src/main/java/com/android/builder/internal/packaging/sign/v2/DigestSource.java
+++ /dev/null
@@ -1,51 +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.builder.internal.packaging.sign.v2;
-
-import java.io.IOException;
-import java.security.MessageDigest;
-
-import com.android.annotations.NonNull;
-
-/**
- * Source of data which is fed into {@link MessageDigest} instances.
- *
- * <p>This abstraction serves two purposes:
- * <ul>
- * <li>Transparent digesting of different types of sources, such as {@code byte[]},
- *     {@code ZFile}, {@link java.nio.ByteBuffer} and/or memory-mapped file.</li>
- * <li>Support sources larger than 2 GB. If all sources were smaller than 2 GB, {@code ByteBuffer}
- *     would have worked as the unifying abstraction.</li>
- * </ul>
- */
-public interface DigestSource {
-    /**
-     * Returns the amount of data (in bytes) contained in this data source.
-     */
-    long size();
-
-    /**
-     * Feeds the specified chunk from this data source into each of the provided
-     * {@link MessageDigest} instances. Each {@code MessageDigest} instance receives the specified
-     * chunk of data in full.
-     *
-     * @param offset index (in bytes) at which the chunk starts relative to the start of this data
-     *        source.
-     * @param size size (in bytes) of the chunk.
-     */
-    void feedDigests(long offset, int size, @NonNull MessageDigest[] digests) throws IOException;
-}
diff --git a/src/main/java/com/android/builder/internal/packaging/sign/v2/SignatureAlgorithm.java b/src/main/java/com/android/builder/internal/packaging/sign/v2/SignatureAlgorithm.java
deleted file mode 100644
index bcd6652..0000000
--- a/src/main/java/com/android/builder/internal/packaging/sign/v2/SignatureAlgorithm.java
+++ /dev/null
@@ -1,114 +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.builder.internal.packaging.sign.v2;
-
-import com.android.builder.internal.utils.ApkZLibPair;
-import java.security.spec.AlgorithmParameterSpec;
-import java.security.spec.MGF1ParameterSpec;
-import java.security.spec.PSSParameterSpec;
-
-/**
- * APK Signature Scheme v2 content digest algorithm.
- */
-public enum SignatureAlgorithm {
-    /**
-     * RSASSA-PSS with SHA2-256 digest, SHA2-256 MGF1, 32 bytes of salt, trailer: 0xbc, content
-     * digested using SHA2-256 in 1 MB chunks.
-     */
-    RSA_PSS_WITH_SHA256(
-            0x0101,
-            ContentDigestAlgorithm.CHUNKED_SHA256,
-            new ApkZLibPair<>("SHA256withRSA/PSS",
-                    new PSSParameterSpec(
-                            "SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 256 / 8, 1))),
-
-    /**
-     * RSASSA-PSS with SHA2-512 digest, SHA2-512 MGF1, 64 bytes of salt, trailer: 0xbc, content
-     * digested using SHA2-512 in 1 MB chunks.
-     */
-    RSA_PSS_WITH_SHA512(
-            0x0102,
-            ContentDigestAlgorithm.CHUNKED_SHA512,
-            new ApkZLibPair<>(
-                    "SHA512withRSA/PSS",
-                    new PSSParameterSpec(
-                            "SHA-512", "MGF1", MGF1ParameterSpec.SHA512, 512 / 8, 1))),
-
-    /** RSASSA-PKCS1-v1_5 with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
-    RSA_PKCS1_V1_5_WITH_SHA256(
-            0x0103,
-            ContentDigestAlgorithm.CHUNKED_SHA256,
-            new ApkZLibPair<>("SHA256withRSA", null)),
-
-    /** RSASSA-PKCS1-v1_5 with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. */
-    RSA_PKCS1_V1_5_WITH_SHA512(
-            0x0104,
-            ContentDigestAlgorithm.CHUNKED_SHA512,
-            new ApkZLibPair<>("SHA512withRSA", null)),
-
-    /** ECDSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
-    ECDSA_WITH_SHA256(
-            0x0201,
-            ContentDigestAlgorithm.CHUNKED_SHA256,
-            new ApkZLibPair<>("SHA256withECDSA", null)),
-
-    /** ECDSA with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. */
-    ECDSA_WITH_SHA512(
-            0x0202,
-            ContentDigestAlgorithm.CHUNKED_SHA512,
-            new ApkZLibPair<>("SHA512withECDSA", null)),
-
-    /** DSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
-    DSA_WITH_SHA256(
-            0x0301,
-            ContentDigestAlgorithm.CHUNKED_SHA256,
-            new ApkZLibPair<>("SHA256withDSA", null));
-
-    private final int mId;
-    private final ContentDigestAlgorithm mContentDigestAlgorithm;
-    private final ApkZLibPair<String, ? extends AlgorithmParameterSpec> mJcaSignatureAlgAndParams;
-
-    private SignatureAlgorithm(int id,
-            ContentDigestAlgorithm contentDigestAlgorithm,
-            ApkZLibPair<String, ? extends AlgorithmParameterSpec> jcaSignatureAlgAndParams) {
-        mId = id;
-        mContentDigestAlgorithm = contentDigestAlgorithm;
-        mJcaSignatureAlgAndParams = jcaSignatureAlgAndParams;
-    }
-
-    /**
-     * Returns the ID of this signature algorithm as used in APK Signature Scheme v2 wire format.
-     */
-    int getId() {
-        return mId;
-    }
-
-    /**
-     * Returns the content digest algorithm associated with this signature algorithm.
-     */
-    ContentDigestAlgorithm getContentDigestAlgorithm() {
-        return mContentDigestAlgorithm;
-    }
-
-    /**
-     * Returns the {@link java.security.Signature} algorithm and the {@link AlgorithmParameterSpec}
-     * (or null if not needed) to parameterize the {@code Signature}.
-     */
-    ApkZLibPair<String, ? extends AlgorithmParameterSpec> getJcaSignatureAlgorithmAndParams() {
-        return mJcaSignatureAlgAndParams;
-    }
-}
diff --git a/src/main/java/com/android/builder/internal/packaging/sign/v2/ZFileDigestSource.java b/src/main/java/com/android/builder/internal/packaging/sign/v2/ZFileDigestSource.java
deleted file mode 100644
index a211ba7..0000000
--- a/src/main/java/com/android/builder/internal/packaging/sign/v2/ZFileDigestSource.java
+++ /dev/null
@@ -1,69 +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.builder.internal.packaging.sign.v2;
-
-import java.io.IOException;
-import java.security.MessageDigest;
-
-import com.android.annotations.NonNull;
-import com.android.builder.internal.packaging.zip.ZFile;
-import com.google.common.base.Preconditions;
-
-/**
- * Contiguous section of {@link ZFile} which is fed into {@link MessageDigest} instances.
- */
-public class ZFileDigestSource implements DigestSource {
-    private final ZFile mFile;
-    private final long mOffset;
-    private final long mSize;
-
-    /**
-     * Constructs a new {@code ZFileDigestSource} representing the section of the file starting
-     * at the provided {@code offset} and extending for the provided {@code size} number of bytes.
-     */
-    public ZFileDigestSource(@NonNull ZFile file, long offset, long size) {
-        Preconditions.checkArgument(offset >= 0, "offset: %s", offset);
-        Preconditions.checkArgument(size >= 0, "size: %s", size);
-        mFile = file;
-        mOffset = offset;
-        mSize = size;
-    }
-
-
-    @Override
-    public long size() {
-        return mSize;
-    }
-
-    @Override
-    public void feedDigests(long offset, int size, @NonNull MessageDigest[] digests)
-            throws IOException {
-        Preconditions.checkArgument(offset >= 0, "offset: %s", offset);
-        Preconditions.checkArgument(size >= 0, "size: %s", size);
-        Preconditions.checkArgument(offset <= mSize, "offset: %s, file size: %s", offset, mSize);
-        long chunkStartOffset = mOffset + offset;
-        long availableSize = mSize - offset;
-        Preconditions.checkArgument(
-                size <= availableSize, "offset: %s, size: %s, file size: %s", offset, size, mSize);
-
-        byte[] chunk = new byte[size];
-        mFile.directFullyRead(chunkStartOffset, chunk);
-        for (MessageDigest md : digests) {
-            md.update(chunk);
-        }
-    }
-}
diff --git a/src/main/java/com/android/builder/internal/packaging/zfile/ApkZFileCreator.java b/src/main/java/com/android/builder/internal/packaging/zfile/ApkZFileCreator.java
deleted file mode 100644
index b07e855..0000000
--- a/src/main/java/com/android/builder/internal/packaging/zfile/ApkZFileCreator.java
+++ /dev/null
@@ -1,172 +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.builder.internal.packaging.zfile;
-
-import com.android.annotations.NonNull;
-import com.android.annotations.Nullable;
-import com.android.builder.internal.packaging.zip.AlignmentRule;
-import com.android.builder.internal.packaging.zip.AlignmentRules;
-import com.android.builder.internal.packaging.zip.StoredEntry;
-import com.android.builder.internal.packaging.zip.ZFile;
-import com.android.builder.internal.packaging.zip.ZFileOptions;
-import com.android.builder.packaging.ApkCreator;
-import com.android.builder.packaging.ApkCreatorFactory;
-import com.google.common.base.Preconditions;
-import com.google.common.io.Closer;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.util.function.Function;
-import java.util.function.Predicate;
-
-/**
- * {@link ApkCreator} that uses {@link ZFileOptions} to generate the APK.
- */
-class ApkZFileCreator implements ApkCreator {
-
-    /**
-     * Suffix for native libraries.
-     */
-    private static final String NATIVE_LIBRARIES_SUFFIX = ".so";
-
-    /**
-     * Shared libraries are alignment at 4096 boundaries.
-     */
-    private static final AlignmentRule SO_RULE =
-            AlignmentRules.constantForSuffix(NATIVE_LIBRARIES_SUFFIX, 4096);
-
-    /**
-     * The zip file.
-     */
-    @NonNull
-    private final ZFile mZip;
-
-    /**
-     * Has the zip file been closed?
-     */
-    private boolean mClosed;
-
-    /**
-     * Predicate defining which files should not be compressed.
-     */
-    @NonNull
-    private final Predicate<String> mNoCompressPredicate;
-
-    /**
-     * Creates a new creator.
-     *
-     * @param creationData the data needed to create the APK
-     * @param options zip file options
-     * @throws IOException failed to create the zip
-     */
-    ApkZFileCreator(
-            @NonNull ApkCreatorFactory.CreationData creationData,
-            @NonNull ZFileOptions options)
-            throws IOException {
-
-        switch (creationData.getNativeLibrariesPackagingMode()) {
-            case COMPRESSED:
-                mNoCompressPredicate = creationData.getNoCompressPredicate();
-                break;
-            case UNCOMPRESSED_AND_ALIGNED:
-                mNoCompressPredicate =
-                        creationData.getNoCompressPredicate().or(
-                                name -> name.endsWith(NATIVE_LIBRARIES_SUFFIX));
-                options.setAlignmentRule(
-                        AlignmentRules.compose(SO_RULE, options.getAlignmentRule()));
-                break;
-            default:
-                throw new AssertionError();
-        }
-
-        mZip = ZFiles.apk(
-                creationData.getApkPath(),
-                options,
-                creationData.getPrivateKey(),
-                creationData.getCertificate(),
-                creationData.isV1SigningEnabled(),
-                creationData.isV2SigningEnabled(),
-                creationData.getBuiltBy(),
-                creationData.getCreatedBy(),
-                creationData.getMinSdkVersion());
-        mClosed = false;
-    }
-
-    @Override
-    public void writeZip(@NonNull File zip, @Nullable Function<String, String> transform,
-            @Nullable Predicate<String> isIgnored) throws IOException {
-        Preconditions.checkState(!mClosed, "mClosed == true");
-        Preconditions.checkArgument(zip.isFile(), "!zip.isFile()");
-
-        Closer closer = Closer.create();
-        try {
-            ZFile toMerge = closer.register(new ZFile(zip));
-
-            Predicate<String> predicate;
-            if (isIgnored == null) {
-                predicate = s -> false;
-            } else {
-                predicate = isIgnored;
-            }
-
-            mZip.mergeFrom(toMerge, predicate);
-        } catch (Throwable t) {
-            throw closer.rethrow(t);
-        } finally {
-            closer.close();
-        }
-    }
-
-    @Override
-    public void writeFile(@NonNull File inputFile, @NonNull String apkPath) throws IOException {
-        Preconditions.checkState(!mClosed, "mClosed == true");
-
-        boolean mayCompress = !mNoCompressPredicate.test(apkPath);
-
-        Closer closer = Closer.create();
-        try {
-            FileInputStream inputFileStream = closer.register(new FileInputStream(inputFile));
-            mZip.add(apkPath, inputFileStream, mayCompress);
-        } catch (IOException e) {
-            throw closer.rethrow(e, IOException.class);
-        } catch (Throwable t) {
-            throw closer.rethrow(t);
-        } finally {
-            closer.close();
-        }
-    }
-
-    @Override
-    public void deleteFile(@NonNull String apkPath) throws IOException {
-        Preconditions.checkState(!mClosed, "mClosed == true");
-
-        StoredEntry entry = mZip.get(apkPath);
-        if (entry != null) {
-            entry.delete();
-        }
-    }
-
-    @Override
-    public void close() throws IOException {
-        if (mClosed) {
-            return;
-        }
-
-        mZip.close();
-        mClosed = true;
-    }
-}
diff --git a/src/main/java/com/android/builder/internal/packaging/zip/ZFileOptions.java b/src/main/java/com/android/builder/internal/packaging/zip/ZFileOptions.java
deleted file mode 100644
index 2345837..0000000
--- a/src/main/java/com/android/builder/internal/packaging/zip/ZFileOptions.java
+++ /dev/null
@@ -1,208 +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.builder.internal.packaging.zip;
-
-import com.android.annotations.NonNull;
-import com.android.builder.internal.packaging.zip.compress.DeflateExecutionCompressor;
-import com.android.builder.internal.packaging.zip.utils.ByteTracker;
-import com.google.common.util.concurrent.MoreExecutors;
-
-import java.util.zip.Deflater;
-
-/**
- * Options to create a {@link ZFile}.
- */
-public class ZFileOptions {
-
-    /**
-     * The byte tracker.
-     */
-    @NonNull
-    private ByteTracker mTracker;
-
-    /**
-     * The compressor to use.
-     */
-    @NonNull
-    private Compressor mCompressor;
-
-    /**
-     * Should timestamps be zeroed?
-     */
-    private boolean mNoTimestamps;
-
-    /**
-     * The alignment rule to use.
-     */
-    @NonNull
-    private AlignmentRule mAlignmentRule;
-
-    /**
-     * Should the extra field be used to cover empty space?
-     */
-    private boolean mCoverEmptySpaceUsingExtraField;
-
-    /**
-     * Should files be automatically sorted before update?
-     */
-    private boolean mAutoSortFiles;
-
-    /**
-     * Should validation of the data descriptors of entries be skipped? See
-     * {@link #getSkipDataDescriptorValidation()}
-     */
-    private boolean mSkipDataDescriptionValidation;
-
-    /**
-     * Creates a new options object. All options are set to their defaults.
-     */
-    public ZFileOptions() {
-        mTracker = new ByteTracker();
-        mCompressor =
-                new DeflateExecutionCompressor(
-                        MoreExecutors.directExecutor(), mTracker, Deflater.DEFAULT_COMPRESSION);
-        mAlignmentRule = AlignmentRules.compose();
-    }
-
-    /**
-     * Obtains the ZFile's byte tracker.
-     *
-     * @return the byte tracker
-     */
-    @NonNull
-    public ByteTracker getTracker() {
-        return mTracker;
-    }
-
-    /**
-     * Obtains the compressor to use.
-     *
-     * @return the compressor
-     */
-    @NonNull
-    public Compressor getCompressor() {
-        return mCompressor;
-    }
-
-    /**
-     * Sets the compressor to use.
-     *
-     * @param compressor the compressor
-     */
-    public void setCompressor(@NonNull Compressor compressor) {
-        mCompressor = compressor;
-    }
-
-    /**
-     * Obtains whether timestamps should be zeroed.
-     *
-     * @return should timestamps be zeroed?
-     */
-    public boolean getNoTimestamps() {
-        return mNoTimestamps;
-    }
-
-    /**
-     * Sets whether timestamps should be zeroed.
-     *
-     * @param noTimestamps should timestamps be zeroed?
-     */
-    public void setNoTimestamps(boolean noTimestamps) {
-        mNoTimestamps = noTimestamps;
-    }
-
-    /**
-     * Obtains the alignment rule.
-     *
-     * @return the alignment rule
-     */
-    @NonNull
-    public AlignmentRule getAlignmentRule() {
-        return mAlignmentRule;
-    }
-
-    /**
-     * Sets the alignment rule.
-     *
-     * @param alignmentRule the alignment rule
-     */
-    public void setAlignmentRule(@NonNull AlignmentRule alignmentRule) {
-        mAlignmentRule = alignmentRule;
-    }
-
-    /**
-     * Obtains whether the extra field should be used to cover empty spaces. See {@link ZFile} for
-     * an explanation on using the extra field for covering empty spaces.
-     *
-     * @return should the extra field be used to cover empty spaces?
-     */
-    public boolean getCoverEmptySpaceUsingExtraField() {
-        return mCoverEmptySpaceUsingExtraField;
-    }
-
-    /**
-     * Sets whether the extra field should be used to cover empty spaces. See {@link ZFile} for an
-     * explanation on using the extra field for covering empty spaces.
-     *
-     * @param coverEmptySpaceUsingExtraField should the extra field be used to cover empty spaces?
-     */
-    public void setCoverEmptySpaceUsingExtraField(boolean coverEmptySpaceUsingExtraField) {
-        mCoverEmptySpaceUsingExtraField = coverEmptySpaceUsingExtraField;
-    }
-
-    /**
-     * Obtains whether files should be automatically sorted before updating the zip file. See
-     * {@link ZFile} for an explanation on automatic sorting.
-     *
-     * @return should the file be automatically sorted?
-     */
-    public boolean getAutoSortFiles() {
-        return mAutoSortFiles;
-    }
-
-    /**
-     * Sets whether files should be automatically sorted before updating the zip file. See
-     * {@link ZFile} for an explanation on automatic sorting.
-     *
-     * @param autoSortFiles should the file be automatically sorted?
-     */
-    public void setAutoSortFiles(boolean autoSortFiles) {
-        mAutoSortFiles = autoSortFiles;
-    }
-
-    /**
-     * Should data descriptor validation be skipped? This should generally be
-     * set to false. However, some tools (proguard -- http://b.android.com/221057) generate zips
-     * with incorrect data descriptors and to open the zips we need to skip the validation of data
-     * descriptors.
-     *
-     * @return should data descriptors be validated?
-     */
-    public boolean getSkipDataDescriptorValidation() {
-        return mSkipDataDescriptionValidation;
-    }
-
-    /**
-     * Sets whether data descriptors validation should be skipped. See
-     * {@link #getSkipDataDescriptorValidation()}.
-     *
-     * @param skip should validation be skipped?
-     */
-    public void setSkipDataDescriptionValidation(boolean skip) {
-        mSkipDataDescriptionValidation = skip;
-    }
-}
diff --git a/src/test/java/com/android/builder/internal/packaging/sign/FullApkSignTest.java b/src/test/java/com/android/apkzlib/sign/FullApkSignTest.java
similarity index 80%
rename from src/test/java/com/android/builder/internal/packaging/sign/FullApkSignTest.java
rename to src/test/java/com/android/apkzlib/sign/FullApkSignTest.java
index e9c2c90..f72f63c 100644
--- a/src/test/java/com/android/builder/internal/packaging/sign/FullApkSignTest.java
+++ b/src/test/java/com/android/apkzlib/sign/FullApkSignTest.java
@@ -14,19 +14,19 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.sign;
+package com.android.apkzlib.sign;
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertNotNull;
 
-import com.android.builder.internal.packaging.zip.AlignmentRule;
-import com.android.builder.internal.packaging.zip.AlignmentRules;
-import com.android.builder.internal.packaging.zip.StoredEntry;
-import com.android.builder.internal.packaging.zip.ZFile;
-import com.android.builder.internal.packaging.zip.ZFileOptions;
-import com.android.builder.internal.packaging.zip.ZFileTestConstants;
-import com.android.builder.internal.utils.ApkZFileTestUtils;
-import com.android.builder.internal.utils.ApkZLibPair;
+import com.android.apkzlib.utils.ApkZLibPair;
+import com.android.apkzlib.zip.AlignmentRule;
+import com.android.apkzlib.zip.AlignmentRules;
+import com.android.apkzlib.zip.StoredEntry;
+import com.android.apkzlib.zip.ZFile;
+import com.android.apkzlib.zip.ZFileOptions;
+import com.android.apkzlib.zip.ZFileTestConstants;
+import com.android.apkzlib.utils.ApkZFileTestUtils;
 import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.security.PrivateKey;
@@ -36,7 +36,7 @@
 import org.junit.rules.TemporaryFolder;
 
 /**
- * Tests that verify {@link FullApkSignExtension}.
+ * Tests that verify APK Signature Scheme v2 signing using {@link SigningExtension}.
  */
 public class FullApkSignTest {
 
@@ -63,9 +63,8 @@
          * Generate a signed zip.
          */
         ZFile zf = new ZFile(out, options);
-        FullApkSignExtension signExtension =
-                new FullApkSignExtension(zf, 13, signData.v2, signData.v1);
-        signExtension.register();
+        new SigningExtension(13, signData.v2, signData.v1, false, true)
+                .register(zf);
         String f1Name = "abc";
         byte[] f1Data = new byte[] { 1, 1, 1, 1 };
         zf.add(f1Name, new ByteArrayInputStream(f1Data));
diff --git a/src/test/java/com/android/builder/internal/packaging/sign/JarSigningTest.java b/src/test/java/com/android/apkzlib/sign/JarSigningTest.java
similarity index 88%
rename from src/test/java/com/android/builder/internal/packaging/sign/JarSigningTest.java
rename to src/test/java/com/android/apkzlib/sign/JarSigningTest.java
index 78c633e..35aeeaf 100644
--- a/src/test/java/com/android/builder/internal/packaging/sign/JarSigningTest.java
+++ b/src/test/java/com/android/apkzlib/sign/JarSigningTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2015 The Android Open Source Project
+ * 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.
@@ -14,16 +14,16 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.sign;
+package com.android.apkzlib.sign;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 
-import com.android.builder.internal.packaging.zip.StoredEntry;
-import com.android.builder.internal.packaging.zip.ZFile;
-import com.android.builder.internal.utils.ApkZFileTestUtils;
-import com.android.builder.internal.utils.ApkZLibPair;
+import com.android.apkzlib.utils.ApkZFileTestUtils;
+import com.android.apkzlib.utils.ApkZLibPair;
+import com.android.apkzlib.zip.StoredEntry;
+import com.android.apkzlib.zip.ZFile;
 import com.google.common.base.Charsets;
 import com.google.common.hash.Hashing;
 import java.io.ByteArrayInputStream;
@@ -48,6 +48,7 @@
         File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
 
         try (ZFile zf = new ZFile(zipFile)) {
+            ApkZFileTestUtils.addAndroidManifest(zf);
             ManifestGenerationExtension manifestExtension =
                     new ManifestGenerationExtension("Me", "Me");
             manifestExtension.register(zf);
@@ -55,9 +56,7 @@
             ApkZLibPair<PrivateKey, X509Certificate> p =
                     SignatureTestUtils.generateSignaturePre18();
 
-            SignatureExtension signatureExtension =
-                    new SignatureExtension(manifestExtension, 12, p.v2, p.v1, null);
-            signatureExtension.register();
+            new SigningExtension(12, p.v2, p.v1, true, false).register(zf);
         }
 
         try (ZFile verifyZFile = new ZFile(zipFile)) {
@@ -78,6 +77,7 @@
         ApkZLibPair<PrivateKey, X509Certificate> p = SignatureTestUtils.generateSignaturePre18();
 
         try (ZFile zf1 = new ZFile(zipFile)) {
+            ApkZFileTestUtils.addAndroidManifest(zf1);
             zf1.add("directory/file",
                     new ByteArrayInputStream("useless text".getBytes(Charsets.US_ASCII)));
         }
@@ -85,7 +85,7 @@
         try (ZFile zf2 = new ZFile(zipFile)) {
             ManifestGenerationExtension me = new ManifestGenerationExtension("Merry", "Christmas");
             me.register(zf2);
-            new SignatureExtension(me, 10, p.v2, p.v1, null).register();
+            new SigningExtension(10, p.v2, p.v1, true, false).register(zf2);
         }
 
         try (ZFile zf3 = new ZFile(zipFile)) {
@@ -121,7 +121,7 @@
             Attributes signAttrs = signature.getAttributes("directory/file");
             assertNotNull(signAttrs);
             assertEquals(1, signAttrs.size());
-            assertEquals("OOQgIEXBissIvva3ydRoaXk29Rk=", signAttrs.getValue("SHA1-Digest"));
+            assertEquals("LGSOwy4uGcUWoc+ZhS8ukzmf0fY=", signAttrs.getValue("SHA1-Digest"));
 
             StoredEntry rsaEntry = zf3.get("META-INF/CERT.RSA");
             assertNotNull(rsaEntry);
@@ -132,6 +132,7 @@
     public void signJarWithPrexistingSimpleTextFilePos18() throws Exception {
         File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
         try (ZFile zf1 = new ZFile(zipFile)) {
+            ApkZFileTestUtils.addAndroidManifest(zf1);
             zf1.add("directory/file", new ByteArrayInputStream("useless text".getBytes(
                     Charsets.US_ASCII)));
         }
@@ -141,7 +142,7 @@
         try (ZFile zf2 = new ZFile(zipFile)) {
             ManifestGenerationExtension me = new ManifestGenerationExtension("Merry", "Christmas");
             me.register(zf2);
-            new SignatureExtension(me, 21, p.v2, p.v1, null).register();
+            new SigningExtension(21, p.v2, p.v1, true, false).register(zf2);
         }
 
         try (ZFile zf3 = new ZFile(zipFile)) {
@@ -178,7 +179,7 @@
             Attributes signAttrs = signature.getAttributes("directory/file");
             assertNotNull(signAttrs);
             assertEquals(1, signAttrs.size());
-            assertEquals("QjupZsopQM/01O6+sWHqH64ilMmoBEtljg9VEqN6aI4=",
+            assertEquals("dBnaLpqNjmUnLlZF4tNqOcDWL8wy8Tsw1ZYFqTZhjIs=",
                     signAttrs.getValue("SHA-256-Digest"));
 
             StoredEntry ecdsaEntry = zf3.get("META-INF/CERT.EC");
@@ -190,15 +191,14 @@
     public void v2SignAddsApkSigningBlock() throws Exception {
         File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
         try (ZFile zf = new ZFile(zipFile)) {
+            ApkZFileTestUtils.addAndroidManifest(zf);
             ManifestGenerationExtension manifestExtension =
                     new ManifestGenerationExtension("Me", "Me");
             manifestExtension.register(zf);
 
             ApkZLibPair<PrivateKey, X509Certificate> p = SignatureTestUtils.generateSignaturePre18();
 
-            FullApkSignExtension signatureExtension =
-                    new FullApkSignExtension(zf, 12, p.v2, p.v1);
-            signatureExtension.register();
+            new SigningExtension(12, p.v2, p.v1, false, true).register(zf);
         }
 
         try (ZFile verifyZFile = new ZFile(zipFile)) {
@@ -224,10 +224,11 @@
         String createdBy = "Uses Android";
 
         try (ZFile zf1 = new ZFile(zipFile)) {
+            ApkZFileTestUtils.addAndroidManifest(zf1);
             zf1.add(file1Name, new ByteArrayInputStream(file1Contents));
             ManifestGenerationExtension me = new ManifestGenerationExtension(builtBy, createdBy);
             me.register(zf1);
-            new SignatureExtension(me, 21, p.v2, p.v1, null).register();
+            new SigningExtension(21, p.v2, p.v1, true, false).register(zf1);
 
             zf1.update();
 
@@ -237,7 +238,7 @@
             try (InputStream manifestIs = manifestEntry.open()) {
                 Manifest manifest = new Manifest(manifestIs);
 
-                assertEquals(1, manifest.getEntries().size());
+                assertEquals(2, manifest.getEntries().size());
 
                 Attributes file1Attrs = manifest.getEntries().get(file1Name);
                 assertNotNull(file1Attrs);
@@ -261,7 +262,7 @@
             try (InputStream manifestIs = manifestEntry.open()) {
                 Manifest manifest = new Manifest(manifestIs);
 
-                assertEquals(1, manifest.getEntries().size());
+                assertEquals(2, manifest.getEntries().size());
 
                 Attributes file1Attrs = manifest.getEntries().get(file1Name);
                 assertNotNull(file1Attrs);
@@ -277,9 +278,10 @@
         file1ShaTxt = Base64.getEncoder().encodeToString(file1Sha);
 
         try (ZFile zf2 = new ZFile(zipFile)) {
+            ApkZFileTestUtils.addAndroidManifest(zf2);
             ManifestGenerationExtension me = new ManifestGenerationExtension(builtBy, createdBy);
             me.register(zf2);
-            new SignatureExtension(me, 21, p.v2, p.v1, null).register();
+            new SigningExtension(21, p.v2, p.v1, true, false).register(zf2);
 
             zf2.add(file1Name, new ByteArrayInputStream(file1Contents));
 
@@ -291,7 +293,7 @@
             try (InputStream manifestIs = manifestEntry.open()) {
                 Manifest manifest = new Manifest(manifestIs);
 
-                assertEquals(1, manifest.getEntries().size());
+                assertEquals(2, manifest.getEntries().size());
 
                 Attributes file1Attrs = manifest.getEntries().get(file1Name);
                 assertNotNull(file1Attrs);
@@ -301,7 +303,7 @@
     }
 
     @Test
-    public void openSignedJarDoesNotForcesWriteifSignatureIsNotCorrect() throws Exception {
+    public void openSignedJarDoesNotForcesWriteIfSignatureIsNotCorrect() throws Exception {
         File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
 
         ApkZLibPair<PrivateKey, X509Certificate> p = SignatureTestUtils.generateSignaturePos18();
@@ -310,9 +312,10 @@
         byte[] fileContents = "Very interesting contents".getBytes(Charsets.US_ASCII);
 
         try (ZFile zf = new ZFile(zipFile)) {
+            ApkZFileTestUtils.addAndroidManifest(zf);
             ManifestGenerationExtension me = new ManifestGenerationExtension("I", "Android");
             me.register(zf);
-            new SignatureExtension(me, 21, p.v2, p.v1, null).register();
+            new SigningExtension(21, p.v2, p.v1, true, false).register(zf);
 
             zf.add(fileName, new ByteArrayInputStream(fileContents));
         }
@@ -327,7 +330,7 @@
         try (ZFile zf = new ZFile(zipFile)) {
             ManifestGenerationExtension me = new ManifestGenerationExtension("I", "Android");
             me.register(zf);
-            new SignatureExtension(me, 21, p.v2, p.v1, null).register();
+            new SigningExtension(21, p.v2, p.v1, true, false).register(zf);
         }
 
         /*
@@ -364,7 +367,7 @@
         try (ZFile zf = new ZFile(zipFile)) {
             ManifestGenerationExtension me = new ManifestGenerationExtension("I", "Android");
             me.register(zf);
-            new SignatureExtension(me, 21, p.v2, p.v1, null).register();
+            new SigningExtension(21, p.v2, p.v1, true, false).register(zf);
         }
 
         /*
diff --git a/src/test/java/com/android/builder/internal/packaging/sign/ManifestGenerationTest.java b/src/test/java/com/android/apkzlib/sign/ManifestGenerationTest.java
similarity index 91%
rename from src/test/java/com/android/builder/internal/packaging/sign/ManifestGenerationTest.java
rename to src/test/java/com/android/apkzlib/sign/ManifestGenerationTest.java
index 5d50522..f0817d0 100644
--- a/src/test/java/com/android/builder/internal/packaging/sign/ManifestGenerationTest.java
+++ b/src/test/java/com/android/apkzlib/sign/ManifestGenerationTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2015 The Android Open Source Project
+ * 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.
@@ -14,25 +14,25 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.sign;
+package com.android.apkzlib.sign;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
-import com.android.builder.internal.packaging.zip.StoredEntry;
-import com.android.builder.internal.packaging.zip.ZFile;
-import com.android.builder.internal.utils.ApkZFileTestUtils;
+import com.android.apkzlib.utils.ApkZFileTestUtils;
+import com.android.apkzlib.zip.StoredEntry;
+import com.android.apkzlib.zip.ZFile;
 import com.google.common.base.Charsets;
 import com.google.common.io.Closer;
 import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.InputStream;
+import java.util.HashSet;
 import java.util.Set;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
-import org.mockito.internal.util.collections.Sets;
 
 public class ManifestGenerationTest {
 
@@ -66,7 +66,7 @@
 
             assertEquals("Manifest-Version: 1.0", lines[0].trim());
 
-            Set<String> linesSet = Sets.newSet();
+            Set<String> linesSet = new HashSet<>();
             for (String l : lines) {
                 linesSet.add(l.trim());
             }
@@ -102,7 +102,7 @@
 
             assertEquals("Manifest-Version: 1.0", lines[0].trim());
 
-            Set<String> linesSet = Sets.newSet();
+            Set<String> linesSet = new HashSet<>();
             for (String l : lines) {
                 linesSet.add(l.trim());
             }
@@ -140,7 +140,7 @@
 
             assertEquals("Manifest-Version: 1.0", lines[0].trim());
 
-            Set<String> linesSet = Sets.newSet();
+            Set<String> linesSet = new HashSet<>();
             for (String l : lines) {
                 linesSet.add(l.trim());
             }
diff --git a/src/test/java/com/android/builder/internal/packaging/sign/SignatureTestUtils.java b/src/test/java/com/android/apkzlib/sign/SignatureTestUtils.java
similarity index 94%
rename from src/test/java/com/android/builder/internal/packaging/sign/SignatureTestUtils.java
rename to src/test/java/com/android/apkzlib/sign/SignatureTestUtils.java
index f827a43..fb1d322 100644
--- a/src/test/java/com/android/builder/internal/packaging/sign/SignatureTestUtils.java
+++ b/src/test/java/com/android/apkzlib/sign/SignatureTestUtils.java
@@ -14,13 +14,12 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.sign;
+package com.android.apkzlib.sign;
 
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.fail;
 
-import com.android.annotations.NonNull;
-import com.android.builder.internal.utils.ApkZLibPair;
+import com.android.apkzlib.utils.ApkZLibPair;
 import java.math.BigInteger;
 import java.security.KeyPair;
 import java.security.KeyPairGenerator;
@@ -30,6 +29,7 @@
 import java.security.interfaces.ECPublicKey;
 import java.security.interfaces.RSAPublicKey;
 import java.util.Date;
+import javax.annotation.Nonnull;
 import javax.security.auth.x500.X500Principal;
 import org.bouncycastle.asn1.x500.X500Name;
 import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
@@ -54,7 +54,7 @@
      * @return the pair with the private key and certificate
      * @throws Exception failed to generate the signature data
      */
-    @NonNull
+    @Nonnull
     public static ApkZLibPair<PrivateKey, X509Certificate> generateSignaturePre18()
             throws Exception {
         return generateSignature("RSA", "SHA1withRSA");
@@ -66,7 +66,7 @@
      * @return the pair with the private key and certificate
      * @throws Exception failed to generate the signature data
      */
-    @NonNull
+    @Nonnull
     public static ApkZLibPair<PrivateKey, X509Certificate> generateSignaturePos18()
             throws Exception {
         return generateSignature("EC", "SHA256withECDSA");
@@ -80,10 +80,10 @@
      * @return the pair with the private key and certificate
      * @throws Exception failed to generate the signature data
      */
-    @NonNull
+    @Nonnull
     public static ApkZLibPair<PrivateKey, X509Certificate> generateSignature(
-            @NonNull String sign,
-            @NonNull String full)
+            @Nonnull String sign,
+            @Nonnull String full)
             throws Exception {
         // http://stackoverflow.com/questions/28538785/
         // easy-way-to-generate-a-self-signed-certificate-for-java-security-keystore-using
diff --git a/src/test/java/com/android/builder/internal/utils/ApkZFileTestUtils.java b/src/test/java/com/android/apkzlib/utils/ApkZFileTestUtils.java
similarity index 70%
rename from src/test/java/com/android/builder/internal/utils/ApkZFileTestUtils.java
rename to src/test/java/com/android/apkzlib/utils/ApkZFileTestUtils.java
index 88e3c80..1ef087f 100644
--- a/src/test/java/com/android/builder/internal/utils/ApkZFileTestUtils.java
+++ b/src/test/java/com/android/apkzlib/utils/ApkZFileTestUtils.java
@@ -14,18 +14,21 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.utils;
+package com.android.apkzlib.utils;
 
 import static org.junit.Assert.assertTrue;
 
-import com.android.annotations.NonNull;
+import com.android.apkzlib.zip.ZFile;
+import com.android.testutils.TestResources;
 import com.google.common.base.Preconditions;
+import com.google.common.io.ByteSource;
 import com.google.common.io.Resources;
+import java.io.ByteArrayInputStream;
 import java.io.EOFException;
 import java.io.File;
 import java.io.IOException;
 import java.io.RandomAccessFile;
-import java.net.URL;
+import javax.annotation.Nonnull;
 
 /**
  * Utility functions for tests.
@@ -41,8 +44,8 @@
      * @return the bytes read
      * @throws Exception failed to read the file
      */
-    @NonNull
-    public static byte[] readSegment(@NonNull File file, long start, int length) throws Exception {
+    @Nonnull
+    public static byte[] readSegment(@Nonnull File file, long start, int length) throws Exception {
         Preconditions.checkArgument(start >= 0, "start < 0");
         Preconditions.checkArgument(length >= 0, "length < 0");
 
@@ -71,24 +74,22 @@
      * @param path the path
      * @return the test resource
      */
-    @NonNull
-    public static File getResource(@NonNull String path) {
-        URL url = Resources.getResource(ApkZFileTestUtils.class, path);
-        File resource = new File(url.getFile());
+    @Nonnull
+    public static File getResource(@Nonnull String path) {
+        File resource = TestResources.getFile(ApkZFileTestUtils.class, path);
         assertTrue(resource.exists());
         return resource;
     }
 
     /**
-     * Sleeps the current thread for enough time to ensure that the local file system had enough
-     * time to notice a "tick". This method is usually called in tests when it is necessary to
-     * ensure filesystem writes are detected through timestamp modification.
+     * Obtains the test resource with the given path.
      *
-     * @throws InterruptedException waiting interrupted
-     * @throws IOException issues creating a temporary file
+     * @param path the path
+     * @return the test resource
      */
-    public static void waitForFileSystemTick() throws InterruptedException, IOException {
-        waitForFileSystemTick(getFreshTimestamp());
+    @Nonnull
+    public static ByteSource getResourceBytes(@Nonnull String path) {
+        return Resources.asByteSource(Resources.getResource(ApkZFileTestUtils.class, path));
     }
 
     /**
@@ -107,6 +108,22 @@
         }
     }
 
+    /*
+     * Adds a basic compiled AndroidManifest to the given ZFile containing minSdkVersion equal 15
+     * and targetSdkVersion equal 25.
+     */
+    public static void addAndroidManifest(ZFile zf) throws IOException {
+        zf.add("AndroidManifest.xml", new ByteArrayInputStream(getAndroidManifest()));
+    }
+
+    /*
+     * Provides a basic compiled AndroidManifest containing minSdkVersion equal 15 and
+     * targetSdkVersion equal 25.
+     */
+    public static byte[] getAndroidManifest() throws IOException {
+        return ApkZFileTestUtils.getResourceBytes("/testData/packaging/AndroidManifest.xml").read();
+    }
+
     /**
      * Obtains the timestamp of a newly-created file.
      *
diff --git a/src/test/java/com/android/builder/internal/utils/CachedFileContentsTest.java b/src/test/java/com/android/apkzlib/utils/CachedFileContentsTest.java
similarity index 98%
rename from src/test/java/com/android/builder/internal/utils/CachedFileContentsTest.java
rename to src/test/java/com/android/apkzlib/utils/CachedFileContentsTest.java
index 282ca47..f9654d7 100644
--- a/src/test/java/com/android/builder/internal/utils/CachedFileContentsTest.java
+++ b/src/test/java/com/android/apkzlib/utils/CachedFileContentsTest.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.utils;
+package com.android.apkzlib.utils;
 
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
diff --git a/src/test/java/com/android/builder/internal/utils/CachedSupplierTest.java b/src/test/java/com/android/apkzlib/utils/CachedSupplierTest.java
similarity index 98%
rename from src/test/java/com/android/builder/internal/utils/CachedSupplierTest.java
rename to src/test/java/com/android/apkzlib/utils/CachedSupplierTest.java
index e8ce7b3..e687bf3 100644
--- a/src/test/java/com/android/builder/internal/utils/CachedSupplierTest.java
+++ b/src/test/java/com/android/apkzlib/utils/CachedSupplierTest.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.utils;
+package com.android.apkzlib.utils;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
diff --git a/src/test/java/com/android/apkzlib/zfile/ApkAlignmentTest.java b/src/test/java/com/android/apkzlib/zfile/ApkAlignmentTest.java
new file mode 100644
index 0000000..1731ba9
--- /dev/null
+++ b/src/test/java/com/android/apkzlib/zfile/ApkAlignmentTest.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2017 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.apkzlib.zfile;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import com.android.apkzlib.zip.CompressionMethod;
+import com.android.apkzlib.zip.StoredEntry;
+import com.android.apkzlib.zip.ZFile;
+import com.android.apkzlib.zip.ZFileOptions;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.nio.file.Files;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+public class ApkAlignmentTest {
+    @Rule public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+
+    @Test
+    public void soFilesUncompressedAndAligned() throws Exception {
+        File apk = new File(mTemporaryFolder.getRoot(), "a.apk");
+
+        File soFile = new File(mTemporaryFolder.getRoot(), "doesnt_work.so");
+        Files.write(soFile.toPath(), new byte[500]);
+
+        ApkZFileCreatorFactory cf = new ApkZFileCreatorFactory(new ZFileOptions());
+        ApkCreatorFactory.CreationData creationData =
+                new ApkCreatorFactory.CreationData(
+                        apk,
+                        null,
+                        null,
+                        false,
+                        false,
+                        null,
+                        null,
+                        20,
+                        NativeLibrariesPackagingMode.UNCOMPRESSED_AND_ALIGNED,
+                        path -> false);
+
+        ApkCreator creator = cf.make(creationData);
+
+        creator.writeFile(soFile, "/doesnt_work.so");
+        creator.close();
+
+        try (ZFile zf = new ZFile(apk)) {
+            StoredEntry soEntry = zf.get("/doesnt_work.so");
+            assertNotNull(soEntry);
+            assertEquals(
+                    CompressionMethod.STORE,
+                    soEntry.getCentralDirectoryHeader().getCompressionInfoWithWait().getMethod());
+            long offset =
+                    soEntry.getCentralDirectoryHeader().getOffset() + soEntry.getLocalHeaderSize();
+            assertTrue(offset % 4096 == 0);
+        }
+    }
+
+    @Test
+    public void soFilesMergedFromZipsCanBeUncompressedAndAligned() throws Exception {
+
+        // Create a zip file with a compressed, unaligned so file.
+        File zipToMerge = new File(mTemporaryFolder.getRoot(), "a.zip");
+        try (ZFile zf = new ZFile(zipToMerge)) {
+            zf.add("/zero.so", new ByteArrayInputStream(new byte[500]));
+        }
+
+        try (ZFile zf = new ZFile(zipToMerge)) {
+            StoredEntry zeroSo = zf.get("/zero.so");
+            assertNotNull(zeroSo);
+            assertEquals(
+                    CompressionMethod.DEFLATE,
+                    zeroSo.getCentralDirectoryHeader().getCompressionInfoWithWait().getMethod());
+            long offset =
+                    zeroSo.getCentralDirectoryHeader().getOffset() + zeroSo.getLocalHeaderSize();
+            assertFalse(offset % 4096 == 0);
+        }
+
+        // Create an APK and merge the zip file.
+        File apk = new File(mTemporaryFolder.getRoot(), "b.apk");
+        ApkZFileCreatorFactory cf = new ApkZFileCreatorFactory(new ZFileOptions());
+        ApkCreatorFactory.CreationData creationData =
+                new ApkCreatorFactory.CreationData(
+                        apk,
+                        null,
+                        null,
+                        false,
+                        false,
+                        null,
+                        null,
+                        20,
+                        NativeLibrariesPackagingMode.UNCOMPRESSED_AND_ALIGNED,
+                        path -> false);
+
+        try (ApkCreator creator = cf.make(creationData)) {
+            creator.writeZip(zipToMerge, null, null);
+        }
+
+        // Make sure the file is uncompressed and aligned.
+        try (ZFile zf = new ZFile(apk)) {
+            StoredEntry soEntry = zf.get("/zero.so");
+            assertNotNull(soEntry);
+            assertEquals(
+                    CompressionMethod.STORE,
+                    soEntry.getCentralDirectoryHeader().getCompressionInfoWithWait().getMethod());
+            long offset =
+                    soEntry.getCentralDirectoryHeader().getOffset() + soEntry.getLocalHeaderSize();
+            assertTrue(offset % 4096 == 0);
+
+            byte[] data = soEntry.read();
+            assertEquals(500, data.length);
+            for (int i = 0; i < data.length; i++) {
+                assertEquals(0, data[i]);
+            }
+        }
+    }
+
+    @Test
+    public void soFilesUncompressedAndNotAligned() throws Exception {
+        File apk = new File(mTemporaryFolder.getRoot(), "a.apk");
+
+        File soFile = new File(mTemporaryFolder.getRoot(), "doesnt_work.so");
+        Files.write(soFile.toPath(), new byte[500]);
+
+        ApkZFileCreatorFactory cf = new ApkZFileCreatorFactory(new ZFileOptions());
+        ApkCreatorFactory.CreationData creationData =
+                new ApkCreatorFactory.CreationData(
+                        apk,
+                        null,
+                        null,
+                        false,
+                        false,
+                        null,
+                        null,
+                        20,
+                        NativeLibrariesPackagingMode.COMPRESSED,
+                        path -> false);
+
+        ApkCreator creator = cf.make(creationData);
+
+        creator.writeFile(soFile, "/doesnt_work.so");
+        creator.close();
+
+        try (ZFile zf = new ZFile(apk)) {
+            StoredEntry soEntry = zf.get("/doesnt_work.so");
+            assertNotNull(soEntry);
+            assertEquals(
+                    CompressionMethod.DEFLATE,
+                    soEntry.getCentralDirectoryHeader().getCompressionInfoWithWait().getMethod());
+            long offset =
+                    soEntry.getCentralDirectoryHeader().getOffset() + soEntry.getLocalHeaderSize();
+            assertTrue(offset % 4096 != 0);
+        }
+    }
+
+    @Test
+    public void soFilesMergedFromZipsCanBeUncompressedAndNotAligned() throws Exception {
+
+        // Create a zip file with a compressed, unaligned so file.
+        File zipToMerge = new File(mTemporaryFolder.getRoot(), "a.zip");
+        try (ZFile zf = new ZFile(zipToMerge)) {
+            zf.add("/zero.so", new ByteArrayInputStream(new byte[500]));
+        }
+
+        try (ZFile zf = new ZFile(zipToMerge)) {
+            StoredEntry zeroSo = zf.get("/zero.so");
+            assertNotNull(zeroSo);
+            assertEquals(
+                    CompressionMethod.DEFLATE,
+                    zeroSo.getCentralDirectoryHeader().getCompressionInfoWithWait().getMethod());
+            long offset =
+                    zeroSo.getCentralDirectoryHeader().getOffset() + zeroSo.getLocalHeaderSize();
+            assertFalse(offset % 4096 == 0);
+        }
+
+        // Create an APK and merge the zip file.
+        File apk = new File(mTemporaryFolder.getRoot(), "b.apk");
+        ApkZFileCreatorFactory cf = new ApkZFileCreatorFactory(new ZFileOptions());
+        ApkCreatorFactory.CreationData creationData =
+                new ApkCreatorFactory.CreationData(
+                        apk,
+                        null,
+                        null,
+                        false,
+                        false,
+                        null,
+                        null,
+                        20,
+                        NativeLibrariesPackagingMode.COMPRESSED,
+                        path -> false);
+
+        try (ApkCreator creator = cf.make(creationData)) {
+            creator.writeZip(zipToMerge, null, null);
+        }
+
+        // Make sure the file is uncompressed and aligned.
+        try (ZFile zf = new ZFile(apk)) {
+            StoredEntry soEntry = zf.get("/zero.so");
+            assertNotNull(soEntry);
+            assertEquals(
+                    CompressionMethod.DEFLATE,
+                    soEntry.getCentralDirectoryHeader().getCompressionInfoWithWait().getMethod());
+            long offset =
+                    soEntry.getCentralDirectoryHeader().getOffset() + soEntry.getLocalHeaderSize();
+            assertTrue(offset % 4096 != 0);
+
+            byte[] data = soEntry.read();
+            assertEquals(500, data.length);
+            for (int i = 0; i < data.length; i++) {
+                assertEquals(0, data[i]);
+            }
+        }
+    }
+}
diff --git a/src/test/java/com/android/builder/internal/packaging/zip/AlignmentTest.java b/src/test/java/com/android/apkzlib/zip/AlignmentTest.java
similarity index 87%
rename from src/test/java/com/android/builder/internal/packaging/zip/AlignmentTest.java
rename to src/test/java/com/android/apkzlib/zip/AlignmentTest.java
index 3241bd3..e94a876 100644
--- a/src/test/java/com/android/builder/internal/packaging/zip/AlignmentTest.java
+++ b/src/test/java/com/android/apkzlib/zip/AlignmentTest.java
@@ -14,9 +14,9 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
 
-import static com.android.builder.internal.utils.ApkZFileTestUtils.readSegment;
+import static com.android.apkzlib.utils.ApkZFileTestUtils.readSegment;
 import static junit.framework.TestCase.assertEquals;
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertFalse;
@@ -770,4 +770,87 @@
         assertArrayEquals(recognizable2, readSegment(zipFile, 150, recognizable2.length));
         assertArrayEquals(twoHundred, readSegment(zipFile, 204, twoHundred.length));
     }
+
+    @Test
+    public void alignCoveringEmptySpaceWhenExtraFieldIsInvalid() throws Exception {
+        File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
+        ZFileOptions options = new ZFileOptions();
+        options.setCoverEmptySpaceUsingExtraField(true);
+        options.setAlignmentRule(AlignmentRules.constant(100));
+        try (ZFile zf = new ZFile(zipFile, options)) {
+            zf.add("foo", new ByteArrayInputStream(new byte[] { 1, 2, 3, 4 }));
+            StoredEntry foo = zf.get("foo");
+            assertNotNull(foo);
+            foo.setLocalExtra(new ExtraField(new byte[] { 0, 0 }));
+            zf.add("bar", new ByteArrayInputStream(new byte[] { 5, 6, 7, 8 }));
+        }
+    }
+
+    @Test
+    public void fourByteAlignment() throws Exception {
+        // When aligning with 4 bytes, there are are only 3 possible cases:
+        // - We're 2 bytes short and so need to add +6 bytes (6 bytes for header + no zeroes)
+        // - We're 3 bytes short and so need to add +7 bytes (6 bytes for header + 1 zero)
+        // - We're 1 byte short and so need to add +9 bytes (6 bytes for header + 3 zeroes)
+
+        File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
+        ZFileOptions options = new ZFileOptions();
+        options.setCoverEmptySpaceUsingExtraField(true);
+        options.setAlignmentRule(AlignmentRules.constant(4));
+        try (ZFile zf = new ZFile(zipFile, options)) {
+            // File header starts at 0.
+            // File name starts at 30 (LOCAL_HEADER_SIZE).
+            // If unaligned we would have data starting at 33, but with aligned we have data
+            // starting at 40 (36 isn't enough for the extra data header).
+            String fooName = "foo";
+            byte[] fooData = new byte[] { 1, 2, 3, 4, 5 };
+            zf.add(fooName, new ByteArrayInputStream(fooData), false);
+            zf.update();
+            StoredEntry foo = zf.get(fooName);
+            long fooOffset = ZFileTestConstants.LOCAL_HEADER_SIZE + fooName.length() + 7;
+            assertEquals(fooOffset, foo.getLocalHeaderSize());
+
+            // Bar header starts at 45 (foo data starts at 40 and is 5 bytes long).
+            // Bar header ends at 75.
+            // If unaligned we would have data starting at 78, but with aligned we have data
+            // starting at 84 (80 isn't enough for the extra header).
+            String barName = "bar";
+            byte[] barData = new byte[] { 6 };
+            zf.add(barName, new ByteArrayInputStream(barData), false);
+            zf.update();
+
+            StoredEntry bar = zf.get(barName);
+            long barStart = bar.getCentralDirectoryHeader().getOffset();
+            assertEquals(fooOffset + fooData.length, barStart);
+
+            long barStartOffset = ZFileTestConstants.LOCAL_HEADER_SIZE + barName.length() + 6;
+            assertEquals(barStartOffset, bar.getLocalHeaderSize());
+
+            // Xpto header starts at 85 (bar data starts at 84 and is 1 byte long).
+            // Xpto header ends at 115.
+            // If unaligned we would have data starting at 119, but with aligned we have data
+            // starting at 128 (120 & 124 are not enough for the extra header).
+            String xptoName = "xpto";
+            byte[] xptoData = new byte[] { 7, 8, 9, 10 };
+            zf.add(xptoName, new ByteArrayInputStream(xptoData), false);
+            zf.update();
+
+            StoredEntry xpto = zf.get(xptoName);
+            long xptoStart = xpto.getCentralDirectoryHeader().getOffset();
+            assertEquals(barStart + barStartOffset + barData.length, xptoStart);
+
+            long xptoStartOffset = ZFileTestConstants.LOCAL_HEADER_SIZE + xptoName.length() + 9;
+            assertEquals(xptoStartOffset, xpto.getLocalHeaderSize());
+
+            // Dummy header starts at 133 (xpto data starts at 128 and is 6 bytes long).
+            String dummyName = "dummy";
+            byte[] dummyData = new byte[] { 11 };
+            zf.add(dummyName, new ByteArrayInputStream(dummyData), false);
+            zf.update();
+
+            StoredEntry dummy = zf.get(dummyName);
+            long dummyStart = dummy.getCentralDirectoryHeader().getOffset();
+            assertEquals(xptoStart + xptoStartOffset + xptoData.length, dummyStart);
+        }
+    }
 }
diff --git a/src/test/java/com/android/builder/internal/packaging/zip/EncodeUtilsTest.java b/src/test/java/com/android/apkzlib/zip/EncodeUtilsTest.java
similarity index 82%
rename from src/test/java/com/android/builder/internal/packaging/zip/EncodeUtilsTest.java
rename to src/test/java/com/android/apkzlib/zip/EncodeUtilsTest.java
index 91e9676..8648aa0 100644
--- a/src/test/java/com/android/builder/internal/packaging/zip/EncodeUtilsTest.java
+++ b/src/test/java/com/android/apkzlib/zip/EncodeUtilsTest.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
@@ -56,4 +56,17 @@
                 (byte) 0xd0, (byte) 0xb0 }, encoded);
         assertEquals(kazakhCapital, EncodeUtils.decode(encoded, flags));
     }
+
+    @Test
+    public void asciiDecodeAsUtf8() {
+        byte[] greatWallChinese =
+                new byte[] {
+                    (byte) 0xe9, (byte) 0x95, (byte) 0xB7, (byte) 0xe5, (byte) 0x9F, (byte) 0x8E
+                };
+
+        GPFlags flags = GPFlags.make(false);
+
+        String text = EncodeUtils.decode(greatWallChinese, flags);
+        assertEquals("\u9577\u57ce", text);
+    }
 }
diff --git a/src/test/java/com/android/builder/internal/packaging/zip/ExtraFieldTest.java b/src/test/java/com/android/apkzlib/zip/ExtraFieldTest.java
similarity index 93%
rename from src/test/java/com/android/builder/internal/packaging/zip/ExtraFieldTest.java
rename to src/test/java/com/android/apkzlib/zip/ExtraFieldTest.java
index d4275d0..2371849 100644
--- a/src/test/java/com/android/builder/internal/packaging/zip/ExtraFieldTest.java
+++ b/src/test/java/com/android/apkzlib/zip/ExtraFieldTest.java
@@ -14,11 +14,12 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
 
 import com.google.common.collect.ImmutableList;
 
@@ -332,4 +333,29 @@
             assertArrayEquals(new byte[] { 0x54, 0x76, 0x04, 0x00, 2, 4, 2, 4 }, sData);
         }
     }
+
+    @Test
+    public void parseInvalidExtraFieldWithInvalidHeader() throws Exception {
+        byte[] raw = new byte[1];
+        ExtraField ef = new ExtraField(raw);
+        try {
+            ef.getSegments();
+            fail();
+        } catch (IOException e) {
+            // Expected.
+        }
+    }
+
+    @Test
+    public void parseInvalidExtraFieldWithInsufficientData() throws Exception {
+        // Remember: 0x05, 0x00 = 5 in little endian!
+        byte[] raw = new byte[] { /* Header */ 0x01, 0x02, /* Size */ 0x05, 0x00, /* Data */ 0x01 };
+        ExtraField ef = new ExtraField(raw);
+        try {
+            ef.getSegments();
+            fail();
+        } catch (IOException e) {
+            // Expected.
+        }
+    }
 }
diff --git a/src/test/java/com/android/builder/internal/packaging/zip/FileUseMapTest.java b/src/test/java/com/android/apkzlib/zip/FileUseMapTest.java
similarity index 98%
rename from src/test/java/com/android/builder/internal/packaging/zip/FileUseMapTest.java
rename to src/test/java/com/android/apkzlib/zip/FileUseMapTest.java
index a53e9cb..0ee0129 100644
--- a/src/test/java/com/android/builder/internal/packaging/zip/FileUseMapTest.java
+++ b/src/test/java/com/android/apkzlib/zip/FileUseMapTest.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.fail;
diff --git a/src/test/java/com/android/builder/internal/packaging/zip/OldApkReadTest.java b/src/test/java/com/android/apkzlib/zip/OldApkReadTest.java
similarity index 90%
rename from src/test/java/com/android/builder/internal/packaging/zip/OldApkReadTest.java
rename to src/test/java/com/android/apkzlib/zip/OldApkReadTest.java
index 3121c44..61a08d7 100644
--- a/src/test/java/com/android/builder/internal/packaging/zip/OldApkReadTest.java
+++ b/src/test/java/com/android/apkzlib/zip/OldApkReadTest.java
@@ -14,12 +14,12 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
 
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
-import com.android.builder.internal.utils.ApkZFileTestUtils;
+import com.android.apkzlib.utils.ApkZFileTestUtils;
 import java.io.File;
 import org.junit.Test;
 
diff --git a/src/test/java/com/android/builder/internal/packaging/zip/ReadWithDifferentCompressionLevelsTest.java b/src/test/java/com/android/apkzlib/zip/ReadWithDifferentCompressionLevelsTest.java
similarity index 92%
rename from src/test/java/com/android/builder/internal/packaging/zip/ReadWithDifferentCompressionLevelsTest.java
rename to src/test/java/com/android/apkzlib/zip/ReadWithDifferentCompressionLevelsTest.java
index 154dfcd..4301710 100644
--- a/src/test/java/com/android/builder/internal/packaging/zip/ReadWithDifferentCompressionLevelsTest.java
+++ b/src/test/java/com/android/apkzlib/zip/ReadWithDifferentCompressionLevelsTest.java
@@ -14,12 +14,12 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
 
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
-import com.android.builder.internal.utils.ApkZFileTestUtils;
+import com.android.apkzlib.utils.ApkZFileTestUtils;
 import java.io.File;
 import org.junit.Test;
 
diff --git a/src/test/java/com/android/builder/internal/packaging/zip/ZFileNotificationTest.java b/src/test/java/com/android/apkzlib/zip/ZFileNotificationTest.java
similarity index 96%
rename from src/test/java/com/android/builder/internal/packaging/zip/ZFileNotificationTest.java
rename to src/test/java/com/android/apkzlib/zip/ZFileNotificationTest.java
index b7c490f..84be460 100644
--- a/src/test/java/com/android/builder/internal/packaging/zip/ZFileNotificationTest.java
+++ b/src/test/java/com/android/apkzlib/zip/ZFileNotificationTest.java
@@ -14,28 +14,26 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
 
-import com.android.annotations.NonNull;
-import com.android.annotations.Nullable;
-import com.android.builder.internal.utils.ApkZLibPair;
-import com.android.builder.internal.utils.IOExceptionRunnable;
+import com.android.apkzlib.utils.ApkZLibPair;
+import com.android.apkzlib.utils.IOExceptionRunnable;
 import com.google.common.collect.Lists;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-import org.mockito.Mockito;
-
 import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.IOException;
 import java.util.List;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.mockito.Mockito;
 
 public class ZFileNotificationTest {
     private static class KeepListener extends ZFileExtension {
@@ -77,7 +75,7 @@
 
         @Nullable
         @Override
-        public IOExceptionRunnable added(@NonNull StoredEntry entry,
+        public IOExceptionRunnable added(@Nonnull StoredEntry entry,
                 @Nullable StoredEntry replaced) {
             added.add(new ApkZLibPair<>(entry, replaced));
             return returnRunnable;
@@ -85,7 +83,7 @@
 
         @Nullable
         @Override
-        public IOExceptionRunnable removed(@NonNull StoredEntry entry) {
+        public IOExceptionRunnable removed(@Nonnull StoredEntry entry) {
             removed.add(entry);
             return returnRunnable;
         }
diff --git a/src/test/java/com/android/apkzlib/zip/ZFileReadOnlyTest.java b/src/test/java/com/android/apkzlib/zip/ZFileReadOnlyTest.java
new file mode 100644
index 0000000..a030a83
--- /dev/null
+++ b/src/test/java/com/android/apkzlib/zip/ZFileReadOnlyTest.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright (C) 2017 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.apkzlib.zip;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import javax.annotation.Nonnull;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+public class ZFileReadOnlyTest {
+    @Rule
+    public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+    @Test
+    public void cannotCreateRoFileOnNonExistingFile() throws Exception {
+        try {
+            new ZFile(new File(temporaryFolder.getRoot(), "foo.zip"), new ZFileOptions(), true);
+            fail();
+        } catch (IOException e) {
+            // Expected.
+        }
+    }
+
+    @Nonnull
+    private File makeTestZip() throws IOException {
+        File zip = new File(temporaryFolder.getRoot(), "foo.zip");
+        try (ZFile zf = new ZFile(zip)) {
+            zf.add("bar", new ByteArrayInputStream(new byte[] { 0, 1, 2, 3, 4, 5 }));
+        }
+
+        return zip;
+    }
+
+    @Test
+    public void cannotUpdateInRoMode() throws Exception {
+        try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) {
+            try {
+                zf.update();
+                fail();
+            } catch (IllegalStateException e) {
+                // Expeted.
+            }
+        }
+    }
+
+    @Test
+    public void cannotAddFilesInRoMode() throws Exception {
+        try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) {
+            try {
+                zf.add("bar2", new ByteArrayInputStream(new byte[] { 6, 7, }));
+                fail();
+            } catch (IllegalStateException e) {
+                // Expeted.
+            }
+        }
+    }
+
+    @Test
+    public void cannotAddRecursivelyInRoMode() throws Exception {
+        File folder = temporaryFolder.newFolder();
+        try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) {
+            try {
+                zf.addAllRecursively(folder);
+                fail();
+            } catch (IllegalStateException e) {
+                // Expeted.
+            }
+        }
+    }
+
+    @Test
+    public void cannotReplaceFilesInRoMode() throws Exception {
+        try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) {
+            try {
+                zf.add("bar", new ByteArrayInputStream(new byte[] { 6, 7 }));
+                fail();
+            } catch (IllegalStateException e) {
+                // Expeted.
+            }
+        }
+    }
+
+    @Test
+    public void cannotDeleteFilesInRoMode() throws Exception {
+        try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) {
+            StoredEntry bar = zf.get("bar");
+            assertNotNull(bar);
+            try {
+                bar.delete();
+                fail();
+            } catch (IllegalStateException e) {
+                // Expeted.
+            }
+        }
+    }
+
+    @Test
+    public void cannotMergeInRoMode() throws Exception {
+        try (ZFile toMerge = new ZFile(new File(temporaryFolder.getRoot(), "a.zip"))) {
+            try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) {
+                try {
+                    zf.mergeFrom(toMerge, s -> false);
+                    fail();
+                } catch (IllegalStateException e) {
+                    // Expeted.
+                }
+            }
+        }
+    }
+
+    @Test
+    public void cannotTouchInRoMode() throws Exception {
+        try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) {
+            try {
+                zf.touch();
+                fail();
+            } catch (IllegalStateException e) {
+                // Expeted.
+            }
+        }
+    }
+
+    @Test
+    public void cannotRealignInRoMode() throws Exception {
+        try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) {
+            try {
+                zf.realign();
+                fail();
+            } catch (IllegalStateException e) {
+                // Expeted.
+            }
+        }
+    }
+
+    @Test
+    public void cannotAddExtensionInRoMode() throws Exception {
+        try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) {
+            try {
+                zf.addZFileExtension(new ZFileExtension() {});
+                fail();
+            } catch (IllegalStateException e) {
+                // Expeted.
+            }
+        }
+    }
+
+    @Test
+    public void cannotDirectWriteInRoMode() throws Exception {
+        try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) {
+            try {
+                zf.directWrite(0, new byte[1]);
+                fail();
+            } catch (IllegalStateException e) {
+                // Expeted.
+            }
+        }
+    }
+
+    @Test
+    public void cannotSetEocdCommentInRoMode() throws Exception {
+        try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) {
+            try {
+                zf.setEocdComment(new byte[2]);
+                fail();
+            } catch (IllegalStateException e) {
+                // Expeted.
+            }
+        }
+    }
+
+    @Test
+    public void cannotSetCentralDirectoryOffsetInRoMode() throws Exception {
+        try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) {
+            try {
+                zf.setExtraDirectoryOffset(4);
+                fail();
+            } catch (IllegalStateException e) {
+                // Expeted.
+            }
+        }
+    }
+
+    @Test
+    public void cannotSortZipContentsInRoMode() throws Exception {
+        try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) {
+            try {
+                zf.sortZipContents();
+                fail();
+            } catch (IllegalStateException e) {
+                // Expeted.
+            }
+        }
+    }
+
+    @Test
+    public void canOpenAndReadFilesInRoMode() throws Exception {
+        try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) {
+            StoredEntry bar = zf.get("bar");
+            assertNotNull(bar);
+            assertArrayEquals(new byte[] { 0, 1, 2, 3, 4, 5 }, bar.read());
+        }
+    }
+
+    @Test
+    public void canGetDirectoryAndEocdBytesInRoMode() throws Exception {
+        try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) {
+            zf.getCentralDirectoryBytes();
+            zf.getEocdBytes();
+            zf.getEocdComment();
+        }
+    }
+
+    @Test
+    public void canDirectReadInRoMode() throws Exception {
+        try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) {
+            zf.directRead(0, new byte[2]);
+        }
+    }
+}
diff --git a/src/test/java/com/android/builder/internal/packaging/zip/ZFileSortTest.java b/src/test/java/com/android/apkzlib/zip/ZFileSortTest.java
similarity index 98%
rename from src/test/java/com/android/builder/internal/packaging/zip/ZFileSortTest.java
rename to src/test/java/com/android/apkzlib/zip/ZFileSortTest.java
index 41e5301..869f73a 100644
--- a/src/test/java/com/android/builder/internal/packaging/zip/ZFileSortTest.java
+++ b/src/test/java/com/android/apkzlib/zip/ZFileSortTest.java
@@ -14,16 +14,16 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
-import com.android.annotations.Nullable;
 import java.io.ByteArrayInputStream;
 import java.io.File;
+import javax.annotation.Nullable;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
diff --git a/src/test/java/com/android/builder/internal/packaging/zip/ZFileTest.java b/src/test/java/com/android/apkzlib/zip/ZFileTest.java
similarity index 75%
rename from src/test/java/com/android/builder/internal/packaging/zip/ZFileTest.java
rename to src/test/java/com/android/apkzlib/zip/ZFileTest.java
index 0b54b44..b7f2979 100644
--- a/src/test/java/com/android/builder/internal/packaging/zip/ZFileTest.java
+++ b/src/test/java/com/android/apkzlib/zip/ZFileTest.java
@@ -14,23 +14,23 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
 
-import static com.android.builder.internal.utils.ApkZFileTestUtils.readSegment;
+import static com.android.apkzlib.utils.ApkZFileTestUtils.readSegment;
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
-import com.android.annotations.NonNull;
-import com.android.builder.internal.packaging.zip.compress.DeflateExecutionCompressor;
-import com.android.builder.internal.packaging.zip.utils.CloseableByteSource;
-import com.android.builder.internal.packaging.zip.utils.RandomAccessFileUtils;
+import com.android.apkzlib.zip.compress.DeflateExecutionCompressor;
+import com.android.apkzlib.zip.utils.CloseableByteSource;
+import com.android.apkzlib.zip.utils.RandomAccessFileUtils;
 import com.google.common.base.Charsets;
 import com.google.common.base.Strings;
 import com.google.common.base.Throwables;
@@ -38,11 +38,6 @@
 import com.google.common.io.ByteStreams;
 import com.google.common.io.Closer;
 import com.google.common.io.Files;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.File;
@@ -51,6 +46,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.RandomAccessFile;
+import java.util.Locale;
 import java.util.Random;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -59,6 +55,10 @@
 import java.util.zip.ZipFile;
 import java.util.zip.ZipInputStream;
 import java.util.zip.ZipOutputStream;
+import javax.annotation.Nonnull;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
 
 public class ZFileTest {
     @Rule
@@ -148,6 +148,21 @@
 
         try (ZFile zf = new ZFile(testZip)) {
             assertEquals(1, zf.entries().size());
+            assertTrue(zf.getCentralDirectoryOffset() > 0);
+            assertTrue(zf.getEocdOffset() > 0);
+        }
+    }
+
+    @Test
+    public void readOnlyV2SignedApkSupport() throws Exception {
+        File testZip = ZipTestUtils.cloneRsrc("v2-signed.apk", mTemporaryFolder);
+
+        assertTrue(testZip.setWritable(false));
+
+        try (ZFile zf = new ZFile(testZip)) {
+            assertEquals(416, zf.entries().size());
+            assertTrue(zf.getCentralDirectoryOffset() > 0);
+            assertTrue(zf.getEocdOffset() > 0);
         }
     }
 
@@ -938,6 +953,31 @@
                     filetMignonKorean + " " + isGoodJapanese,
                     entry.getCentralDirectoryHeader().getName());
             assertArrayEquals(
+                    "Stuff about food is good.\n".getBytes(Charsets.US_ASCII), entry.read());
+        }
+    }
+
+    @Test
+    public void utf8NamesSupportedOnReadingWithoutUtf8Flag() throws Exception {
+        File zip = ZipTestUtils.cloneRsrc("zip-with-utf8-filename.zip", mTemporaryFolder);
+
+        // Reset bytes 7 and 122 that have the flag in the local header and central directory.
+        byte[] data = Files.toByteArray(zip);
+        data[7] = 0;
+        data[122] = 0;
+        Files.write(data, zip);
+
+        try (ZFile f = new ZFile(zip)) {
+            assertEquals(1, f.entries().size());
+
+            StoredEntry entry = f.entries().iterator().next();
+            String filetMignonKorean = "\uc548\uc2eC \uc694\ub9ac";
+            String isGoodJapanese = "\u3068\u3066\u3082\u826f\u3044";
+
+            assertEquals(
+                    filetMignonKorean + " " + isGoodJapanese,
+                    entry.getCentralDirectoryHeader().getName());
+            assertArrayEquals(
                     "Stuff about food is good.\n".getBytes(Charsets.US_ASCII),
                     entry.read());
         }
@@ -1027,9 +1067,9 @@
         boolean[] done = new boolean[1];
         options.setCompressor(new DeflateExecutionCompressor(executor, options.getTracker(),
                 Deflater.BEST_COMPRESSION) {
-            @NonNull
+            @Nonnull
             @Override
-            protected CompressionResult immediateCompress(@NonNull CloseableByteSource source)
+            protected CompressionResult immediateCompress(@Nonnull CloseableByteSource source)
                     throws Exception {
                 Thread.sleep(500);
                 CompressionResult cr = super.immediateCompress(source);
@@ -1371,6 +1411,7 @@
         /*
          * Open the zip file and compute where the local header CRC32 is.
          */
+        long crcOffset;
         try (ZFile zf = new ZFile(zipFile)) {
             StoredEntry se = zf.get("foo");
             assertNotNull(se);
@@ -1378,41 +1419,403 @@
 
             /*
              * Twelve bytes from the CD offset, we have the start of the CRC32 of the zip entry.
-             * Corrupt it.
              */
-            byte[] crc = new byte[4];
-            zf.directFullyRead(cdOffset - 12, crc);
-            crc[0]++;
-            zf.directWrite(cdOffset - 12, crc);
+            crcOffset = cdOffset - 12;
         }
 
         /*
-         * Now open the zip file and it should fail.
+         * Corrupt the CRC32.
          */
-        try {
-            new ZFile(zipFile);
-            fail();
-        } catch (IOException e) {
-            /*
-             * We should be complaining about the CRC32 somewhere...
-             */
-            boolean foundCrc32Complain = false;
-
-            assertTrue(
-                    Throwables.getCausalChain(e).stream()
-                            .map(Throwable::getMessage)
-                            .anyMatch(s -> s.contains("CRC32")));
+        byte[] crc = readSegment(zipFile, crcOffset, 4);
+        crc[0]++;
+        try (RandomAccessFile raf = new RandomAccessFile(zipFile, "rw")) {
+            raf.seek(crcOffset);
+            raf.write(crc);
         }
 
         /*
-         * But opening with data validation skip should work.
+         * Now open the zip file and it should write a message in the log.
          */
         ZFileOptions options = new ZFileOptions();
-        options.setSkipDataDescriptionValidation(true);
+        options.setVerifyLogFactory(VerifyLogs::unlimited);
         try (ZFile zf = new ZFile(zipFile, options)) {
+            VerifyLog vl = zf.getVerifyLog();
+            assertTrue(vl.getLogs().isEmpty());
+            StoredEntry fooEntry = zf.get("foo");
+            vl = fooEntry.getVerifyLog();
+            assertEquals(1, vl.getLogs().size());
+            assertTrue(vl.getLogs().get(0).contains("CRC32"));
+        }
+    }
+
+    @Test
+    public void detectIncorrectVersionToExtractInCentralDirectory() throws Exception {
+        File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
+
+        /*
+         * Create a valid zip file.
+         */
+        try (ZFile zf = new ZFile(zipFile)) {
+            zf.add("foo", new ByteArrayInputStream(new byte[0]));
+        }
+
+        /*
+         * Change the "version to extract" in the central directory to 0x7777.
+         */
+        int versionToExtractOffset =
+                ZFileTestConstants.LOCAL_HEADER_SIZE
+                        + 3
+                        + CentralDirectory.F_VERSION_EXTRACT.offset();
+        byte[] allZipBytes = Files.toByteArray(zipFile);
+        allZipBytes[versionToExtractOffset] = 0x77;
+        allZipBytes[versionToExtractOffset + 1] = 0x77;
+        Files.write(allZipBytes, zipFile);
+
+        /*
+         * Opening the file and it should write a message in the log. The entry has the right
+         * version to extract (20), but it issues a warning because it is not equal to the one
+         * in the central directory.
+         */
+        ZFileOptions options = new ZFileOptions();
+        options.setVerifyLogFactory(VerifyLogs::unlimited);
+        try (ZFile zf = new ZFile(zipFile, options)) {
+            VerifyLog vl = zf.getVerifyLog();
+            assertEquals(1, vl.getLogs().size());
+            assertTrue(vl.getLogs().get(0).toLowerCase(Locale.US).contains("version"));
+            assertTrue(vl.getLogs().get(0).toLowerCase(Locale.US).contains("extract"));
+            StoredEntry fooEntry = zf.get("foo");
+            vl = fooEntry.getVerifyLog();
+            assertEquals(1, vl.getLogs().size());
+            assertTrue(vl.getLogs().get(0).toLowerCase(Locale.US).contains("version"));
+            assertTrue(vl.getLogs().get(0).toLowerCase(Locale.US).contains("extract"));
+        }
+    }
+
+    @Test
+    public void detectIncorrectVersionToExtractInLocalHeader() throws Exception {
+        File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
+
+        /*
+         * Create a valid zip file.
+         */
+        try (ZFile zf = new ZFile(zipFile)) {
+            zf.add("foo", new ByteArrayInputStream(new byte[0]));
+        }
+
+        /*
+         * Change the "version to extract" in the local header to 0x7777.
+         */
+        int versionToExtractOffset = StoredEntry.F_VERSION_EXTRACT.offset();
+        byte[] allZipBytes = Files.toByteArray(zipFile);
+        allZipBytes[versionToExtractOffset] = 0x77;
+        allZipBytes[versionToExtractOffset + 1] = 0x77;
+        Files.write(allZipBytes, zipFile);
+
+        /*
+         * Opening the file should log an error message.
+         */
+        ZFileOptions options = new ZFileOptions();
+        options.setVerifyLogFactory(VerifyLogs::unlimited);
+        try (ZFile zf = new ZFile(zipFile, options)) {
+            VerifyLog vl = zf.getVerifyLog();
+            assertTrue(vl.getLogs().isEmpty());
+            StoredEntry fooEntry = zf.get("foo");
+            vl = fooEntry.getVerifyLog();
+            assertEquals(1, vl.getLogs().size());
+            assertTrue(vl.getLogs().get(0).toLowerCase(Locale.US).contains("version"));
+            assertTrue(vl.getLogs().get(0).toLowerCase(Locale.US).contains("extract"));
+        }
+    }
+
+    @Test
+    public void sortZipContentsWithDeferredCrc32() throws Exception {
+        /*
+         * Create a zip file with deferred CRC32 and files in non-alphabetical order.
+         * ZipOutputStream always creates deferred CRC32 entries.
+         */
+        File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
+        try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) {
+            zos.putNextEntry(new ZipEntry("b"));
+            zos.write(new byte[1000]);
+            zos.putNextEntry(new ZipEntry("a"));
+            zos.write(new byte[1000]);
+        }
+
+        /*
+         * Now open the zip using a ZFile and sort the contents and check that the deferred CRC32
+         * bits were reset.
+         */
+        try (ZFile zf = new ZFile(zipFile)) {
+            StoredEntry a = zf.get("a");
+            assertNotNull(a);
+            assertNotSame(DataDescriptorType.NO_DATA_DESCRIPTOR, a.getDataDescriptorType());
+            StoredEntry b = zf.get("b");
+            assertNotNull(b);
+            assertNotSame(DataDescriptorType.NO_DATA_DESCRIPTOR, b.getDataDescriptorType());
+            assertTrue(
+                    a.getCentralDirectoryHeader().getOffset()
+                            > b.getCentralDirectoryHeader().getOffset());
+
+            zf.sortZipContents();
+            zf.update();
+
+            a = zf.get("a");
+            assertNotNull(a);
+            assertSame(DataDescriptorType.NO_DATA_DESCRIPTOR, a.getDataDescriptorType());
+            b = zf.get("b");
+            assertNotNull(b);
+            assertSame(DataDescriptorType.NO_DATA_DESCRIPTOR, b.getDataDescriptorType());
+
+            assertTrue(
+                    a.getCentralDirectoryHeader().getOffset()
+                            < b.getCentralDirectoryHeader().getOffset());
+        }
+
+        /*
+         * Open the file again and check there are no warnings.
+         */
+        try (ZFile zf = new ZFile(zipFile)) {
+            VerifyLog vl = zf.getVerifyLog();
+            assertEquals(0, vl.getLogs().size());
+
+            StoredEntry a = zf.get("a");
+            assertNotNull(a);
+            vl = a.getVerifyLog();
+            assertEquals(0, vl.getLogs().size());
+
+            StoredEntry b = zf.get("b");
+            assertNotNull(b);
+            vl = b.getVerifyLog();
+            assertEquals(0, vl.getLogs().size());
+        }
+    }
+
+    @Test
+    public void alignZipContentsWithDeferredCrc32() throws Exception {
+        /*
+         * Create an unaligned zip file with deferred CRC32 and files in non-alphabetical order.
+         * We need an uncompressed file to make realigning have any effect.
+         */
+        File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
+        try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) {
+            zos.putNextEntry(new ZipEntry("x"));
+            zos.write(new byte[1000]);
+            zos.putNextEntry(new ZipEntry("y"));
+            zos.write(new byte[1000]);
+            ZipEntry zEntry = new ZipEntry("z");
+            zEntry.setSize(1000);
+            zEntry.setMethod(ZipEntry.STORED);
+            zEntry.setCrc(Hashing.crc32().hashBytes(new byte[1000]).asInt());
+            zos.putNextEntry(zEntry);
+            zos.write(new byte[1000]);
+        }
+
+        /*
+         * Now open the zip using a ZFile and realign the contents and check that the deferred CRC32
+         * bits were reset.
+         */
+        ZFileOptions options = new ZFileOptions();
+        options.setAlignmentRule(AlignmentRules.constant(2000));
+        try (ZFile zf = new ZFile(zipFile, options)) {
+            StoredEntry x = zf.get("x");
+            assertNotNull(x);
+            assertNotSame(DataDescriptorType.NO_DATA_DESCRIPTOR, x.getDataDescriptorType());
+            StoredEntry y = zf.get("y");
+            assertNotNull(y);
+            assertNotSame(DataDescriptorType.NO_DATA_DESCRIPTOR, y.getDataDescriptorType());
+            StoredEntry z = zf.get("z");
+            assertNotNull(z);
+            assertSame(DataDescriptorType.NO_DATA_DESCRIPTOR, z.getDataDescriptorType());
+
+            zf.realign();
+            zf.update();
+
+            x = zf.get("x");
+            assertNotNull(x);
+            assertSame(DataDescriptorType.NO_DATA_DESCRIPTOR, x.getDataDescriptorType());
+            y = zf.get("y");
+            assertNotNull(y);
+            assertSame(DataDescriptorType.NO_DATA_DESCRIPTOR, y.getDataDescriptorType());
+            z = zf.get("z");
+            assertNotNull(z);
+            assertSame(DataDescriptorType.NO_DATA_DESCRIPTOR, z.getDataDescriptorType());
+        }
+    }
+
+    @Test
+    public void openingZFileDoesNotRemoveDataDescriptors() throws Exception {
+        /*
+         * Create a zip file with deferred CRC32.
+         */
+        File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
+        try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) {
+            zos.putNextEntry(new ZipEntry("a"));
+            zos.write(new byte[1000]);
+        }
+
+        /*
+         * Open using ZFile and check that the deferred CRC32 is there.
+         */
+        try (ZFile zf = new ZFile(zipFile)) {
+            StoredEntry se = zf.get("a");
+            assertNotNull(se);
+            assertNotEquals(DataDescriptorType.NO_DATA_DESCRIPTOR, se.getDataDescriptorType());
+        }
+
+        /*
+         * Open using ZFile (again) and check that the deferred CRC32 is there.
+         */
+        try (ZFile zf = new ZFile(zipFile)) {
+            StoredEntry se = zf.get("a");
+            assertNotNull(se);
+            assertNotEquals(DataDescriptorType.NO_DATA_DESCRIPTOR, se.getDataDescriptorType());
+        }
+    }
+
+    @Test
+    public void zipCommentsAreSaved() throws Exception {
+        File zipFileWithComments = new File(mTemporaryFolder.getRoot(), "a.zip");
+        try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFileWithComments))) {
+            zos.setComment("foo");
+        }
+
+        /*
+         * Open the zip file and check the comment is there.
+         */
+        try (ZFile zf = new ZFile(zipFileWithComments)) {
+            byte[] comment = zf.getEocdComment();
+            assertArrayEquals(new byte[] { 'f', 'o', 'o' }, comment);
+
             /*
-             * Nothing to do.
+             * Modify the comment and write the file.
              */
+            zf.setEocdComment(new byte[] { 'b', 'a', 'r', 'r' });
+        }
+
+        /*
+         * Open the file and see that the comment is there (both with java and zfile).
+         */
+        try (ZipFile zf2 = new ZipFile(zipFileWithComments)) {
+            assertEquals("barr", zf2.getComment());
+        }
+
+        try (ZFile zf3 = new ZFile(zipFileWithComments)) {
+            assertArrayEquals(new byte[] { 'b', 'a', 'r', 'r' }, zf3.getEocdComment());
+        }
+    }
+
+    @Test
+    public void eocdCommentsWithMoreThan64kNotAllowed() throws Exception {
+        File zipFileWithComments = new File(mTemporaryFolder.getRoot(), "a.zip");
+        try (ZFile zf = new ZFile(zipFileWithComments)) {
+            try {
+                zf.setEocdComment(new byte[65536]);
+                fail();
+            } catch (IllegalArgumentException e) {
+                // Expected.
+            }
+
+            zf.setEocdComment(new byte[65535]);
+        }
+    }
+
+    @Test
+    public void eocdCommentsWithTheEocdMarkerAreAllowed() throws Exception {
+        File zipFileWithComments = new File(mTemporaryFolder.getRoot(), "a.zip");
+        byte[] data = new byte[100];
+        data[50] = 0x50; // Signature
+        data[51] = 0x4b;
+        data[52] = 0x05;
+        data[53] = 0x06;
+        data[54] = 0x00; // Number of disk
+        data[55] = 0x00;
+        data[56] = 0x00; // Disk CD start
+        data[57] = 0x00;
+        data[54] = 0x01; // Total records 1
+        data[55] = 0x00;
+        data[56] = 0x02; // Total records 2, must be = to total records 1
+        data[57] = 0x00;
+
+        try (ZFile zf = new ZFile(zipFileWithComments)) {
+            zf.setEocdComment(data);
+        }
+
+        try (ZFile zf = new ZFile(zipFileWithComments)) {
+            assertArrayEquals(data, zf.getEocdComment());
+        }
+    }
+
+    @Test
+    public void eocdCommentsWithTheEocdMarkerThatAreInvalidAreNotAllowed() throws Exception {
+        File zipFileWithComments = new File(mTemporaryFolder.getRoot(), "a.zip");
+        byte[] data = new byte[100];
+        data[50] = 0x50;
+        data[51] = 0x4b;
+        data[52] = 0x05;
+        data[53] = 0x06;
+        data[67] = 0x00;
+
+        try (ZFile zf = new ZFile(zipFileWithComments)) {
+            try {
+                zf.setEocdComment(data);
+                fail();
+            } catch (IllegalArgumentException e) {
+                // Expected.
+            }
+        }
+    }
+
+    @Test
+    public void zipCommentsArePreservedWithFileChanges() throws Exception {
+        File zipFileWithComments = new File(mTemporaryFolder.getRoot(), "a.zip");
+        byte[] comment = new byte[] { 1, 3, 4 };
+        try (ZFile zf = new ZFile(zipFileWithComments)) {
+            zf.add("foo", new ByteArrayInputStream(new byte[50]));
+            zf.setEocdComment(comment);
+        }
+
+        try (ZFile zf = new ZFile(zipFileWithComments)) {
+            assertArrayEquals(comment, zf.getEocdComment());
+            zf.add("bar", new ByteArrayInputStream(new byte[100]));
+        }
+
+        try (ZFile zf = new ZFile(zipFileWithComments)) {
+            assertArrayEquals(comment, zf.getEocdComment());
+        }
+    }
+
+    @Test
+    public void overlappingZipEntries() throws Exception {
+        File myZip = ZipTestUtils.cloneRsrc("overlapping.zip", mTemporaryFolder);
+        try (ZFile zf = new ZFile(myZip)) {
+            fail();
+        } catch (IOException e) {
+            assertTrue(Throwables.getStackTraceAsString(e).contains("overlapping/bbb"));
+            assertTrue(Throwables.getStackTraceAsString(e).contains("overlapping/ddd"));
+            assertFalse(Throwables.getStackTraceAsString(e).contains("Central Directory"));
+        }
+    }
+
+    @Test
+    public void overlappingZipEntryWithCentralDirectory() throws Exception {
+        File myZip = ZipTestUtils.cloneRsrc("overlapping2.zip", mTemporaryFolder);
+        try (ZFile zf = new ZFile(myZip)) {
+            fail();
+        } catch (IOException e) {
+            assertFalse(Throwables.getStackTraceAsString(e).contains("overlapping/bbb"));
+            assertTrue(Throwables.getStackTraceAsString(e).contains("overlapping/ddd"));
+            assertTrue(Throwables.getStackTraceAsString(e).contains("Central Directory"));
+        }
+    }
+
+    @Test
+    public void readFileWithOffsetBeyondFileEnd() throws Exception {
+        File myZip = ZipTestUtils.cloneRsrc("entry-outside-file.zip", mTemporaryFolder);
+        try (ZFile zf = new ZFile(myZip)) {
+            fail();
+        } catch (IOException e) {
+            assertTrue(Throwables.getStackTraceAsString(e).contains("entry-outside-file/foo"));
+            assertTrue(Throwables.getStackTraceAsString(e).contains("EOF"));
         }
     }
 }
diff --git a/src/test/java/com/android/builder/internal/packaging/zip/ZFileTestConstants.java b/src/test/java/com/android/apkzlib/zip/ZFileTestConstants.java
similarity index 95%
rename from src/test/java/com/android/builder/internal/packaging/zip/ZFileTestConstants.java
rename to src/test/java/com/android/apkzlib/zip/ZFileTestConstants.java
index 40189cb..fbf5739 100644
--- a/src/test/java/com/android/builder/internal/packaging/zip/ZFileTestConstants.java
+++ b/src/test/java/com/android/apkzlib/zip/ZFileTestConstants.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
 
 /**
  * Constants used in tests.
diff --git a/src/test/java/com/android/builder/internal/packaging/zip/ZipMergeTest.java b/src/test/java/com/android/apkzlib/zip/ZipMergeTest.java
similarity index 92%
rename from src/test/java/com/android/builder/internal/packaging/zip/ZipMergeTest.java
rename to src/test/java/com/android/apkzlib/zip/ZipMergeTest.java
index fb29395..0090bc7 100644
--- a/src/test/java/com/android/builder/internal/packaging/zip/ZipMergeTest.java
+++ b/src/test/java/com/android/apkzlib/zip/ZipMergeTest.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
@@ -23,22 +23,19 @@
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 
-import com.android.builder.internal.utils.CachedFileContents;
+import com.android.apkzlib.utils.CachedFileContents;
 import com.google.common.base.Charsets;
 import com.google.common.hash.Hashing;
 import com.google.common.io.ByteStreams;
 import com.google.common.io.Closer;
-import com.google.common.io.Files;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-
 import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileOutputStream;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipOutputStream;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
 
 public class ZipMergeTest {
     @Rule
@@ -96,7 +93,7 @@
     public void mergeZipWithDeferredCrc() throws Exception {
         File foo = mTemporaryFolder.newFile("foo");
 
-        byte[] wBytes = Files.toByteArray(ZipTestUtils.rsrcFile("text-files/wikipedia.html"));
+        byte[] wBytes = ZipTestUtils.rsrcBytes("text-files/wikipedia.html");
 
         try (ZipOutputStream fooOut = new ZipOutputStream(new FileOutputStream(foo))) {
             fooOut.putNextEntry(new ZipEntry("w"));
@@ -127,8 +124,8 @@
     public void mergeZipKeepsDeflatedAndStored() throws Exception {
         File foo = mTemporaryFolder.newFile("foo");
 
-        byte[] wBytes = Files.toByteArray(ZipTestUtils.rsrcFile("text-files/wikipedia.html"));
-        byte[] lBytes = Files.toByteArray(ZipTestUtils.rsrcFile("images/lena.png"));
+        byte[] wBytes = ZipTestUtils.rsrcBytes("text-files/wikipedia.html");
+        byte[] lBytes = ZipTestUtils.rsrcBytes("images/lena.png");
 
         try (ZipOutputStream fooOut = new ZipOutputStream(new FileOutputStream(foo))) {
             fooOut.putNextEntry(new ZipEntry("w"));
@@ -175,8 +172,8 @@
     public void mergeZipWithSorting() throws Exception {
         File foo = mTemporaryFolder.newFile("foo");
 
-        byte[] wBytes = Files.toByteArray(ZipTestUtils.rsrcFile("text-files/wikipedia.html"));
-        byte[] lBytes = Files.toByteArray(ZipTestUtils.rsrcFile("images/lena.png"));
+        byte[] wBytes = ZipTestUtils.rsrcBytes("text-files/wikipedia.html");
+        byte[] lBytes = ZipTestUtils.rsrcBytes("images/lena.png");
 
         try (ZipOutputStream fooOut = new ZipOutputStream(new FileOutputStream(foo))) {
             fooOut.putNextEntry(new ZipEntry("w"));
diff --git a/src/test/java/com/android/builder/internal/packaging/zip/ZipTestUtils.java b/src/test/java/com/android/apkzlib/zip/ZipTestUtils.java
similarity index 71%
rename from src/test/java/com/android/builder/internal/packaging/zip/ZipTestUtils.java
rename to src/test/java/com/android/apkzlib/zip/ZipTestUtils.java
index 7d2e6df..aaff18b 100644
--- a/src/test/java/com/android/builder/internal/packaging/zip/ZipTestUtils.java
+++ b/src/test/java/com/android/apkzlib/zip/ZipTestUtils.java
@@ -14,19 +14,16 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
 
 import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
 
-import com.android.annotations.NonNull;
-import com.android.builder.internal.utils.ApkZFileTestUtils;
+import com.android.apkzlib.utils.ApkZFileTestUtils;
 import com.google.common.io.Files;
-
-import org.junit.rules.TemporaryFolder;
-
 import java.io.File;
 import java.io.IOException;
+import javax.annotation.Nonnull;
+import org.junit.rules.TemporaryFolder;
 
 /**
  * Utility method for zip tests.
@@ -34,19 +31,15 @@
 class ZipTestUtils {
 
     /**
-     * Obtains the file with a resource with the given name. This is a file that lays in
-     * the packaging subdirectory of test resources.
+     * Obtains the data of a resource with the given name.
      *
      * @param rsrcName the resource name inside packaging resource folder
-     * @return the resource file, guaranteed to exist
+     * @return the resource data
+     * @throws IOException I/O failed
      */
-    @NonNull
-    static File rsrcFile(@NonNull String rsrcName) {
-        File packagingRoot = ApkZFileTestUtils.getResource("/testData/packaging");
-        String rsrcPath = packagingRoot.getAbsolutePath() + "/" + rsrcName;
-        File rsrcFile = new File(rsrcPath);
-        assertTrue(rsrcFile.isFile());
-        return rsrcFile;
+    @Nonnull
+    static byte[] rsrcBytes(@Nonnull String rsrcName) throws IOException {
+        return ApkZFileTestUtils.getResourceBytes("/testData/packaging/" + rsrcName).read();
     }
 
     /**
@@ -59,7 +52,7 @@
      * @return the file that was created with the resource
      * @throws IOException failed to clone the resource
      */
-    static File cloneRsrc(@NonNull String rsrcName, @NonNull TemporaryFolder folder)
+    static File cloneRsrc(@Nonnull String rsrcName, @Nonnull TemporaryFolder folder)
             throws IOException {
         String cloneName;
         if (rsrcName.contains("/")) {
@@ -83,12 +76,15 @@
      * @return the file that was created with the resource
      * @throws IOException failed to clone the resource
      */
-    static File cloneRsrc(@NonNull String rsrcName, @NonNull TemporaryFolder folder,
-            @NonNull String cloneName) throws IOException {
+    static File cloneRsrc(
+            @Nonnull String rsrcName,
+            @Nonnull TemporaryFolder folder,
+            @Nonnull String cloneName)
+            throws IOException {
         File result = new File(folder.getRoot(), cloneName);
         assertFalse(result.exists());
 
-        Files.copy(rsrcFile(rsrcName), result);
+        Files.write(rsrcBytes(rsrcName), result);
         return result;
     }
 }
diff --git a/src/test/java/com/android/builder/internal/packaging/zip/ZipToolsTest.java b/src/test/java/com/android/apkzlib/zip/ZipToolsTest.java
similarity index 84%
rename from src/test/java/com/android/builder/internal/packaging/zip/ZipToolsTest.java
rename to src/test/java/com/android/apkzlib/zip/ZipToolsTest.java
index c0bd788..e45f117 100644
--- a/src/test/java/com/android/builder/internal/packaging/zip/ZipToolsTest.java
+++ b/src/test/java/com/android/apkzlib/zip/ZipToolsTest.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip;
+package com.android.apkzlib.zip;
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
@@ -22,31 +22,28 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
-import com.android.annotations.NonNull;
-import com.android.annotations.Nullable;
 import com.google.common.base.Charsets;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.common.io.ByteStreams;
 import com.google.common.io.Files;
-
-import org.junit.Assume;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-
 import java.io.ByteArrayInputStream;
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.InputStream;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import org.junit.Assume;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
 
 @RunWith(Parameterized.class)
 public class ZipToolsTest {
@@ -70,7 +67,7 @@
     public String mName;
 
     @Rule
-    @NonNull
+    @Nonnull
     public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
 
     @Parameterized.Parameters(name = "{4} {index}")
@@ -103,11 +100,11 @@
 
     private File cloneZipFile() throws Exception {
         File zfile = mTemporaryFolder.newFile("file.zip");
-        Files.copy(ZipTestUtils.rsrcFile(mZipFile), zfile);
+        Files.write(ZipTestUtils.rsrcBytes(mZipFile), zfile);
         return zfile;
     }
 
-    private static void assertFileInZip(@NonNull ZFile zfile, @NonNull String name) throws Exception {
+    private static void assertFileInZip(@Nonnull ZFile zfile, @Nonnull String name) throws Exception {
         StoredEntry root = zfile.get(name);
         assertNotNull(root);
 
@@ -115,7 +112,7 @@
         byte[] inZipData = ByteStreams.toByteArray(is);
         is.close();
 
-        byte[] inFileData = Files.toByteArray(ZipTestUtils.rsrcFile(name));
+        byte[] inFileData = ZipTestUtils.rsrcBytes(name);
         assertArrayEquals(inFileData, inZipData);
     }
 
@@ -156,14 +153,18 @@
 
         File zfile = new File (mTemporaryFolder.getRoot(), "zfile.zip");
         try (ZFile zf = new ZFile(zfile, options)) {
-            zf.add("root", new FileInputStream(ZipTestUtils.rsrcFile("root")));
+            zf.add("root", new ByteArrayInputStream(ZipTestUtils.rsrcBytes("root")));
             zf.add("images/", new ByteArrayInputStream(new byte[0]));
-            zf.add("images/lena.png", new FileInputStream(ZipTestUtils.rsrcFile("images/lena.png")));
+            zf.add(
+                    "images/lena.png",
+                    new ByteArrayInputStream(ZipTestUtils.rsrcBytes("images/lena.png")));
             zf.add("text-files/", new ByteArrayInputStream(new byte[0]));
-            zf.add("text-files/rfc2460.txt", new FileInputStream(
-                    ZipTestUtils.rsrcFile("text-files/rfc2460.txt")));
-            zf.add("text-files/wikipedia.html",
-                    new FileInputStream(ZipTestUtils.rsrcFile("text-files/wikipedia.html")));
+            zf.add(
+                    "text-files/rfc2460.txt",
+                    new ByteArrayInputStream(ZipTestUtils.rsrcBytes("text-files/rfc2460.txt")));
+            zf.add(
+                    "text-files/wikipedia.html",
+                    new ByteArrayInputStream(ZipTestUtils.rsrcBytes("text-files/wikipedia.html")));
         }
 
         List<String> command = Lists.newArrayList(mUnzipCommand);
@@ -200,13 +201,13 @@
 
         assertSize(new String[] { "images/", "images" }, 0, sizes);
         assertSize(new String[] { "text-files/", "text-files"}, 0, sizes);
-        assertSize(new String[] { "root" }, ZipTestUtils.rsrcFile("root").length(), sizes);
+        assertSize(new String[] { "root" }, ZipTestUtils.rsrcBytes("root").length, sizes);
         assertSize(new String[] { "images/lena.png", "images\\lena.png" },
-                ZipTestUtils.rsrcFile("images/lena.png").length(), sizes);
+                ZipTestUtils.rsrcBytes("images/lena.png").length, sizes);
         assertSize(new String[] { "text-files/rfc2460.txt", "text-files\\rfc2460.txt" },
-                ZipTestUtils.rsrcFile("text-files/rfc2460.txt").length(), sizes);
+                ZipTestUtils.rsrcBytes("text-files/rfc2460.txt").length, sizes);
         assertSize(new String[] { "text-files/wikipedia.html", "text-files\\wikipedia.html" },
-                ZipTestUtils.rsrcFile("text-files/wikipedia.html").length(), sizes);
+                ZipTestUtils.rsrcBytes("text-files/wikipedia.html").length, sizes);
     }
 
     private static void assertSize(String[] names, long size, Map<String, Integer> sizes) {
diff --git a/src/test/java/com/android/builder/internal/packaging/zip/compress/MultiCompressorTest.java b/src/test/java/com/android/apkzlib/zip/compress/MultiCompressorTest.java
similarity index 81%
rename from src/test/java/com/android/builder/internal/packaging/zip/compress/MultiCompressorTest.java
rename to src/test/java/com/android/apkzlib/zip/compress/MultiCompressorTest.java
index 2fc7bf7..4f2eaf0 100644
--- a/src/test/java/com/android/builder/internal/packaging/zip/compress/MultiCompressorTest.java
+++ b/src/test/java/com/android/apkzlib/zip/compress/MultiCompressorTest.java
@@ -14,19 +14,18 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip.compress;
+package com.android.apkzlib.zip.compress;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
-import com.android.builder.internal.packaging.zip.CentralDirectoryHeaderCompressInfo;
-import com.android.builder.internal.packaging.zip.CompressionMethod;
-import com.android.builder.internal.packaging.zip.StoredEntry;
-import com.android.builder.internal.packaging.zip.ZFile;
-import com.android.builder.internal.packaging.zip.ZFileOptions;
-import com.android.builder.internal.utils.ApkZFileTestUtils;
-import com.google.common.io.Files;
+import com.android.apkzlib.utils.ApkZFileTestUtils;
+import com.android.apkzlib.zip.CentralDirectoryHeaderCompressInfo;
+import com.android.apkzlib.zip.CompressionMethod;
+import com.android.apkzlib.zip.StoredEntry;
+import com.android.apkzlib.zip.ZFile;
+import com.android.apkzlib.zip.ZFileOptions;
 import com.google.common.util.concurrent.MoreExecutors;
 import java.io.ByteArrayInputStream;
 import java.io.File;
@@ -40,11 +39,9 @@
     public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
 
     private static byte[] getCompressibleData() throws Exception {
-        File textFiles = ApkZFileTestUtils.getResource("/testData/packaging/text-files");
-        assertTrue(textFiles.isDirectory());
-        File wikipediaFile = new File(textFiles, "wikipedia.html");
-        assertTrue(wikipediaFile.isFile());
-        return Files.asByteSource(wikipediaFile).read();
+        return ApkZFileTestUtils
+                .getResourceBytes("/testData/packaging/text-files/wikipedia.html")
+                .read();
     }
 
     private static byte[] compress(byte[] data, int level) throws Exception {
@@ -109,8 +106,9 @@
         File resultFile = new File(mTemporaryFolder.getRoot(), "result.zip");
 
         ZFileOptions resultOptions = new ZFileOptions();
-        resultOptions.setCompressor(new BestAndDefaultDeflateExecutorCompressor(
-                MoreExecutors.sameThreadExecutor(), resultOptions.getTracker(), ratio + 0.001));
+        resultOptions.setCompressor(
+                new BestAndDefaultDeflateExecutorCompressor(
+                        MoreExecutors.directExecutor(), resultOptions.getTracker(), ratio + 0.001));
 
         try (
                 ZFile defaultZFile = new ZFile(defaultFile);
@@ -138,8 +136,9 @@
         File resultFile = new File(mTemporaryFolder.getRoot(), "result.zip");
 
         ZFileOptions resultOptions = new ZFileOptions();
-        resultOptions.setCompressor(new BestAndDefaultDeflateExecutorCompressor(
-                MoreExecutors.sameThreadExecutor(), resultOptions.getTracker(), ratio - 0.001));
+        resultOptions.setCompressor(
+                new BestAndDefaultDeflateExecutorCompressor(
+                        MoreExecutors.directExecutor(), resultOptions.getTracker(), ratio - 0.001));
 
         try (
                 ZFile defaultZFile = new ZFile(defaultFile);
diff --git a/src/test/java/com/android/builder/internal/packaging/zip/utils/LittleEndianUtilsTest.java b/src/test/java/com/android/apkzlib/zip/utils/LittleEndianUtilsTest.java
similarity index 98%
rename from src/test/java/com/android/builder/internal/packaging/zip/utils/LittleEndianUtilsTest.java
rename to src/test/java/com/android/apkzlib/zip/utils/LittleEndianUtilsTest.java
index 855f39f..3264290 100644
--- a/src/test/java/com/android/builder/internal/packaging/zip/utils/LittleEndianUtilsTest.java
+++ b/src/test/java/com/android/apkzlib/zip/utils/LittleEndianUtilsTest.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip.utils;
+package com.android.apkzlib.zip.utils;
 
 import static junit.framework.TestCase.assertEquals;
 import static org.junit.Assert.assertArrayEquals;
diff --git a/src/test/java/com/android/builder/internal/packaging/zip/utils/MsDosDateTimeUtilsTest.java b/src/test/java/com/android/apkzlib/zip/utils/MsDosDateTimeUtilsTest.java
similarity index 96%
rename from src/test/java/com/android/builder/internal/packaging/zip/utils/MsDosDateTimeUtilsTest.java
rename to src/test/java/com/android/apkzlib/zip/utils/MsDosDateTimeUtilsTest.java
index 4c098c8..012d587 100644
--- a/src/test/java/com/android/builder/internal/packaging/zip/utils/MsDosDateTimeUtilsTest.java
+++ b/src/test/java/com/android/apkzlib/zip/utils/MsDosDateTimeUtilsTest.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.builder.internal.packaging.zip.utils;
+package com.android.apkzlib.zip.utils;
 
 import static org.junit.Assert.assertEquals;
 
diff --git a/src/test/resources/testData/packaging/AndroidManifest.xml b/src/test/resources/testData/packaging/AndroidManifest.xml
new file mode 100644
index 0000000..060ec31
--- /dev/null
+++ b/src/test/resources/testData/packaging/AndroidManifest.xml
Binary files differ
diff --git a/src/test/resources/testData/packaging/entry-outside-file.zip b/src/test/resources/testData/packaging/entry-outside-file.zip
new file mode 100644
index 0000000..ffd6be9
--- /dev/null
+++ b/src/test/resources/testData/packaging/entry-outside-file.zip
Binary files differ
diff --git a/src/test/resources/testData/packaging/overlapping.zip b/src/test/resources/testData/packaging/overlapping.zip
new file mode 100644
index 0000000..7f6144c
--- /dev/null
+++ b/src/test/resources/testData/packaging/overlapping.zip
Binary files differ
diff --git a/src/test/resources/testData/packaging/overlapping2.zip b/src/test/resources/testData/packaging/overlapping2.zip
new file mode 100644
index 0000000..eecefa9
--- /dev/null
+++ b/src/test/resources/testData/packaging/overlapping2.zip
Binary files differ
diff --git a/src/test/resources/testData/packaging/text-files/.gitattributes b/src/test/resources/testData/packaging/text-files/.gitattributes
new file mode 100644
index 0000000..fa1385d
--- /dev/null
+++ b/src/test/resources/testData/packaging/text-files/.gitattributes
@@ -0,0 +1 @@
+* -text
diff --git a/src/test/resources/testData/packaging/v2-signed.apk b/src/test/resources/testData/packaging/v2-signed.apk
new file mode 100644
index 0000000..7f48475
--- /dev/null
+++ b/src/test/resources/testData/packaging/v2-signed.apk
Binary files differ