diff options
author | Paulo Casanova <pasc@google.com> | 2016-11-14 16:02:08 +0000 |
---|---|---|
committer | Paulo Casanova <pasc@google.com> | 2016-11-16 05:38:00 +0000 |
commit | 5a1ccbecca5fc359bf488e717db310d307c0c9fc (patch) | |
tree | b414a0e3428c16f5cb24b9bff8c89b9a792d8211 /src/test/java/com/android/apkzlib | |
parent | a5c71db7e08ae4e72804a64e470c6d4e816e2ee7 (diff) | |
download | apkzlib-5a1ccbecca5fc359bf488e717db310d307c0c9fc.tar.gz |
Renamed apkzlib packages.
Test: Included
Change-Id: I9cce74b77719003875deaa5a0056e35f2930429e
Diffstat (limited to 'src/test/java/com/android/apkzlib')
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); + } +} |