blob: fa69f5d9710724260ba57f12a0c1c62cc1cf4b69 [file] [log] [blame]
/*
* 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.tools.build.apkzlib.zip;
import static com.android.tools.build.apkzlib.utils.ApkZFileTestUtils.readSegment;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import com.android.tools.build.apkzlib.zip.compress.DeflateExecutionCompressor;
import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource;
import com.android.tools.build.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 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.Locale;
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;
import javax.annotation.Nonnull;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
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());
assertTrue(zf.getCentralDirectoryOffset() > 0);
assertTrue(zf.getEocdOffset() > 0);
}
}
@Test
public void readOnlyV2SignedApkSupport() throws Exception {
File testZip = ZipTestUtils.cloneRsrc("v2-signed.apk", mTemporaryFolder);
assertTrue(testZip.setWritable(false));
try (ZFile zf = new ZFile(testZip)) {
assertEquals(416, zf.entries().size());
assertTrue(zf.getCentralDirectoryOffset() > 0);
assertTrue(zf.getEocdOffset() > 0);
}
}
@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 utf8NamesSupportedOnReadingWithoutUtf8Flag() throws Exception {
File zip = ZipTestUtils.cloneRsrc("zip-with-utf8-filename.zip", mTemporaryFolder);
// Reset bytes 7 and 122 that have the flag in the local header and central directory.
byte[] data = Files.toByteArray(zip);
data[7] = 0;
data[122] = 0;
Files.write(data, zip);
try (ZFile f = new ZFile(zip)) {
assertEquals(1, f.entries().size());
StoredEntry entry = f.entries().iterator().next();
String filetMignonKorean = "\uc548\uc2eC \uc694\ub9ac";
String isGoodJapanese = "\u3068\u3066\u3082\u826f\u3044";
assertEquals(
filetMignonKorean + " " + isGoodJapanese,
entry.getCentralDirectoryHeader().getName());
assertArrayEquals(
"Stuff about food is good.\n".getBytes(Charsets.US_ASCII),
entry.read());
}
}
@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.
*/
long crcOffset;
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.
*/
crcOffset = cdOffset - 12;
}
/*
* Corrupt the CRC32.
*/
byte[] crc = readSegment(zipFile, crcOffset, 4);
crc[0]++;
try (RandomAccessFile raf = new RandomAccessFile(zipFile, "rw")) {
raf.seek(crcOffset);
raf.write(crc);
}
/*
* Now open the zip file and it should write a message in the log.
*/
ZFileOptions options = new ZFileOptions();
options.setVerifyLogFactory(VerifyLogs::unlimited);
try (ZFile zf = new ZFile(zipFile, options)) {
VerifyLog vl = zf.getVerifyLog();
assertTrue(vl.getLogs().isEmpty());
StoredEntry fooEntry = zf.get("foo");
vl = fooEntry.getVerifyLog();
assertEquals(1, vl.getLogs().size());
assertTrue(vl.getLogs().get(0).contains("CRC32"));
}
}
@Test
public void detectIncorrectVersionToExtractInCentralDirectory() throws Exception {
File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
/*
* Create a valid zip file.
*/
try (ZFile zf = new ZFile(zipFile)) {
zf.add("foo", new ByteArrayInputStream(new byte[0]));
}
/*
* Change the "version to extract" in the central directory to 0x7777.
*/
int versionToExtractOffset =
ZFileTestConstants.LOCAL_HEADER_SIZE
+ 3
+ CentralDirectory.F_VERSION_EXTRACT.offset();
byte[] allZipBytes = Files.toByteArray(zipFile);
allZipBytes[versionToExtractOffset] = 0x77;
allZipBytes[versionToExtractOffset + 1] = 0x77;
Files.write(allZipBytes, zipFile);
/*
* Opening the file and it should write a message in the log. The entry has the right
* version to extract (20), but it issues a warning because it is not equal to the one
* in the central directory.
*/
ZFileOptions options = new ZFileOptions();
options.setVerifyLogFactory(VerifyLogs::unlimited);
try (ZFile zf = new ZFile(zipFile, options)) {
VerifyLog vl = zf.getVerifyLog();
assertEquals(1, vl.getLogs().size());
assertTrue(vl.getLogs().get(0).toLowerCase(Locale.US).contains("version"));
assertTrue(vl.getLogs().get(0).toLowerCase(Locale.US).contains("extract"));
StoredEntry fooEntry = zf.get("foo");
vl = fooEntry.getVerifyLog();
assertEquals(1, vl.getLogs().size());
assertTrue(vl.getLogs().get(0).toLowerCase(Locale.US).contains("version"));
assertTrue(vl.getLogs().get(0).toLowerCase(Locale.US).contains("extract"));
}
}
@Test
public void detectIncorrectVersionToExtractInLocalHeader() throws Exception {
File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
/*
* Create a valid zip file.
*/
try (ZFile zf = new ZFile(zipFile)) {
zf.add("foo", new ByteArrayInputStream(new byte[0]));
}
/*
* Change the "version to extract" in the local header to 0x7777.
*/
int versionToExtractOffset = StoredEntry.F_VERSION_EXTRACT.offset();
byte[] allZipBytes = Files.toByteArray(zipFile);
allZipBytes[versionToExtractOffset] = 0x77;
allZipBytes[versionToExtractOffset + 1] = 0x77;
Files.write(allZipBytes, zipFile);
/*
* Opening the file should log an error message.
*/
ZFileOptions options = new ZFileOptions();
options.setVerifyLogFactory(VerifyLogs::unlimited);
try (ZFile zf = new ZFile(zipFile, options)) {
VerifyLog vl = zf.getVerifyLog();
assertTrue(vl.getLogs().isEmpty());
StoredEntry fooEntry = zf.get("foo");
vl = fooEntry.getVerifyLog();
assertEquals(1, vl.getLogs().size());
assertTrue(vl.getLogs().get(0).toLowerCase(Locale.US).contains("version"));
assertTrue(vl.getLogs().get(0).toLowerCase(Locale.US).contains("extract"));
}
}
@Test
public void sortZipContentsWithDeferredCrc32() throws Exception {
/*
* Create a zip file with deferred CRC32 and files in non-alphabetical order.
* ZipOutputStream always creates deferred CRC32 entries.
*/
File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) {
zos.putNextEntry(new ZipEntry("b"));
zos.write(new byte[1000]);
zos.putNextEntry(new ZipEntry("a"));
zos.write(new byte[1000]);
}
/*
* Now open the zip using a ZFile and sort the contents and check that the deferred CRC32
* bits were reset.
*/
try (ZFile zf = new ZFile(zipFile)) {
StoredEntry a = zf.get("a");
assertNotNull(a);
assertNotSame(DataDescriptorType.NO_DATA_DESCRIPTOR, a.getDataDescriptorType());
StoredEntry b = zf.get("b");
assertNotNull(b);
assertNotSame(DataDescriptorType.NO_DATA_DESCRIPTOR, b.getDataDescriptorType());
assertTrue(
a.getCentralDirectoryHeader().getOffset()
> b.getCentralDirectoryHeader().getOffset());
zf.sortZipContents();
zf.update();
a = zf.get("a");
assertNotNull(a);
assertSame(DataDescriptorType.NO_DATA_DESCRIPTOR, a.getDataDescriptorType());
b = zf.get("b");
assertNotNull(b);
assertSame(DataDescriptorType.NO_DATA_DESCRIPTOR, b.getDataDescriptorType());
assertTrue(
a.getCentralDirectoryHeader().getOffset()
< b.getCentralDirectoryHeader().getOffset());
}
/*
* Open the file again and check there are no warnings.
*/
try (ZFile zf = new ZFile(zipFile)) {
VerifyLog vl = zf.getVerifyLog();
assertEquals(0, vl.getLogs().size());
StoredEntry a = zf.get("a");
assertNotNull(a);
vl = a.getVerifyLog();
assertEquals(0, vl.getLogs().size());
StoredEntry b = zf.get("b");
assertNotNull(b);
vl = b.getVerifyLog();
assertEquals(0, vl.getLogs().size());
}
}
@Test
public void alignZipContentsWithDeferredCrc32() throws Exception {
/*
* Create an unaligned zip file with deferred CRC32 and files in non-alphabetical order.
* We need an uncompressed file to make realigning have any effect.
*/
File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) {
zos.putNextEntry(new ZipEntry("x"));
zos.write(new byte[1000]);
zos.putNextEntry(new ZipEntry("y"));
zos.write(new byte[1000]);
ZipEntry zEntry = new ZipEntry("z");
zEntry.setSize(1000);
zEntry.setMethod(ZipEntry.STORED);
zEntry.setCrc(Hashing.crc32().hashBytes(new byte[1000]).asInt());
zos.putNextEntry(zEntry);
zos.write(new byte[1000]);
}
/*
* Now open the zip using a ZFile and realign the contents and check that the deferred CRC32
* bits were reset.
*/
ZFileOptions options = new ZFileOptions();
options.setAlignmentRule(AlignmentRules.constant(2000));
try (ZFile zf = new ZFile(zipFile, options)) {
StoredEntry x = zf.get("x");
assertNotNull(x);
assertNotSame(DataDescriptorType.NO_DATA_DESCRIPTOR, x.getDataDescriptorType());
StoredEntry y = zf.get("y");
assertNotNull(y);
assertNotSame(DataDescriptorType.NO_DATA_DESCRIPTOR, y.getDataDescriptorType());
StoredEntry z = zf.get("z");
assertNotNull(z);
assertSame(DataDescriptorType.NO_DATA_DESCRIPTOR, z.getDataDescriptorType());
zf.realign();
zf.update();
x = zf.get("x");
assertNotNull(x);
assertSame(DataDescriptorType.NO_DATA_DESCRIPTOR, x.getDataDescriptorType());
y = zf.get("y");
assertNotNull(y);
assertSame(DataDescriptorType.NO_DATA_DESCRIPTOR, y.getDataDescriptorType());
z = zf.get("z");
assertNotNull(z);
assertSame(DataDescriptorType.NO_DATA_DESCRIPTOR, z.getDataDescriptorType());
}
}
@Test
public void openingZFileDoesNotRemoveDataDescriptors() throws Exception {
/*
* Create a zip file with deferred CRC32.
*/
File zipFile = new File(mTemporaryFolder.getRoot(), "a.zip");
try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) {
zos.putNextEntry(new ZipEntry("a"));
zos.write(new byte[1000]);
}
/*
* Open using ZFile and check that the deferred CRC32 is there.
*/
try (ZFile zf = new ZFile(zipFile)) {
StoredEntry se = zf.get("a");
assertNotNull(se);
assertNotEquals(DataDescriptorType.NO_DATA_DESCRIPTOR, se.getDataDescriptorType());
}
/*
* Open using ZFile (again) and check that the deferred CRC32 is there.
*/
try (ZFile zf = new ZFile(zipFile)) {
StoredEntry se = zf.get("a");
assertNotNull(se);
assertNotEquals(DataDescriptorType.NO_DATA_DESCRIPTOR, se.getDataDescriptorType());
}
}
@Test
public void zipCommentsAreSaved() throws Exception {
File zipFileWithComments = new File(mTemporaryFolder.getRoot(), "a.zip");
try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFileWithComments))) {
zos.setComment("foo");
}
/*
* Open the zip file and check the comment is there.
*/
try (ZFile zf = new ZFile(zipFileWithComments)) {
byte[] comment = zf.getEocdComment();
assertArrayEquals(new byte[] { 'f', 'o', 'o' }, comment);
/*
* Modify the comment and write the file.
*/
zf.setEocdComment(new byte[] { 'b', 'a', 'r', 'r' });
}
/*
* Open the file and see that the comment is there (both with java and zfile).
*/
try (ZipFile zf2 = new ZipFile(zipFileWithComments)) {
assertEquals("barr", zf2.getComment());
}
try (ZFile zf3 = new ZFile(zipFileWithComments)) {
assertArrayEquals(new byte[] { 'b', 'a', 'r', 'r' }, zf3.getEocdComment());
}
}
@Test
public void eocdCommentsWithMoreThan64kNotAllowed() throws Exception {
File zipFileWithComments = new File(mTemporaryFolder.getRoot(), "a.zip");
try (ZFile zf = new ZFile(zipFileWithComments)) {
try {
zf.setEocdComment(new byte[65536]);
fail();
} catch (IllegalArgumentException e) {
// Expected.
}
zf.setEocdComment(new byte[65535]);
}
}
@Test
public void eocdCommentsWithTheEocdMarkerAreAllowed() throws Exception {
File zipFileWithComments = new File(mTemporaryFolder.getRoot(), "a.zip");
byte[] data = new byte[100];
data[50] = 0x50; // Signature
data[51] = 0x4b;
data[52] = 0x05;
data[53] = 0x06;
data[54] = 0x00; // Number of disk
data[55] = 0x00;
data[56] = 0x00; // Disk CD start
data[57] = 0x00;
data[54] = 0x01; // Total records 1
data[55] = 0x00;
data[56] = 0x02; // Total records 2, must be = to total records 1
data[57] = 0x00;
try (ZFile zf = new ZFile(zipFileWithComments)) {
zf.setEocdComment(data);
}
try (ZFile zf = new ZFile(zipFileWithComments)) {
assertArrayEquals(data, zf.getEocdComment());
}
}
@Test
public void eocdCommentsWithTheEocdMarkerThatAreInvalidAreNotAllowed() throws Exception {
File zipFileWithComments = new File(mTemporaryFolder.getRoot(), "a.zip");
byte[] data = new byte[100];
data[50] = 0x50;
data[51] = 0x4b;
data[52] = 0x05;
data[53] = 0x06;
data[67] = 0x00;
try (ZFile zf = new ZFile(zipFileWithComments)) {
try {
zf.setEocdComment(data);
fail();
} catch (IllegalArgumentException e) {
// Expected.
}
}
}
@Test
public void zipCommentsArePreservedWithFileChanges() throws Exception {
File zipFileWithComments = new File(mTemporaryFolder.getRoot(), "a.zip");
byte[] comment = new byte[] { 1, 3, 4 };
try (ZFile zf = new ZFile(zipFileWithComments)) {
zf.add("foo", new ByteArrayInputStream(new byte[50]));
zf.setEocdComment(comment);
}
try (ZFile zf = new ZFile(zipFileWithComments)) {
assertArrayEquals(comment, zf.getEocdComment());
zf.add("bar", new ByteArrayInputStream(new byte[100]));
}
try (ZFile zf = new ZFile(zipFileWithComments)) {
assertArrayEquals(comment, zf.getEocdComment());
}
}
@Test
public void overlappingZipEntries() throws Exception {
File myZip = ZipTestUtils.cloneRsrc("overlapping.zip", mTemporaryFolder);
try (ZFile zf = new ZFile(myZip)) {
fail();
} catch (IOException e) {
assertTrue(Throwables.getStackTraceAsString(e).contains("overlapping/bbb"));
assertTrue(Throwables.getStackTraceAsString(e).contains("overlapping/ddd"));
assertFalse(Throwables.getStackTraceAsString(e).contains("Central Directory"));
}
}
@Test
public void overlappingZipEntryWithCentralDirectory() throws Exception {
File myZip = ZipTestUtils.cloneRsrc("overlapping2.zip", mTemporaryFolder);
try (ZFile zf = new ZFile(myZip)) {
fail();
} catch (IOException e) {
assertFalse(Throwables.getStackTraceAsString(e).contains("overlapping/bbb"));
assertTrue(Throwables.getStackTraceAsString(e).contains("overlapping/ddd"));
assertTrue(Throwables.getStackTraceAsString(e).contains("Central Directory"));
}
}
@Test
public void readFileWithOffsetBeyondFileEnd() throws Exception {
File myZip = ZipTestUtils.cloneRsrc("entry-outside-file.zip", mTemporaryFolder);
try (ZFile zf = new ZFile(myZip)) {
fail();
} catch (IOException e) {
assertTrue(Throwables.getStackTraceAsString(e).contains("entry-outside-file/foo"));
assertTrue(Throwables.getStackTraceAsString(e).contains("EOF"));
}
}
}