summaryrefslogtreecommitdiff
path: root/src/test/java/com/android/apkzlib
diff options
context:
space:
mode:
authorPaulo Casanova <pasc@google.com>2016-11-14 16:02:08 +0000
committerPaulo Casanova <pasc@google.com>2016-11-16 05:38:00 +0000
commit5a1ccbecca5fc359bf488e717db310d307c0c9fc (patch)
treeb414a0e3428c16f5cb24b9bff8c89b9a792d8211 /src/test/java/com/android/apkzlib
parenta5c71db7e08ae4e72804a64e470c6d4e816e2ee7 (diff)
downloadapkzlib-5a1ccbecca5fc359bf488e717db310d307c0c9fc.tar.gz
Renamed apkzlib packages.
Test: Included Change-Id: I9cce74b77719003875deaa5a0056e35f2930429e
Diffstat (limited to 'src/test/java/com/android/apkzlib')
-rw-r--r--src/test/java/com/android/apkzlib/sign/FullApkSignTest.java105
-rw-r--r--src/test/java/com/android/apkzlib/sign/JarSigningTest.java375
-rw-r--r--src/test/java/com/android/apkzlib/sign/ManifestGenerationTest.java180
-rw-r--r--src/test/java/com/android/apkzlib/sign/SignatureTestUtils.java133
-rw-r--r--src/test/java/com/android/apkzlib/utils/ApkZFileTestUtils.java122
-rw-r--r--src/test/java/com/android/apkzlib/utils/CachedFileContentsTest.java123
-rw-r--r--src/test/java/com/android/apkzlib/utils/CachedSupplierTest.java112
-rw-r--r--src/test/java/com/android/apkzlib/zip/AlignmentTest.java773
-rw-r--r--src/test/java/com/android/apkzlib/zip/EncodeUtilsTest.java59
-rw-r--r--src/test/java/com/android/apkzlib/zip/ExtraFieldTest.java335
-rw-r--r--src/test/java/com/android/apkzlib/zip/FileUseMapTest.java134
-rw-r--r--src/test/java/com/android/apkzlib/zip/OldApkReadTest.java38
-rw-r--r--src/test/java/com/android/apkzlib/zip/ReadWithDifferentCompressionLevelsTest.java47
-rw-r--r--src/test/java/com/android/apkzlib/zip/ZFileNotificationTest.java422
-rw-r--r--src/test/java/com/android/apkzlib/zip/ZFileSortTest.java218
-rw-r--r--src/test/java/com/android/apkzlib/zip/ZFileTest.java1418
-rw-r--r--src/test/java/com/android/apkzlib/zip/ZFileTestConstants.java38
-rw-r--r--src/test/java/com/android/apkzlib/zip/ZipMergeTest.java208
-rw-r--r--src/test/java/com/android/apkzlib/zip/ZipTestUtils.java94
-rw-r--r--src/test/java/com/android/apkzlib/zip/ZipToolsTest.java222
-rw-r--r--src/test/java/com/android/apkzlib/zip/compress/MultiCompressorTest.java156
-rw-r--r--src/test/java/com/android/apkzlib/zip/utils/LittleEndianUtilsTest.java112
-rw-r--r--src/test/java/com/android/apkzlib/zip/utils/MsDosDateTimeUtilsTest.java71
23 files changed, 5495 insertions, 0 deletions
diff --git a/src/test/java/com/android/apkzlib/sign/FullApkSignTest.java b/src/test/java/com/android/apkzlib/sign/FullApkSignTest.java
new file mode 100644
index 0000000..d41978a
--- /dev/null
+++ b/src/test/java/com/android/apkzlib/sign/FullApkSignTest.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apkzlib.sign;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertNotNull;
+
+import com.android.apkzlib.utils.ApkZLibPair;
+import com.android.apkzlib.zip.AlignmentRule;
+import com.android.apkzlib.zip.AlignmentRules;
+import com.android.apkzlib.zip.StoredEntry;
+import com.android.apkzlib.zip.ZFile;
+import com.android.apkzlib.zip.ZFileOptions;
+import com.android.apkzlib.zip.ZFileTestConstants;
+import com.android.apkzlib.utils.ApkZFileTestUtils;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+/**
+ * Tests that verify {@link FullApkSignExtension}.
+ */
+public class FullApkSignTest {
+
+ /**
+ * Folder used for tests.
+ */
+ @Rule
+ public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+
+ @Test
+ public void testSignature() throws Exception {
+ File out = new File(mTemporaryFolder.getRoot(), "apk");
+
+ ApkZLibPair<PrivateKey, X509Certificate> signData =
+ SignatureTestUtils.generateSignaturePre18();
+
+ // The byte arrays below are larger when compressed, so we end up storing them uncompressed,
+ // which would normally cause them to be 4-aligned. Disable that, to make calculations
+ // easier.
+ ZFileOptions options = new ZFileOptions();
+ options.setAlignmentRule(AlignmentRules.constant(AlignmentRule.NO_ALIGNMENT));
+
+ /*
+ * Generate a signed zip.
+ */
+ ZFile zf = new ZFile(out, options);
+ FullApkSignExtension signExtension =
+ new FullApkSignExtension(zf, 13, signData.v2, signData.v1);
+ signExtension.register();
+ String f1Name = "abc";
+ byte[] f1Data = new byte[] { 1, 1, 1, 1 };
+ zf.add(f1Name, new ByteArrayInputStream(f1Data));
+ String f2Name = "defg";
+ byte[] f2Data = new byte[] { 2, 2, 2, 2, 3, 3, 3, 3};
+ zf.add(f2Name, new ByteArrayInputStream(f2Data));
+ zf.close();
+
+ /*
+ * We should see the data in place.
+ */
+ int f1DataStart = ZFileTestConstants.LOCAL_HEADER_SIZE + f1Name.length();
+ int f1DataEnd = f1DataStart + f1Data.length;
+ int f2DataStart = f1DataEnd + ZFileTestConstants.LOCAL_HEADER_SIZE + f2Name.length();
+ int f2DataEnd = f2DataStart + f2Data.length;
+
+ byte[] read1 = ApkZFileTestUtils.readSegment(out, f1DataStart, f1Data.length);
+ assertArrayEquals(f1Data, read1);
+ byte[] read2 = ApkZFileTestUtils.readSegment(out, f2DataStart, f2Data.length);
+ assertArrayEquals(f2Data, read2);
+
+ /*
+ * Read the signed zip.
+ */
+ ZFile zf2 = new ZFile(out);
+
+ StoredEntry se1 = zf2.get(f1Name);
+ assertNotNull(se1);
+ assertArrayEquals(f1Data, se1.read());
+
+ StoredEntry se2 = zf2.get(f2Name);
+ assertNotNull(se2);
+ assertArrayEquals(f2Data, se2.read());
+
+ zf2.close();
+ }
+}
diff --git a/src/test/java/com/android/apkzlib/sign/JarSigningTest.java b/src/test/java/com/android/apkzlib/sign/JarSigningTest.java
new file mode 100644
index 0000000..191bf53
--- /dev/null
+++ b/src/test/java/com/android/apkzlib/sign/JarSigningTest.java
@@ -0,0 +1,375 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apkzlib.sign;
+
+import 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.google.common.base.Charsets;
+import com.google.common.hash.Hashing;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.InputStream;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.util.Base64;
+import java.util.jar.Attributes;
+import java.util.jar.Manifest;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+public class JarSigningTest {
+
+ @Rule
+ public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+
+ @Test
+ public void signEmptyJar() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
+
+ try (ZFile zf = new ZFile(zipFile)) {
+ ManifestGenerationExtension manifestExtension =
+ new ManifestGenerationExtension("Me", "Me");
+ manifestExtension.register(zf);
+
+ ApkZLibPair<PrivateKey, X509Certificate> p =
+ SignatureTestUtils.generateSignaturePre18();
+
+ SignatureExtension signatureExtension =
+ new SignatureExtension(manifestExtension, 12, p.v2, p.v1, null);
+ signatureExtension.register();
+ }
+
+ try (ZFile verifyZFile = new ZFile(zipFile)) {
+ StoredEntry manifestEntry = verifyZFile.get("META-INF/MANIFEST.MF");
+ assertNotNull(manifestEntry);
+
+ Manifest manifest = new Manifest(new ByteArrayInputStream(manifestEntry.read()));
+ assertEquals(3, manifest.getMainAttributes().size());
+ assertEquals("1.0", manifest.getMainAttributes().getValue("Manifest-Version"));
+ assertEquals("Me", manifest.getMainAttributes().getValue("Created-By"));
+ assertEquals("Me", manifest.getMainAttributes().getValue("Built-By"));
+ }
+ }
+
+ @Test
+ public void signJarWithPrexistingSimpleTextFilePre18() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
+ ApkZLibPair<PrivateKey, X509Certificate> p = SignatureTestUtils.generateSignaturePre18();
+
+ try (ZFile zf1 = new ZFile(zipFile)) {
+ zf1.add("directory/file",
+ new ByteArrayInputStream("useless text".getBytes(Charsets.US_ASCII)));
+ }
+
+ try (ZFile zf2 = new ZFile(zipFile)) {
+ ManifestGenerationExtension me = new ManifestGenerationExtension("Merry", "Christmas");
+ me.register(zf2);
+ new SignatureExtension(me, 10, p.v2, p.v1, null).register();
+ }
+
+ try (ZFile zf3 = new ZFile(zipFile)) {
+ StoredEntry manifestEntry = zf3.get("META-INF/MANIFEST.MF");
+ assertNotNull(manifestEntry);
+
+ Manifest manifest = new Manifest(new ByteArrayInputStream(manifestEntry.read()));
+ assertEquals(3, manifest.getMainAttributes().size());
+ assertEquals("1.0", manifest.getMainAttributes().getValue("Manifest-Version"));
+ assertEquals("Merry", manifest.getMainAttributes().getValue("Built-By"));
+ assertEquals("Christmas", manifest.getMainAttributes().getValue("Created-By"));
+
+ Attributes attrs = manifest.getAttributes("directory/file");
+ assertNotNull(attrs);
+ assertEquals(1, attrs.size());
+ assertEquals("OOQgIEXBissIvva3ydRoaXk29Rk=", attrs.getValue("SHA1-Digest"));
+
+ StoredEntry signatureEntry = zf3.get("META-INF/CERT.SF");
+ assertNotNull(signatureEntry);
+
+ Manifest signature = new Manifest(new ByteArrayInputStream(signatureEntry.read()));
+ assertEquals(3, signature.getMainAttributes().size());
+ assertEquals("1.0", signature.getMainAttributes().getValue("Signature-Version"));
+ assertEquals("1.0 (Android)", signature.getMainAttributes().getValue("Created-By"));
+
+ byte[] manifestTextBytes = manifestEntry.read();
+ byte[] manifestSha1Bytes = Hashing.sha1().hashBytes(manifestTextBytes).asBytes();
+ String manifestSha1 = Base64.getEncoder().encodeToString(manifestSha1Bytes);
+
+ assertEquals(manifestSha1,
+ signature.getMainAttributes().getValue("SHA1-Digest-Manifest"));
+
+ Attributes signAttrs = signature.getAttributes("directory/file");
+ assertNotNull(signAttrs);
+ assertEquals(1, signAttrs.size());
+ assertEquals("OOQgIEXBissIvva3ydRoaXk29Rk=", signAttrs.getValue("SHA1-Digest"));
+
+ StoredEntry rsaEntry = zf3.get("META-INF/CERT.RSA");
+ assertNotNull(rsaEntry);
+ }
+ }
+
+ @Test
+ public void signJarWithPrexistingSimpleTextFilePos18() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
+ try (ZFile zf1 = new ZFile(zipFile)) {
+ zf1.add("directory/file", new ByteArrayInputStream("useless text".getBytes(
+ Charsets.US_ASCII)));
+ }
+
+ ApkZLibPair<PrivateKey, X509Certificate> p = SignatureTestUtils.generateSignaturePos18();
+
+ try (ZFile zf2 = new ZFile(zipFile)) {
+ ManifestGenerationExtension me = new ManifestGenerationExtension("Merry", "Christmas");
+ me.register(zf2);
+ new SignatureExtension(me, 21, p.v2, p.v1, null).register();
+ }
+
+ try (ZFile zf3 = new ZFile(zipFile)) {
+ StoredEntry manifestEntry = zf3.get("META-INF/MANIFEST.MF");
+ assertNotNull(manifestEntry);
+
+ Manifest manifest = new Manifest(new ByteArrayInputStream(manifestEntry.read()));
+ assertEquals(3, manifest.getMainAttributes().size());
+ assertEquals("1.0", manifest.getMainAttributes().getValue("Manifest-Version"));
+ assertEquals("Merry", manifest.getMainAttributes().getValue("Built-By"));
+ assertEquals("Christmas", manifest.getMainAttributes().getValue("Created-By"));
+
+ Attributes attrs = manifest.getAttributes("directory/file");
+ assertNotNull(attrs);
+ assertEquals(1, attrs.size());
+ assertEquals("QjupZsopQM/01O6+sWHqH64ilMmoBEtljg9VEqN6aI4=",
+ attrs.getValue("SHA-256-Digest"));
+
+ StoredEntry signatureEntry = zf3.get("META-INF/CERT.SF");
+ assertNotNull(signatureEntry);
+
+ Manifest signature = new Manifest(new ByteArrayInputStream(signatureEntry.read()));
+ assertEquals(3, signature.getMainAttributes().size());
+ assertEquals("1.0", signature.getMainAttributes().getValue("Signature-Version"));
+ assertEquals("1.0 (Android)", signature.getMainAttributes().getValue("Created-By"));
+
+ byte[] manifestTextBytes = manifestEntry.read();
+ byte[] manifestSha256Bytes = Hashing.sha256().hashBytes(manifestTextBytes).asBytes();
+ String manifestSha256 = Base64.getEncoder().encodeToString(manifestSha256Bytes);
+
+ assertEquals(manifestSha256, signature.getMainAttributes().getValue(
+ "SHA-256-Digest-Manifest"));
+
+ Attributes signAttrs = signature.getAttributes("directory/file");
+ assertNotNull(signAttrs);
+ assertEquals(1, signAttrs.size());
+ assertEquals("QjupZsopQM/01O6+sWHqH64ilMmoBEtljg9VEqN6aI4=",
+ signAttrs.getValue("SHA-256-Digest"));
+
+ StoredEntry ecdsaEntry = zf3.get("META-INF/CERT.EC");
+ assertNotNull(ecdsaEntry);
+ }
+ }
+
+ @Test
+ public void v2SignAddsApkSigningBlock() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
+ try (ZFile zf = new ZFile(zipFile)) {
+ ManifestGenerationExtension manifestExtension =
+ new ManifestGenerationExtension("Me", "Me");
+ manifestExtension.register(zf);
+
+ ApkZLibPair<PrivateKey, X509Certificate> p = SignatureTestUtils.generateSignaturePre18();
+
+ FullApkSignExtension signatureExtension =
+ new FullApkSignExtension(zf, 12, p.v2, p.v1);
+ signatureExtension.register();
+ }
+
+ try (ZFile verifyZFile = new ZFile(zipFile)) {
+ long centralDirOffset = verifyZFile.getCentralDirectoryOffset();
+ byte[] apkSigningBlockMagic = new byte[16];
+ verifyZFile.directFullyRead(
+ centralDirOffset - apkSigningBlockMagic.length, apkSigningBlockMagic);
+ assertEquals("APK Sig Block 42", new String(apkSigningBlockMagic, "US-ASCII"));
+ }
+ }
+
+ @Test
+ public void v1ReSignOnFileChange() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
+ ApkZLibPair<PrivateKey, X509Certificate> p = SignatureTestUtils.generateSignaturePos18();
+
+ byte[] file1Contents = "I am a test file".getBytes(Charsets.US_ASCII);
+ String file1Name = "path/to/file1";
+ byte[] file1Sha = Hashing.sha256().hashBytes(file1Contents).asBytes();
+ String file1ShaTxt = Base64.getEncoder().encodeToString(file1Sha);
+
+ String builtBy = "Santa Claus";
+ String createdBy = "Uses Android";
+
+ try (ZFile zf1 = new ZFile(zipFile)) {
+ zf1.add(file1Name, new ByteArrayInputStream(file1Contents));
+ ManifestGenerationExtension me = new ManifestGenerationExtension(builtBy, createdBy);
+ me.register(zf1);
+ new SignatureExtension(me, 21, p.v2, p.v1, null).register();
+
+ zf1.update();
+
+ StoredEntry manifestEntry = zf1.get("META-INF/MANIFEST.MF");
+ assertNotNull(manifestEntry);
+
+ try (InputStream manifestIs = manifestEntry.open()) {
+ Manifest manifest = new Manifest(manifestIs);
+
+ assertEquals(1, manifest.getEntries().size());
+
+ Attributes file1Attrs = manifest.getEntries().get(file1Name);
+ assertNotNull(file1Attrs);
+ assertEquals(file1ShaTxt, file1Attrs.getValue("SHA-256-Digest"));
+ }
+
+ /*
+ * Change the file without closing the zip.
+ */
+ file1Contents = "I am a modified test file".getBytes(Charsets.US_ASCII);
+ file1Sha = Hashing.sha256().hashBytes(file1Contents).asBytes();
+ file1ShaTxt = Base64.getEncoder().encodeToString(file1Sha);
+
+ zf1.add(file1Name, new ByteArrayInputStream(file1Contents));
+
+ zf1.update();
+
+ manifestEntry = zf1.get("META-INF/MANIFEST.MF");
+ assertNotNull(manifestEntry);
+
+ try (InputStream manifestIs = manifestEntry.open()) {
+ Manifest manifest = new Manifest(manifestIs);
+
+ assertEquals(1, manifest.getEntries().size());
+
+ Attributes file1Attrs = manifest.getEntries().get(file1Name);
+ assertNotNull(file1Attrs);
+ assertEquals(file1ShaTxt, file1Attrs.getValue("SHA-256-Digest"));
+ }
+ }
+
+ /*
+ * Change the file closing the zip.
+ */
+ file1Contents = "I have changed again!".getBytes(Charsets.US_ASCII);
+ file1Sha = Hashing.sha256().hashBytes(file1Contents).asBytes();
+ file1ShaTxt = Base64.getEncoder().encodeToString(file1Sha);
+
+ try (ZFile zf2 = new ZFile(zipFile)) {
+ ManifestGenerationExtension me = new ManifestGenerationExtension(builtBy, createdBy);
+ me.register(zf2);
+ new SignatureExtension(me, 21, p.v2, p.v1, null).register();
+
+ zf2.add(file1Name, new ByteArrayInputStream(file1Contents));
+
+ zf2.update();
+
+ StoredEntry manifestEntry = zf2.get("META-INF/MANIFEST.MF");
+ assertNotNull(manifestEntry);
+
+ try (InputStream manifestIs = manifestEntry.open()) {
+ Manifest manifest = new Manifest(manifestIs);
+
+ assertEquals(1, manifest.getEntries().size());
+
+ Attributes file1Attrs = manifest.getEntries().get(file1Name);
+ assertNotNull(file1Attrs);
+ assertEquals(file1ShaTxt, file1Attrs.getValue("SHA-256-Digest"));
+ }
+ }
+ }
+
+ @Test
+ public void openSignedJarDoesNotForcesWriteifSignatureIsNotCorrect() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
+
+ ApkZLibPair<PrivateKey, X509Certificate> p = SignatureTestUtils.generateSignaturePos18();
+
+ String fileName = "file";
+ byte[] fileContents = "Very interesting contents".getBytes(Charsets.US_ASCII);
+
+ try (ZFile zf = new ZFile(zipFile)) {
+ ManifestGenerationExtension me = new ManifestGenerationExtension("I", "Android");
+ me.register(zf);
+ new SignatureExtension(me, 21, p.v2, p.v1, null).register();
+
+ zf.add(fileName, new ByteArrayInputStream(fileContents));
+ }
+
+ long fileTimestamp = zipFile.lastModified();
+
+ ApkZFileTestUtils.waitForFileSystemTick(fileTimestamp);
+
+ /*
+ * Open the zip file, but don't touch it.
+ */
+ try (ZFile zf = new ZFile(zipFile)) {
+ ManifestGenerationExtension me = new ManifestGenerationExtension("I", "Android");
+ me.register(zf);
+ new SignatureExtension(me, 21, p.v2, p.v1, null).register();
+ }
+
+ /*
+ * Check the file wasn't touched.
+ */
+ assertEquals(fileTimestamp, zipFile.lastModified());
+
+ /*
+ * Change the file contents ignoring any signing.
+ */
+ fileContents = "Not so interesting contents".getBytes(Charsets.US_ASCII);
+ try (ZFile zf = new ZFile(zipFile)) {
+ zf.add(fileName, new ByteArrayInputStream(fileContents));
+ }
+
+ fileTimestamp = zipFile.lastModified();
+
+ /*
+ * Wait to make sure the timestamp can increase.
+ */
+ while (true) {
+ File notUsed = mTemporaryFolder.newFile();
+ long notTimestamp = notUsed.lastModified();
+ notUsed.delete();
+ if (notTimestamp > fileTimestamp) {
+ break;
+ }
+ }
+
+ /*
+ * Open the zip file, but do any changes. The need to updating the signature should force
+ * a file update.
+ */
+ try (ZFile zf = new ZFile(zipFile)) {
+ ManifestGenerationExtension me = new ManifestGenerationExtension("I", "Android");
+ me.register(zf);
+ new SignatureExtension(me, 21, p.v2, p.v1, null).register();
+ }
+
+ /*
+ * Check the file was touched.
+ */
+ assertNotEquals(fileTimestamp, zipFile.lastModified());
+ }
+}
diff --git a/src/test/java/com/android/apkzlib/sign/ManifestGenerationTest.java b/src/test/java/com/android/apkzlib/sign/ManifestGenerationTest.java
new file mode 100644
index 0000000..746617b
--- /dev/null
+++ b/src/test/java/com/android/apkzlib/sign/ManifestGenerationTest.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apkzlib.sign;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import com.android.apkzlib.zip.StoredEntry;
+import com.android.apkzlib.zip.ZFile;
+import com.android.apkzlib.utils.ApkZFileTestUtils;
+import com.google.common.base.Charsets;
+import com.google.common.io.Closer;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.InputStream;
+import java.util.Set;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.mockito.internal.util.collections.Sets;
+
+public class ManifestGenerationTest {
+
+ private static final String WIKI_PATH = "/testData/packaging/text-files/wikipedia.html";
+
+ @Rule
+ public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+
+ @Test
+ public void elementaryManifestGeneration() throws Exception {
+ File zip = new File(mTemporaryFolder.getRoot(), "f.zip");
+
+ try (ZFile zf = new ZFile(zip)) {
+ zf.add("abc", new ByteArrayInputStream(new byte[]{1}));
+ zf.add("x/", new ByteArrayInputStream(new byte[0]));
+ zf.add("x/abc", new ByteArrayInputStream(new byte[]{2}));
+
+ ManifestGenerationExtension extension =
+ new ManifestGenerationExtension("Me, of course", "Myself");
+ extension.register(zf);
+
+ zf.update();
+
+ StoredEntry se = zf.get("META-INF/MANIFEST.MF");
+ assertNotNull(se);
+
+ String text = new String(se.read(), Charsets.US_ASCII);
+ text = text.trim();
+ String lines[] = text.split(System.getProperty("line.separator"));
+ assertEquals(3, lines.length);
+
+ assertEquals("Manifest-Version: 1.0", lines[0].trim());
+
+ Set<String> linesSet = Sets.newSet();
+ for (String l : lines) {
+ linesSet.add(l.trim());
+ }
+
+ assertTrue(linesSet.contains("Built-By: Me, of course"));
+ assertTrue(linesSet.contains("Created-By: Myself"));
+ }
+ }
+
+ @Test
+ public void manifestGenerationOnHalfWrittenFile() throws Exception {
+ File zip = new File(mTemporaryFolder.getRoot(), "f.zip");
+ try (Closer closer = Closer.create()) {
+ ZFile zf = closer.register(new ZFile(zip));
+
+ try (InputStream wiki = getClass().getResourceAsStream(WIKI_PATH)) {
+ zf.add("wiki", wiki);
+ }
+
+ ManifestGenerationExtension extension =
+ new ManifestGenerationExtension("Me, of course", "Myself");
+ extension.register(zf);
+
+ zf.close();
+
+ StoredEntry se = zf.get("META-INF/MANIFEST.MF");
+ assertNotNull(se);
+
+ String text = new String(se.read(), Charsets.US_ASCII);
+ text = text.trim();
+ String lines[] = text.split(System.getProperty("line.separator"));
+ assertEquals(3, lines.length);
+
+ assertEquals("Manifest-Version: 1.0", lines[0].trim());
+
+ Set<String> linesSet = Sets.newSet();
+ for (String l : lines) {
+ linesSet.add(l.trim());
+ }
+
+ assertTrue(linesSet.contains("Built-By: Me, of course"));
+ assertTrue(linesSet.contains("Created-By: Myself"));
+ }
+ }
+
+ @Test
+ public void manifestGenerationOnExistingFile() throws Exception {
+ File zip = new File(mTemporaryFolder.getRoot(), "f.zip");
+ try (Closer closer = Closer.create()) {
+ ZFile zf = closer.register(new ZFile(zip));
+
+ try (InputStream wiki = getClass().getResourceAsStream(WIKI_PATH)) {
+ zf.add("wiki", wiki);
+ }
+
+ zf.close();
+
+ ManifestGenerationExtension extension =
+ new ManifestGenerationExtension("Me, of course", "Myself");
+ extension.register(zf);
+
+ zf.close();
+
+ StoredEntry se = zf.get("META-INF/MANIFEST.MF");
+ assertNotNull(se);
+
+ String text = new String(se.read(), Charsets.US_ASCII);
+ text = text.trim();
+ String lines[] = text.split(System.getProperty("line.separator"));
+ assertEquals(3, lines.length);
+
+ assertEquals("Manifest-Version: 1.0", lines[0].trim());
+
+ Set<String> linesSet = Sets.newSet();
+ for (String l : lines) {
+ linesSet.add(l.trim());
+ }
+
+ assertTrue(linesSet.contains("Built-By: Me, of course"));
+ assertTrue(linesSet.contains("Created-By: Myself"));
+ }
+ }
+
+ @Test
+ public void manifestGenerationOnIncrementalNoChanges() throws Exception {
+ File zip = new File(mTemporaryFolder.getRoot(), "f.zip");
+ try (Closer closer = Closer.create()) {
+ ZFile zf = closer.register(new ZFile(zip));
+
+ ManifestGenerationExtension extension =
+ new ManifestGenerationExtension("Me, of course", "Myself");
+ extension.register(zf);
+
+ try (InputStream wiki = getClass().getResourceAsStream(WIKI_PATH)) {
+ zf.add("wiki", wiki);
+ }
+
+ zf.close();
+
+ long timeOfWriting = zip.lastModified();
+
+ ApkZFileTestUtils.waitForFileSystemTick(timeOfWriting);
+
+ zf = closer.register(new ZFile(zip));
+ zf.close();
+
+ long secondTimeOfWriting = zip.lastModified();
+ assertEquals(timeOfWriting, secondTimeOfWriting);
+ }
+ }
+}
diff --git a/src/test/java/com/android/apkzlib/sign/SignatureTestUtils.java b/src/test/java/com/android/apkzlib/sign/SignatureTestUtils.java
new file mode 100644
index 0000000..71610ef
--- /dev/null
+++ b/src/test/java/com/android/apkzlib/sign/SignatureTestUtils.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apkzlib.sign;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+import com.android.annotations.NonNull;
+import com.android.apkzlib.utils.ApkZLibPair;
+import java.math.BigInteger;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.ECPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.util.Date;
+import javax.security.auth.x500.X500Principal;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
+import org.bouncycastle.cert.X509CertificateHolder;
+import org.bouncycastle.cert.X509v1CertificateBuilder;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.crypto.params.RSAKeyParameters;
+import org.bouncycastle.crypto.util.SubjectPublicKeyInfoFactory;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.operator.ContentSigner;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+import org.junit.Assume;
+
+/**
+ * Utilities to use signatures in tests.
+ */
+public class SignatureTestUtils {
+
+ /**
+ * Generates a private key / certificate for pre-18 systems.
+ *
+ * @return the pair with the private key and certificate
+ * @throws Exception failed to generate the signature data
+ */
+ @NonNull
+ public static ApkZLibPair<PrivateKey, X509Certificate> generateSignaturePre18()
+ throws Exception {
+ return generateSignature("RSA", "SHA1withRSA");
+ }
+
+ /**
+ * Generates a private key / certificate for post-18 systems.
+ *
+ * @return the pair with the private key and certificate
+ * @throws Exception failed to generate the signature data
+ */
+ @NonNull
+ public static ApkZLibPair<PrivateKey, X509Certificate> generateSignaturePos18()
+ throws Exception {
+ return generateSignature("EC", "SHA256withECDSA");
+ }
+
+ /**
+ * Generates a private key / certificate.
+ *
+ * @param sign the asymmetric cypher, <em>e.g.</em>, {@code RSA}
+ * @param full the full signature algorithm name, <em>e.g.</em>, {@code SHA1withRSA}
+ * @return the pair with the private key and certificate
+ * @throws Exception failed to generate the signature data
+ */
+ @NonNull
+ public static ApkZLibPair<PrivateKey, X509Certificate> generateSignature(
+ @NonNull String sign,
+ @NonNull String full)
+ throws Exception {
+ // http://stackoverflow.com/questions/28538785/
+ // easy-way-to-generate-a-self-signed-certificate-for-java-security-keystore-using
+
+ KeyPairGenerator generator = null;
+ try {
+ generator = KeyPairGenerator.getInstance(sign);
+ } catch (NoSuchAlgorithmException e) {
+ Assume.assumeNoException("Algorithm " + sign + " not supported.", e);
+ }
+
+ assertNotNull(generator);
+ KeyPair keyPair = generator.generateKeyPair();
+
+ Date notBefore = new Date(System.currentTimeMillis() - 24 * 60 * 60 * 1000);
+ Date notAfter = new Date(System.currentTimeMillis() + 365L * 24 * 60 * 60 * 1000);
+
+ X500Name issuer = new X500Name(new X500Principal("cn=Myself").getName());
+
+ SubjectPublicKeyInfo publicKeyInfo;
+
+ if (keyPair.getPublic() instanceof RSAPublicKey) {
+ RSAPublicKey rsaPublicKey = (RSAPublicKey) keyPair.getPublic();
+ publicKeyInfo = SubjectPublicKeyInfoFactory.createSubjectPublicKeyInfo(
+ new RSAKeyParameters(false, rsaPublicKey.getModulus(),
+ rsaPublicKey.getPublicExponent()));
+ } else if (keyPair.getPublic() instanceof ECPublicKey) {
+ publicKeyInfo = SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded());
+ } else {
+ fail();
+ publicKeyInfo = null;
+ }
+
+ X509v1CertificateBuilder builder = new X509v1CertificateBuilder(issuer, BigInteger.ONE,
+ notBefore, notAfter, issuer, publicKeyInfo);
+
+ ContentSigner signer = new JcaContentSignerBuilder(full).setProvider(
+ new BouncyCastleProvider()).build(keyPair.getPrivate());
+ X509CertificateHolder holder = builder.build(signer);
+
+ JcaX509CertificateConverter converter = new JcaX509CertificateConverter()
+ .setProvider(new BouncyCastleProvider());
+
+ return new ApkZLibPair(keyPair.getPrivate(), converter.getCertificate(holder));
+ }
+
+}
diff --git a/src/test/java/com/android/apkzlib/utils/ApkZFileTestUtils.java b/src/test/java/com/android/apkzlib/utils/ApkZFileTestUtils.java
new file mode 100644
index 0000000..bb379b5
--- /dev/null
+++ b/src/test/java/com/android/apkzlib/utils/ApkZFileTestUtils.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apkzlib.utils;
+
+import static org.junit.Assert.assertTrue;
+
+import com.android.annotations.NonNull;
+import com.google.common.base.Preconditions;
+import com.google.common.io.Resources;
+import java.io.EOFException;
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.net.URL;
+
+/**
+ * Utility functions for tests.
+ */
+public final class ApkZFileTestUtils {
+
+ /**
+ * Reads a portion of a file to memory.
+ *
+ * @param file the file to read data from
+ * @param start the offset in the file to start reading
+ * @param length the number of bytes to read
+ * @return the bytes read
+ * @throws Exception failed to read the file
+ */
+ @NonNull
+ public static byte[] readSegment(@NonNull File file, long start, int length) throws Exception {
+ Preconditions.checkArgument(start >= 0, "start < 0");
+ Preconditions.checkArgument(length >= 0, "length < 0");
+
+ byte data[];
+ try (RandomAccessFile raf = new RandomAccessFile(file, "r")) {
+ raf.seek(start);
+
+ data = new byte[length];
+ int tot = 0;
+ while (tot < length) {
+ int r = raf.read(data, tot, length - tot);
+ if (r < 0) {
+ throw new EOFException();
+ }
+
+ tot += r;
+ }
+ }
+
+ return data;
+ }
+
+ /**
+ * Obtains the test resource with the given path.
+ *
+ * @param path the path
+ * @return the test resource
+ */
+ @NonNull
+ public static File getResource(@NonNull String path) {
+ URL url = Resources.getResource(ApkZFileTestUtils.class, path);
+ File resource = new File(url.getFile());
+ assertTrue(resource.exists());
+ return resource;
+ }
+
+ /**
+ * Sleeps the current thread for enough time to ensure that the local file system had enough
+ * time to notice a "tick". This method is usually called in tests when it is necessary to
+ * ensure filesystem writes are detected through timestamp modification.
+ *
+ * @throws InterruptedException waiting interrupted
+ * @throws IOException issues creating a temporary file
+ */
+ public static void waitForFileSystemTick() throws InterruptedException, IOException {
+ waitForFileSystemTick(getFreshTimestamp());
+ }
+
+ /**
+ * Sleeps the current thread for enough time to ensure that the local file system had enough
+ * time to notice a "tick". This method is usually called in tests when it is necessary to
+ * ensure filesystem writes are detected through timestamp modification.
+ *
+ * @param currentTimestamp last timestamp read from disk
+ * @throws InterruptedException waiting interrupted
+ * @throws IOException issues creating a temporary file
+ */
+ public static void waitForFileSystemTick(long currentTimestamp)
+ throws InterruptedException, IOException {
+ while (getFreshTimestamp() <= currentTimestamp) {
+ Thread.sleep(100);
+ }
+ }
+
+ /**
+ * Obtains the timestamp of a newly-created file.
+ *
+ * @return the timestamp
+ * @throws IOException the I/O Exception
+ */
+ private static long getFreshTimestamp() throws IOException {
+ File notUsed = File.createTempFile(ApkZFileTestUtils.class.getName(), "waitForFSTick");
+ long freshTimestamp = notUsed.lastModified();
+ assertTrue(notUsed.delete());
+ return freshTimestamp;
+ }
+}
diff --git a/src/test/java/com/android/apkzlib/utils/CachedFileContentsTest.java b/src/test/java/com/android/apkzlib/utils/CachedFileContentsTest.java
new file mode 100644
index 0000000..f9654d7
--- /dev/null
+++ b/src/test/java/com/android/apkzlib/utils/CachedFileContentsTest.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apkzlib.utils;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.base.Charsets;
+import com.google.common.io.Files;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+
+public class CachedFileContentsTest {
+ @Rule
+ public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+
+ @Test
+ public void createFileAndCheckWithNoChanges() throws Exception {
+ File f = mTemporaryFolder.newFile("test");
+ Files.write("abc", f, Charsets.US_ASCII);
+
+ Object cache = new Object();
+
+ CachedFileContents<Object> cachedFile = new CachedFileContents<>(f);
+ cachedFile.closed(cache);
+
+ assertTrue(cachedFile.isValid());
+ assertSame(cache, cachedFile.getCache());
+ }
+
+ @Test
+ public void createFileAndCheckChanges() throws Exception {
+ File f = mTemporaryFolder.newFile("test");
+ Files.write("abc", f, Charsets.US_ASCII);
+
+ Object cache = new Object();
+
+ CachedFileContents<Object> cachedFile = new CachedFileContents<>(f);
+ cachedFile.closed(cache);
+
+ Files.write("def", f, Charsets.US_ASCII);
+
+ assertFalse(cachedFile.isValid());
+ assertNull(cachedFile.getCache());
+ }
+
+ @Test
+ public void createFileUpdateAndCheckChanges() throws Exception {
+ File f = mTemporaryFolder.newFile("test");
+ Files.write("abc", f, Charsets.US_ASCII);
+
+ Object cache = new Object();
+
+ CachedFileContents<Object> cachedFile = new CachedFileContents<>(f);
+ cachedFile.closed(cache);
+
+ Files.write("def", f, Charsets.US_ASCII);
+ cachedFile.closed(cache);
+
+ assertTrue(cachedFile.isValid());
+ assertSame(cache, cachedFile.getCache());
+ }
+
+ @Test
+ public void immediateChangesDetected() throws Exception {
+ File f = mTemporaryFolder.newFile("foo");
+ Files.write("bar", f, Charsets.US_ASCII);
+
+ CachedFileContents<Object> cachedFile = new CachedFileContents<>(f);
+ cachedFile.closed(null);
+
+ Files.write("xpto", f, Charsets.US_ASCII);
+ assertFalse(cachedFile.isValid());
+ }
+
+ @Test
+ public void immediateChangesDetectedEvenWithHackedTs() throws Exception {
+ File f = mTemporaryFolder.newFile("foo");
+ Files.write("bar", f, Charsets.US_ASCII);
+
+ CachedFileContents<Object> cachedFile = new CachedFileContents<>(f);
+ cachedFile.closed(null);
+ long lastTs = f.lastModified();
+
+ Files.write("xpto", f, Charsets.US_ASCII);
+ f.setLastModified(lastTs);
+ assertFalse(cachedFile.isValid());
+ }
+
+ @Test
+ public void immediateChangesWithNoContentChangeNotDetected() throws Exception {
+ File f = mTemporaryFolder.newFile("foo");
+ Files.write("bar", f, Charsets.US_ASCII);
+
+ CachedFileContents<Object> cachedFile = new CachedFileContents<>(f);
+ cachedFile.closed(null);
+ long lastTs = f.lastModified();
+
+ Files.write("bar", f, Charsets.US_ASCII);
+ f.setLastModified(lastTs);
+ assertTrue(cachedFile.isValid());
+ }
+}
diff --git a/src/test/java/com/android/apkzlib/utils/CachedSupplierTest.java b/src/test/java/com/android/apkzlib/utils/CachedSupplierTest.java
new file mode 100644
index 0000000..e687bf3
--- /dev/null
+++ b/src/test/java/com/android/apkzlib/utils/CachedSupplierTest.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apkzlib.utils;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import org.junit.Test;
+
+import java.util.function.Supplier;
+
+public class CachedSupplierTest {
+
+ @Test
+ public void testGetsOnlyOnce() {
+ TestSupplier ts = new TestSupplier();
+ CachedSupplier<String> cs = new CachedSupplier<>(ts);
+ assertFalse(cs.isValid());
+
+ ts.value = "foo";
+ assertEquals(0, ts.invocationCount);
+ assertEquals("foo", cs.get());
+ assertEquals(1, ts.invocationCount);
+ assertTrue(cs.isValid());
+
+ ts.value = "bar";
+ assertEquals("foo", cs.get());
+ assertEquals(1, ts.invocationCount);
+ assertTrue(cs.isValid());
+ }
+
+ @Test
+ public void cacheCanBePreset() {
+ TestSupplier ts = new TestSupplier();
+ ts.value = "foo";
+ CachedSupplier<String> cs = new CachedSupplier<>(ts);
+ cs.precomputed("bar");
+ assertTrue(cs.isValid());
+
+ assertEquals("bar", cs.get());
+ assertEquals(0, ts.invocationCount);
+ }
+
+ @Test
+ public void exceptionThrownBySupplier() {
+ CachedSupplier<String> cs = new CachedSupplier<>(() -> {
+ throw new RuntimeException("foo");
+ });
+ assertFalse(cs.isValid());
+
+ try {
+ cs.get();
+ fail();
+ } catch (RuntimeException e) {
+ assertEquals("foo", e.getMessage());
+ }
+
+ assertFalse(cs.isValid());
+
+ try {
+ cs.get();
+ fail();
+ } catch (RuntimeException e) {
+ assertEquals("foo", e.getMessage());
+ }
+ }
+
+ @Test
+ public void reset() {
+ TestSupplier ts = new TestSupplier();
+ ts.value = "foo";
+ CachedSupplier<String> cs = new CachedSupplier<>(ts);
+ assertFalse(cs.isValid());
+
+ assertEquals("foo", cs.get());
+ assertEquals(1, ts.invocationCount);
+ assertTrue(cs.isValid());
+ ts.value = "bar";
+
+ cs.reset();
+ assertFalse(cs.isValid());
+ assertEquals("bar", cs.get());
+ assertEquals(2, ts.invocationCount);
+ }
+
+ static class TestSupplier implements Supplier<String> {
+ int invocationCount = 0;
+ String value;
+
+ @Override
+ public String get() {
+ invocationCount++;
+ return value;
+ }
+ }
+}
diff --git a/src/test/java/com/android/apkzlib/zip/AlignmentTest.java b/src/test/java/com/android/apkzlib/zip/AlignmentTest.java
new file mode 100644
index 0000000..3dfe917
--- /dev/null
+++ b/src/test/java/com/android/apkzlib/zip/AlignmentTest.java
@@ -0,0 +1,773 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apkzlib.zip;
+
+import static com.android.apkzlib.utils.ApkZFileTestUtils.readSegment;
+import static junit.framework.TestCase.assertEquals;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.base.Charsets;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.util.Random;
+import java.util.Set;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+public class AlignmentTest {
+
+ private static final AlignmentRule SUFFIX_ALIGNMENT_RULES =
+ AlignmentRules.compose(
+ // Disable 4-aligning of uncompressed *.u files, so we can more easily
+ // calculate offsets for testing.
+ AlignmentRules.constantForSuffix(".u", 1),
+ AlignmentRules.constantForSuffix(".a", 1024));
+ @Rule
+ public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+
+ @Test
+ public void addAlignedFile() throws Exception {
+ File newZFile = new File(mTemporaryFolder.getRoot(), "test.zip");
+
+ byte testBytes[] = "This is some text.".getBytes(Charsets.US_ASCII);
+
+ ZFileOptions options = new ZFileOptions();
+ options.setAlignmentRule(AlignmentRules.constantForSuffix(".txt", 1024));
+ try (ZFile zf = new ZFile(newZFile, options)) {
+ zf.add("test.txt", new ByteArrayInputStream(testBytes), false);
+ }
+
+ byte found[] = readSegment(newZFile, 1024, testBytes.length);
+ assertArrayEquals(testBytes, found);
+ }
+
+ @Test
+ public void addNonAlignedFile() throws Exception {
+ File newZFile = new File(mTemporaryFolder.getRoot(), "test.zip");
+
+ byte testBytes[] = "This is some text.".getBytes(Charsets.US_ASCII);
+
+ ZFileOptions options = new ZFileOptions();
+ options.setAlignmentRule(AlignmentRules.constantForSuffix(".txt", 1024));
+ try (ZFile zf = new ZFile(newZFile, options)) {
+ zf.add("test.txt.foo", new ByteArrayInputStream(testBytes), false);
+ }
+
+ assertTrue(newZFile.length() < 1024);
+ }
+
+ @Test
+ public void realignSingleFile() throws Exception {
+ File newZFile = new File(mTemporaryFolder.getRoot(), "test.zip");
+
+ byte testBytes0[] = "Text number 1".getBytes(Charsets.US_ASCII);
+ byte testBytes1[] = "Text number 2, which is actually 1".getBytes(Charsets.US_ASCII);
+
+ long offset0;
+ try (ZFile zf = new ZFile(newZFile)) {
+ zf.add("file1.txt", new ByteArrayInputStream(testBytes1), false);
+ zf.add("file0.txt", new ByteArrayInputStream(testBytes0), false);
+ zf.close();
+
+ StoredEntry se0 = zf.get("file0.txt");
+ assertNotNull(se0);
+ offset0 = se0.getCentralDirectoryHeader().getOffset();
+
+ StoredEntry se1 = zf.get("file1.txt");
+ assertNotNull(se1);
+
+ assertTrue(newZFile.length() < 1024);
+ }
+
+ ZFileOptions options = new ZFileOptions();
+ options.setAlignmentRule(AlignmentRules.constantForSuffix(".txt", 1024));
+ try (ZFile zf = new ZFile(newZFile, options)) {
+ StoredEntry se1 = zf.get("file1.txt");
+ assertNotNull(se1);
+ se1.realign();
+ zf.close();
+
+ StoredEntry se0 = zf.get("file0.txt");
+ assertNotNull(se0);
+ assertEquals(offset0, se0.getCentralDirectoryHeader().getOffset());
+
+ se1 = zf.get("file1.txt");
+ assertNotNull(se1);
+ assertTrue(se1.getCentralDirectoryHeader().getOffset() > 950);
+ assertTrue(se1.getCentralDirectoryHeader().getOffset() < 1024);
+ assertArrayEquals(testBytes1, readSegment(newZFile, 1024, testBytes1.length));
+
+ assertTrue(newZFile.length() > 1024);
+ }
+ }
+
+ @Test
+ public void realignFile() throws Exception {
+ File newZFile = new File(mTemporaryFolder.getRoot(), "test.zip");
+
+ byte testBytes0[] = "Text number 1".getBytes(Charsets.US_ASCII);
+ byte testBytes1[] = "Text number 2, which is actually 1".getBytes(Charsets.US_ASCII);
+
+ try (ZFile zf = new ZFile(newZFile)) {
+ zf.add("file0.txt", new ByteArrayInputStream(testBytes0), false);
+ zf.add("file1.txt", new ByteArrayInputStream(testBytes1), false);
+ }
+
+ assertTrue(newZFile.length() < 1024);
+
+ ZFileOptions options = new ZFileOptions();
+ options.setAlignmentRule(AlignmentRules.constantForSuffix(".txt", 1024));
+ try (ZFile zf = new ZFile(newZFile, options)) {
+ zf.realign();
+ zf.update();
+
+ StoredEntry se0 = zf.get("file0.txt");
+ assertNotNull(se0);
+ long off0 = 1024;
+
+ StoredEntry se1 = zf.get("file1.txt");
+ assertNotNull(se1);
+ long off1 = 2048;
+
+ /*
+ * ZFile does not guarantee any order.
+ */
+ if (se1.getCentralDirectoryHeader().getOffset() <
+ se0.getCentralDirectoryHeader().getOffset()) {
+ off0 = 2048;
+ off1 = 1024;
+ }
+
+ assertArrayEquals(testBytes0, readSegment(newZFile, off0, testBytes0.length));
+ assertArrayEquals(testBytes1, readSegment(newZFile, off1, testBytes1.length));
+ }
+ }
+
+ @Test
+ public void realignAlignedEntry() throws Exception {
+ File newZFile = new File(mTemporaryFolder.getRoot(), "test.zip");
+
+ byte testBytes[] = "This is some text.".getBytes(Charsets.US_ASCII);
+
+ ZFileOptions options = new ZFileOptions();
+ options.setAlignmentRule(AlignmentRules.constantForSuffix(".txt", 1024));
+ try (ZFile zf = new ZFile(newZFile, options)) {
+ zf.add("test.txt", new ByteArrayInputStream(testBytes), false);
+ }
+
+ assertArrayEquals(testBytes, readSegment(newZFile, 1024, testBytes.length));
+
+ int flen = (int) newZFile.length();
+
+ try (ZFile zf = new ZFile(newZFile)) {
+ StoredEntry entry = zf.get("test.txt");
+ assertNotNull(entry);
+ assertFalse(entry.realign());
+ }
+
+ assertEquals(flen, (int) newZFile.length());
+ assertArrayEquals(testBytes, readSegment(newZFile, 1024, testBytes.length));
+ }
+
+ @Test
+ public void alignmentRulesDoNotAffectAddedFiles() throws Exception {
+ File newZFile = new File(mTemporaryFolder.getRoot(), "test.zip");
+
+ byte testBytes0[] = "Text number 1".getBytes(Charsets.US_ASCII);
+ byte testBytes1[] = "Text number 2, which is actually 1".getBytes(Charsets.US_ASCII);
+
+ try (ZFile zf = new ZFile(newZFile)) {
+ zf.add("file0.txt", new ByteArrayInputStream(testBytes0), false);
+ }
+
+ ZFileOptions options = new ZFileOptions();
+ options.setAlignmentRule(AlignmentRules.constantForSuffix(".txt", 1024));
+ try (ZFile zf = new ZFile(newZFile, options)) {
+ zf.add("file1.txt", new ByteArrayInputStream(testBytes1), false);
+ zf.update();
+
+ StoredEntry se0 = zf.get("file0.txt");
+ assertNotNull(se0);
+
+ StoredEntry se1 = zf.get("file1.txt");
+ assertNotNull(se1);
+ assertArrayEquals(testBytes1, readSegment(newZFile, 1024, testBytes1.length));
+ }
+ }
+
+ @Test
+ public void realignStreamedZip() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "test.zip");
+
+ byte[] pattern = new byte[1024];
+ new Random().nextBytes(pattern);
+
+ String name = "";
+ try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) {
+ for (int j = 0; j < 10; j++) {
+ name = name + "a";
+ ZipEntry ze = new ZipEntry(name);
+ zos.putNextEntry(ze);
+ for (int i = 0; i < 1000; i++) {
+ zos.write(pattern);
+ }
+ }
+ }
+
+ ZFileOptions options = new ZFileOptions();
+ options.setAlignmentRule(AlignmentRules.constant(10));
+ try (ZFile zf = new ZFile(zipFile, options)) {
+ zf.realign();
+ }
+ }
+
+ @Test
+ public void alignFirstEntryUsingExtraField() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "test.zip");
+
+ byte[] recognizable = new byte[] { 1, 2, 3, 4, 4, 3, 2, 1 };
+
+ ZFileOptions options = new ZFileOptions();
+ options.setCoverEmptySpaceUsingExtraField(true);
+ options.setAlignmentRule(AlignmentRules.constant(1024));
+ try (ZFile zf = new ZFile(zipFile, options)) {
+ zf.add("foo", new ByteArrayInputStream(recognizable), false);
+ }
+
+ /*
+ * Contents should be at 1024 bytes.
+ */
+ assertArrayEquals(recognizable, readSegment(zipFile, 1024, recognizable.length));
+
+ /*
+ * But local header should be in the beginning.
+ */
+ try (ZFile zf = new ZFile(zipFile)) {
+ StoredEntry entry = zf.get("foo");
+ assertNotNull(entry);
+ assertEquals(0, entry.getCentralDirectoryHeader().getOffset());
+ }
+ }
+
+ @Test
+ public void alignFirstEntryUsingOffset() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "test.zip");
+
+ byte[] recognizable = new byte[] { 1, 2, 3, 4, 4, 3, 2, 1 };
+
+ ZFileOptions options = new ZFileOptions();
+ options.setCoverEmptySpaceUsingExtraField(false);
+ options.setAlignmentRule(AlignmentRules.constant(1024));
+ try (ZFile zf = new ZFile(zipFile, options)) {
+ zf.add("foo", new ByteArrayInputStream(recognizable), false);
+ }
+
+ /*
+ * Contents should be at 1024 bytes.
+ */
+ assertArrayEquals(recognizable, readSegment(zipFile, 1024, recognizable.length));
+
+ /*
+ * Local header should start at 991 (1024 - LOCAL_HEADER_SIZE - 3).
+ */
+ try (ZFile zf = new ZFile(zipFile)) {
+ StoredEntry entry = zf.get("foo");
+ assertNotNull(entry);
+ assertEquals(991, entry.getCentralDirectoryHeader().getOffset());
+ }
+ }
+
+ @Test
+ public void alignMiddleEntryUsingExtraField() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "test.zip");
+
+ byte[] recognizable = new byte[] { 1, 2, 3, 4, 4, 3, 2, 1 };
+
+ ZFileOptions options = new ZFileOptions();
+ options.setCoverEmptySpaceUsingExtraField(true);
+ options.setAlignmentRule(SUFFIX_ALIGNMENT_RULES);
+ try (ZFile zf = new ZFile(zipFile, options)) {
+ zf.add("first.u", new ByteArrayInputStream(new byte[1024]), false);
+ zf.add("middle.a", new ByteArrayInputStream(recognizable), false);
+ zf.add("last.u", new ByteArrayInputStream(new byte[1024]), false);
+ }
+
+ /*
+ * Contents should be at 2048 bytes.
+ */
+ assertArrayEquals(recognizable, readSegment(zipFile, 2048, recognizable.length));
+
+ /*
+ * But local header should be right after the first entry.
+ */
+ try (ZFile zf = new ZFile(zipFile)) {
+ StoredEntry middleEntry = zf.get("middle.a");
+ assertNotNull(middleEntry);
+ assertEquals(
+ ZFileTestConstants.LOCAL_HEADER_SIZE + "first.u".length() + 1024,
+ middleEntry.getCentralDirectoryHeader().getOffset());
+ }
+ }
+
+ @Test
+ public void alignMiddleEntryUsingOffset() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "test.zip");
+
+ byte[] recognizable = new byte[] { 1, 2, 3, 4, 4, 3, 2, 1 };
+
+ ZFileOptions options = new ZFileOptions();
+ options.setCoverEmptySpaceUsingExtraField(false);
+ options.setAlignmentRule(AlignmentRules.constantForSuffix(".a", 1024));
+ try (ZFile zf = new ZFile(zipFile, options)) {
+ zf.add("bar1", new ByteArrayInputStream(new byte[1024]), false);
+ zf.add("foo.a", new ByteArrayInputStream(recognizable), false);
+ zf.add("bar2", new ByteArrayInputStream(new byte[1024]), false);
+ }
+
+ /*
+ * Contents should be at 2048 bytes.
+ */
+ assertArrayEquals(recognizable, readSegment(zipFile, 2048, recognizable.length));
+
+ /*
+ * Local header should start at 2015 (2048 - LOCAL_HEADER_SIZE - 5).
+ */
+ try (ZFile zf = new ZFile(zipFile)) {
+ StoredEntry entry = zf.get("foo.a");
+ assertNotNull(entry);
+ assertEquals(2013, entry.getCentralDirectoryHeader().getOffset());
+ }
+ }
+
+ @Test
+ public void alignUsingOffsetAllowsSmallSpaces() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "test.zip");
+
+ int fixedLh = ZFileTestConstants.LOCAL_HEADER_SIZE + 3;
+
+ byte[] recognizable = new byte[] { 1, 2, 3, 4, 4, 3, 2, 1 };
+
+ ZFileOptions options = new ZFileOptions();
+ options.setCoverEmptySpaceUsingExtraField(false);
+ options.setAlignmentRule(AlignmentRules.constant(fixedLh));
+ try (ZFile zf = new ZFile(zipFile, options)) {
+ zf.add("f", new ByteArrayInputStream(recognizable), false);
+ }
+
+ assertArrayEquals(recognizable, readSegment(zipFile, fixedLh, recognizable.length));
+ }
+
+ @Test
+ public void alignUsingExtraFieldDoesNotAllowSmallSpaces() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "test.zip");
+
+ int fixedLh = ZFileTestConstants.LOCAL_HEADER_SIZE + 3;
+
+ byte[] recognizable = new byte[] { 1, 2, 3, 4, 4, 3, 2, 1 };
+
+ ZFileOptions options = new ZFileOptions();
+ options.setCoverEmptySpaceUsingExtraField(true);
+ options.setAlignmentRule(AlignmentRules.constant(fixedLh));
+ try (ZFile zf = new ZFile(zipFile, options)) {
+ zf.add("f", new ByteArrayInputStream(recognizable), false);
+ }
+
+ assertArrayEquals(recognizable, readSegment(zipFile, fixedLh * 2, recognizable.length));
+ }
+
+ @Test
+ public void extraFieldSpaceUsedForAlignmentCanBeReclaimedBeforeUpdate() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "test.zip");
+
+ byte[] recognizable1 = new byte[] { 1, 2, 3, 4, 4, 3, 2, 1 };
+ byte[] recognizable2 = new byte[] { 9, 9, 8, 8, 7, 7, 6, 6, 5, 5, 4, 4 };
+
+ ZFileOptions options = new ZFileOptions();
+ options.setCoverEmptySpaceUsingExtraField(true);
+ options.setAlignmentRule(SUFFIX_ALIGNMENT_RULES);
+ try (ZFile zf = new ZFile(zipFile, options)) {
+ zf.add("f.a", new ByteArrayInputStream(recognizable1), false);
+ zf.add("f.u", new ByteArrayInputStream(recognizable2), false);
+ }
+
+ assertArrayEquals(recognizable1, readSegment(zipFile, 1024, recognizable1.length));
+ assertArrayEquals(
+ recognizable2,
+ readSegment(
+ zipFile,
+ ZFileTestConstants.LOCAL_HEADER_SIZE + "f.u".length(),
+ recognizable2.length));
+ }
+
+ @Test
+ @Ignore("See ZFile.readData() contents to understand why this is ignored")
+ public void extraFieldSpaceUsedForAlignmentCanBeReclaimedAfterUpdate() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "test.zip");
+
+ byte[] recognizable1 = new byte[] { 1, 2, 3, 4, 4, 3, 2, 1 };
+ byte[] recognizable2 = new byte[] { 9, 9, 8, 8, 7, 7, 6, 6, 5, 5, 4, 4 };
+
+ ZFileOptions options = new ZFileOptions();
+ options.setCoverEmptySpaceUsingExtraField(true);
+ options.setAlignmentRule(AlignmentRules.constantForSuffix(".a", 1024));
+ try (ZFile zf = new ZFile(zipFile, options)) {
+ zf.add("f.a", new ByteArrayInputStream(recognizable1), false);
+ }
+
+ try (ZFile zf = new ZFile(zipFile, options)) {
+ zf.add("f.b", new ByteArrayInputStream(recognizable2), false);
+ }
+
+ assertArrayEquals(recognizable1, readSegment(zipFile, 1024, recognizable1.length));
+ assertArrayEquals(
+ recognizable2,
+ readSegment(
+ zipFile,
+ ZFileTestConstants.LOCAL_HEADER_SIZE + "f.b".length(),
+ recognizable2.length));
+ }
+
+ @Test
+ public void fillEmptySpaceWithExtraFieldAfterDelete() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "large.zip");
+
+ byte[] recognizable1 = new byte[] { 1, 2, 3, 4, 4, 3, 2, 1 };
+ byte[] recognizable2 = new byte[] { 9, 8, 7, 6, 5, 4, 3, 2 };
+
+ ZFileOptions options = new ZFileOptions();
+ options.setCoverEmptySpaceUsingExtraField(true);
+ options.setAlignmentRule(SUFFIX_ALIGNMENT_RULES);
+ try (ZFile zf = new ZFile(zipFile, options)) {
+ zf.add("first.u", new ByteArrayInputStream(recognizable1), false);
+ zf.add("second.u", new ByteArrayInputStream(recognizable2), false);
+
+ zf.update();
+
+ StoredEntry firstEntry = zf.get("first.u");
+ assertNotNull(firstEntry);
+ firstEntry.delete();
+ }
+
+ try (ZFile zf = new ZFile(zipFile)) {
+ Set<StoredEntry> entries = zf.entries();
+ assertEquals(1, entries.size());
+
+ StoredEntry entry = entries.iterator().next();
+ assertEquals("second.u", entry.getCentralDirectoryHeader().getName());
+ assertEquals(0, entry.getCentralDirectoryHeader().getOffset());
+ assertEquals(
+ ZFileTestConstants.LOCAL_HEADER_SIZE
+ + "first.u".length()
+ + recognizable1.length,
+ entry.getLocalExtra().size());
+ }
+ }
+
+ @Test
+ public void fillInLargeGapsWithExtraField() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "large.zip");
+
+ byte[] recognizable1 = new byte[] { 1, 2, 3, 4, 4, 3, 2, 1 };
+ byte[] recognizable2 = new byte[] { 9, 8, 7, 6, 5, 4, 3, 2 };
+ byte[] bigEmpty = new byte[10 * 1024];
+
+ ZFileOptions options = new ZFileOptions();
+ options.setCoverEmptySpaceUsingExtraField(true);
+ options.setAlignmentRule(SUFFIX_ALIGNMENT_RULES);
+ try (ZFile zf = new ZFile(zipFile, options)) {
+ zf.add("begin.u", new ByteArrayInputStream(recognizable1), false);
+ zf.add("middle.u", new ByteArrayInputStream(bigEmpty), false);
+ zf.add("end.u", new ByteArrayInputStream(recognizable2), false);
+
+ zf.update();
+
+ StoredEntry middleEntry = zf.get("middle.u");
+ assertNotNull(middleEntry);
+ middleEntry.delete();
+ }
+
+ /*
+ * Find the two recognizable files.
+ */
+ int recognizable1Start = ZFileTestConstants.LOCAL_HEADER_SIZE + "begin.u".length();
+ assertArrayEquals(
+ recognizable1,
+ readSegment(zipFile, recognizable1Start, recognizable1.length));
+
+ int recognizable2Start =
+ 3 * ZFileTestConstants.LOCAL_HEADER_SIZE
+ + "begin.u".length()
+ + "middle.u".length()
+ + "end.u".length()
+ + recognizable1.length
+ + bigEmpty.length;
+ assertArrayEquals(
+ recognizable2,
+ readSegment(zipFile, recognizable2Start, recognizable2.length));
+ }
+
+ @Test
+ public void fillHoleWithExactEntry() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
+
+ Random random = new Random();
+
+ byte[] fourtyFour = new byte[44];
+ random.nextBytes(fourtyFour);
+ byte[] recognizable = new byte[] { 1, 5, 5, 1, 5, 1, 1, 5 };
+ byte[] twoHundred = new byte[200];
+ random.nextBytes(twoHundred);
+
+ /*
+ * Start | Header End | Name end | Contents End | Name
+ * 0 | 30 | 59 | 103 | "File taking exactly 103 bytes"
+ * 103 | 133 | 136 | 144 | "foo"
+ * 144 | 174 | 196 | 396 | "File taking more space"
+ */
+ try (ZFile zf = new ZFile(zipFile)) {
+ zf.add("File taking exactly 103 bytes", new ByteArrayInputStream(fourtyFour), false);
+ zf.add("foo", new ByteArrayInputStream(recognizable), false);
+ zf.add("File taking more space", new ByteArrayInputStream(twoHundred), false);
+ }
+
+ assertArrayEquals(fourtyFour, readSegment(zipFile, 59, fourtyFour.length));
+ assertArrayEquals(recognizable, readSegment(zipFile, 136, recognizable.length));
+ assertArrayEquals(twoHundred, readSegment(zipFile, 196, twoHundred.length));
+
+ /*
+ * Remove the middle file.
+ */
+ try (ZFile zf = new ZFile(zipFile)) {
+ StoredEntry fooEntry = zf.get("foo");
+ assertNotNull(fooEntry);
+ fooEntry.delete();
+ }
+
+ /*
+ * Add the file again with 4-byte alignment. Because the file fits exactly in the hole, it
+ * is placed there.
+ */
+ byte[] recognizable2 = new byte[] { 2, 6, 6, 2, 6, 2, 2, 6 };
+
+ ZFileOptions zfo = new ZFileOptions();
+ zfo.setCoverEmptySpaceUsingExtraField(true);
+ zfo.setAlignmentRule(AlignmentRules.constant(4));
+ try (ZFile zf = new ZFile(zipFile, zfo)) {
+ zf.add("bar", new ByteArrayInputStream(recognizable2), false);
+ }
+
+ assertArrayEquals(fourtyFour, readSegment(zipFile, 59, fourtyFour.length));
+ assertArrayEquals(recognizable2, readSegment(zipFile, 136, recognizable2.length));
+ assertArrayEquals(twoHundred, readSegment(zipFile, 196, twoHundred.length));
+ }
+
+ @Test
+ public void fillHoleWithSmallEntry() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
+
+ Random random = new Random();
+
+ byte[] fourtyFour = new byte[44];
+ random.nextBytes(fourtyFour);
+ byte[] recognizable = new byte[] { 1, 5, 5, 1, 5, 1, 1, 5, 1, 5, 5, 1, 5, 1, 1, 5 };
+ byte[] twoHundred = new byte[200];
+ random.nextBytes(twoHundred);
+
+ /*
+ * Start | Header End | Name end | Contents End | Name
+ * 0 | 30 | 59 | 103 | "File taking exactly 103 bytes"
+ * 103 | 133 | 136 | 152 | "foo"
+ * 152 | 182 | 204 | 404 | "File taking more space"
+ */
+ try (ZFile zf = new ZFile(zipFile)) {
+ zf.add("File taking exactly 103 bytes", new ByteArrayInputStream(fourtyFour), false);
+ zf.add("foo", new ByteArrayInputStream(recognizable), false);
+ zf.add("File taking more space", new ByteArrayInputStream(twoHundred), false);
+ }
+
+ assertArrayEquals(fourtyFour, readSegment(zipFile, 59, fourtyFour.length));
+ assertArrayEquals(recognizable, readSegment(zipFile, 136, recognizable.length));
+ assertArrayEquals(twoHundred, readSegment(zipFile, 204, twoHundred.length));
+
+ /*
+ * Remove the middle file.
+ */
+ try (ZFile zf = new ZFile(zipFile)) {
+ StoredEntry fooEntry = zf.get("foo");
+ assertNotNull(fooEntry);
+ fooEntry.delete();
+ }
+
+ /*
+ * Add a smaller file. It should fit nicely as:
+ *
+ * Start | Header End | Name end | Contents End | Name
+ * 0 | 30 | 59 | 103 | "File taking exactly 103 bytes"
+ * 103 | 133 | 136 | 140 | "bar"
+ * 140 - 152 (empty)
+ * 152 | 182 | 204 | 404 | "File taking more space"
+ */
+ byte[] recognizable2 = new byte[] { 7, 7, 7, 7 };
+
+ ZFileOptions zfo = new ZFileOptions();
+ zfo.setCoverEmptySpaceUsingExtraField(true);
+ zfo.setAlignmentRule(AlignmentRules.constant(4));
+ try (ZFile zf = new ZFile(zipFile, zfo)) {
+ zf.add("bar", new ByteArrayInputStream(recognizable2), false);
+ }
+
+ assertArrayEquals(fourtyFour, readSegment(zipFile, 59, fourtyFour.length));
+ assertArrayEquals(recognizable2, readSegment(zipFile, 136, recognizable2.length));
+ assertArrayEquals(twoHundred, readSegment(zipFile, 204, twoHundred.length));
+ }
+
+ @Test
+ public void fillHoleWithSmallerEntryNotEnoughFreeSpace() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
+
+ Random random = new Random();
+
+ byte[] fourtyFour = new byte[44];
+ random.nextBytes(fourtyFour);
+ byte[] recognizable = new byte[] { 1, 5, 5, 1, 5, 1, 1, 5, 1, 5, 5, 1, 5, 1, 1, 5 };
+ byte[] twoHundred = new byte[200];
+ random.nextBytes(twoHundred);
+
+ /*
+ * Start | Header End | Name end | Contents End | Name
+ * 0 | 30 | 59 | 103 | "File taking exactly 103 bytes"
+ * 103 | 133 | 136 | 152 | "foo"
+ * 152 | 182 | 204 | 404 | "File taking more space"
+ */
+ try (ZFile zf = new ZFile(zipFile)) {
+ zf.add("File taking exactly 103 bytes", new ByteArrayInputStream(fourtyFour), false);
+ zf.add("foo", new ByteArrayInputStream(recognizable), false);
+ zf.add("File taking more space", new ByteArrayInputStream(twoHundred), false);
+ }
+
+ assertArrayEquals(fourtyFour, readSegment(zipFile, 59, fourtyFour.length));
+ assertArrayEquals(recognizable, readSegment(zipFile, 136, recognizable.length));
+ assertArrayEquals(twoHundred, readSegment(zipFile, 204, twoHundred.length));
+
+ /*
+ * Remove the middle file.
+ */
+ try (ZFile zf = new ZFile(zipFile)) {
+ StoredEntry fooEntry = zf.get("foo");
+ assertNotNull(fooEntry);
+ fooEntry.delete();
+ }
+
+ /*
+ * Add a smaller file. But it can't fit because it would leave less than 6 bytes to
+ * cover in the next file:
+ *
+ * Start | Header End | Name end | Contents End | Name
+ * 0 | 30 | 59 | 103 | "File taking exactly 103 bytes"
+ * 103 | 133 | 136 | 148 | "foo"
+ * 148 - 152 (empty)
+ * 152 | 182 | 204 | 404 | "File taking more space"
+ *
+ * So we end up with:
+ *
+ * Start | Header End | Name end | Contents End | Name
+ * 0 | 30 | 59 | 103 | "File taking exactly 103 bytes"
+ * 152 | 182 | 204 | 404 | "File taking more space"
+ * 404 | 434 -> 441 | 444 | 456 | "bar"
+ */
+ byte[] recognizable2 = new byte[] { 7, 7, 7, 7, 8, 8, 8, 8, 9, 9, 9, 9 };
+
+ ZFileOptions zfo = new ZFileOptions();
+ zfo.setCoverEmptySpaceUsingExtraField(true);
+ zfo.setAlignmentRule(AlignmentRules.constant(4));
+ try (ZFile zf = new ZFile(zipFile, zfo)) {
+ zf.add("bar", new ByteArrayInputStream(recognizable2), false);
+ }
+
+ assertArrayEquals(fourtyFour, readSegment(zipFile, 59, fourtyFour.length));
+ assertArrayEquals(recognizable2, readSegment(zipFile, 444, recognizable2.length));
+ assertArrayEquals(twoHundred, readSegment(zipFile, 204, twoHundred.length));
+ }
+
+ @Test
+ public void fillHoleWithSmallerEntryEnoughFreeSpaceButRequiresExtraOffset() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
+
+ Random random = new Random();
+
+ byte[] fourtyFour = new byte[44];
+ random.nextBytes(fourtyFour);
+ byte[] recognizable = new byte[] { 1, 5, 5, 1, 5, 1, 1, 5, 1, 5, 5, 1, 5, 1, 1, 5 };
+ byte[] twoHundred = new byte[200];
+ random.nextBytes(twoHundred);
+
+ /*
+ * Start | Header End | Name end | Contents End | Name
+ * 0 | 30 | 59 | 103 | "File taking exactly 103 bytes"
+ * 103 | 133 | 136 | 152 | "foo"
+ * 152 | 182 | 204 | 404 | "File taking more space"
+ */
+ try (ZFile zf = new ZFile(zipFile)) {
+ zf.add("File taking exactly 103 bytes", new ByteArrayInputStream(fourtyFour), false);
+ zf.add("foo", new ByteArrayInputStream(recognizable), false);
+ zf.add("File taking more space", new ByteArrayInputStream(twoHundred), false);
+ }
+
+ assertArrayEquals(fourtyFour, readSegment(zipFile, 59, fourtyFour.length));
+ assertArrayEquals(recognizable, readSegment(zipFile, 136, recognizable.length));
+ assertArrayEquals(twoHundred, readSegment(zipFile, 204, twoHundred.length));
+
+ /*
+ * Remove the middle file.
+ */
+ try (ZFile zf = new ZFile(zipFile)) {
+ StoredEntry fooEntry = zf.get("foo");
+ assertNotNull(fooEntry);
+ fooEntry.delete();
+ }
+
+ /*
+ * Add a smaller file. It will fit, but not aligned at 140 because that would require
+ * adding less than 6 bytes in the local header. It has to move to 150.
+ *
+ * Start | Header End | Name end | Contents End | Name
+ * 0 | 30 | 59 | 103 | "File taking exactly 103 bytes"
+ * 103 | 133 | 150 | 152 | "foo"
+ * 152 | 182 | 204 | 404 | "File taking more space"
+ */
+ byte[] recognizable2 = new byte[] { 10, 10 };
+
+ ZFileOptions zfo = new ZFileOptions();
+ zfo.setCoverEmptySpaceUsingExtraField(true);
+ zfo.setAlignmentRule(AlignmentRules.constant(10));
+ try (ZFile zf = new ZFile(zipFile, zfo)) {
+ zf.add("bar", new ByteArrayInputStream(recognizable2), false);
+ }
+
+ assertArrayEquals(fourtyFour, readSegment(zipFile, 59, fourtyFour.length));
+ assertArrayEquals(recognizable2, readSegment(zipFile, 150, recognizable2.length));
+ assertArrayEquals(twoHundred, readSegment(zipFile, 204, twoHundred.length));
+ }
+}
diff --git a/src/test/java/com/android/apkzlib/zip/EncodeUtilsTest.java b/src/test/java/com/android/apkzlib/zip/EncodeUtilsTest.java
new file mode 100644
index 0000000..0eaf1cb
--- /dev/null
+++ b/src/test/java/com/android/apkzlib/zip/EncodeUtilsTest.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apkzlib.zip;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+public class EncodeUtilsTest {
+ @Test
+ public void canEncodeAsciiWithAsciiString() {
+ assertTrue(EncodeUtils.canAsciiEncode("foo"));
+ }
+
+ @Test
+ public void cannotEncodeAscuuWithUtf8String() {
+ String greekInGreek ="\u3b53\ubb3b\ub3b7\u3bd3\ub93b\ua3ac";
+ assertFalse(EncodeUtils.canAsciiEncode(greekInGreek));
+ }
+
+ @Test
+ public void asciiEncodeAndDecode() {
+ String text = "foo";
+ GPFlags flags = GPFlags.make(false);
+
+ byte[] encoded = EncodeUtils.encode(text, flags);
+ assertArrayEquals(new byte[] { 0x66, 0x6f, 0x6f }, encoded);
+ assertEquals(text, EncodeUtils.decode(encoded, flags));
+ }
+
+ @Test
+ public void utf8EncodeAndDecode() {
+ String kazakhCapital = "\u0410\u0441\u0442\u0430\u043d\u0430";
+ GPFlags flags = GPFlags.make(true);
+
+ byte[] encoded = EncodeUtils.encode(kazakhCapital, flags);
+ assertArrayEquals(new byte[] { (byte) 0xd0, (byte) 0x90, (byte) 0xd1, (byte) 0x81,
+ (byte) 0xd1, (byte) 0x82, (byte) 0xd0, (byte) 0xb0, (byte) 0xd0, (byte) 0xbd,
+ (byte) 0xd0, (byte) 0xb0 }, encoded);
+ assertEquals(kazakhCapital, EncodeUtils.decode(encoded, flags));
+ }
+}
diff --git a/src/test/java/com/android/apkzlib/zip/ExtraFieldTest.java b/src/test/java/com/android/apkzlib/zip/ExtraFieldTest.java
new file mode 100644
index 0000000..d80ccc4
--- /dev/null
+++ b/src/test/java/com/android/apkzlib/zip/ExtraFieldTest.java
@@ -0,0 +1,335 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apkzlib.zip;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+/**
+ * Test setting, removing and updating the extra field of zip entries.
+ */
+@RunWith(Parameterized.class)
+public class ExtraFieldTest {
+
+ @Rule
+ public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+
+ private File mZipFile;
+
+ @Parameterized.Parameter
+ public Function<StoredEntry, ExtraField> mExtraFieldGetter;
+
+ @Parameterized.Parameter(1)
+ public BiConsumer<StoredEntry, ExtraField> mExtraFieldSetter;
+
+ @Before
+ public final void before() throws Exception {
+ mZipFile = mTemporaryFolder.newFile();
+ mZipFile.delete();
+ }
+
+ @Parameterized.Parameters
+ public static ImmutableList<Object[]> getParameters() {
+ Function<StoredEntry, ExtraField> localGet = StoredEntry::getLocalExtra;
+ BiConsumer<StoredEntry, ExtraField> localSet = (se, ef) -> {
+ try {
+ se.setLocalExtra(ef);
+ } catch (IOException e) {
+ throw new AssertionError(e);
+ }
+ };
+
+ Function<StoredEntry, ExtraField> centralGet =
+ se -> se.getCentralDirectoryHeader().getExtraField();
+ BiConsumer<StoredEntry, ExtraField> centralSet = (se, ef) -> {
+ try {
+ se.getCentralDirectoryHeader().setExtraField(ef);
+ } catch (Exception e) {
+ throw new AssertionError(e);
+ }
+ };
+
+ return ImmutableList.of(
+ new Object[]{ localGet, localSet },
+ new Object[]{ centralGet, centralSet });
+ }
+
+ @Test
+ public void readEntryWithNoExtraField() throws Exception {
+ try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(mZipFile))) {
+ zos.putNextEntry(new ZipEntry("foo"));
+ zos.write(new byte[] { 1, 2, 3 });
+ }
+
+ try (ZFile zf = new ZFile(mZipFile)) {
+ StoredEntry foo = zf.get("foo");
+ assertNotNull(foo);
+ assertEquals(3, foo.getCentralDirectoryHeader().getUncompressedSize());
+ assertEquals(0, mExtraFieldGetter.apply(foo).size());
+ }
+ }
+
+ @Test
+ public void readSingleExtraField() throws Exception {
+ /*
+ * Header ID: 0x0A0B
+ * Data Size: 0x0004
+ * Data: 0x01 0x02 0x03 0x04
+ *
+ * In little endian is:
+ *
+ * 0xCDAB040001020304
+ */
+ byte[] extraField = new byte[] { 0x0B, 0x0A, 0x04, 0x00, 0x01, 0x02, 0x03, 0x04 };
+
+ try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(mZipFile))) {
+ ZipEntry ze = new ZipEntry("foo");
+ ze.setExtra(extraField);
+ zos.putNextEntry(ze);
+ zos.write(new byte[] { 1, 2, 3 });
+ }
+
+ try (ZFile zf = new ZFile(mZipFile)) {
+ StoredEntry foo = zf.get("foo");
+ assertNotNull(foo);
+ assertEquals(3, foo.getCentralDirectoryHeader().getUncompressedSize());
+ assertEquals(8, mExtraFieldGetter.apply(foo).size());
+ ImmutableList<ExtraField.Segment> segments = mExtraFieldGetter.apply(foo).getSegments();
+ assertEquals(1, segments.size());
+ assertEquals(0x0A0B, segments.get(0).getHeaderId());
+ byte[] segData = new byte[8];
+ segments.get(0).write(ByteBuffer.wrap(segData));
+ assertArrayEquals(extraField, segData);
+ }
+ }
+
+ @Test
+ public void readMultipleExtraFields() throws Exception {
+ /*
+ * Header ID: 0x0A01
+ * Data Size: 0x0002
+ * Data: 0x01 0x02
+ *
+ * Header ID: 0x0A02
+ * Data Size: 0x0001
+ * Data: 0x03
+ *
+ * Header ID: 0x0A02
+ * Data Size: 0x0001
+ * Dataa: 0x04
+ *
+ * In little endian is:
+ *
+ * 0x010A02000102 020A010003 020A010004
+ */
+ byte[] extraField =
+ new byte[] {
+ 0x01, 0x0A, 0x02, 0x00, 0x01, 0x02,
+ 0x02, 0x0A, 0x01, 0x00, 0x03,
+ 0x02, 0x0A, 0x01, 0x00, 0x04 };
+
+ try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(mZipFile))) {
+ ZipEntry ze = new ZipEntry("foo");
+
+ ze.setExtra(extraField);
+ zos.putNextEntry(ze);
+ zos.write(new byte[] { 1, 2, 3 });
+ }
+
+ try (ZFile zf = new ZFile(mZipFile)) {
+ StoredEntry foo = zf.get("foo");
+ assertNotNull(foo);
+ assertEquals(3, foo.getCentralDirectoryHeader().getUncompressedSize());
+ assertEquals(16, mExtraFieldGetter.apply(foo).size());
+ ImmutableList<ExtraField.Segment> segments = mExtraFieldGetter.apply(foo).getSegments();
+ assertEquals(3, segments.size());
+
+ assertEquals(0x0A01, segments.get(0).getHeaderId());
+ byte[] segData = new byte[6];
+ segments.get(0).write(ByteBuffer.wrap(segData));
+ assertArrayEquals(new byte[] { 0x01, 0x0A, 0x02, 0x00, 0x01, 0x02 }, segData);
+
+ assertEquals(0x0A02, segments.get(1).getHeaderId());
+ segData = new byte[5];
+ segments.get(1).write(ByteBuffer.wrap(segData));
+ assertArrayEquals(new byte[] { 0x02, 0x0A, 0x01, 0x00, 0x03 }, segData);
+
+ assertEquals(0x0A02, segments.get(2).getHeaderId());
+ segData = new byte[5];
+ segments.get(2).write(ByteBuffer.wrap(segData));
+ assertArrayEquals(new byte[] { 0x02, 0x0A, 0x01, 0x00, 0x04 }, segData);
+ }
+ }
+
+ @Test
+ public void addExtraFieldToExistingEntry() throws Exception {
+ try (ZFile zf = new ZFile(mZipFile)) {
+ zf.add("before", new ByteArrayInputStream(new byte[] { 0, 1, 2 }));
+ zf.add("extra", new ByteArrayInputStream(new byte[] { 3, 4, 5 }));
+ zf.add("after", new ByteArrayInputStream(new byte[] { 6, 7, 8 }));
+ }
+
+ try (ZFile zf = new ZFile(mZipFile)) {
+ StoredEntry ex = zf.get("extra");
+ assertNotNull(ex);
+ mExtraFieldSetter.accept(ex,
+ new ExtraField(
+ ImmutableList.of(
+ new ExtraField.RawDataSegment(
+ 0x7654,
+ new byte[] { 1, 1, 3, 3 }))));
+ }
+
+ try (ZFile zf = new ZFile(mZipFile)) {
+ StoredEntry before = zf.get("before");
+ assertNotNull(before);
+ assertArrayEquals(new byte[] { 0, 1, 2 }, before.read());
+
+ StoredEntry extra = zf.get("extra");
+ assertNotNull(extra);
+ assertArrayEquals(new byte[] { 3, 4, 5 }, extra.read());
+
+ StoredEntry after = zf.get("after");
+ assertNotNull(after);
+ assertArrayEquals(new byte[] { 6, 7, 8 }, after.read());
+
+ ExtraField ef = mExtraFieldGetter.apply(extra);
+ assertEquals(1, ef.getSegments().size());
+ ExtraField.Segment s = ef.getSingleSegment(0x7654);
+ assertNotNull(s);
+ byte[] sData = new byte[8];
+ s.write(ByteBuffer.wrap(sData));
+ assertArrayEquals(new byte[] { 0x54, 0x76, 0x04, 0x00, 1, 1, 3, 3 }, sData);
+ }
+ }
+
+ @Test
+ public void removeExtraFieldFromExistingEntry() throws Exception {
+ try (ZFile zf = new ZFile(mZipFile)) {
+ zf.add("before", new ByteArrayInputStream(new byte[] { 0, 1, 2 }));
+ zf.add("extra", new ByteArrayInputStream(new byte[] { 3, 4, 5 }));
+ zf.add("after", new ByteArrayInputStream(new byte[] { 6, 7, 8 }));
+ }
+
+ try (ZFile zf = new ZFile(mZipFile)) {
+ StoredEntry ex = zf.get("extra");
+ assertNotNull(ex);
+ mExtraFieldSetter.accept(ex,
+ new ExtraField(
+ ImmutableList.of(
+ new ExtraField.RawDataSegment(
+ 0x7654,
+ new byte[] { 1, 1, 3, 3 }))));
+ }
+
+ try (ZFile zf = new ZFile(mZipFile)) {
+ StoredEntry ex = zf.get("extra");
+ assertNotNull(ex);
+ mExtraFieldSetter.accept(ex, new ExtraField());
+ }
+
+ try (ZFile zf = new ZFile(mZipFile)) {
+ StoredEntry before = zf.get("before");
+ assertNotNull(before);
+ assertArrayEquals(new byte[] { 0, 1, 2 }, before.read());
+
+ StoredEntry extra = zf.get("extra");
+ assertNotNull(extra);
+ assertArrayEquals(new byte[] { 3, 4, 5 }, extra.read());
+
+ StoredEntry after = zf.get("after");
+ assertNotNull(after);
+ assertArrayEquals(new byte[] { 6, 7, 8 }, after.read());
+
+ ExtraField ef = mExtraFieldGetter.apply(extra);
+ assertEquals(0, ef.getSegments().size());
+ }
+ }
+
+ @Test
+ public void updateExtraFieldOfExistingEntry() throws Exception {
+ try (ZFile zf = new ZFile(mZipFile)) {
+ zf.add("before", new ByteArrayInputStream(new byte[] { 0, 1, 2 }));
+ zf.add("extra", new ByteArrayInputStream(new byte[] { 3, 4, 5 }));
+ zf.add("after", new ByteArrayInputStream(new byte[] { 6, 7, 8 }));
+ }
+
+ try (ZFile zf = new ZFile(mZipFile)) {
+ StoredEntry ex = zf.get("extra");
+ assertNotNull(ex);
+ mExtraFieldSetter.accept(ex,
+ new ExtraField(
+ ImmutableList.of(
+ new ExtraField.RawDataSegment(
+ 0x7654,
+ new byte[] { 1, 1, 3, 3 }))));
+ }
+
+ try (ZFile zf = new ZFile(mZipFile)) {
+ StoredEntry ex = zf.get("extra");
+ assertNotNull(ex);
+ mExtraFieldSetter.accept(ex,
+ new ExtraField(
+ ImmutableList.of(
+ new ExtraField.RawDataSegment(
+ 0x7654,
+ new byte[] { 2, 4, 2, 4 }))));
+ }
+
+ try (ZFile zf = new ZFile(mZipFile)) {
+ StoredEntry before = zf.get("before");
+ assertNotNull(before);
+ assertArrayEquals(new byte[] { 0, 1, 2 }, before.read());
+
+ StoredEntry extra = zf.get("extra");
+ assertNotNull(extra);
+ assertArrayEquals(new byte[] { 3, 4, 5 }, extra.read());
+
+ StoredEntry after = zf.get("after");
+ assertNotNull(after);
+ assertArrayEquals(new byte[] { 6, 7, 8 }, after.read());
+
+ ExtraField ef = mExtraFieldGetter.apply(extra);
+ assertEquals(1, ef.getSegments().size());
+ ExtraField.Segment s = ef.getSingleSegment(0x7654);
+ assertNotNull(s);
+ byte[] sData = new byte[8];
+ s.write(ByteBuffer.wrap(sData));
+ assertArrayEquals(new byte[] { 0x54, 0x76, 0x04, 0x00, 2, 4, 2, 4 }, sData);
+ }
+ }
+}
diff --git a/src/test/java/com/android/apkzlib/zip/FileUseMapTest.java b/src/test/java/com/android/apkzlib/zip/FileUseMapTest.java
new file mode 100644
index 0000000..0ee0129
--- /dev/null
+++ b/src/test/java/com/android/apkzlib/zip/FileUseMapTest.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apkzlib.zip;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import com.google.common.base.Stopwatch;
+
+import org.junit.Ignore;
+import org.junit.Test;
+
+import java.text.DecimalFormat;
+import java.util.Random;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Tests for {@link FileUseMap}.
+ */
+public class FileUseMapTest {
+
+ /**
+ * Verifies that as elements are added to the map, the performance of adding new elements
+ * is not significantly downgraded. This test creates a map and does several runs until
+ * a maximum is reached or a time limit is reached.
+ *
+ * <p>In each run, a random block is requested from the map with a random alignment and offset.
+ * The time for each run is saved.
+ *
+ * <p>After all runs are completed, the average time of the first runs (the head time) and
+ * the average time of the last runs (the tail time) is computed, as well as the average
+ * time.
+ *
+ * <p>The test passes if the average tail set time is (1) at most twice as long as the average
+ * and (2) is at most three times as long as the head set. This ensures that performance can
+ * degrade somewhat as the file map size increases, but not too much.
+ */
+ @Test
+ @Ignore("This test relies on magic ratios to detect when performance is bad.")
+ public void addPerformanceTest() {
+ final long MAP_SIZE = 10000000;
+ final int MAX_RUNS = 10000;
+ final long MAX_TEST_DURATION_MS = 1000;
+ final int MAX_RANDOM_BLOCK_SIZE = 1000;
+ final int MAX_RANDOM_ALIGNMENT = 10;
+ final int HEAD_SET_SIZE = 1000;
+ final int TAIL_SET_SIZE = 1000;
+ final double MAX_TAIL_HEAD_RATIO = 3.0;
+ final double MAX_TAIL_TOTAL_RATIO = 2.0;
+
+ long mapSize = MAP_SIZE;
+ FileUseMap map = new FileUseMap(mapSize, 0);
+ Random rand = new Random(0);
+
+ long[] runs = new long[MAX_RUNS];
+ int currentRun = 0;
+
+ Stopwatch testStopwatch = Stopwatch.createStarted();
+ while (testStopwatch.elapsed(TimeUnit.MILLISECONDS) < MAX_TEST_DURATION_MS
+ && currentRun < runs.length) {
+ Stopwatch runStopwatch = Stopwatch.createStarted();
+
+ long blockSize = 1 + rand.nextInt(MAX_RANDOM_BLOCK_SIZE);
+ long start = map.locateFree(blockSize, rand.nextInt(MAX_RANDOM_ALIGNMENT),
+ rand.nextInt(MAX_RANDOM_ALIGNMENT), FileUseMap.PositionAlgorithm.BEST_FIT);
+ long end = start + blockSize;
+ if (end >= mapSize) {
+ mapSize *= 2;
+ map.extend(mapSize);
+ }
+
+ map.add(start, end, new Object());
+
+ runs[currentRun] = runStopwatch.elapsed(TimeUnit.NANOSECONDS);
+ currentRun++;
+ }
+
+ double initialAvg = 0;
+ for (int i = 0; i < HEAD_SET_SIZE; i++) {
+ initialAvg += runs[i];
+ }
+
+ initialAvg /= HEAD_SET_SIZE;
+
+ double endAvg = 0;
+ for (int i = currentRun - TAIL_SET_SIZE; i < currentRun; i++) {
+ endAvg += runs[i];
+ }
+
+ endAvg /= TAIL_SET_SIZE;
+
+ double totalAvg = 0;
+ for (int i = 0; i < runs.length; i++) {
+ totalAvg += runs[i];
+ }
+
+ totalAvg /= currentRun;
+
+ if (endAvg > totalAvg * MAX_TAIL_TOTAL_RATIO || endAvg > initialAvg * MAX_TAIL_HEAD_RATIO) {
+ DecimalFormat df = new DecimalFormat("#,###");
+
+ fail("Add performance at end is too bad. Performance in the beginning is "
+ + df.format(initialAvg) + "ns per insertion and at the end is "
+ + df.format(endAvg) + "ns. Average over the total of " + currentRun + " runs "
+ + "is " + df.format(totalAvg) + "ns.");
+ }
+ }
+
+ @Test
+ public void testSizeComputation() {
+ FileUseMap m = new FileUseMap(200, 0);
+
+ assertEquals(200, m.size());
+ assertEquals(0, m.usedSize());
+
+ m.add(10, 20, new Object());
+ assertEquals(200, m.size());
+ assertEquals(20, m.usedSize());
+ }
+}
diff --git a/src/test/java/com/android/apkzlib/zip/OldApkReadTest.java b/src/test/java/com/android/apkzlib/zip/OldApkReadTest.java
new file mode 100644
index 0000000..61a08d7
--- /dev/null
+++ b/src/test/java/com/android/apkzlib/zip/OldApkReadTest.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apkzlib.zip;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import com.android.apkzlib.utils.ApkZFileTestUtils;
+import java.io.File;
+import org.junit.Test;
+
+public class OldApkReadTest {
+
+ @Test
+ public void testReadOldApk() throws Exception {
+ File apkFile = ApkZFileTestUtils.getResource("/testData/packaging/test.apk");
+ assertTrue(apkFile.exists());
+
+ try (ZFile zf = new ZFile(apkFile, new ZFileOptions())) {
+ StoredEntry classesDex = zf.get("classes.dex");
+ assertNotNull(classesDex);
+ }
+ }
+}
diff --git a/src/test/java/com/android/apkzlib/zip/ReadWithDifferentCompressionLevelsTest.java b/src/test/java/com/android/apkzlib/zip/ReadWithDifferentCompressionLevelsTest.java
new file mode 100644
index 0000000..4301710
--- /dev/null
+++ b/src/test/java/com/android/apkzlib/zip/ReadWithDifferentCompressionLevelsTest.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apkzlib.zip;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import com.android.apkzlib.utils.ApkZFileTestUtils;
+import java.io.File;
+import org.junit.Test;
+
+public class ReadWithDifferentCompressionLevelsTest {
+
+ @Test
+ public void readL9() throws Exception {
+ File l9File = ApkZFileTestUtils.getResource("/testData/packaging/l9.zip");
+ assertTrue(l9File.isFile());
+
+ try (ZFile read = new ZFile(l9File, new ZFileOptions())) {
+ assertNotNull(read.get("text-files/rfc2460.txt"));
+ }
+ }
+
+ @Test
+ public void readL1() throws Exception {
+ File l1File = ApkZFileTestUtils.getResource("/testData/packaging/l1.zip");
+ assertTrue(l1File.isFile());
+
+ try (ZFile read = new ZFile(l1File, new ZFileOptions())) {
+ assertNotNull(read.get("text-files/rfc2460.txt"));
+ }
+ }
+}
diff --git a/src/test/java/com/android/apkzlib/zip/ZFileNotificationTest.java b/src/test/java/com/android/apkzlib/zip/ZFileNotificationTest.java
new file mode 100644
index 0000000..8d01f0d
--- /dev/null
+++ b/src/test/java/com/android/apkzlib/zip/ZFileNotificationTest.java
@@ -0,0 +1,422 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apkzlib.zip;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.apkzlib.utils.ApkZLibPair;
+import com.android.apkzlib.utils.IOExceptionRunnable;
+import com.google.common.collect.Lists;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.mockito.Mockito;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+public class ZFileNotificationTest {
+ private static class KeepListener extends ZFileExtension {
+ public int open;
+ public int beforeUpdated;
+ public int updated;
+ public int closed;
+ public List<ApkZLibPair<StoredEntry, StoredEntry>> added;
+ public List<StoredEntry> removed;
+ public IOExceptionRunnable returnRunnable;
+
+ KeepListener() {
+ reset();
+ }
+
+ @Nullable
+ @Override
+ public IOExceptionRunnable open() {
+ open++;
+ return returnRunnable;
+ }
+
+ @Nullable
+ @Override
+ public IOExceptionRunnable beforeUpdate() {
+ beforeUpdated++;
+ return returnRunnable;
+ }
+
+ @Override
+ public void updated() {
+ updated++;
+ }
+
+ @Override
+ public void closed() {
+ closed++;
+ }
+
+ @Nullable
+ @Override
+ public IOExceptionRunnable added(@NonNull StoredEntry entry,
+ @Nullable StoredEntry replaced) {
+ added.add(new ApkZLibPair<>(entry, replaced));
+ return returnRunnable;
+ }
+
+ @Nullable
+ @Override
+ public IOExceptionRunnable removed(@NonNull StoredEntry entry) {
+ removed.add(entry);
+ return returnRunnable;
+ }
+
+ void reset() {
+ open = 0;
+ beforeUpdated = 0;
+ updated = 0;
+ closed = 0;
+ added = Lists.newArrayList();
+ removed = Lists.newArrayList();
+ }
+
+ void assertClear() {
+ assertEquals(0, open);
+ assertEquals(0, beforeUpdated);
+ assertEquals(0, updated);
+ assertEquals(0, closed);
+ assertEquals(0, added.size());
+ assertEquals(0, removed.size());
+ }
+ }
+
+ @Rule
+ public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+
+ @Test
+ public void notifyAddFile() throws Exception {
+ try (ZFile zf = new ZFile(new File(mTemporaryFolder.getRoot(), "a.zip"))) {
+ KeepListener kl = new KeepListener();
+ zf.addZFileExtension(kl);
+
+ kl.assertClear();
+
+ zf.add("foo", new ByteArrayInputStream(new byte[] { 1, 2 }));
+ zf.finishAllBackgroundTasks();
+ assertEquals(1, kl.added.size());
+ StoredEntry addedSe = kl.added.get(0).v1;
+ assertNull(kl.added.get(0).v2);
+ kl.added.clear();
+ kl.assertClear();
+
+ StoredEntry foo = zf.get("foo");
+ assertNotNull(foo);
+
+ assertSame(foo, addedSe);
+ }
+ }
+
+ @Test
+ public void notifyRemoveFile() throws Exception {
+ try (ZFile zf = new ZFile(new File(mTemporaryFolder.getRoot(), "a.zip"))) {
+ KeepListener kl = new KeepListener();
+ zf.addZFileExtension(kl);
+
+ kl.assertClear();
+
+ zf.add("foo", new ByteArrayInputStream(new byte[] { 1, 2 }));
+ zf.finishAllBackgroundTasks();
+ kl.reset();
+
+ StoredEntry foo = zf.get("foo");
+ assertNotNull(foo);
+
+ foo.delete();
+ assertEquals(1, kl.removed.size());
+ assertSame(foo, kl.removed.get(0));
+ kl.removed.clear();
+ kl.assertClear();
+ }
+ }
+
+ @Test
+ public void notifyUpdateFile() throws Exception {
+ try (ZFile zf = new ZFile(new File(mTemporaryFolder.getRoot(), "a.zip"))) {
+ KeepListener kl = new KeepListener();
+ zf.addZFileExtension(kl);
+
+ kl.assertClear();
+
+ zf.add("foo", new ByteArrayInputStream(new byte[] { 1, 2 }));
+ zf.finishAllBackgroundTasks();
+ StoredEntry foo1 = zf.get("foo");
+ kl.reset();
+
+ zf.add("foo", new ByteArrayInputStream(new byte[] { 2, 3 }));
+ zf.finishAllBackgroundTasks();
+ StoredEntry foo2 = zf.get("foo");
+
+ assertEquals(1, kl.added.size());
+ assertSame(foo2, kl.added.get(0).v1);
+ assertSame(foo1, kl.added.get(0).v2);
+
+ kl.added.clear();
+ kl.assertClear();
+ }
+ }
+
+ @Test
+ public void notifyOpenUpdateClose() throws Exception {
+ KeepListener kl = new KeepListener();
+ try (ZFile zf = new ZFile(new File(mTemporaryFolder.getRoot(), "a.zip"))) {
+ zf.addZFileExtension(kl);
+
+ kl.assertClear();
+
+ zf.add("foo", new ByteArrayInputStream(new byte[] { 1, 2 }));
+ zf.finishAllBackgroundTasks();
+ kl.reset();
+ }
+
+ assertEquals(1, kl.open);
+ kl.open = 0;
+ assertEquals(1, kl.beforeUpdated);
+ assertEquals(1, kl.updated);
+ kl.beforeUpdated = 0;
+ kl.updated = 0;
+ assertEquals(1, kl.closed);
+ kl.closed = 0;
+ kl.assertClear();
+ }
+
+ @Test
+ public void notifyOpenUpdate() throws Exception {
+ KeepListener kl = new KeepListener();
+ try (ZFile zf = new ZFile(new File(mTemporaryFolder.getRoot(), "a.zip"))) {
+ zf.addZFileExtension(kl);
+
+ kl.assertClear();
+
+ zf.add("foo", new ByteArrayInputStream(new byte[] { 1, 2 }));
+ zf.finishAllBackgroundTasks();
+ kl.reset();
+ zf.update();
+
+ assertEquals(1, kl.open);
+ kl.open = 0;
+ assertEquals(1, kl.beforeUpdated);
+ assertEquals(1, kl.updated);
+ kl.beforeUpdated = 0;
+ kl.updated = 0;
+ kl.assertClear();
+ }
+ }
+
+ @Test
+ public void notifyUpdate() throws Exception {
+ try (ZFile zf = new ZFile(new File(mTemporaryFolder.getRoot(), "a.zip"))) {
+ KeepListener kl = new KeepListener();
+ zf.addZFileExtension(kl);
+
+ kl.assertClear();
+
+ zf.add("foo", new ByteArrayInputStream(new byte[] { 1, 2 }));
+ zf.update();
+ kl.reset();
+
+ zf.add("bar", new ByteArrayInputStream(new byte[] { 2, 3 }));
+ zf.finishAllBackgroundTasks();
+ kl.reset();
+
+ zf.update();
+ assertEquals(1, kl.beforeUpdated);
+ assertEquals(1, kl.updated);
+ kl.beforeUpdated = 0;
+ kl.updated = 0;
+ kl.assertClear();
+ }
+ }
+
+ @Test
+ public void removedListenersAreNotNotified() throws Exception {
+ try (ZFile zf = new ZFile(new File(mTemporaryFolder.getRoot(), "a.zip"))) {
+ KeepListener kl = new KeepListener();
+ zf.addZFileExtension(kl);
+
+ kl.assertClear();
+
+ zf.add("foo", new ByteArrayInputStream(new byte[] { 1, 2 }));
+ zf.finishAllBackgroundTasks();
+ assertEquals(1, kl.added.size());
+ kl.added.clear();
+ kl.assertClear();
+
+ zf.removeZFileExtension(kl);
+
+ zf.add("foo", new ByteArrayInputStream(new byte[] { 2, 3 }));
+ zf.finishAllBackgroundTasks();
+ kl.assertClear();
+ }
+ }
+
+ @Test
+ public void actionsExecutedAtEndOfNotification() throws Exception {
+ try (ZFile zf = new ZFile(new File(mTemporaryFolder.getRoot(), "a.zip"))) {
+
+ IOException death[] = new IOException[1];
+
+ KeepListener kl1 = new KeepListener();
+ zf.addZFileExtension(kl1);
+ kl1.returnRunnable = new IOExceptionRunnable() {
+ private boolean once = false;
+
+ @Override
+ public void run() {
+ if (once) {
+ return;
+ }
+
+ once = true;
+
+ try {
+ zf.add("foo", new ByteArrayInputStream(new byte[] { 1, 2 }));
+ } catch (IOException e) {
+ death[0] = e;
+ }
+ }
+ };
+
+ KeepListener kl2 = new KeepListener();
+ zf.addZFileExtension(kl2);
+ kl2.returnRunnable = new IOExceptionRunnable() {
+ private boolean once = false;
+
+ @Override
+ public void run() {
+ if (once) {
+ return;
+ }
+
+ once = true;
+ try {
+ zf.add("bar", new ByteArrayInputStream(new byte[] { 1, 2 }));
+ } catch (IOException e) {
+ death[0] = e;
+ }
+ }
+ };
+
+ kl1.assertClear();
+ kl2.assertClear();
+
+ zf.add("xpto", new ByteArrayInputStream(new byte[] { 1, 2 }));
+ zf.finishAllBackgroundTasks();
+
+ assertEquals(3, kl1.added.size());
+ kl1.added.clear();
+ kl1.assertClear();
+ assertEquals(3, kl2.added.size());
+ kl2.added.clear();
+ kl2.assertClear();
+
+ assertNull(death[0]);
+ }
+ }
+
+ @Test
+ public void canAddFilesDuringUpdateNotification() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
+ try (ZFile zf = new ZFile(zipFile)) {
+ IOException death[] = new IOException[1];
+
+ KeepListener kl1 = new KeepListener();
+ zf.addZFileExtension(kl1);
+
+ zf.add("foo", new ByteArrayInputStream(new byte[] { 1, 2 }));
+ zf.finishAllBackgroundTasks();
+
+ kl1.returnRunnable = new IOExceptionRunnable() {
+ private boolean once = false;
+
+ @Override
+ public void run() {
+ if (once) {
+ return;
+ }
+
+ once = true;
+
+ try {
+ zf.add("bar", new ByteArrayInputStream(new byte[] { 1, 2 }));
+ } catch (IOException e) {
+ death[0] = e;
+ }
+ }
+ };
+ }
+
+ try (ZFile zf2 = new ZFile(zipFile)) {
+ StoredEntry fooFile = zf2.get("foo");
+ assertNotNull(fooFile);
+ StoredEntry barFile = zf2.get("bar");
+ assertNotNull(barFile);
+ }
+ }
+
+ @Test
+ public void notifyOnceEntriesWritten() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
+ ZFileExtension ext = Mockito.mock(ZFileExtension.class);
+ try (ZFile zf = new ZFile(zipFile)) {
+ zf.addZFileExtension(ext);
+
+ zf.add("foo", new ByteArrayInputStream(new byte[] { 1, 2 }));
+ zf.finishAllBackgroundTasks();
+
+ Mockito.verify(ext, Mockito.times(0)).entriesWritten();
+ }
+
+ Mockito.verify(ext, Mockito.times(1)).entriesWritten();
+ }
+
+ @Test
+ public void notifyTwiceEntriesWrittenIfCdChanged() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
+ ZFileExtension ext = Mockito.mock(ZFileExtension.class);
+ try (ZFile zf = new ZFile(zipFile)) {
+ Mockito.doAnswer((invocation) -> {
+ zf.setExtraDirectoryOffset(10);
+ Mockito.doNothing().when(ext).entriesWritten();
+ return null;
+ }).when(ext).entriesWritten();
+
+ zf.addZFileExtension(ext);
+
+ zf.add("foo", new ByteArrayInputStream(new byte[] { 1, 2 }));
+ zf.finishAllBackgroundTasks();
+
+ Mockito.verify(ext, Mockito.times(0)).entriesWritten();
+ }
+
+ Mockito.verify(ext, Mockito.times(2)).entriesWritten();
+ }
+}
diff --git a/src/test/java/com/android/apkzlib/zip/ZFileSortTest.java b/src/test/java/com/android/apkzlib/zip/ZFileSortTest.java
new file mode 100644
index 0000000..6ad7bd1
--- /dev/null
+++ b/src/test/java/com/android/apkzlib/zip/ZFileSortTest.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apkzlib.zip;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import com.android.annotations.Nullable;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+public class ZFileSortTest {
+ @Rule
+ public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+ private File mFile;
+ private ZFile mZFile;
+ private StoredEntry mMaryEntry;
+ private long mMaryOffset;
+ private StoredEntry mAndrewEntry;
+ private long mAndrewOffset;
+ private StoredEntry mBethEntry;
+ private long mBethOffset;
+ private StoredEntry mPeterEntry;
+ private long mPeterOffset;
+
+ @Before
+ public final void before() throws Exception {
+ mFile = new File(mTemporaryFolder.getRoot(), "a.zip");
+ setupZFile(null);
+ }
+
+ @After
+ public final void after() throws Exception {
+ mZFile.close();
+ }
+
+ /**
+ * Recreates the zip file, if one already exist.
+ *
+ * @param options the options for the file, may be {@code null} in which case the default
+ * options will be used
+ * @throws Exception failed to re-create the file
+ */
+ private void setupZFile(@Nullable ZFileOptions options) throws Exception {
+ if (mZFile != null) {
+ mZFile.close();
+ }
+
+ if (mFile.exists()) {
+ assertTrue(mFile.delete());
+ }
+
+ if (options == null) {
+ options = new ZFileOptions();
+ }
+
+ mZFile = new ZFile(mFile, options);
+
+ mZFile.add("Mary.xml", new ByteArrayInputStream(new byte[] { 1, 2, 3 }));
+ mZFile.add("Andrew.txt", new ByteArrayInputStream(new byte[] { 4, 5 }));
+ mZFile.add("Beth.png", new ByteArrayInputStream(new byte[] { 6, 7, 8, 9 }));
+ mZFile.add("Peter.html", new ByteArrayInputStream(new byte[] { 10 }));
+ mZFile.finishAllBackgroundTasks();
+ }
+
+ private void readEntries() throws Exception {
+ mMaryEntry = mZFile.get("Mary.xml");
+ assertNotNull(mMaryEntry);
+ mMaryOffset = mMaryEntry.getCentralDirectoryHeader().getOffset();
+ assertArrayEquals(new byte[] { 1, 2, 3 }, mMaryEntry.read());
+
+ mAndrewEntry = mZFile.get("Andrew.txt");
+ assertNotNull(mAndrewEntry);
+ mAndrewOffset = mAndrewEntry.getCentralDirectoryHeader().getOffset();
+ assertArrayEquals(new byte[] { 4, 5 }, mAndrewEntry.read());
+
+ mBethEntry = mZFile.get("Beth.png");
+ assertNotNull(mBethEntry);
+ mBethOffset = mBethEntry.getCentralDirectoryHeader().getOffset();
+ assertArrayEquals(new byte[] { 6, 7, 8, 9 }, mBethEntry.read());
+
+ mPeterEntry = mZFile.get("Peter.html");
+ assertNotNull(mPeterEntry);
+ mPeterOffset = mPeterEntry.getCentralDirectoryHeader().getOffset();
+ assertArrayEquals(new byte[] { 10 }, mPeterEntry.read());
+ }
+
+ @Test
+ public void noSort() throws Exception {
+ readEntries();
+
+ assertEquals(-1, mMaryOffset);
+ assertEquals(-1, mAndrewOffset);
+ assertEquals(-1, mBethOffset);
+ assertEquals(-1, mPeterOffset);
+
+ mZFile.update();
+
+ readEntries();
+
+ assertTrue(mMaryOffset >= 0);
+ assertTrue(mMaryOffset < mAndrewOffset);
+ assertTrue(mAndrewOffset < mBethOffset);
+ assertTrue(mBethOffset < mPeterOffset);
+ }
+
+ @Test
+ public void sortFilesBeforeUpdate() throws Exception {
+ readEntries();
+ mZFile.sortZipContents();
+
+ mZFile.update();
+
+ readEntries();
+
+ assertTrue(mAndrewOffset >= 0);
+ assertTrue(mBethOffset > mAndrewOffset);
+ assertTrue(mMaryOffset > mBethOffset);
+ assertTrue(mPeterOffset > mMaryOffset);
+ }
+
+ @Test
+ public void autoSort() throws Exception {
+ ZFileOptions options = new ZFileOptions();
+ options.setAutoSortFiles(true);
+ setupZFile(options);
+
+ readEntries();
+
+ mZFile.update();
+
+ readEntries();
+
+ assertTrue(mAndrewOffset >= 0);
+ assertTrue(mBethOffset > mAndrewOffset);
+ assertTrue(mMaryOffset > mBethOffset);
+ assertTrue(mPeterOffset > mMaryOffset);
+ }
+
+ @Test
+ public void sortFilesAfterUpdate() throws Exception {
+ readEntries();
+
+ mZFile.update();
+
+ mZFile.sortZipContents();
+
+ readEntries();
+
+ assertEquals(-1, mMaryOffset);
+ assertEquals(-1, mAndrewOffset);
+ assertEquals(-1, mBethOffset);
+ assertEquals(-1, mPeterOffset);
+
+ mZFile.update();
+
+ readEntries();
+
+ assertTrue(mAndrewOffset >= 0);
+ assertTrue(mBethOffset > mAndrewOffset);
+ assertTrue(mMaryOffset > mBethOffset);
+ assertTrue(mPeterOffset > mMaryOffset);
+ }
+
+ @Test
+ public void sortFilesWithAlignment() throws Exception {
+ mZFile.close();
+
+ ZFileOptions options = new ZFileOptions();
+ options.setAlignmentRule(AlignmentRules.constantForSuffix(".xml", 1024));
+ mZFile = new ZFile(mFile, options);
+
+ mZFile.sortZipContents();
+ mZFile.update();
+
+ readEntries();
+ assertTrue(mAndrewOffset >= 0);
+ assertTrue(mBethOffset > mAndrewOffset);
+ assertTrue(mPeterOffset > mBethOffset);
+ assertTrue(mMaryOffset > mPeterOffset);
+ }
+
+ @Test
+ public void sortFilesOnClosedFile() throws Exception {
+ mZFile.close();
+ mZFile = new ZFile(mFile);
+ mZFile.sortZipContents();
+ mZFile.update();
+
+ readEntries();
+
+ assertTrue(mAndrewOffset >= 0);
+ assertTrue(mBethOffset > mAndrewOffset);
+ assertTrue(mMaryOffset > mBethOffset);
+ assertTrue(mPeterOffset > mMaryOffset);
+ }
+}
diff --git a/src/test/java/com/android/apkzlib/zip/ZFileTest.java b/src/test/java/com/android/apkzlib/zip/ZFileTest.java
new file mode 100644
index 0000000..3619691
--- /dev/null
+++ b/src/test/java/com/android/apkzlib/zip/ZFileTest.java
@@ -0,0 +1,1418 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apkzlib.zip;
+
+import static com.android.apkzlib.utils.ApkZFileTestUtils.readSegment;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.android.annotations.NonNull;
+import com.android.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;
+import com.google.common.io.Files;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.RandomAccessFile;
+import java.util.Random;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.zip.Deflater;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipInputStream;
+import java.util.zip.ZipOutputStream;
+
+public class ZFileTest {
+ @Rule
+ public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+
+ @Test
+ public void getZipPath() throws Exception {
+ File temporaryDir = mTemporaryFolder.getRoot();
+ File zpath = new File(temporaryDir, "a");
+ try (ZFile zf = new ZFile(zpath)) {
+ assertEquals(zpath, zf.getFile());
+ }
+ }
+
+ @Test
+ public void readNonExistingFile() throws Exception {
+ File temporaryDir = mTemporaryFolder.getRoot();
+ File zf = new File(temporaryDir, "a");
+ try (ZFile azf = new ZFile(zf)) {
+ azf.touch();
+ }
+
+ assertTrue(zf.exists());
+ }
+
+ @Test(expected = IOException.class)
+ public void readExistingEmptyFile() throws Exception {
+ File temporaryDir = mTemporaryFolder.getRoot();
+ File zf = new File(temporaryDir, "a");
+ Files.write(new byte[0], zf);
+ try (ZFile azf = new ZFile(zf)) {
+ /*
+ * Just open and close.
+ */
+ }
+ }
+
+ @Test
+ public void readAlmostEmptyZip() throws Exception {
+ File zf = ZipTestUtils.cloneRsrc("empty-zip.zip", mTemporaryFolder);
+
+ try (ZFile azf = new ZFile(zf)) {
+ assertEquals(1, azf.entries().size());
+
+ StoredEntry z = azf.get("z/");
+ assertNotNull(z);
+ assertSame(StoredEntryType.DIRECTORY, z.getType());
+ }
+ }
+
+ @Test
+ public void readZipWithTwoFilesOneDirectory() throws Exception {
+ File zf = ZipTestUtils.cloneRsrc("simple-zip.zip", mTemporaryFolder);
+
+ try (ZFile azf = new ZFile(zf)) {
+ assertEquals(3, azf.entries().size());
+
+ StoredEntry e0 = azf.get("dir/");
+ assertNotNull(e0);
+ assertSame(StoredEntryType.DIRECTORY, e0.getType());
+
+ StoredEntry e1 = azf.get("dir/inside");
+ assertNotNull(e1);
+ assertSame(StoredEntryType.FILE, e1.getType());
+ ByteArrayOutputStream e1BytesOut = new ByteArrayOutputStream();
+ ByteStreams.copy(e1.open(), e1BytesOut);
+ byte e1Bytes[] = e1BytesOut.toByteArray();
+ String e1Txt = new String(e1Bytes, Charsets.US_ASCII);
+ assertEquals("inside", e1Txt);
+
+ StoredEntry e2 = azf.get("file.txt");
+ assertNotNull(e2);
+ assertSame(StoredEntryType.FILE, e2.getType());
+ ByteArrayOutputStream e2BytesOut = new ByteArrayOutputStream();
+ ByteStreams.copy(e2.open(), e2BytesOut);
+ byte e2Bytes[] = e2BytesOut.toByteArray();
+ String e2Txt = new String(e2Bytes, Charsets.US_ASCII);
+ assertEquals("file with more text to allow deflating to be useful", e2Txt);
+ }
+ }
+
+ @Test
+ public void readOnlyZipSupport() throws Exception {
+ File testZip = ZipTestUtils.cloneRsrc("empty-zip.zip", mTemporaryFolder);
+
+ assertTrue(testZip.setWritable(false));
+
+ try (ZFile zf = new ZFile(testZip)) {
+ assertEquals(1, zf.entries().size());
+ }
+ }
+
+ @Test
+ public void compressedFilesReadableByJavaZip() throws Exception {
+ File testZip = new File(mTemporaryFolder.getRoot(), "t.zip");
+
+ File wiki = ZipTestUtils
+ .cloneRsrc("text-files/wikipedia.html", mTemporaryFolder, "wiki");
+ File rfc = ZipTestUtils.cloneRsrc("text-files/rfc2460.txt", mTemporaryFolder, "rfc");
+ File lena = ZipTestUtils.cloneRsrc("images/lena.png", mTemporaryFolder, "lena");
+ byte[] wikiData = Files.toByteArray(wiki);
+ byte[] rfcData = Files.toByteArray(rfc);
+ byte[] lenaData = Files.toByteArray(lena);
+
+ try (ZFile zf = new ZFile(testZip)) {
+ zf.add("wiki", new ByteArrayInputStream(wikiData));
+ zf.add("rfc", new ByteArrayInputStream(rfcData));
+ zf.add("lena", new ByteArrayInputStream(lenaData));
+ }
+
+ try(ZipFile jz = new ZipFile(testZip)) {
+ ZipEntry ze = jz.getEntry("wiki");
+ assertNotNull(ze);
+ assertEquals(ZipEntry.DEFLATED, ze.getMethod());
+ assertTrue(ze.getCompressedSize() < wikiData.length);
+ InputStream zeis = jz.getInputStream(ze);
+ assertArrayEquals(wikiData, ByteStreams.toByteArray(zeis));
+ zeis.close();
+
+ ze = jz.getEntry("rfc");
+ assertNotNull(ze);
+ assertEquals(ZipEntry.DEFLATED, ze.getMethod());
+ assertTrue(ze.getCompressedSize() < rfcData.length);
+ zeis = jz.getInputStream(ze);
+ assertArrayEquals(rfcData, ByteStreams.toByteArray(zeis));
+ zeis.close();
+
+ ze = jz.getEntry("lena");
+ assertNotNull(ze);
+ assertEquals(ZipEntry.STORED, ze.getMethod());
+ assertTrue(ze.getCompressedSize() == lenaData.length);
+ zeis = jz.getInputStream(ze);
+ assertArrayEquals(lenaData, ByteStreams.toByteArray(zeis));
+ zeis.close();
+ }
+ }
+
+ @Test
+ public void removeFileFromZip() throws Exception {
+ File zipFile = mTemporaryFolder.newFile("test.zip");
+
+ try(ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) {
+ ZipEntry entry = new ZipEntry("foo/");
+ entry.setMethod(ZipEntry.STORED);
+ entry.setSize(0);
+ entry.setCompressedSize(0);
+ entry.setCrc(0);
+ zos.putNextEntry(entry);
+ zos.putNextEntry(new ZipEntry("foo/bar"));
+ zos.write(new byte[] { 1, 2, 3, 4 });
+ zos.closeEntry();
+ }
+
+ try (ZFile zf = new ZFile(zipFile)) {
+ assertEquals(2, zf.entries().size());
+ for (StoredEntry e : zf.entries()) {
+ if (e.getType() == StoredEntryType.FILE) {
+ e.delete();
+ }
+ }
+
+ zf.update();
+
+ try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile))) {
+ ZipEntry e1 = zis.getNextEntry();
+ assertNotNull(e1);
+
+ assertEquals("foo/", e1.getName());
+
+ ZipEntry e2 = zis.getNextEntry();
+ assertNull(e2);
+ }
+ }
+ }
+
+ @Test
+ public void addFileToZip() throws Exception {
+ File zipFile = mTemporaryFolder.newFile("test.zip");
+
+ try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) {
+ ZipEntry fooDir = new ZipEntry("foo/");
+ fooDir.setCrc(0);
+ fooDir.setCompressedSize(0);
+ fooDir.setSize(0);
+ fooDir.setMethod(ZipEntry.STORED);
+ zos.putNextEntry(fooDir);
+ zos.closeEntry();
+ }
+
+ ZFile zf = new ZFile(zipFile);
+ assertEquals(1, zf.entries().size());
+
+ zf.update();
+
+ try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile))) {
+ ZipEntry e1 = zis.getNextEntry();
+ assertNotNull(e1);
+
+ assertEquals("foo/", e1.getName());
+
+ ZipEntry e2 = zis.getNextEntry();
+ assertNull(e2);
+ }
+ }
+
+ @Test
+ public void createNewZip() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "test.zip");
+
+ ZFile zf = new ZFile(zipFile);
+ zf.add("foo", new ByteArrayInputStream(new byte[] { 0, 1 }));
+ zf.close();
+
+ try (ZipFile jzf = new ZipFile(zipFile)) {
+ assertEquals(1, jzf.size());
+
+ ZipEntry fooEntry = jzf.getEntry("foo");
+ assertNotNull(fooEntry);
+ assertEquals("foo", fooEntry.getName());
+ assertEquals(2, fooEntry.getSize());
+
+ InputStream is = jzf.getInputStream(fooEntry);
+ assertEquals(0, is.read());
+ assertEquals(1, is.read());
+ assertEquals(-1, is.read());
+
+ is.close();
+ }
+ }
+
+ @Test
+ public void replaceFileWithSmallerInMiddle() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "test.zip");
+
+ try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) {
+ zos.putNextEntry(new ZipEntry("file1"));
+ zos.write(new byte[] { 1, 2, 3, 4, 5 });
+ zos.putNextEntry(new ZipEntry("file2"));
+ zos.write(new byte[] { 6, 7, 8 });
+ zos.putNextEntry(new ZipEntry("file3"));
+ zos.write(new byte[] { 9, 0, 1, 2, 3, 4 });
+ }
+
+ int totalSize = (int) zipFile.length();
+
+ try (ZFile zf = new ZFile(zipFile)) {
+ assertEquals(3, zf.entries().size());
+
+ StoredEntry file2 = zf.get("file2");
+ assertNotNull(file2);
+ assertEquals(3, file2.getCentralDirectoryHeader().getUncompressedSize());
+
+ assertArrayEquals(new byte[] { 6, 7, 8 }, file2.read());
+
+ zf.add("file2", new ByteArrayInputStream(new byte[] { 11, 12 }));
+
+ int newTotalSize = (int) zipFile.length();
+ assertTrue(newTotalSize + " == " + totalSize, newTotalSize == totalSize);
+
+ file2 = zf.get("file2");
+ assertNotNull(file2);
+ assertArrayEquals(new byte[] { 11, 12 }, file2.read());
+ }
+
+ try (ZFile zf2 = new ZFile(zipFile)) {
+ StoredEntry file2 = zf2.get("file2");
+ assertNotNull(file2);
+ assertArrayEquals(new byte[] { 11, 12 }, file2.read());
+ }
+ }
+
+ @Test
+ public void replaceFileWithSmallerAtEnd() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "test.zip");
+ try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) {
+ zos.putNextEntry(new ZipEntry("file1"));
+ zos.write(new byte[]{1, 2, 3, 4, 5});
+ zos.putNextEntry(new ZipEntry("file2"));
+ zos.write(new byte[]{6, 7, 8});
+ zos.putNextEntry(new ZipEntry("file3"));
+ zos.write(new byte[]{9, 0, 1, 2, 3, 4});
+ }
+
+ int totalSize = (int) zipFile.length();
+
+ try (ZFile zf = new ZFile(zipFile)) {
+ assertEquals(3, zf.entries().size());
+
+ StoredEntry file3 = zf.get("file3");
+ assertNotNull(file3);
+ assertEquals(6, file3.getCentralDirectoryHeader().getUncompressedSize());
+
+ assertArrayEquals(new byte[]{9, 0, 1, 2, 3, 4}, file3.read());
+
+ zf.add("file3", new ByteArrayInputStream(new byte[]{11, 12}));
+ zf.close();
+
+ int newTotalSize = (int) zipFile.length();
+ assertTrue(newTotalSize + " < " + totalSize, newTotalSize < totalSize);
+
+ file3 = zf.get("file3");
+ assertNotNull(file3);
+ assertArrayEquals(new byte[]{11, 12,}, file3.read());
+ }
+
+ try (ZFile zf2 = new ZFile(zipFile)) {
+ StoredEntry file3 = zf2.get("file3");
+ assertNotNull(file3);
+ assertArrayEquals(new byte[]{11, 12,}, file3.read());
+ }
+ }
+
+ @Test
+ public void replaceFileWithBiggerAtBegin() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "test.zip");
+
+ try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) {
+ zos.putNextEntry(new ZipEntry("file1"));
+ zos.write(new byte[]{1, 2, 3, 4, 5});
+ zos.putNextEntry(new ZipEntry("file2"));
+ zos.write(new byte[]{6, 7, 8});
+ zos.putNextEntry(new ZipEntry("file3"));
+ zos.write(new byte[]{9, 0, 1, 2, 3, 4});
+ }
+
+ int totalSize = (int) zipFile.length();
+ byte[] newData = new byte[100];
+
+ try (ZFile zf = new ZFile(zipFile)) {
+ assertEquals(3, zf.entries().size());
+
+ StoredEntry file1 = zf.get("file1");
+ assertNotNull(file1);
+ assertEquals(5, file1.getCentralDirectoryHeader().getUncompressedSize());
+
+ assertArrayEquals(new byte[]{1, 2, 3, 4, 5}, file1.read());
+
+ /*
+ * Need some data because java zip API uses data descriptors which we don't and makes
+ * the entries bigger (meaning just adding a couple of bytes would still fit in the
+ * same place).
+ */
+ Random r = new Random();
+ r.nextBytes(newData);
+
+ zf.add("file1", new ByteArrayInputStream(newData));
+ zf.close();
+
+ int newTotalSize = (int) zipFile.length();
+ assertTrue(newTotalSize + " > " + totalSize, newTotalSize > totalSize);
+
+ file1 = zf.get("file1");
+ assertNotNull(file1);
+ assertArrayEquals(newData, file1.read());
+ }
+
+ try (ZFile zf2 = new ZFile(zipFile)) {
+ StoredEntry file1 = zf2.get("file1");
+ assertNotNull(file1);
+ assertArrayEquals(newData, file1.read());
+ }
+ }
+
+ @Test
+ public void replaceFileWithBiggerAtEnd() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "test.zip");
+
+ try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) {
+ zos.putNextEntry(new ZipEntry("file1"));
+ zos.write(new byte[]{1, 2, 3, 4, 5});
+ zos.putNextEntry(new ZipEntry("file2"));
+ zos.write(new byte[]{6, 7, 8});
+ zos.putNextEntry(new ZipEntry("file3"));
+ zos.write(new byte[]{9, 0, 1, 2, 3, 4});
+ }
+
+ int totalSize = (int) zipFile.length();
+ byte[] newData = new byte[100];
+
+ try (ZFile zf = new ZFile(zipFile)) {
+ assertEquals(3, zf.entries().size());
+
+ StoredEntry file3 = zf.get("file3");
+ assertNotNull(file3);
+ assertEquals(6, file3.getCentralDirectoryHeader().getUncompressedSize());
+
+ assertArrayEquals(new byte[]{9, 0, 1, 2, 3, 4}, file3.read());
+
+ /*
+ * Need some data because java zip API uses data descriptors which we don't and makes
+ * the entries bigger (meaning just adding a couple of bytes would still fit in the
+ * same place).
+ */
+ Random r = new Random();
+ r.nextBytes(newData);
+
+ zf.add("file3", new ByteArrayInputStream(newData));
+ zf.close();
+
+ int newTotalSize = (int) zipFile.length();
+ assertTrue(newTotalSize + " > " + totalSize, newTotalSize > totalSize);
+
+ file3 = zf.get("file3");
+ assertNotNull(file3);
+ assertArrayEquals(newData, file3.read());
+ }
+
+ try (ZFile zf2 = new ZFile(zipFile)) {
+ StoredEntry file3 = zf2.get("file3");
+ assertNotNull(file3);
+ assertArrayEquals(newData, file3.read());
+ }
+ }
+
+ @Test
+ public void ignoredFilesDuringMerge() throws Exception {
+ File zip1 = mTemporaryFolder.newFile("t1.zip");
+
+ try (ZipOutputStream zos1 = new ZipOutputStream(new FileOutputStream(zip1))) {
+ zos1.putNextEntry(new ZipEntry("only_in_1"));
+ zos1.write(new byte[] { 1, 2 });
+ zos1.putNextEntry(new ZipEntry("overridden_by_2"));
+ zos1.write(new byte[] { 2, 3 });
+ zos1.putNextEntry(new ZipEntry("not_overridden_by_2"));
+ zos1.write(new byte[] { 3, 4 });
+ }
+
+ File zip2 = mTemporaryFolder.newFile("t2.zip");
+ try (ZipOutputStream zos2 = new ZipOutputStream(new FileOutputStream(zip2))) {
+ zos2.putNextEntry(new ZipEntry("only_in_2"));
+ zos2.write(new byte[] { 4, 5 });
+ zos2.putNextEntry(new ZipEntry("overridden_by_2"));
+ zos2.write(new byte[] { 5, 6 });
+ zos2.putNextEntry(new ZipEntry("not_overridden_by_2"));
+ zos2.write(new byte[] { 6, 7 });
+ zos2.putNextEntry(new ZipEntry("ignored_in_2"));
+ zos2.write(new byte[] { 7, 8 });
+ }
+
+ try (
+ ZFile zf1 = new ZFile(zip1);
+ ZFile zf2 = new ZFile(zip2)) {
+ zf1.mergeFrom(zf2, (input) -> input.matches("not.*") || input.matches(".*gnored.*"));
+
+ StoredEntry only_in_1 = zf1.get("only_in_1");
+ assertNotNull(only_in_1);
+ assertArrayEquals(new byte[]{1, 2}, only_in_1.read());
+
+ StoredEntry overridden_by_2 = zf1.get("overridden_by_2");
+ assertNotNull(overridden_by_2);
+ assertArrayEquals(new byte[]{5, 6}, overridden_by_2.read());
+
+ StoredEntry not_overridden_by_2 = zf1.get("not_overridden_by_2");
+ assertNotNull(not_overridden_by_2);
+ assertArrayEquals(new byte[]{3, 4}, not_overridden_by_2.read());
+
+ StoredEntry only_in_2 = zf1.get("only_in_2");
+ assertNotNull(only_in_2);
+ assertArrayEquals(new byte[]{4, 5}, only_in_2.read());
+
+ StoredEntry ignored_in_2 = zf1.get("ignored_in_2");
+ assertNull(ignored_in_2);
+ }
+ }
+
+ @Test
+ public void addingFileDoesNotAddDirectoriesAutomatically() throws Exception {
+ File zip = new File(mTemporaryFolder.getRoot(), "z.zip");
+ try (ZFile zf = new ZFile(zip)) {
+ zf.add("a/b/c", new ByteArrayInputStream(new byte[]{1, 2, 3}));
+ zf.update();
+ assertEquals(1, zf.entries().size());
+
+ StoredEntry c = zf.get("a/b/c");
+ assertNotNull(c);
+ assertEquals(3, c.read().length);
+ }
+ }
+
+ @Test
+ public void zipFileWithEocdSignatureInComment() throws Exception {
+ File zip = mTemporaryFolder.newFile("f.zip");
+ try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zip))) {
+ zos.putNextEntry(new ZipEntry("a"));
+ zos.write(new byte[] { 1, 2, 3 });
+ zos.setComment("Random comment with XXXX weird characters. There must be enough "
+ + "characters to survive skipping back the EOCD size.");
+ }
+
+ byte zipBytes[] = Files.toByteArray(zip);
+ boolean didX4 = false;
+ for (int i = 0; i < zipBytes.length - 3; i++) {
+ boolean x4 = true;
+ for (int j = 0; j < 4; j++) {
+ if (zipBytes[i + j] != 'X') {
+ x4 = false;
+ break;
+ }
+ }
+
+ if (x4) {
+ zipBytes[i] = (byte) 0x50;
+ zipBytes[i + 1] = (byte) 0x4b;
+ zipBytes[i + 2] = (byte) 0x05;
+ zipBytes[i + 3] = (byte) 0x06;
+ didX4 = true;
+ break;
+ }
+ }
+
+ assertTrue(didX4);
+
+ Files.write(zipBytes, zip);
+
+ try (ZFile zf = new ZFile(zip)) {
+ assertEquals(1, zf.entries().size());
+ StoredEntry a = zf.get("a");
+ assertNotNull(a);
+ assertArrayEquals(new byte[]{1, 2, 3}, a.read());
+ }
+ }
+
+ @Test
+ public void addFileRecursively() throws Exception {
+ File tdir = mTemporaryFolder.newFolder();
+ File tfile = new File(tdir, "blah-blah");
+ Files.write("blah", tfile, Charsets.US_ASCII);
+
+ File zip = new File(tdir, "f.zip");
+ try (ZFile zf = new ZFile(zip)) {
+ zf.addAllRecursively(tfile);
+
+ StoredEntry blahEntry = zf.get("blah-blah");
+ assertNotNull(blahEntry);
+ String contents = new String(blahEntry.read(), Charsets.US_ASCII);
+ assertEquals("blah", contents);
+ }
+ }
+
+ @Test
+ public void addDirectoryRecursively() throws Exception {
+ File tdir = mTemporaryFolder.newFolder();
+
+ String boom = Strings.repeat("BOOM!", 100);
+ String kaboom = Strings.repeat("KABOOM!", 100);
+
+ Files.write(boom, new File(tdir, "danger"), Charsets.US_ASCII);
+ Files.write(kaboom, new File(tdir, "do not touch"), Charsets.US_ASCII);
+ File safeDir = new File(tdir, "safe");
+ assertTrue(safeDir.mkdir());
+
+ String iLoveChocolate = Strings.repeat("I love chocolate! ", 200);
+ String iLoveOrange = Strings.repeat("I love orange! ", 50);
+ String loremIpsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean vitae "
+ + "turpis quis justo scelerisque vulputate in et magna. Suspendisse eleifend "
+ + "ultricies nisi, placerat consequat risus accumsan et. Pellentesque habitant "
+ + "morbi tristique senectus et netus et malesuada fames ac turpis egestas. "
+ + "Integer vitae leo purus. Nulla facilisi. Duis ligula libero, lacinia a "
+ + "malesuada a, viverra tempor sapien. Donec eget consequat sapien, ultrices"
+ + "interdum diam. Maecenas ipsum erat, suscipit at iaculis a, mollis nec risus. "
+ + "Quisque tristique ac velit sed auctor. Nulla lacus diam, tristique id sem non, "
+ + "pellentesque commodo mauris.";
+
+ Files.write(iLoveChocolate, new File(safeDir, "eat.sweet"), Charsets.US_ASCII);
+ Files.write(iLoveOrange, new File(safeDir, "eat.fruit"), Charsets.US_ASCII);
+ Files.write(loremIpsum, new File(safeDir, "bedtime.reading.txt"), Charsets.US_ASCII);
+
+ File zip = new File(tdir, "f.zip");
+ try (ZFile zf = new ZFile(zip)) {
+ zf.addAllRecursively(tdir, (f) -> !f.getName().startsWith("eat."));
+
+ assertEquals(6, zf.entries().size());
+
+ StoredEntry boomEntry = zf.get("danger");
+ assertNotNull(boomEntry);
+ assertEquals(CompressionMethod.DEFLATE,
+ boomEntry.getCentralDirectoryHeader().getCompressionInfoWithWait().getMethod());
+ assertEquals(boom, new String(boomEntry.read(), Charsets.US_ASCII));
+
+ StoredEntry kaboomEntry = zf.get("do not touch");
+ assertNotNull(kaboomEntry);
+ assertEquals(CompressionMethod.DEFLATE,
+ kaboomEntry
+ .getCentralDirectoryHeader()
+ .getCompressionInfoWithWait()
+ .getMethod());
+ assertEquals(kaboom, new String(kaboomEntry.read(), Charsets.US_ASCII));
+
+ StoredEntry safeEntry = zf.get("safe/");
+ assertNotNull(safeEntry);
+ assertEquals(0, safeEntry.read().length);
+
+ StoredEntry choc = zf.get("safe/eat.sweet");
+ assertNotNull(choc);
+ assertEquals(CompressionMethod.STORE,
+ choc.getCentralDirectoryHeader().getCompressionInfoWithWait().getMethod());
+ assertEquals(iLoveChocolate, new String(choc.read(), Charsets.US_ASCII));
+
+ StoredEntry orangeEntry = zf.get("safe/eat.fruit");
+ assertNotNull(orangeEntry);
+ assertEquals(CompressionMethod.STORE,
+ orangeEntry
+ .getCentralDirectoryHeader()
+ .getCompressionInfoWithWait()
+ .getMethod());
+ assertEquals(iLoveOrange, new String(orangeEntry.read(), Charsets.US_ASCII));
+
+ StoredEntry loremEntry = zf.get("safe/bedtime.reading.txt");
+ assertNotNull(loremEntry);
+ assertEquals(CompressionMethod.DEFLATE,
+ loremEntry
+ .getCentralDirectoryHeader()
+ .getCompressionInfoWithWait()
+ .getMethod());
+ assertEquals(loremIpsum, new String(loremEntry.read(), Charsets.US_ASCII));
+ }
+ }
+
+ @Test
+ public void extraDirectoryOffsetEmptyFile() throws Exception {
+ File zipNoOffsetFile = new File(mTemporaryFolder.getRoot(), "a.zip");
+ File zipWithOffsetFile = new File(mTemporaryFolder.getRoot(), "b.zip");
+
+ int offset = 31;
+
+ long zipNoOffsetSize;
+ try (
+ ZFile zipNoOffset = new ZFile(zipNoOffsetFile);
+ ZFile zipWithOffset = new ZFile(zipWithOffsetFile)) {
+ zipWithOffset.setExtraDirectoryOffset(offset);
+
+ zipNoOffset.close();
+ zipWithOffset.close();
+
+ zipNoOffsetSize = zipNoOffsetFile.length();
+ long zipWithOffsetSize = zipWithOffsetFile.length();
+
+ assertEquals(zipNoOffsetSize + offset, zipWithOffsetSize);
+
+ /*
+ * EOCD with no comment has 22 bytes.
+ */
+ assertEquals(0, zipNoOffset.getCentralDirectoryOffset());
+ assertEquals(0, zipNoOffset.getCentralDirectorySize());
+ assertEquals(0, zipNoOffset.getEocdOffset());
+ assertEquals(ZFileTestConstants.EOCD_SIZE, zipNoOffset.getEocdSize());
+ assertEquals(offset, zipWithOffset.getCentralDirectoryOffset());
+ assertEquals(0, zipWithOffset.getCentralDirectorySize());
+ assertEquals(offset, zipWithOffset.getEocdOffset());
+ assertEquals(ZFileTestConstants.EOCD_SIZE, zipWithOffset.getEocdSize());
+ }
+
+ /*
+ * The EOCDs should not differ up until the end of the Central Directory size and should
+ * not differ after the offset
+ */
+ int p1Start = 0;
+ int p1Size = Eocd.F_CD_SIZE.endOffset();
+ int p2Start = Eocd.F_CD_OFFSET.endOffset();
+ int p2Size = (int) zipNoOffsetSize - p2Start;
+
+ byte[] noOffsetData1 = readSegment(zipNoOffsetFile, p1Start, p1Size);
+ byte[] noOffsetData2 = readSegment(zipNoOffsetFile, p2Start, p2Size);
+ byte[] withOffsetData1 = readSegment(zipWithOffsetFile, offset, p1Size);
+ byte[] withOffsetData2 = readSegment(zipWithOffsetFile, offset + p2Start, p2Size);
+
+ assertArrayEquals(noOffsetData1, withOffsetData1);
+ assertArrayEquals(noOffsetData2, withOffsetData2);
+
+ try (ZFile readWithOffset = new ZFile(zipWithOffsetFile)) {
+ assertEquals(0, readWithOffset.entries().size());
+ }
+ }
+
+ @Test
+ public void extraDirectoryOffsetNonEmptyFile() throws Exception {
+ File zipNoOffsetFile = new File(mTemporaryFolder.getRoot(), "a.zip");
+ File zipWithOffsetFile = new File(mTemporaryFolder.getRoot(), "b.zip");
+
+ int cdSize;
+
+ // The byte arrays below are larger when compressed, so we end up storing them uncompressed,
+ // which would normally cause them to be 4-aligned. Disable that, to make calculations
+ // easier.
+ ZFileOptions options = new ZFileOptions();
+ options.setAlignmentRule(AlignmentRules.constant(AlignmentRule.NO_ALIGNMENT));
+
+ try (ZFile zipNoOffset = new ZFile(zipNoOffsetFile, options);
+ ZFile zipWithOffset = new ZFile(zipWithOffsetFile, options)) {
+ zipWithOffset.setExtraDirectoryOffset(37);
+
+ zipNoOffset.add("x", new ByteArrayInputStream(new byte[]{1, 2}));
+ zipWithOffset.add("x", new ByteArrayInputStream(new byte[]{1, 2}));
+
+ zipNoOffset.close();
+ zipWithOffset.close();
+
+ long zipNoOffsetSize = zipNoOffsetFile.length();
+ long zipWithOffsetSize = zipWithOffsetFile.length();
+
+ assertEquals(zipNoOffsetSize + 37, zipWithOffsetSize);
+
+ /*
+ * Local file header has 30 bytes + name.
+ * Central directory entry has 46 bytes + name
+ * EOCD with no comment has 22 bytes.
+ */
+ assertEquals(ZFileTestConstants.LOCAL_HEADER_SIZE + 1 + 2,
+ zipNoOffset.getCentralDirectoryOffset());
+ cdSize = (int) zipNoOffset.getCentralDirectorySize();
+ assertEquals(ZFileTestConstants.CENTRAL_DIRECTORY_ENTRY_SIZE + 1, cdSize);
+ assertEquals(ZFileTestConstants.LOCAL_HEADER_SIZE + 1 + 2 + cdSize,
+ zipNoOffset.getEocdOffset());
+ assertEquals(ZFileTestConstants.EOCD_SIZE, zipNoOffset.getEocdSize());
+ assertEquals(ZFileTestConstants.LOCAL_HEADER_SIZE + 1 + 2 + 37,
+ zipWithOffset.getCentralDirectoryOffset());
+ assertEquals(cdSize, zipWithOffset.getCentralDirectorySize());
+ assertEquals(ZFileTestConstants.LOCAL_HEADER_SIZE + 1 + 2 + 37 + cdSize,
+ zipWithOffset.getEocdOffset());
+ assertEquals(ZFileTestConstants.EOCD_SIZE, zipWithOffset.getEocdSize());
+ }
+
+ /*
+ * The files should be equal: until the end of the first entry, from the beginning of the
+ * central directory until the offset field in the EOCD and after the offset field.
+ */
+ int p1Start = 0;
+ int p1Size = ZFileTestConstants.LOCAL_HEADER_SIZE + 1 + 2;
+ int p2Start = ZFileTestConstants.LOCAL_HEADER_SIZE + 1 + 2;
+ int p2Size = cdSize + Eocd.F_CD_SIZE.endOffset();
+ int p3Start = p2Start + cdSize + Eocd.F_CD_OFFSET.endOffset();
+ int p3Size = ZFileTestConstants.EOCD_SIZE - Eocd.F_CD_OFFSET.endOffset();
+
+ byte[] noOffsetData1 = readSegment(zipNoOffsetFile, p1Start, p1Size);
+ byte[] noOffsetData2 = readSegment(zipNoOffsetFile, p2Start, p2Size);
+ byte[] noOffsetData3 = readSegment(zipNoOffsetFile, p3Start, p3Size);
+ byte[] withOffsetData1 = readSegment(zipWithOffsetFile, p1Start, p1Size);
+ byte[] withOffsetData2 = readSegment(zipWithOffsetFile, 37 + p2Start, p2Size);
+ byte[] withOffsetData3 = readSegment(zipWithOffsetFile, 37 + p3Start, p3Size);
+
+ assertArrayEquals(noOffsetData1, withOffsetData1);
+ assertArrayEquals(noOffsetData2, withOffsetData2);
+ assertArrayEquals(noOffsetData3, withOffsetData3);
+
+ try (ZFile readWithOffset = new ZFile(zipWithOffsetFile)) {
+ assertEquals(1, readWithOffset.entries().size());
+ }
+ }
+
+ @Test
+ public void changeExtraDirectoryOffset() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
+
+ try (ZFile zip = new ZFile(zipFile)) {
+ zip.add("x", new ByteArrayInputStream(new byte[]{1, 2}));
+ zip.close();
+
+ long noOffsetSize = zipFile.length();
+
+ zip.setExtraDirectoryOffset(177);
+ zip.close();
+
+ long withOffsetSize = zipFile.length();
+
+ assertEquals(noOffsetSize + 177, withOffsetSize);
+ }
+ }
+
+ @Test
+ public void computeOffsetWhenReadingEmptyFile() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
+
+ try (ZFile zip = new ZFile(zipFile)) {
+ zip.setExtraDirectoryOffset(18);
+ }
+
+ try (ZFile zip = new ZFile(zipFile)) {
+ assertEquals(18, zip.getExtraDirectoryOffset());
+ }
+ }
+
+ @Test
+ public void computeOffsetWhenReadingNonEmptyFile() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
+
+ try (ZFile zip = new ZFile(zipFile)) {
+ zip.setExtraDirectoryOffset(287);
+ zip.add("x", new ByteArrayInputStream(new byte[]{1, 2}));
+ }
+
+ try (ZFile zip = new ZFile(zipFile)) {
+ assertEquals(287, zip.getExtraDirectoryOffset());
+ }
+ }
+
+ @Test
+ public void obtainingCDAndEocdWhenEntriesWrittenOnEmptyZip() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
+
+ byte[][] cd = new byte[1][];
+ byte[][] eocd = new byte[1][];
+
+ try (ZFile zip = new ZFile(zipFile)) {
+ zip.addZFileExtension(new ZFileExtension() {
+ @Override
+ public void entriesWritten() throws IOException {
+ cd[0] = zip.getCentralDirectoryBytes();
+ eocd[0] = zip.getEocdBytes();
+ }
+ });
+ }
+
+ assertNotNull(cd[0]);
+ assertEquals(0, cd[0].length);
+ assertNotNull(eocd[0]);
+ assertEquals(22, eocd[0].length);
+ }
+
+ @Test
+ public void obtainingCDAndEocdWhenEntriesWrittenOnNonEmptyZip() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
+
+ byte[][] cd = new byte[1][];
+ byte[][] eocd = new byte[1][];
+
+ try (ZFile zip = new ZFile(zipFile)) {
+ zip.add("foo", new ByteArrayInputStream(new byte[0]));
+ zip.addZFileExtension(new ZFileExtension() {
+ @Override
+ public void entriesWritten() throws IOException {
+ cd[0] = zip.getCentralDirectoryBytes();
+ eocd[0] = zip.getEocdBytes();
+ }
+ });
+ }
+
+ /*
+ * Central directory entry has 46 bytes + name
+ * EOCD with no comment has 22 bytes.
+ */
+ assertNotNull(cd[0]);
+ assertEquals(46 + 3, cd[0].length);
+ assertNotNull(eocd[0]);
+ assertEquals(22, eocd[0].length);
+ }
+
+ @Test
+ public void java7JarSupported() throws Exception {
+ File jar = ZipTestUtils.cloneRsrc("j7.jar", mTemporaryFolder);
+
+ try (ZFile j = new ZFile(jar)) {
+ assertEquals(8, j.entries().size());
+ }
+ }
+
+ @Test
+ public void java8JarSupported() throws Exception {
+ File jar = ZipTestUtils.cloneRsrc("j8.jar", mTemporaryFolder);
+
+ try (ZFile j = new ZFile(jar)) {
+ assertEquals(8, j.entries().size());
+ }
+ }
+
+ @Test
+ public void utf8NamesSupportedOnReading() throws Exception {
+ File zip = ZipTestUtils.cloneRsrc("zip-with-utf8-filename.zip", mTemporaryFolder);
+
+ 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());
+ }
+ }
+
+ @Test
+ public void utf8NamesSupportedOnWriting() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
+ String lettuceIsHealthyArmenian = "\u0533\u0561\u0566\u0561\u0580\u0020\u0561\u057C"
+ + "\u0578\u0572\u057B";
+
+ try (ZFile zip = new ZFile(zipFile)) {
+ zip.add(lettuceIsHealthyArmenian, new ByteArrayInputStream(new byte[]{0}));
+ }
+
+ try (ZFile zip2 = new ZFile(zipFile)) {
+ assertEquals(1, zip2.entries().size());
+ StoredEntry entry = zip2.entries().iterator().next();
+ assertEquals(lettuceIsHealthyArmenian, entry.getCentralDirectoryHeader().getName());
+ }
+ }
+
+ @Test
+ public void zipMemoryUsageIsZeroAfterClose() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
+
+ ZFileOptions options = new ZFileOptions();
+ long used;
+ try (ZFile zip = new ZFile(zipFile, options)) {
+
+ assertEquals(0, options.getTracker().getBytesUsed());
+ assertEquals(0, options.getTracker().getMaxBytesUsed());
+
+ zip.add("Blah", new ByteArrayInputStream(new byte[500]));
+ used = options.getTracker().getBytesUsed();
+ assertTrue(used > 500);
+ assertEquals(used, options.getTracker().getMaxBytesUsed());
+ }
+
+ assertEquals(0, options.getTracker().getBytesUsed());
+ assertEquals(used, options.getTracker().getMaxBytesUsed());
+ }
+
+ @Test
+ public void unusedZipAreasAreClearedOnWrite() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
+ ZFileOptions options = new ZFileOptions();
+ options.setAlignmentRule(AlignmentRules.constantForSuffix(".txt", 1000));
+ try (ZFile zf = new ZFile(zipFile, options)) {
+ zf.add("test1.txt", new ByteArrayInputStream(new byte[]{1}), false);
+ }
+
+ /*
+ * Write dummy data in some unused portion of the file.
+ */
+ try (RandomAccessFile raf = new RandomAccessFile(zipFile, "rw")) {
+
+ raf.seek(500);
+ byte[] dummyData = "Dummy".getBytes(Charsets.US_ASCII);
+ raf.write(dummyData);
+ }
+
+ try (ZFile zf = new ZFile(zipFile)) {
+ zf.touch();
+ }
+
+ try (RandomAccessFile raf = new RandomAccessFile(zipFile, "r")) {
+
+ /*
+ * test1.txt won't take more than 200 bytes. Additionally, the header for
+ */
+ byte[] data = new byte[900];
+ RandomAccessFileUtils.fullyRead(raf, data);
+
+ byte[] zeroData = new byte[data.length];
+ assertArrayEquals(zeroData, data);
+ }
+ }
+
+ @Test
+ public void deferredCompression() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
+
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+
+ ZFileOptions options = new ZFileOptions();
+ boolean[] done = new boolean[1];
+ options.setCompressor(new DeflateExecutionCompressor(executor, options.getTracker(),
+ Deflater.BEST_COMPRESSION) {
+ @NonNull
+ @Override
+ protected CompressionResult immediateCompress(@NonNull CloseableByteSource source)
+ throws Exception {
+ Thread.sleep(500);
+ CompressionResult cr = super.immediateCompress(source);
+ done[0] = true;
+ return cr;
+ }
+ });
+
+ try (ZFile zip = new ZFile(zipFile, options)) {
+ byte sequences = 100;
+ int seqCount = 1000;
+ byte[] compressableData = new byte[sequences * seqCount];
+ for (byte i = 0; i < sequences; i++) {
+ for (int j = 0; j < seqCount; j++) {
+ compressableData[i * seqCount + j] = i;
+ }
+ }
+
+ zip.add("compressedFile", new ByteArrayInputStream(compressableData));
+ assertFalse(done[0]);
+
+ /*
+ * Even before closing, eventually all the stream will be read.
+ */
+ long tooLong = System.currentTimeMillis() + 10000;
+ while (!done[0] && System.currentTimeMillis() < tooLong) {
+ Thread.sleep(10);
+ }
+
+ assertTrue(done[0]);
+ }
+
+ executor.shutdownNow();
+ }
+
+ @Test
+ public void zipFileWithEocdMarkerInComment() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "x");
+
+ try (Closer closer = Closer.create()) {
+ ZipOutputStream zos = closer.register(
+ new ZipOutputStream(new FileOutputStream(zipFile)));
+ zos.setComment("\u0065\u4b50");
+ zos.putNextEntry(new ZipEntry("foo"));
+ zos.write(new byte[] { 1, 2, 3, 4 });
+ zos.close();
+
+ ZFile zf = closer.register(new ZFile(zipFile));
+ StoredEntry entry = zf.get("foo");
+ assertNotNull(entry);
+ assertEquals(4, entry.getCentralDirectoryHeader().getUncompressedSize());
+ }
+ }
+
+ @Test
+ public void zipFileWithEocdMarkerInFileName() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "x");
+
+ String fname = "tricky-\u0050\u004b\u0005\u0006";
+ byte[] bytes = new byte[] { 1, 2, 3, 4 };
+
+ try (Closer closer = Closer.create()) {
+ ZipOutputStream zos = closer.register(
+ new ZipOutputStream(new FileOutputStream(zipFile)));
+ zos.putNextEntry(new ZipEntry(fname));
+ zos.write(bytes);
+ zos.close();
+
+ ZFile zf = closer.register(new ZFile(zipFile));
+ StoredEntry entry = zf.get(fname);
+ assertNotNull(entry);
+ assertEquals(4, entry.getCentralDirectoryHeader().getUncompressedSize());
+ }
+ }
+
+ @Test
+ public void zipFileWithEocdMarkerInFileContents() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "x");
+
+ byte[] bytes = new byte[] { 0x50, 0x4b, 0x05, 0x06 };
+
+ try (Closer closer = Closer.create()) {
+ ZipOutputStream zos = closer.register(
+ new ZipOutputStream(new FileOutputStream(zipFile)));
+ ZipEntry zipEntry = new ZipEntry("file");
+ zipEntry.setMethod(ZipEntry.STORED);
+ zipEntry.setCompressedSize(4);
+ zipEntry.setSize(4);
+ zipEntry.setCrc(Hashing.crc32().hashBytes(bytes).padToLong());
+ zos.putNextEntry(zipEntry);
+ zos.write(bytes);
+ zos.close();
+
+ ZFile zf = closer.register(new ZFile(zipFile));
+ StoredEntry entry = zf.get("file");
+ assertNotNull(entry);
+ assertEquals(4, entry.getCentralDirectoryHeader().getUncompressedSize());
+ }
+ }
+
+ @Test
+ public void replaceVeryLargeFileWithBiggerInMiddleOfZip() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "x");
+
+ long small1Offset;
+ long small2Offset;
+ ZFileOptions coverOptions = new ZFileOptions();
+ coverOptions.setCoverEmptySpaceUsingExtraField(true);
+ try (ZFile zf = new ZFile(zipFile, coverOptions)) {
+ zf.add("small1", new ByteArrayInputStream(new byte[] { 0, 1 }));
+ }
+
+ try (ZFile zf = new ZFile(zipFile, coverOptions)) {
+ zf.add("verybig", new ByteArrayInputStream(new byte[100_000]), false);
+ }
+
+ try (ZFile zf = new ZFile(zipFile, coverOptions)) {
+ zf.add("small2", new ByteArrayInputStream(new byte[] { 0, 1 }));
+ }
+
+ try (ZFile zf = new ZFile(zipFile, coverOptions)) {
+ StoredEntry se = zf.get("small1");
+ assertNotNull(se);
+ small1Offset = se.getCentralDirectoryHeader().getOffset();
+
+ se = zf.get("small2");
+ assertNotNull(se);
+ small2Offset = se.getCentralDirectoryHeader().getOffset();
+
+ se = zf.get("verybig");
+ assertNotNull(se);
+ se.delete();
+
+ zf.add("evenbigger", new ByteArrayInputStream(new byte[110_000]), false);
+ }
+
+ try (ZFile zf = new ZFile(zipFile, coverOptions)) {
+ StoredEntry se = zf.get("small1");
+ assertNotNull(se);
+ assertEquals(se.getCentralDirectoryHeader().getOffset(), small1Offset);
+
+ se = zf.get("small2");
+ assertNotNull(se);
+ assertNotEquals(se.getCentralDirectoryHeader().getOffset(), small2Offset);
+ }
+ }
+
+ @Test
+ public void regressionRepackingDoesNotFail() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "x");
+
+ ZFileOptions coverOptions = new ZFileOptions();
+ coverOptions.setCoverEmptySpaceUsingExtraField(true);
+ try (ZFile zf = new ZFile(zipFile, coverOptions)) {
+ zf.add("small_1", new ByteArrayInputStream(new byte[] { 0, 1 }));
+ zf.add("very_big", new ByteArrayInputStream(new byte[100_000]), false);
+ zf.add("small_2", new ByteArrayInputStream(new byte[] { 0, 1 }));
+ zf.add("big", new ByteArrayInputStream(new byte[10_000]), false);
+ zf.add("small_3", new ByteArrayInputStream(new byte[] { 0, 1 }));
+ }
+
+ /*
+ * Regression we're covering is that small_2 cannot be extended to cover up for the space
+ * taken by very_big and needs to be repositioned. However, the algorithm to reposition
+ * will put it in the best-fitting block, which is the one in "big", failing to actually
+ * move it backwards in the file.
+ */
+ try (ZFile zf = new ZFile(zipFile, coverOptions)) {
+ StoredEntry se = zf.get("big");
+ assertNotNull(se);
+ se.delete();
+
+ se = zf.get("very_big");
+ assertNotNull(se);
+ se.delete();
+ }
+ }
+
+ @Test
+ public void cannotAddMoreThan0x7fffExtraField() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
+
+ ZFileOptions zfo = new ZFileOptions();
+ zfo.setCoverEmptySpaceUsingExtraField(true);
+
+ /*
+ * Create a zip file with:
+ *
+ * [small file][large file with exactly 0x8000 bytes][small file 2]
+ */
+ long smallFile1Offset;
+ long smallFile2Offset;
+ long largeFileOffset;
+ String largeFileName = "Large file";
+ try (ZFile zf = new ZFile(zipFile, zfo)) {
+ zf.add("Small file", new ByteArrayInputStream(new byte[] { 0, 1 }));
+
+ int largeFileTotalSize = 0x8000;
+ int largeFileContentsSize =
+ largeFileTotalSize
+ - ZFileTestConstants.LOCAL_HEADER_SIZE
+ - largeFileName.length();
+
+ zf.add(largeFileName, new ByteArrayInputStream(new byte[largeFileContentsSize]), false);
+ zf.add("Small file 2", new ByteArrayInputStream(new byte[] { 0, 1 }));
+
+ zf.update();
+
+ StoredEntry sfEntry = zf.get("Small file");
+ assertNotNull(sfEntry);
+ smallFile1Offset = sfEntry.getCentralDirectoryHeader().getOffset();
+ assertEquals(0, smallFile1Offset);
+
+ StoredEntry lfEntry = zf.get(largeFileName);
+ assertNotNull(lfEntry);
+ largeFileOffset = lfEntry.getCentralDirectoryHeader().getOffset();
+
+ StoredEntry sf2Entry = zf.get("Small file 2");
+ assertNotNull(sf2Entry);
+ smallFile2Offset = sf2Entry.getCentralDirectoryHeader().getOffset();
+
+ assertEquals(largeFileTotalSize, smallFile2Offset - largeFileOffset);
+ }
+
+ /*
+ * Remove the large file from the zip file and check that small file 2 has been moved, but
+ * no extra field has been added.
+ */
+ try (ZFile zf = new ZFile(zipFile, zfo)) {
+ StoredEntry lfEntry = zf.get(largeFileName);
+ assertNotNull(lfEntry);
+ lfEntry.delete();
+
+ zf.update();
+
+ StoredEntry sfEntry = zf.get("Small file");
+ assertNotNull(sfEntry);
+ smallFile1Offset = sfEntry.getCentralDirectoryHeader().getOffset();
+ assertEquals(0, smallFile1Offset);
+
+ StoredEntry sf2Entry = zf.get("Small file 2");
+ assertNotNull(sf2Entry);
+ long newSmallFile2Offset = sf2Entry.getCentralDirectoryHeader().getOffset();
+ assertEquals(largeFileOffset, newSmallFile2Offset);
+
+ assertEquals(0, sf2Entry.getLocalExtra().size());
+ }
+ }
+
+ @Test
+ public void canAddMoreThan0x7fffExtraField() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
+
+ ZFileOptions zfo = new ZFileOptions();
+ zfo.setCoverEmptySpaceUsingExtraField(true);
+
+ /*
+ * Create a zip file with:
+ *
+ * [small file][large file with exactly 0x7fff bytes][small file 2]
+ */
+ long smallFile1Offset;
+ long smallFile2Offset;
+ long largeFileOffset;
+ String largeFileName = "Large file";
+ int largeFileTotalSize = 0x7fff;
+ try (ZFile zf = new ZFile(zipFile, zfo)) {
+ zf.add("Small file", new ByteArrayInputStream(new byte[] { 0, 1 }));
+
+ int largeFileContentsSize =
+ largeFileTotalSize
+ - ZFileTestConstants.LOCAL_HEADER_SIZE
+ - largeFileName.length();
+
+ zf.add(largeFileName, new ByteArrayInputStream(new byte[largeFileContentsSize]), false);
+ zf.add("Small file 2", new ByteArrayInputStream(new byte[] { 0, 1 }));
+
+ zf.update();
+
+ StoredEntry sfEntry = zf.get("Small file");
+ assertNotNull(sfEntry);
+ smallFile1Offset = sfEntry.getCentralDirectoryHeader().getOffset();
+ assertEquals(0, smallFile1Offset);
+
+ StoredEntry lfEntry = zf.get(largeFileName);
+ assertNotNull(lfEntry);
+ largeFileOffset = lfEntry.getCentralDirectoryHeader().getOffset();
+
+ StoredEntry sf2Entry = zf.get("Small file 2");
+ assertNotNull(sf2Entry);
+ smallFile2Offset = sf2Entry.getCentralDirectoryHeader().getOffset();
+
+ assertEquals(largeFileTotalSize, smallFile2Offset - largeFileOffset);
+ }
+
+ /*
+ * Remove the large file from the zip file and check that small file 2 has been moved back
+ * but with 0x7fff extra space added.
+ */
+ try (ZFile zf = new ZFile(zipFile, zfo)) {
+ StoredEntry lfEntry = zf.get(largeFileName);
+ assertNotNull(lfEntry);
+ lfEntry.delete();
+
+ zf.update();
+
+ StoredEntry sfEntry = zf.get("Small file");
+ assertNotNull(sfEntry);
+ smallFile1Offset = sfEntry.getCentralDirectoryHeader().getOffset();
+ assertEquals(0, smallFile1Offset);
+
+ StoredEntry sf2Entry = zf.get("Small file 2");
+ assertNotNull(sf2Entry);
+ long newSmallFile2Offset = sf2Entry.getCentralDirectoryHeader().getOffset();
+
+ assertEquals(largeFileOffset, newSmallFile2Offset);
+ assertEquals(largeFileTotalSize, sf2Entry.getLocalExtra().size());
+ }
+ }
+
+ @Test
+ public void detectIncorrectCRC32InLocalHeader() throws Exception {
+ File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
+
+ /*
+ * Zip files created by ZFile never have data descriptors so we need to create one using
+ * java's zip.
+ */
+ try (
+ FileOutputStream fos = new FileOutputStream(zipFile);
+ ZipOutputStream zos = new ZipOutputStream(fos)) {
+ ZipEntry ze = new ZipEntry("foo");
+ zos.putNextEntry(ze);
+ byte[] randomBytes = new byte[512];
+ new Random().nextBytes(randomBytes);
+ zos.write(randomBytes);
+ }
+
+ /*
+ * Open the zip file and compute where the local header CRC32 is.
+ */
+ try (ZFile zf = new ZFile(zipFile)) {
+ StoredEntry se = zf.get("foo");
+ assertNotNull(se);
+ long cdOffset = zf.getCentralDirectoryOffset();
+
+ /*
+ * Twelve bytes from the CD offset, we have the start of the CRC32 of the zip entry.
+ * Corrupt it.
+ */
+ byte[] crc = new byte[4];
+ zf.directFullyRead(cdOffset - 12, crc);
+ crc[0]++;
+ zf.directWrite(cdOffset - 12, crc);
+ }
+
+ /*
+ * Now open the zip file and it should fail.
+ */
+ try {
+ new ZFile(zipFile);
+ fail();
+ } catch (IOException e) {
+ /*
+ * We should be complaining about the CRC32 somewhere...
+ */
+ boolean foundCrc32Complain = false;
+
+ assertTrue(
+ Throwables.getCausalChain(e).stream()
+ .map(Throwable::getMessage)
+ .anyMatch(s -> s.contains("CRC32")));
+ }
+
+ /*
+ * But opening with data validation skip should work.
+ */
+ ZFileOptions options = new ZFileOptions();
+ options.setSkipDataDescriptionValidation(true);
+ try (ZFile zf = new ZFile(zipFile, options)) {
+ /*
+ * Nothing to do.
+ */
+ }
+ }
+}
diff --git a/src/test/java/com/android/apkzlib/zip/ZFileTestConstants.java b/src/test/java/com/android/apkzlib/zip/ZFileTestConstants.java
new file mode 100644
index 0000000..fbf5739
--- /dev/null
+++ b/src/test/java/com/android/apkzlib/zip/ZFileTestConstants.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apkzlib.zip;
+
+/**
+ * Constants used in tests.
+ */
+public interface ZFileTestConstants {
+
+ /**
+ * Number of bytes in a zip entry local header, not considering name and comment.
+ */
+ int LOCAL_HEADER_SIZE = 30;
+
+ /**
+ * Number of bytes in a zip central directory entry, not considering name and comment.
+ */
+ int CENTRAL_DIRECTORY_ENTRY_SIZE = 46;
+
+ /**
+ * Number of bytes in an EOCD without comment.
+ */
+ int EOCD_SIZE = 22;
+}
diff --git a/src/test/java/com/android/apkzlib/zip/ZipMergeTest.java b/src/test/java/com/android/apkzlib/zip/ZipMergeTest.java
new file mode 100644
index 0000000..aa3e787
--- /dev/null
+++ b/src/test/java/com/android/apkzlib/zip/ZipMergeTest.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apkzlib.zip;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import com.android.apkzlib.utils.CachedFileContents;
+import com.google.common.base.Charsets;
+import com.google.common.hash.Hashing;
+import com.google.common.io.ByteStreams;
+import com.google.common.io.Closer;
+import com.google.common.io.Files;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+public class ZipMergeTest {
+ @Rule
+ public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+
+ @Test
+ public void mergeZip() throws Exception {
+ File aZip = ZipTestUtils.cloneRsrc("simple-zip.zip", mTemporaryFolder, "a.zip");
+
+ CachedFileContents<Object> changeDetector;
+ File merged = new File(mTemporaryFolder.getRoot(), "r.zip");
+ try (ZFile mergedZf = new ZFile(merged)) {
+ mergedZf.mergeFrom(new ZFile(aZip), f -> false);
+ mergedZf.close();
+
+ assertEquals(3, mergedZf.entries().size());
+
+ StoredEntry e0 = mergedZf.get("dir/");
+ assertNotNull(e0);
+ assertSame(StoredEntryType.DIRECTORY, e0.getType());
+
+ StoredEntry e1 = mergedZf.get("dir/inside");
+ assertNotNull(e1);
+ assertSame(StoredEntryType.FILE, e1.getType());
+ ByteArrayOutputStream e1BytesOut = new ByteArrayOutputStream();
+ ByteStreams.copy(e1.open(), e1BytesOut);
+ byte e1Bytes[] = e1BytesOut.toByteArray();
+ String e1Txt = new String(e1Bytes, Charsets.US_ASCII);
+ assertEquals("inside", e1Txt);
+
+ StoredEntry e2 = mergedZf.get("file.txt");
+ assertNotNull(e2);
+ assertSame(StoredEntryType.FILE, e2.getType());
+ ByteArrayOutputStream e2BytesOut = new ByteArrayOutputStream();
+ ByteStreams.copy(e2.open(), e2BytesOut);
+ byte e2Bytes[] = e2BytesOut.toByteArray();
+ String e2Txt = new String(e2Bytes, Charsets.US_ASCII);
+ assertEquals("file with more text to allow deflating to be useful", e2Txt);
+
+ changeDetector = new CachedFileContents<>(merged);
+ changeDetector.closed(null);
+
+ /*
+ * Clone aZip into bZip and merge. Should have no effect on the final zip file.
+ */
+ File bZip = ZipTestUtils.cloneRsrc("simple-zip.zip", mTemporaryFolder, "b.zip");
+
+ mergedZf.mergeFrom(new ZFile(bZip), f -> false);
+ }
+
+ assertTrue(changeDetector.isValid());
+ }
+
+ @Test
+ public void mergeZipWithDeferredCrc() throws Exception {
+ File foo = mTemporaryFolder.newFile("foo");
+
+ byte[] wBytes = Files.toByteArray(ZipTestUtils.rsrcFile("text-files/wikipedia.html"));
+
+ try (ZipOutputStream fooOut = new ZipOutputStream(new FileOutputStream(foo))) {
+ fooOut.putNextEntry(new ZipEntry("w"));
+ fooOut.write(wBytes);
+ }
+
+ try (Closer closer = Closer.create()) {
+ ZFile fooZf = closer.register(new ZFile(foo));
+ StoredEntry wStored = fooZf.get("w");
+ assertNotNull(wStored);
+ assertTrue(wStored.getCentralDirectoryHeader().getGpBit().isDeferredCrc());
+ assertEquals(CompressionMethod.DEFLATE,
+ wStored.getCentralDirectoryHeader().getCompressionInfoWithWait().getMethod());
+
+ ZFile merged = closer.register(new ZFile(new File(mTemporaryFolder.getRoot(), "bar")));
+ merged.mergeFrom(fooZf, f -> false);
+ merged.update();
+
+ StoredEntry wmStored = merged.get("w");
+ assertNotNull(wmStored);
+ assertFalse(wmStored.getCentralDirectoryHeader().getGpBit().isDeferredCrc());
+ assertEquals(CompressionMethod.DEFLATE,
+ wmStored.getCentralDirectoryHeader().getCompressionInfoWithWait().getMethod());
+ }
+ }
+
+ @Test
+ public void mergeZipKeepsDeflatedAndStored() throws Exception {
+ File foo = mTemporaryFolder.newFile("foo");
+
+ byte[] wBytes = Files.toByteArray(ZipTestUtils.rsrcFile("text-files/wikipedia.html"));
+ byte[] lBytes = Files.toByteArray(ZipTestUtils.rsrcFile("images/lena.png"));
+
+ try (ZipOutputStream fooOut = new ZipOutputStream(new FileOutputStream(foo))) {
+ fooOut.putNextEntry(new ZipEntry("w"));
+ fooOut.write(wBytes);
+ ZipEntry le = new ZipEntry("l");
+ le.setMethod(ZipEntry.STORED);
+ le.setSize(lBytes.length);
+ le.setCrc(Hashing.crc32().hashBytes(lBytes).padToLong());
+ fooOut.putNextEntry(le);
+ fooOut.write(lBytes);
+ }
+
+ try (Closer closer = Closer.create()) {
+ ZFile fooZf = closer.register(new ZFile(foo));
+ StoredEntry wStored = fooZf.get("w");
+ assertNotNull(wStored);
+ assertEquals(CompressionMethod.DEFLATE,
+ wStored.getCentralDirectoryHeader().getCompressionInfoWithWait().getMethod());
+ StoredEntry lStored = fooZf.get("l");
+ assertNotNull(lStored);
+ assertEquals(CompressionMethod.STORE,
+ lStored.getCentralDirectoryHeader().getCompressionInfoWithWait().getMethod());
+
+ ZFile merged = closer.register(new ZFile(new File(mTemporaryFolder.getRoot(), "bar")));
+ merged.mergeFrom(fooZf, f -> false);
+ merged.update();
+
+ StoredEntry wmStored = merged.get("w");
+ assertNotNull(wmStored);
+ assertFalse(wmStored.getCentralDirectoryHeader().getGpBit().isDeferredCrc());
+ assertEquals(CompressionMethod.DEFLATE,
+ wmStored.getCentralDirectoryHeader().getCompressionInfoWithWait().getMethod());
+ assertArrayEquals(wBytes, wmStored.read());
+
+ StoredEntry lmStored = merged.get("l");
+ assertNotNull(lmStored);
+ assertEquals(CompressionMethod.STORE,
+ lmStored.getCentralDirectoryHeader().getCompressionInfoWithWait().getMethod());
+ assertArrayEquals(lBytes, lmStored.read());
+ }
+ }
+
+ @Test
+ public void mergeZipWithSorting() throws Exception {
+ File foo = mTemporaryFolder.newFile("foo");
+
+ byte[] wBytes = Files.toByteArray(ZipTestUtils.rsrcFile("text-files/wikipedia.html"));
+ byte[] lBytes = Files.toByteArray(ZipTestUtils.rsrcFile("images/lena.png"));
+
+ try (ZipOutputStream fooOut = new ZipOutputStream(new FileOutputStream(foo))) {
+ fooOut.putNextEntry(new ZipEntry("w"));
+ fooOut.write(wBytes);
+ ZipEntry le = new ZipEntry("l");
+ le.setMethod(ZipEntry.STORED);
+ le.setSize(lBytes.length);
+ le.setCrc(Hashing.crc32().hashBytes(lBytes).padToLong());
+ fooOut.putNextEntry(le);
+ fooOut.write(lBytes);
+ }
+
+ try (
+ ZFile fooZf = new ZFile(foo);
+ ZFile merged = new ZFile(new File(mTemporaryFolder.getRoot(), "bar"))) {
+ merged.mergeFrom(fooZf, f -> false);
+ merged.sortZipContents();
+ merged.update();
+
+ StoredEntry wmStored = merged.get("w");
+ assertNotNull(wmStored);
+ assertArrayEquals(wBytes, wmStored.read());
+
+ StoredEntry lmStored = merged.get("l");
+ assertNotNull(lmStored);
+ assertArrayEquals(lBytes, lmStored.read());
+ }
+ }
+}
diff --git a/src/test/java/com/android/apkzlib/zip/ZipTestUtils.java b/src/test/java/com/android/apkzlib/zip/ZipTestUtils.java
new file mode 100644
index 0000000..d9c6aa8
--- /dev/null
+++ b/src/test/java/com/android/apkzlib/zip/ZipTestUtils.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apkzlib.zip;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.android.annotations.NonNull;
+import com.android.apkzlib.utils.ApkZFileTestUtils;
+import com.google.common.io.Files;
+
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * Utility method for zip tests.
+ */
+class ZipTestUtils {
+
+ /**
+ * Obtains the file with a resource with the given name. This is a file that lays in
+ * the packaging subdirectory of test resources.
+ *
+ * @param rsrcName the resource name inside packaging resource folder
+ * @return the resource file, guaranteed to exist
+ */
+ @NonNull
+ static File rsrcFile(@NonNull String rsrcName) {
+ File packagingRoot = ApkZFileTestUtils.getResource("/testData/packaging");
+ String rsrcPath = packagingRoot.getAbsolutePath() + "/" + rsrcName;
+ File rsrcFile = new File(rsrcPath);
+ assertTrue(rsrcFile.isFile());
+ return rsrcFile;
+ }
+
+ /**
+ * Clones a resource to a temporary folder. Generally, resources do not need to be cloned to
+ * be used. However, in code where there is danger of changing resource files and corrupting
+ * the source directory, cloning should be done before accessing the resources.
+ *
+ * @param rsrcName the resource name
+ * @param folder the temporary folder
+ * @return the file that was created with the resource
+ * @throws IOException failed to clone the resource
+ */
+ static File cloneRsrc(@NonNull String rsrcName, @NonNull TemporaryFolder folder)
+ throws IOException {
+ String cloneName;
+ if (rsrcName.contains("/")) {
+ cloneName = rsrcName.substring(rsrcName.lastIndexOf('/') + 1);
+ } else {
+ cloneName = rsrcName;
+ }
+
+ return cloneRsrc(rsrcName, folder, cloneName);
+ }
+
+ /**
+ * Clones a resource to a temporary folder. Generally, resources do not need to be cloned to
+ * be used. However, in code where there is danger of changing resource files and corrupting
+ * the source directory, cloning should be done before accessing the resources.
+ *
+ * @param rsrcName the resource name
+ * @param folder the temporary folder
+ * @param cloneName the name of the cloned resource that will be created inside the temporary
+ * folder
+ * @return the file that was created with the resource
+ * @throws IOException failed to clone the resource
+ */
+ static File cloneRsrc(@NonNull String rsrcName, @NonNull TemporaryFolder folder,
+ @NonNull String cloneName) throws IOException {
+ File result = new File(folder.getRoot(), cloneName);
+ assertFalse(result.exists());
+
+ Files.copy(rsrcFile(rsrcName), result);
+ return result;
+ }
+}
diff --git a/src/test/java/com/android/apkzlib/zip/ZipToolsTest.java b/src/test/java/com/android/apkzlib/zip/ZipToolsTest.java
new file mode 100644
index 0000000..e7f837e
--- /dev/null
+++ b/src/test/java/com/android/apkzlib/zip/ZipToolsTest.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apkzlib.zip;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.google.common.base.Charsets;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.io.ByteStreams;
+import com.google.common.io.Files;
+
+import org.junit.Assume;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+@RunWith(Parameterized.class)
+public class ZipToolsTest {
+
+ @Parameterized.Parameter(0)
+ @Nullable
+ public String mZipFile;
+
+ @Parameterized.Parameter(1)
+ @Nullable
+ public List<String> mUnzipCommand;
+
+ @Parameterized.Parameter(2)
+ @Nullable
+ public String mUnzipLineRegex;
+
+ @Parameterized.Parameter(3)
+ public boolean mToolStoresDirectories;
+
+ @Parameterized.Parameter(4)
+ public String mName;
+
+ @Rule
+ @NonNull
+ public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+
+ @Parameterized.Parameters(name = "{4} {index}")
+ public static Iterable<Object[]> getConfigurations() {
+ return Arrays.asList(new Object[][] {
+ {
+ "linux-zip.zip",
+ ImmutableList.of("/usr/bin/unzip", "-v"),
+ "^\\s*(?<size>\\d+)\\s+(?:Stored|Defl:N).*\\s(?<name>\\S+)\\S*$",
+ true,
+ "Linux Zip"
+ },
+ {
+ "windows-7zip.zip",
+ ImmutableList.of("c:\\Program Files\\7-Zip\\7z.exe", "l"),
+ "^(?:\\S+\\s+){3}(?<size>\\d+)\\s+\\d+\\s+(?<name>\\S+)\\s*$",
+ true,
+ "Windows 7-Zip"
+ },
+ {
+ "windows-cf.zip",
+ ImmutableList.of(
+ "Cannot use compressed folders from cmd line to list zip contents"),
+ "",
+ false,
+ "Windows Compressed Folders"
+ }
+ });
+ }
+
+ private File cloneZipFile() throws Exception {
+ File zfile = mTemporaryFolder.newFile("file.zip");
+ Files.copy(ZipTestUtils.rsrcFile(mZipFile), zfile);
+ return zfile;
+ }
+
+ private static void assertFileInZip(@NonNull ZFile zfile, @NonNull String name) throws Exception {
+ StoredEntry root = zfile.get(name);
+ assertNotNull(root);
+
+ InputStream is = root.open();
+ byte[] inZipData = ByteStreams.toByteArray(is);
+ is.close();
+
+ byte[] inFileData = Files.toByteArray(ZipTestUtils.rsrcFile(name));
+ assertArrayEquals(inFileData, inZipData);
+ }
+
+ @Test
+ public void zfileReadsZipFile() throws Exception {
+ try (ZFile zf = new ZFile(cloneZipFile())) {
+ if (mToolStoresDirectories) {
+ assertEquals(6, zf.entries().size());
+ } else {
+ assertEquals(4, zf.entries().size());
+ }
+
+ assertFileInZip(zf, "root");
+ assertFileInZip(zf, "images/lena.png");
+ assertFileInZip(zf, "text-files/rfc2460.txt");
+ assertFileInZip(zf, "text-files/wikipedia.html");
+ }
+ }
+
+ @Test
+ public void toolReadsZfFile() throws Exception {
+ testReadZFile(false);
+ }
+
+ @Test
+ public void toolReadsAlignedZfFile() throws Exception {
+ testReadZFile(true);
+ }
+
+ private void testReadZFile(boolean align) throws Exception {
+ String unzipcmd = mUnzipCommand.get(0);
+ Assume.assumeTrue(new File(unzipcmd).canExecute());
+
+ ZFileOptions options = new ZFileOptions();
+ if (align) {
+ options.setAlignmentRule(AlignmentRules.constant(500));
+ }
+
+ File zfile = new File (mTemporaryFolder.getRoot(), "zfile.zip");
+ try (ZFile zf = new ZFile(zfile, options)) {
+ zf.add("root", new FileInputStream(ZipTestUtils.rsrcFile("root")));
+ zf.add("images/", new ByteArrayInputStream(new byte[0]));
+ zf.add("images/lena.png", new FileInputStream(ZipTestUtils.rsrcFile("images/lena.png")));
+ zf.add("text-files/", new ByteArrayInputStream(new byte[0]));
+ zf.add("text-files/rfc2460.txt", new FileInputStream(
+ ZipTestUtils.rsrcFile("text-files/rfc2460.txt")));
+ zf.add("text-files/wikipedia.html",
+ new FileInputStream(ZipTestUtils.rsrcFile("text-files/wikipedia.html")));
+ }
+
+ List<String> command = Lists.newArrayList(mUnzipCommand);
+ command.add(zfile.getAbsolutePath());
+ ProcessBuilder pb = new ProcessBuilder(command);
+ Process proc = pb.start();
+ InputStream is = proc.getInputStream();
+ byte output[] = ByteStreams.toByteArray(is);
+ String text = new String(output, Charsets.US_ASCII);
+ String lines[] = text.split("\n");
+ Map<String, Integer> sizes = Maps.newHashMap();
+ for (String l : lines) {
+ Matcher m = Pattern.compile(mUnzipLineRegex).matcher(l);
+ if (m.matches()) {
+ String sizeTxt = m.group("size");
+ int size = Integer.parseInt(sizeTxt);
+ String name = m.group("name");
+ sizes.put(name, size);
+ }
+ }
+
+ assertEquals(6, sizes.size());
+
+ /*
+ * The "images" directory may show up as "images" or "images/".
+ */
+ String imagesKey = "images/";
+ if (!sizes.containsKey(imagesKey)) {
+ imagesKey = "images";
+ }
+
+ assertTrue(sizes.containsKey(imagesKey));
+ assertEquals(0, sizes.get(imagesKey).intValue());
+
+ assertSize(new String[] { "images/", "images" }, 0, sizes);
+ assertSize(new String[] { "text-files/", "text-files"}, 0, sizes);
+ assertSize(new String[] { "root" }, ZipTestUtils.rsrcFile("root").length(), sizes);
+ assertSize(new String[] { "images/lena.png", "images\\lena.png" },
+ ZipTestUtils.rsrcFile("images/lena.png").length(), sizes);
+ assertSize(new String[] { "text-files/rfc2460.txt", "text-files\\rfc2460.txt" },
+ ZipTestUtils.rsrcFile("text-files/rfc2460.txt").length(), sizes);
+ assertSize(new String[] { "text-files/wikipedia.html", "text-files\\wikipedia.html" },
+ ZipTestUtils.rsrcFile("text-files/wikipedia.html").length(), sizes);
+ }
+
+ private static void assertSize(String[] names, long size, Map<String, Integer> sizes) {
+ for (String n : names) {
+ if (sizes.containsKey(n)) {
+ assertEquals((long) sizes.get(n), size);
+ return;
+ }
+ }
+
+ fail();
+ }
+}
diff --git a/src/test/java/com/android/apkzlib/zip/compress/MultiCompressorTest.java b/src/test/java/com/android/apkzlib/zip/compress/MultiCompressorTest.java
new file mode 100644
index 0000000..97903b2
--- /dev/null
+++ b/src/test/java/com/android/apkzlib/zip/compress/MultiCompressorTest.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apkzlib.zip.compress;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import com.android.apkzlib.zip.CentralDirectoryHeaderCompressInfo;
+import com.android.apkzlib.zip.CompressionMethod;
+import com.android.apkzlib.zip.StoredEntry;
+import com.android.apkzlib.zip.ZFile;
+import com.android.apkzlib.zip.ZFileOptions;
+import com.android.apkzlib.utils.ApkZFileTestUtils;
+import com.google.common.io.Files;
+import com.google.common.util.concurrent.MoreExecutors;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.util.zip.Deflater;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+public class MultiCompressorTest {
+ @Rule
+ public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+
+ private static byte[] getCompressibleData() throws Exception {
+ File textFiles = ApkZFileTestUtils.getResource("/testData/packaging/text-files");
+ assertTrue(textFiles.isDirectory());
+ File wikipediaFile = new File(textFiles, "wikipedia.html");
+ assertTrue(wikipediaFile.isFile());
+ return Files.asByteSource(wikipediaFile).read();
+ }
+
+ private static byte[] compress(byte[] data, int level) throws Exception {
+ Deflater deflater = new Deflater(level);
+ deflater.setInput(data);
+ deflater.finish();
+
+ byte[] resultAll = new byte[data.length * 2];
+ int resultAllCount = deflater.deflate(resultAll);
+
+ byte[] result = new byte[resultAllCount];
+ System.arraycopy(resultAll, 0, result, 0, resultAllCount);
+ return result;
+ }
+
+ @Test
+ public void storeIsBest() throws Exception {
+ File zip = new File(mTemporaryFolder.getRoot(), "test.zip");
+
+ try (ZFile zf = new ZFile(zip)) {
+ zf.add("file", new ByteArrayInputStream(new byte[0]));
+ StoredEntry entry = zf.get("file");
+ assertNotNull(entry);
+
+ CentralDirectoryHeaderCompressInfo ci =
+ entry.getCentralDirectoryHeader().getCompressionInfoWithWait();
+
+ assertEquals(0, ci.getCompressedSize());
+ assertEquals(CompressionMethod.STORE, ci.getMethod());
+ }
+ }
+
+ @Test
+ public void sameCompressionResultButBetterThanStore() throws Exception {
+ File zip = new File(mTemporaryFolder.getRoot(), "test.zip");
+
+ byte[] data = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
+
+ try (ZFile zf = new ZFile(zip)) {
+ zf.add("file", new ByteArrayInputStream(data));
+ StoredEntry entry = zf.get("file");
+ assertNotNull(entry);
+
+ CentralDirectoryHeaderCompressInfo ci =
+ entry.getCentralDirectoryHeader().getCompressionInfoWithWait();
+
+ assertEquals(CompressionMethod.DEFLATE, ci.getMethod());
+ assertTrue(ci.getCompressedSize() < data.length);
+ }
+ }
+
+ @Test
+ public void bestBetterThanDefault() throws Exception {
+ byte[] data = getCompressibleData();
+ int bestSize = compress(data, Deflater.BEST_COMPRESSION).length;
+ int defaultSize = compress(data, Deflater.DEFAULT_COMPRESSION).length;
+
+ double ratio = bestSize / (double) defaultSize;
+ assertTrue(ratio < 1.0);
+
+ File defaultFile = new File(mTemporaryFolder.getRoot(), "default.zip");
+ File resultFile = new File(mTemporaryFolder.getRoot(), "result.zip");
+
+ ZFileOptions resultOptions = new ZFileOptions();
+ resultOptions.setCompressor(new BestAndDefaultDeflateExecutorCompressor(
+ MoreExecutors.sameThreadExecutor(), resultOptions.getTracker(), ratio + 0.001));
+
+ try (
+ ZFile defaultZFile = new ZFile(defaultFile);
+ ZFile resultZFile = new ZFile(resultFile, resultOptions)) {
+ defaultZFile.add("wikipedia.html", new ByteArrayInputStream(data));
+ resultZFile.add("wikipedia.html", new ByteArrayInputStream(data));
+ }
+
+ long defaultFileSize = defaultFile.length();
+ long resultFileSize = resultFile.length();
+
+ assertTrue(resultFileSize < defaultFileSize);
+ }
+
+ @Test
+ public void bestBetterThanDefaultButNotEnough() throws Exception {
+ byte[] data = getCompressibleData();
+ int bestSize = compress(data, Deflater.BEST_COMPRESSION).length;
+ int defaultSize = compress(data, Deflater.DEFAULT_COMPRESSION).length;
+
+ double ratio = bestSize / (double) defaultSize;
+ assertTrue(ratio < 1.0);
+
+ File defaultFile = new File(mTemporaryFolder.getRoot(), "default.zip");
+ File resultFile = new File(mTemporaryFolder.getRoot(), "result.zip");
+
+ ZFileOptions resultOptions = new ZFileOptions();
+ resultOptions.setCompressor(new BestAndDefaultDeflateExecutorCompressor(
+ MoreExecutors.sameThreadExecutor(), resultOptions.getTracker(), ratio - 0.001));
+
+ try (
+ ZFile defaultZFile = new ZFile(defaultFile);
+ ZFile resultZFile = new ZFile(resultFile, resultOptions)) {
+ defaultZFile.add("wikipedia.html", new ByteArrayInputStream(data));
+ resultZFile.add("wikipedia.html", new ByteArrayInputStream(data));
+ }
+
+ long defaultFileSize = defaultFile.length();
+ long resultFileSize = resultFile.length();
+
+ assertTrue(resultFileSize == defaultFileSize);
+ }
+}
diff --git a/src/test/java/com/android/apkzlib/zip/utils/LittleEndianUtilsTest.java b/src/test/java/com/android/apkzlib/zip/utils/LittleEndianUtilsTest.java
new file mode 100644
index 0000000..3264290
--- /dev/null
+++ b/src/test/java/com/android/apkzlib/zip/utils/LittleEndianUtilsTest.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apkzlib.zip.utils;
+
+import static junit.framework.TestCase.assertEquals;
+import static org.junit.Assert.assertArrayEquals;
+
+import org.junit.Test;
+
+import java.nio.ByteBuffer;
+import java.util.Random;
+
+public class LittleEndianUtilsTest {
+ @Test
+ public void read2Le() throws Exception {
+ assertEquals(0x0102, LittleEndianUtils.readUnsigned2Le(ByteBuffer.wrap(
+ new byte[] { 2, 1 })));
+ assertEquals(0xfedc, LittleEndianUtils.readUnsigned2Le(ByteBuffer.wrap(
+ new byte[] { (byte) 0xdc, (byte) 0xfe })));
+ }
+
+ @Test
+ public void write2Le() throws Exception {
+ ByteBuffer out = ByteBuffer.allocate(2);
+ LittleEndianUtils.writeUnsigned2Le(out, 0x0102);
+ assertArrayEquals(new byte[] { 2, 1 }, out.array());
+
+ out = ByteBuffer.allocate(2);
+ LittleEndianUtils.writeUnsigned2Le(out, 0xfedc);
+ assertArrayEquals(new byte[] { (byte) 0xdc, (byte) 0xfe }, out.array());
+ }
+
+ @Test
+ public void readWrite2Le() throws Exception {
+ Random r = new Random();
+
+ int range = 0x0000ffff;
+
+ final int COUNT = 1000;
+ int[] data = new int[COUNT];
+ for (int i = 0; i < data.length; i++) {
+ data[i] = r.nextInt(range);
+ }
+
+ ByteBuffer out = ByteBuffer.allocate(COUNT * 2);
+ for (int d : data) {
+ LittleEndianUtils.writeUnsigned2Le(out, d);
+ }
+
+ ByteBuffer in = ByteBuffer.wrap(out.array());
+ for (int i = 0; i < data.length; i++) {
+ assertEquals(data[i], LittleEndianUtils.readUnsigned2Le(in));
+ }
+ }
+
+ @Test
+ public void read4Le() throws Exception {
+ assertEquals(0x01020304, LittleEndianUtils.readUnsigned4Le(ByteBuffer.wrap(
+ new byte[] { 4, 3, 2, 1 })));
+ assertEquals(0xfedcba98L, LittleEndianUtils.readUnsigned4Le(ByteBuffer.wrap(
+ new byte[] { (byte) 0x98, (byte) 0xba, (byte) 0xdc, (byte) 0xfe })));
+ }
+
+ @Test
+ public void write4Le() throws Exception {
+ ByteBuffer out = ByteBuffer.allocate(4);
+ LittleEndianUtils.writeUnsigned4Le(out, 0x01020304);
+ assertArrayEquals(new byte[] { 4, 3, 2, 1 }, out.array());
+
+ out = ByteBuffer.allocate(4);
+ LittleEndianUtils.writeUnsigned4Le(out, 0xfedcba98L);
+ assertArrayEquals(new byte[] { (byte) 0x98, (byte) 0xba, (byte) 0xdc, (byte) 0xfe },
+ out.array());
+ }
+
+ @Test
+ public void readWrite4Le() throws Exception {
+ Random r = new Random();
+
+ final int COUNT = 1000;
+ long[] data = new long[COUNT];
+ for (int i = 0; i < data.length; i++) {
+ do {
+ data[i] = r.nextInt() - (long) Integer.MIN_VALUE;
+ } while (data[i] < 0);
+ }
+
+ ByteBuffer out = ByteBuffer.allocate(COUNT * 4);
+ for (long d : data) {
+ LittleEndianUtils.writeUnsigned4Le(out, d);
+ }
+
+ ByteBuffer in = ByteBuffer.wrap(out.array());
+ for (int i = 0; i < data.length; i++) {
+ assertEquals(data[i], LittleEndianUtils.readUnsigned4Le(in));
+ }
+ }
+}
diff --git a/src/test/java/com/android/apkzlib/zip/utils/MsDosDateTimeUtilsTest.java b/src/test/java/com/android/apkzlib/zip/utils/MsDosDateTimeUtilsTest.java
new file mode 100644
index 0000000..012d587
--- /dev/null
+++ b/src/test/java/com/android/apkzlib/zip/utils/MsDosDateTimeUtilsTest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apkzlib.zip.utils;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
+import java.util.Calendar;
+
+public class MsDosDateTimeUtilsTest {
+ @Test
+ public void packDate() throws Exception {
+ Calendar c = Calendar.getInstance();
+
+ c.set(Calendar.YEAR, 2016);
+ c.set(Calendar.MONTH, 0);
+ c.set(Calendar.DAY_OF_MONTH, 5);
+
+ long time = c.getTime().getTime();
+
+ int packed = MsDosDateTimeUtils.packDate(time);
+
+ // Year = 2016 - 1980 = 36 (0000 0000 0[010 0100])
+ // Month = 1 (0000 0000 0000 [0001])
+ // Day = 5 (0000 0000 000[0 0101])
+ // Packs as 010 0100 | 0001 | 00101
+ // Or 0100 1000 0010 0101
+ // In hex 4 8 2 5
+
+ int expectedDateBits = 0x4825;
+ assertEquals(expectedDateBits, packed);
+ }
+
+ @Test
+ public void packTime() throws Exception {
+ Calendar c = Calendar.getInstance();
+
+ c.set(Calendar.HOUR_OF_DAY, 8);
+ c.set(Calendar.MINUTE, 45);
+ c.set(Calendar.SECOND, 20);
+
+ long time = c.getTime().getTime();
+
+ int packed = MsDosDateTimeUtils.packTime(time);
+
+ // Hour = 8 (0000 0000 000[0 1000])
+ // Minute = 45 (0000 0000 00[10 1101])
+ // Second = 20 / 2 = 10 (0000 0000 000[0 1010])
+ // Pack as 0 1000 | 10 1101 | 0 1010
+ // Or 0100 0101 1010 1010
+ // In hex 4 5 A A
+
+ int expectedTimeBits = 0x45AA;
+ assertEquals(expectedTimeBits, packed);
+ }
+}