summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--BUILD12
-rw-r--r--apkzlib.iml3
-rw-r--r--build.gradle2
-rw-r--r--src/main/java/com/android/apkzlib/sign/SignatureAlgorithm.java23
-rw-r--r--src/main/java/com/android/apkzlib/zfile/ApkCreator.java3
-rw-r--r--src/main/java/com/android/apkzlib/zfile/ApkZFileCreator.java30
-rw-r--r--src/main/java/com/android/apkzlib/zip/CentralDirectory.java10
-rw-r--r--src/main/java/com/android/apkzlib/zip/CentralDirectoryHeader.java4
-rw-r--r--src/main/java/com/android/apkzlib/zip/EncodeUtils.java33
-rw-r--r--src/main/java/com/android/apkzlib/zip/Eocd.java22
-rw-r--r--src/main/java/com/android/apkzlib/zip/ExtraField.java19
-rw-r--r--src/main/java/com/android/apkzlib/zip/FileUseMap.java37
-rw-r--r--src/main/java/com/android/apkzlib/zip/StoredEntry.java22
-rw-r--r--src/main/java/com/android/apkzlib/zip/ZFile.java325
-rw-r--r--src/main/java/com/android/apkzlib/zip/ZFileOptions.java22
-rw-r--r--src/main/java/com/android/apkzlib/zip/compress/ExecutorCompressor.java2
-rw-r--r--src/test/java/com/android/apkzlib/sign/JarSigningTest.java19
-rw-r--r--src/test/java/com/android/apkzlib/utils/ApkZFileTestUtils.java18
-rw-r--r--src/test/java/com/android/apkzlib/zfile/ApkAlignmentTest.java231
-rw-r--r--src/test/java/com/android/apkzlib/zip/AlignmentTest.java68
-rw-r--r--src/test/java/com/android/apkzlib/zip/EncodeUtilsTest.java13
-rw-r--r--src/test/java/com/android/apkzlib/zip/ExtraFieldTest.java26
-rw-r--r--src/test/java/com/android/apkzlib/zip/ZFileReadOnlyTest.java240
-rw-r--r--src/test/java/com/android/apkzlib/zip/ZFileTest.java191
-rw-r--r--src/test/java/com/android/apkzlib/zip/compress/MultiCompressorTest.java10
-rw-r--r--src/test/resources/testData/packaging/AndroidManifest.xmlbin0 -> 1436 bytes
-rw-r--r--src/test/resources/testData/packaging/entry-outside-file.zipbin0 -> 364 bytes
-rw-r--r--src/test/resources/testData/packaging/overlapping.zipbin0 -> 502 bytes
-rw-r--r--src/test/resources/testData/packaging/overlapping2.zipbin0 -> 502 bytes
-rw-r--r--src/test/resources/testData/packaging/text-files/.gitattributes1
-rw-r--r--src/test/resources/testData/packaging/v2-signed.apkbin0 -> 1501947 bytes
31 files changed, 1283 insertions, 103 deletions
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 b94d559..999fffa 100644
--- a/apkzlib.iml
+++ b/apkzlib.iml
@@ -9,12 +9,13 @@
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
- <orderEntry type="library" name="jsr-305" level="project" />
+ <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 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 @@ 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.
diff --git a/src/main/java/com/android/apkzlib/zfile/ApkCreator.java b/src/main/java/com/android/apkzlib/zfile/ApkCreator.java
index d602202..3cac7dc 100644
--- a/src/main/java/com/android/apkzlib/zfile/ApkCreator.java
+++ b/src/main/java/com/android/apkzlib/zfile/ApkCreator.java
@@ -65,4 +65,7 @@ public interface ApkCreator extends Closeable {
* @throws IOException failed to remove the entry
*/
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/apkzlib/zfile/ApkZFileCreator.java b/src/main/java/com/android/apkzlib/zfile/ApkZFileCreator.java
index b96901c..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 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;
@@ -114,14 +115,30 @@ class ApkZFileCreator implements ApkCreator {
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 {
@@ -159,6 +176,11 @@ class ApkZFileCreator implements ApkCreator {
}
@Override
+ public boolean hasPendingChangesWithWait() throws IOException {
+ return zip.hasPendingChangesWithWait();
+ }
+
+ @Override
public void close() throws IOException {
if (closed) {
return;
diff --git a/src/main/java/com/android/apkzlib/zip/CentralDirectory.java b/src/main/java/com/android/apkzlib/zip/CentralDirectory.java
index 4305d80..44389c1 100644
--- a/src/main/java/com/android/apkzlib/zip/CentralDirectory.java
+++ b/src/main/java/com/android/apkzlib/zip/CentralDirectory.java
@@ -341,7 +341,9 @@ class CentralDirectory {
+ ".");
}
- 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);
@@ -362,11 +364,7 @@ class CentralDirectory {
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);
diff --git a/src/main/java/com/android/apkzlib/zip/CentralDirectoryHeader.java b/src/main/java/com/android/apkzlib/zip/CentralDirectoryHeader.java
index 7e7e50b..f10477f 100644
--- a/src/main/java/com/android/apkzlib/zip/CentralDirectoryHeader.java
+++ b/src/main/java/com/android/apkzlib/zip/CentralDirectoryHeader.java
@@ -128,6 +128,7 @@ public class CentralDirectoryHeader implements Cloneable {
* 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
@@ -135,6 +136,7 @@ public class CentralDirectoryHeader implements Cloneable {
*/
CentralDirectoryHeader(
@Nonnull String name,
+ @Nonnull byte[] encodedFileName,
long uncompressedSize,
@Nonnull Future<CentralDirectoryHeaderCompressInfo> compressInfo,
@Nonnull GPFlags flags,
@@ -156,7 +158,7 @@ public class CentralDirectoryHeader implements Cloneable {
internalAttributes = 0;
externalAttributes = 0;
offset = -1;
- encodedFileName = EncodeUtils.encode(name, gpBit);
+ this.encodedFileName = encodedFileName;
this.compressInfo = compressInfo;
file = zFile;
}
diff --git a/src/main/java/com/android/apkzlib/zip/EncodeUtils.java b/src/main/java/com/android/apkzlib/zip/EncodeUtils.java
index 6579d1c..259f64e 100644
--- a/src/main/java/com/android/apkzlib/zip/EncodeUtils.java
+++ b/src/main/java/com/android/apkzlib/zip/EncodeUtils.java
@@ -19,7 +19,9 @@ package com.android.apkzlib.zip;
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;
/**
@@ -52,10 +54,9 @@ public class EncodeUtils {
+ "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);
}
/**
@@ -67,8 +68,32 @@ public class EncodeUtils {
*/
@Nonnull
public static String decode(@Nonnull byte[] data, @Nonnull GPFlags flags) {
- Charset charset = flagsCharset(flags);
- return charset.decode(ByteBuffer.wrap(data)).toString();
+ 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();
+ }
+ }
}
/**
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 @@ class Eocd {
* @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 @@ class Eocd {
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 @@ class Eocd {
* @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 @@ class Eocd {
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 @@ public class ExtraField {
}
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 class ExtraField {
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 class ExtraField {
*/
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 @@ class FileUseMap {
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.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;
@@ -347,6 +348,23 @@ public class StoredEntry {
}
/**
+ * 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 @@ public class StoredEntry {
* 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 @@ public class StoredEntry {
* @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 @@ public class StoredEntry {
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 c92e99b..9034f4c 100644
--- a/src/main/java/com/android/apkzlib/zip/ZFile.java
+++ b/src/main/java/com/android/apkzlib/zip/ZFile.java
@@ -25,6 +25,7 @@ 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;
@@ -37,6 +38,7 @@ import com.google.common.primitives.Ints;
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;
@@ -199,9 +201,14 @@ public class ZFile implements Closeable {
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.
@@ -209,6 +216,11 @@ public class ZFile implements Closeable {
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;
@@ -220,9 +232,11 @@ public class ZFile implements Closeable {
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.
@@ -255,6 +269,9 @@ public class ZFile implements Closeable {
/**
* 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;
@@ -299,6 +316,9 @@ public class ZFile implements Closeable {
/**
* 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 dirty;
@@ -331,8 +351,8 @@ public class ZFile implements Closeable {
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;
@@ -381,6 +401,24 @@ public class ZFile implements Closeable {
@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
@@ -406,12 +444,30 @@ public class ZFile implements Closeable {
* @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();
@@ -433,6 +489,8 @@ public class ZFile implements Closeable {
if (file.exists()) {
openReadOnly();
+ } else if (readOnly) {
+ throw new IOException("File does not exist but read-only mode requested");
} else {
dirty = true;
}
@@ -450,13 +508,25 @@ public class ZFile implements Closeable {
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);
}
}
@@ -527,7 +597,7 @@ public class ZFile implements Closeable {
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;
@@ -555,6 +625,51 @@ public class ZFile implements Closeable {
* 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);
@@ -575,9 +690,13 @@ public class ZFile implements Closeable {
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;
}
/**
@@ -601,7 +720,6 @@ public class ZFile implements Closeable {
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
@@ -622,10 +740,10 @@ public class ZFile implements Closeable {
/*
* 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.
@@ -677,7 +795,7 @@ public class ZFile implements Closeable {
*/
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) {
@@ -778,7 +896,7 @@ public class ZFile implements Closeable {
return -1;
}
- byte b[] = new byte[1];
+ byte[] b = new byte[1];
int r = directRead(mCurr, b);
if (r > 0) {
mCurr++;
@@ -825,8 +943,11 @@ public class ZFile implements Closeable {
* @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");
@@ -843,6 +964,17 @@ public class ZFile implements Closeable {
}
/**
+ * 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.
*
@@ -850,6 +982,8 @@ public class ZFile implements Closeable {
* the compressor but only reported here
*/
public void update() throws IOException {
+ checkNotInReadOnlyMode();
+
/*
* Process all background stuff before calling in the extensions.
*/
@@ -1145,7 +1279,9 @@ public class ZFile implements Closeable {
// 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 -> {
@@ -1157,6 +1293,8 @@ public class ZFile implements Closeable {
/**
* 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) {
@@ -1166,6 +1304,10 @@ public class ZFile implements Closeable {
if (eocdEntry != null) {
map.remove(eocdEntry);
+
+ Eocd eocd = eocdEntry.getStore();
+ Verify.verify(eocd != null);
+ eocdComment = eocd.getComment();
eocdEntry = null;
}
}
@@ -1340,7 +1482,9 @@ public class ZFile implements Closeable {
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();
@@ -1433,6 +1577,9 @@ public class ZFile implements Closeable {
* 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;
}
@@ -1481,8 +1628,10 @@ public class ZFile implements Closeable {
* 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);
}
@@ -1509,12 +1658,14 @@ public class ZFile implements Closeable {
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);
@@ -1551,21 +1702,28 @@ public class ZFile implements Closeable {
throws IOException {
if (mayCompress) {
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);
- }
- });
+ 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);
+ }
+ },
+ 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);
@@ -1591,9 +1749,11 @@ public class ZFile implements Closeable {
* @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.
@@ -1803,9 +1963,12 @@ public class ZFile implements Closeable {
* @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;
@@ -1859,7 +2022,7 @@ public class ZFile implements Closeable {
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);
@@ -1895,8 +2058,11 @@ public class ZFile implements Closeable {
/**
* 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;
}
@@ -1923,8 +2089,11 @@ public class ZFile implements Closeable {
* 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();
@@ -2038,8 +2207,10 @@ public class ZFile implements Closeable {
* 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);
}
@@ -2047,8 +2218,10 @@ public class ZFile implements Closeable {
* 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);
}
@@ -2092,9 +2265,12 @@ public class ZFile implements Closeable {
* @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");
@@ -2119,8 +2295,10 @@ public class ZFile implements Closeable {
* @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);
}
@@ -2257,8 +2435,10 @@ public class ZFile implements Closeable {
* @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);
}
@@ -2269,10 +2449,13 @@ public class ZFile implements Closeable {
* 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.
@@ -2381,13 +2564,81 @@ public class ZFile implements Closeable {
}
/**
+ * 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) {
@@ -2423,8 +2674,10 @@ public class ZFile implements Closeable {
* 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();
@@ -2487,8 +2740,16 @@ public class ZFile implements Closeable {
}
/**
- * Hint to where files should be positioned.
+ * 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/apkzlib/zip/ZFileOptions.java b/src/main/java/com/android/apkzlib/zip/ZFileOptions.java
index 3d28d81..08a1d83 100644
--- a/src/main/java/com/android/apkzlib/zip/ZFileOptions.java
+++ b/src/main/java/com/android/apkzlib/zip/ZFileOptions.java
@@ -105,8 +105,9 @@ public class ZFileOptions {
*
* @param compressor the compressor
*/
- public void setCompressor(@Nonnull Compressor compressor) {
+ public ZFileOptions setCompressor(@Nonnull Compressor compressor) {
this.compressor = compressor;
+ return this;
}
/**
@@ -123,8 +124,9 @@ public class ZFileOptions {
*
* @param noTimestamps should timestamps be zeroed?
*/
- public void setNoTimestamps(boolean noTimestamps) {
+ public ZFileOptions setNoTimestamps(boolean noTimestamps) {
this.noTimestamps = noTimestamps;
+ return this;
}
/**
@@ -142,8 +144,9 @@ public class ZFileOptions {
*
* @param alignmentRule the alignment rule
*/
- public void setAlignmentRule(@Nonnull AlignmentRule alignmentRule) {
+ public ZFileOptions setAlignmentRule(@Nonnull AlignmentRule alignmentRule) {
this.alignmentRule = alignmentRule;
+ return this;
}
/**
@@ -162,8 +165,9 @@ public class ZFileOptions {
*
* @param coverEmptySpaceUsingExtraField should the extra field be used to cover empty spaces?
*/
- public void setCoverEmptySpaceUsingExtraField(boolean coverEmptySpaceUsingExtraField) {
+ public ZFileOptions setCoverEmptySpaceUsingExtraField(boolean coverEmptySpaceUsingExtraField) {
this.coverEmptySpaceUsingExtraField = coverEmptySpaceUsingExtraField;
+ return this;
}
/**
@@ -177,13 +181,14 @@ public class ZFileOptions {
}
/**
- * Sets whether files should be automatically sorted before updating the zip file. See
- * {@link ZFile} for an explanation on automatic sorting.
+ * 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) {
+ public ZFileOptions setAutoSortFiles(boolean autoSortFiles) {
this.autoSortFiles = autoSortFiles;
+ return this;
}
/**
@@ -191,8 +196,9 @@ public class ZFileOptions {
*
* @param verifyLogFactory verification log factory
*/
- public void setVerifyLogFactory(@Nonnull Supplier<VerifyLog> verifyLogFactory) {
+ public ZFileOptions setVerifyLogFactory(@Nonnull Supplier<VerifyLog> verifyLogFactory) {
this.verifyLogFactory = verifyLogFactory;
+ return this;
}
/**
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 @@ public abstract class ExecutorCompressor implements Compressor {
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.assertEquals;
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 @@ public class JarSigningTest {
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 @@ public class JarSigningTest {
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 class JarSigningTest {
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 class JarSigningTest {
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 @@ public class JarSigningTest {
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 @@ public class JarSigningTest {
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 @@ public class JarSigningTest {
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 @@ public class JarSigningTest {
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 @@ public class JarSigningTest {
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 @@ public class JarSigningTest {
}
@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 @@ public class JarSigningTest {
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 @@ package com.android.apkzlib.utils;
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 @@ public final class ApkZFileTestUtils {
}
}
+ /*
+ * 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 @@ public class AlignmentTest {
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/EncodeUtilsTest.java b/src/test/java/com/android/apkzlib/zip/EncodeUtilsTest.java
index 0eaf1cb..8648aa0 100644
--- a/src/test/java/com/android/apkzlib/zip/EncodeUtilsTest.java
+++ b/src/test/java/com/android/apkzlib/zip/EncodeUtilsTest.java
@@ -56,4 +56,17 @@ public class EncodeUtilsTest {
(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/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 @@ 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 @@ public class ExtraFieldTest {
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 a596399..b7f2979 100644
--- a/src/test/java/com/android/apkzlib/zip/ZFileTest.java
+++ b/src/test/java/com/android/apkzlib/zip/ZFileTest.java
@@ -26,14 +26,14 @@ 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.apkzlib.utils.ApkZFileTestUtils;
import com.android.apkzlib.zip.compress.DeflateExecutionCompressor;
import com.android.apkzlib.zip.utils.CloseableByteSource;
import com.android.apkzlib.zip.utils.RandomAccessFileUtils;
-import com.android.testutils.TestUtils;
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;
@@ -148,6 +148,21 @@ public class ZFileTest {
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 @@ public class ZFileTest {
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());
}
@@ -1631,4 +1671,151 @@ public class ZFileTest {
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 @@ public class MultiCompressorTest {
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 @@ public class MultiCompressorTest {
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
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