Updated ZFile to use UTF-8 if needed. am: 6fb3edd101 am: 1289953ac0
am: 5481f75284

Change-Id: I63d6af990320b6ac328ab51aebe3fd56618045d9
diff --git a/BUILD b/BUILD
index 1c80dd8..0094d45 100644
--- a/BUILD
+++ b/BUILD
@@ -1,21 +1,13 @@
 licenses(["notice"])  # Apache License 2.0
 
-load("//tools/base/bazel:utils.bzl", "srcjar")
-
-srcjar(
-    name = "srcjar",
-    java_library = ":apkzlib",
-    visibility = ["//tools/base/build-system/builder:__pkg__"],
-)
-
 java_library(
     name = "apkzlib",
     srcs = glob([
         "src/main/java/**/*.java",
     ]),
-    visibility = ["//visibility:private"],  # These sources are compiled into builder.
+    visibility = ["//tools/base/build-system/builder:__pkg__"],
     deps = [
-        "//tools/base/build-system:tools.apksig",
+        "//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",
diff --git a/apkzlib.iml b/apkzlib.iml
index 3c33d1b..999fffa 100644
--- a/apkzlib.iml
+++ b/apkzlib.iml
@@ -16,5 +16,6 @@
     <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 f49541a..771d1b8 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,4 +1,4 @@
-apply plugin: 'java'
+apply from: "$rootDir/buildSrc/base/baseJava.gradle"
 
 dependencies {
     compile 'com.google.code.findbugs:jsr305:1.3.9'
diff --git a/src/main/java/com/android/apkzlib/sign/SignatureAlgorithm.java b/src/main/java/com/android/apkzlib/sign/SignatureAlgorithm.java
index 4166767..0667252 100644
--- a/src/main/java/com/android/apkzlib/sign/SignatureAlgorithm.java
+++ b/src/main/java/com/android/apkzlib/sign/SignatureAlgorithm.java
@@ -23,26 +23,17 @@
  * 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.
diff --git a/src/main/java/com/android/apkzlib/zfile/ApkZFileCreator.java b/src/main/java/com/android/apkzlib/zfile/ApkZFileCreator.java
index e56f99b..c85ad44 100644
--- a/src/main/java/com/android/apkzlib/zfile/ApkZFileCreator.java
+++ b/src/main/java/com/android/apkzlib/zfile/ApkZFileCreator.java
@@ -26,6 +26,7 @@
 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;
@@ -114,14 +115,30 @@
         try {
             ZFile toMerge = closer.register(new ZFile(zip));
 
-            Predicate<String> predicate;
+            Predicate<String> ignorePredicate;
             if (isIgnored == null) {
-                predicate = s -> false;
+                ignorePredicate = s -> false;
             } else {
-                predicate = isIgnored;
+                ignorePredicate = isIgnored;
             }
 
-            this.zip.mergeFrom(toMerge, predicate);
+            // 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 {
diff --git a/src/main/java/com/android/apkzlib/zip/EncodeUtils.java b/src/main/java/com/android/apkzlib/zip/EncodeUtils.java
index 94cabbe..259f64e 100644
--- a/src/main/java/com/android/apkzlib/zip/EncodeUtils.java
+++ b/src/main/java/com/android/apkzlib/zip/EncodeUtils.java
@@ -68,22 +68,30 @@
      */
     @Nonnull
     public static String decode(@Nonnull byte[] data, @Nonnull GPFlags flags) {
-        Charset charset = flagsCharset(flags);
+        return decode(data, flagsCharset(flags));
+    }
 
-        while (true) {
-            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 == Charsets.US_ASCII) {
-                    charset = Charsets.UTF_8;
-                } else {
-                    return charset.decode(ByteBuffer.wrap(data)).toString();
-                }
+    /**
+     * 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();
             }
         }
     }
diff --git a/src/main/java/com/android/apkzlib/zip/Eocd.java b/src/main/java/com/android/apkzlib/zip/Eocd.java
index 36a0a6e..1568840 100644
--- a/src/main/java/com/android/apkzlib/zip/Eocd.java
+++ b/src/main/java/com/android/apkzlib/zip/Eocd.java
@@ -167,8 +167,9 @@
      * @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");
@@ -176,8 +177,8 @@
         this.totalRecords = totalRecords;
         this.directoryOffset = directoryOffset;
         this.directorySize = directorySize;
-        comment = new byte[0];
-        byteSupplier = new CachedSupplier<byte[]>(this::computeByteRepresentation);
+        this.comment = comment;
+        byteSupplier = new CachedSupplier<>(this::computeByteRepresentation);
     }
 
     /**
@@ -214,7 +215,7 @@
      * @return the size, in bytes, of the EOCD
      */
     long getEocdSize() {
-        return F_COMMENT_SIZE.endOffset() + comment.length;
+        return (long) F_COMMENT_SIZE.endOffset() + comment.length;
     }
 
     /**
@@ -228,6 +229,19 @@
         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;
+    }
+
     /**
      * Computes the byte representation of the EOCD.
      *
diff --git a/src/main/java/com/android/apkzlib/zip/ExtraField.java b/src/main/java/com/android/apkzlib/zip/ExtraField.java
index 4e11519..d70fa7f 100644
--- a/src/main/java/com/android/apkzlib/zip/ExtraField.java
+++ b/src/main/java/com/android/apkzlib/zip/ExtraField.java
@@ -158,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);
@@ -324,6 +334,11 @@
     public static class AlignmentSegment implements Segment {
 
         /**
+         * Minimum size for an alignment segment.
+         */
+        public static final int MINIMUM_SIZE = 6;
+
+        /**
          * The alignment value.
          */
         private int alignment;
@@ -341,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).
              */
             this.alignment = alignment;
-            padding = totalSize - 6;
+            padding = totalSize - MINIMUM_SIZE;
         }
 
         /**
diff --git a/src/main/java/com/android/apkzlib/zip/FileUseMap.java b/src/main/java/com/android/apkzlib/zip/FileUseMap.java
index a72a956..8a76878 100644
--- a/src/main/java/com/android/apkzlib/zip/FileUseMap.java
+++ b/src/main/java/com/android/apkzlib/zip/FileUseMap.java
@@ -538,6 +538,43 @@
         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(", ");
diff --git a/src/main/java/com/android/apkzlib/zip/StoredEntry.java b/src/main/java/com/android/apkzlib/zip/StoredEntry.java
index 664734e..854bf3a 100644
--- a/src/main/java/com/android/apkzlib/zip/StoredEntry.java
+++ b/src/main/java/com/android/apkzlib/zip/StoredEntry.java
@@ -24,6 +24,7 @@
 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;
@@ -347,6 +348,23 @@
     }
 
     /**
+     * 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
@@ -362,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);
@@ -374,6 +393,7 @@
      * @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(!deleted, "deleted");
@@ -481,7 +501,7 @@
 
         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];
+        byte[] ddData = new byte[DataDescriptorType.DATA_DESCRIPTOR_WITH_SIGNATURE.size];
         file.directFullyRead(ddStart, ddData);
 
         ByteBuffer ddBytes = ByteBuffer.wrap(ddData);
diff --git a/src/main/java/com/android/apkzlib/zip/ZFile.java b/src/main/java/com/android/apkzlib/zip/ZFile.java
index d4d730e..9034f4c 100644
--- a/src/main/java/com/android/apkzlib/zip/ZFile.java
+++ b/src/main/java/com/android/apkzlib/zip/ZFile.java
@@ -201,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.
@@ -211,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;
@@ -222,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.
@@ -257,6 +269,9 @@
     /**
      * 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> eocdEntry;
@@ -336,8 +351,8 @@
     private final List<IOExceptionRunnable> toRun;
 
     /**
-     * {@code true} when {@link #notify(com.android.apkzlib.utils.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 isNotifying;
 
@@ -386,6 +401,24 @@
     @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;
+
 
     /**
      * Creates a new zip file. If the zip file does not exist, then no file is created at this
@@ -411,12 +444,30 @@
      * @throws IOException some file exists but could not be read
      */
     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);
+        this.readOnly = readOnly;
         dirty = false;
         closedControl = null;
         alignmentRule = options.getAlignmentRule();
@@ -438,6 +489,8 @@
 
         if (file.exists()) {
             openReadOnly();
+        } else if (readOnly) {
+            throw new IOException("File does not exist but read-only mode requested");
         } else {
             dirty = true;
         }
@@ -455,7 +508,15 @@
 
                 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) {
@@ -536,7 +597,7 @@
         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;
@@ -564,6 +625,51 @@
                  * file.
                  */
 
+                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);
 
@@ -614,7 +720,6 @@
         byte[] last = new byte[lastToRead];
         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
@@ -635,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.
@@ -838,8 +943,11 @@
      * @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 {
+        checkNotInReadOnlyMode();
+
         String path = entry.getCentralDirectoryHeader().getName();
         FileUseMapEntry<StoredEntry> mapEntry = entries.get(path);
         Preconditions.checkNotNull(mapEntry, "mapEntry == null");
@@ -856,6 +964,17 @@
     }
 
     /**
+     * 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.
      *
@@ -863,6 +982,8 @@
      * the compressor but only reported here
      */
     public void update() throws IOException {
+        checkNotInReadOnlyMode();
+
         /*
          * Process all background stuff before calling in the extensions.
          */
@@ -1158,7 +1279,9 @@
         // 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 -> {
@@ -1170,6 +1293,8 @@
     /**
      * 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 (directoryEntry != null) {
@@ -1179,6 +1304,10 @@
 
         if (eocdEntry != null) {
             map.remove(eocdEntry);
+
+            Eocd eocd = eocdEntry.getStore();
+            Verify.verify(eocd != null);
+            eocdComment = eocd.getComment();
             eocdEntry = null;
         }
     }
@@ -1353,7 +1482,9 @@
             dirStart = extraDirectoryOffset;
         }
 
-        Eocd eocd = new Eocd(entries.size(), dirStart, dirSize);
+        Verify.verify(eocdComment != null);
+        Eocd eocd = new Eocd(entries.size(), dirStart, dirSize, eocdComment);
+        eocdComment = null;
 
         byte[] eocdBytes = eocd.toBytes();
         long eocdOffset = map.size();
@@ -1446,6 +1577,9 @@
      * has been modified outside the control of this object
      */
     private void reopenRw() throws IOException {
+        // 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;
         }
@@ -1494,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 {
+        checkNotInReadOnlyMode();
         add(name, stream, true);
     }
 
@@ -1613,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)
             throws IOException {
+        checkNotInReadOnlyMode();
 
         /*
          * Clean pending background work, if needed.
@@ -1825,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)
             throws IOException {
+        checkNotInReadOnlyMode();
+
         for (StoredEntry fromEntry : src.entries()) {
             if (ignoreFilter.test(fromEntry.getCentralDirectoryHeader().getName())) {
                 continue;
@@ -1917,8 +2058,11 @@
     /**
      * 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() {
+        checkNotInReadOnlyMode();
         dirty = true;
     }
 
@@ -1945,8 +2089,11 @@
      * 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();
@@ -2060,8 +2207,10 @@
      * 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) {
+        checkNotInReadOnlyMode();
         extensions.add(extension);
     }
 
@@ -2069,8 +2218,10 @@
      * 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) {
+        checkNotInReadOnlyMode();
         extensions.remove(extension);
     }
 
@@ -2114,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)
             throws IOException {
+        checkNotInReadOnlyMode();
+
         Preconditions.checkArgument(offset >= 0, "offset < 0");
         Preconditions.checkArgument(start >= 0, "start >= 0");
         Preconditions.checkArgument(count >= 0, "count >= 0");
@@ -2141,8 +2295,10 @@
      * @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 {
+        checkNotInReadOnlyMode();
         directWrite(offset, data, 0, data.length);
     }
 
@@ -2279,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 {
+        checkNotInReadOnlyMode();
         addAllRecursively(file, f -> true);
     }
 
@@ -2291,10 +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 {
+        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.
@@ -2403,13 +2564,81 @@
     }
 
     /**
+     * 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;
+    }
+
+    /**
      * Sets an extra offset for the central directory. See class description for details. Changing
      * this value will mark the file as dirty and force a rewrite of the central directory when
      * 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 (extraDirectoryOffset != offset) {
@@ -2445,8 +2674,10 @@
      * 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();
diff --git a/src/main/java/com/android/apkzlib/zip/compress/ExecutorCompressor.java b/src/main/java/com/android/apkzlib/zip/compress/ExecutorCompressor.java
index 96ad281..54be20c 100644
--- a/src/main/java/com/android/apkzlib/zip/compress/ExecutorCompressor.java
+++ b/src/main/java/com/android/apkzlib/zip/compress/ExecutorCompressor.java
@@ -52,7 +52,7 @@
         executor.execute(() -> {
             try {
                 future.set(immediateCompress(source));
-            } catch (Exception e) {
+            } catch (Throwable e) {
                 future.setException(e);
             }
         });
diff --git a/src/test/java/com/android/apkzlib/sign/JarSigningTest.java b/src/test/java/com/android/apkzlib/sign/JarSigningTest.java
index ea48cfa..35aeeaf 100644
--- a/src/test/java/com/android/apkzlib/sign/JarSigningTest.java
+++ b/src/test/java/com/android/apkzlib/sign/JarSigningTest.java
@@ -20,10 +20,10 @@
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 
-import com.android.apkzlib.zip.StoredEntry;
-import com.android.apkzlib.zip.ZFile;
 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);
@@ -76,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)));
         }
@@ -130,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)));
         }
@@ -188,6 +191,7 @@
     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);
@@ -220,6 +224,7 @@
         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);
@@ -233,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);
@@ -257,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);
@@ -273,6 +278,7 @@
         file1ShaTxt = Base64.getEncoder().encodeToString(file1Sha);
 
         try (ZFile zf2 = new ZFile(zipFile)) {
+            ApkZFileTestUtils.addAndroidManifest(zf2);
             ManifestGenerationExtension me = new ManifestGenerationExtension(builtBy, createdBy);
             me.register(zf2);
             new SigningExtension(21, p.v2, p.v1, true, false).register(zf2);
@@ -287,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);
@@ -297,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();
@@ -306,6 +312,7 @@
         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 SigningExtension(21, p.v2, p.v1, true, false).register(zf);
diff --git a/src/test/java/com/android/apkzlib/utils/ApkZFileTestUtils.java b/src/test/java/com/android/apkzlib/utils/ApkZFileTestUtils.java
index 916ef46..1ef087f 100644
--- a/src/test/java/com/android/apkzlib/utils/ApkZFileTestUtils.java
+++ b/src/test/java/com/android/apkzlib/utils/ApkZFileTestUtils.java
@@ -18,10 +18,12 @@
 
 import static org.junit.Assert.assertTrue;
 
+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;
@@ -106,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/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/apkzlib/zip/AlignmentTest.java b/src/test/java/com/android/apkzlib/zip/AlignmentTest.java
index 0b825a0..e94a876 100644
--- a/src/test/java/com/android/apkzlib/zip/AlignmentTest.java
+++ b/src/test/java/com/android/apkzlib/zip/AlignmentTest.java
@@ -785,4 +785,72 @@
             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/apkzlib/zip/ExtraFieldTest.java b/src/test/java/com/android/apkzlib/zip/ExtraFieldTest.java
index d80ccc4..2371849 100644
--- a/src/test/java/com/android/apkzlib/zip/ExtraFieldTest.java
+++ b/src/test/java/com/android/apkzlib/zip/ExtraFieldTest.java
@@ -19,6 +19,7 @@
 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/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/apkzlib/zip/ZFileTest.java b/src/test/java/com/android/apkzlib/zip/ZFileTest.java
index f6062e7..b7f2979 100644
--- a/src/test/java/com/android/apkzlib/zip/ZFileTest.java
+++ b/src/test/java/com/android/apkzlib/zip/ZFileTest.java
@@ -26,12 +26,14 @@
 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.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;
 import com.google.common.hash.Hashing;
 import com.google.common.io.ByteStreams;
 import com.google.common.io.Closer;
@@ -1669,4 +1671,151 @@
             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);
+
+            /*
+             * 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/apkzlib/zip/compress/MultiCompressorTest.java b/src/test/java/com/android/apkzlib/zip/compress/MultiCompressorTest.java
index f19962c..4f2eaf0 100644
--- a/src/test/java/com/android/apkzlib/zip/compress/MultiCompressorTest.java
+++ b/src/test/java/com/android/apkzlib/zip/compress/MultiCompressorTest.java
@@ -106,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);
@@ -135,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/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