blob: b8e118df1bda749a5c6340e04db896874e766513 [file] [log] [blame]
// Copyright 2015 The Bazel Authors. All rights reserved.
//
// 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.google.devtools.build.singlejar;
import static com.google.common.truth.Truth.assertThat;
import static java.nio.charset.StandardCharsets.ISO_8859_1;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.assertThrows;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.devtools.build.singlejar.ZipCombiner.OutputMode;
import com.google.devtools.build.singlejar.ZipEntryFilter.CustomMergeStrategy;
import com.google.devtools.build.zip.ExtraData;
import com.google.devtools.build.zip.ZipFileEntry;
import com.google.devtools.build.zip.ZipReader;
import com.google.devtools.build.zip.ZipUtil;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.jar.JarOutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/**
* Unit tests for {@link ZipCombiner}.
*/
@RunWith(JUnit4.class)
public class ZipCombinerTest {
@Rule public TemporaryFolder tmp = new TemporaryFolder();
@Rule public ExpectedException thrown = ExpectedException.none();
private File sampleZip() throws IOException {
ZipFactory factory = new ZipFactory();
factory.addFile("hello.txt", "Hello World!");
return writeInputStreamToFile(factory.toInputStream());
}
private File sampleZip2() throws IOException {
ZipFactory factory = new ZipFactory();
factory.addFile("hello2.txt", "Hello World 2!");
return writeInputStreamToFile(factory.toInputStream());
}
private File sampleZipWithTwoEntries() throws IOException {
ZipFactory factory = new ZipFactory();
factory.addFile("hello.txt", "Hello World!");
factory.addFile("hello2.txt", "Hello World 2!");
return writeInputStreamToFile(factory.toInputStream());
}
private File sampleZipWithOneUncompressedEntry() throws IOException {
ZipFactory factory = new ZipFactory();
factory.addFile("hello.txt", "Hello World!", false);
return writeInputStreamToFile(factory.toInputStream());
}
private File sampleZipWithTwoUncompressedEntries() throws IOException {
ZipFactory factory = new ZipFactory();
factory.addFile("hello.txt", "Hello World!", false);
factory.addFile("hello2.txt", "Hello World 2!", false);
return writeInputStreamToFile(factory.toInputStream());
}
private File writeInputStreamToFile(InputStream in) throws IOException {
File out = tmp.newFile();
Files.copy(in, out.toPath(), StandardCopyOption.REPLACE_EXISTING);
return out;
}
private void assertEntry(ZipInputStream zipInput, String filename, long time, byte[] content)
throws IOException {
ZipEntry zipEntry = zipInput.getNextEntry();
assertThat(zipEntry).isNotNull();
assertThat(zipEntry.getName()).isEqualTo(filename);
assertThat(zipEntry.getTime()).isEqualTo(time);
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int bytesCopied;
while ((bytesCopied = zipInput.read(buffer)) != -1) {
out.write(buffer, 0, bytesCopied);
}
assertThat(out.toByteArray()).isEqualTo(content);
}
private void assertEntry(ZipInputStream zipInput, String filename, byte[] content)
throws IOException {
assertEntry(zipInput, filename, ZipUtil.DOS_EPOCH, content);
}
private void assertEntry(ZipInputStream zipInput, String filename, String content)
throws IOException {
assertEntry(zipInput, filename, content.getBytes(ISO_8859_1));
}
private void assertEntry(ZipInputStream zipInput, String filename, Date date, String content)
throws IOException {
assertEntry(zipInput, filename, date.getTime(), content.getBytes(ISO_8859_1));
}
@Test
public void testCompressedDontCare() throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (ZipCombiner zipCombiner = new ZipCombiner(out)) {
zipCombiner.addZip(sampleZip());
}
FakeZipFile expectedResult = new FakeZipFile().addEntry("hello.txt", "Hello World!", true);
expectedResult.assertSame(out.toByteArray());
}
@Test
public void testCompressedForceDeflate() throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (ZipCombiner zipCombiner = new ZipCombiner(OutputMode.FORCE_DEFLATE, out)) {
zipCombiner.addZip(sampleZip());
}
FakeZipFile expectedResult = new FakeZipFile().addEntry("hello.txt", "Hello World!", true);
expectedResult.assertSame(out.toByteArray());
}
@Test
public void testCompressedForceStored() throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (ZipCombiner zipCombiner = new ZipCombiner(OutputMode.FORCE_STORED, out)) {
zipCombiner.addZip(sampleZip());
}
FakeZipFile expectedResult = new FakeZipFile().addEntry("hello.txt", "Hello World!", false);
expectedResult.assertSame(out.toByteArray());
}
@Test
public void testUncompressedDontCare() throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (ZipCombiner zipCombiner = new ZipCombiner(out)) {
zipCombiner.addZip(sampleZipWithOneUncompressedEntry());
}
FakeZipFile expectedResult = new FakeZipFile().addEntry("hello.txt", "Hello World!", false);
expectedResult.assertSame(out.toByteArray());
}
@Test
public void testUncompressedForceDeflate() throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (ZipCombiner zipCombiner = new ZipCombiner(OutputMode.FORCE_DEFLATE, out)) {
zipCombiner.addZip(sampleZipWithOneUncompressedEntry());
}
FakeZipFile expectedResult = new FakeZipFile().addEntry("hello.txt", "Hello World!", true);
expectedResult.assertSame(out.toByteArray());
}
@Test
public void testUncompressedForceStored() throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (ZipCombiner zipCombiner = new ZipCombiner(OutputMode.FORCE_STORED, out)) {
zipCombiner.addZip(sampleZipWithOneUncompressedEntry());
}
FakeZipFile expectedResult = new FakeZipFile().addEntry("hello.txt", "Hello World!", false);
expectedResult.assertSame(out.toByteArray());
}
@Test
public void testCopyTwoEntries() throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (ZipCombiner zipCombiner = new ZipCombiner(out)) {
zipCombiner.addZip(sampleZipWithTwoEntries());
}
ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
assertEntry(zipInput, "hello.txt", "Hello World!");
assertEntry(zipInput, "hello2.txt", "Hello World 2!");
assertThat(zipInput.getNextEntry()).isNull();
}
@Test
public void testCopyTwoUncompressedEntries() throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (ZipCombiner zipCombiner = new ZipCombiner(out)) {
zipCombiner.addZip(sampleZipWithTwoUncompressedEntries());
}
ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
assertEntry(zipInput, "hello.txt", "Hello World!");
assertEntry(zipInput, "hello2.txt", "Hello World 2!");
assertThat(zipInput.getNextEntry()).isNull();
}
@Test
public void testCombine() throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (ZipCombiner zipCombiner = new ZipCombiner(out)) {
zipCombiner.addZip(sampleZip());
zipCombiner.addZip(sampleZip2());
}
ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
assertEntry(zipInput, "hello.txt", "Hello World!");
assertEntry(zipInput, "hello2.txt", "Hello World 2!");
assertThat(zipInput.getNextEntry()).isNull();
}
@Test
public void testDuplicateEntry() throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (ZipCombiner zipCombiner = new ZipCombiner(out)) {
zipCombiner.addZip(sampleZip());
zipCombiner.addZip(sampleZip());
}
ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
assertEntry(zipInput, "hello.txt", "Hello World!");
assertThat(zipInput.getNextEntry()).isNull();
}
@Test
public void testBadZipFileNoEntry() throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (ZipCombiner zipCombiner = new ZipCombiner(out)) {
thrown.expect(ZipException.class);
thrown.expectMessage("It does not contain an end of central directory record.");
zipCombiner.addZip(writeInputStreamToFile(new ByteArrayInputStream(new byte[] {1, 2, 3, 4})));
}
ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
assertThat(zipInput.getNextEntry()).isNull();
}
private InputStream asStream(String content) {
return new ByteArrayInputStream(content.getBytes(UTF_8));
}
@Test
public void testAddFile() throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (ZipCombiner zipCombiner = new ZipCombiner(out)) {
zipCombiner.addFile("hello.txt", ZipCombiner.DOS_EPOCH, asStream("Hello World!"));
}
ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
assertEntry(zipInput, "hello.txt", "Hello World!");
assertThat(zipInput.getNextEntry()).isNull();
}
@Test
public void testAddFileAndDuplicateZipEntry() throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (ZipCombiner zipCombiner = new ZipCombiner(out)) {
zipCombiner.addFile("hello.txt", ZipCombiner.DOS_EPOCH, asStream("Hello World!"));
zipCombiner.addZip(sampleZip());
}
ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
assertEntry(zipInput, "hello.txt", "Hello World!");
assertThat(zipInput.getNextEntry()).isNull();
}
static final class MergeStrategyPlaceHolder implements CustomMergeStrategy {
@Override
public void finish(OutputStream out) {
throw new UnsupportedOperationException();
}
@Override
public void merge(InputStream in, OutputStream out) {
throw new UnsupportedOperationException();
}
}
private static final CustomMergeStrategy COPY_PLACEHOLDER = new MergeStrategyPlaceHolder();
private static final CustomMergeStrategy SKIP_PLACEHOLDER = new MergeStrategyPlaceHolder();
/**
* A mock implementation that either uses the specified behavior or calls
* through to copy.
*/
class MockZipEntryFilter implements ZipEntryFilter {
private Date date = ZipCombiner.DOS_EPOCH;
private final List<String> calls = new ArrayList<>();
// File name to merge strategy map.
private final Map<String, CustomMergeStrategy> behavior =
new HashMap<>();
private final ListMultimap<String, String> renameMap = ArrayListMultimap.create();
@Override
public void accept(String filename, StrategyCallback callback) throws IOException {
calls.add(filename);
CustomMergeStrategy strategy = behavior.get(filename);
if (strategy == null) {
callback.copy(null);
} else if (strategy == COPY_PLACEHOLDER) {
List<String> names = renameMap.get(filename);
if (names != null && !names.isEmpty()) {
// rename to the next name in list of replacement names.
String newName = names.get(0);
callback.rename(newName, null);
// Unless this is the last replacment names, we pop the used name.
// The lastreplacement name applies any additional entries.
if (names.size() > 1) {
names.remove(0);
}
} else {
callback.copy(null);
}
} else if (strategy == SKIP_PLACEHOLDER) {
callback.skip();
} else {
callback.customMerge(date, strategy);
}
}
}
@Test
public void testCopyCallsFilter() throws IOException {
MockZipEntryFilter mockFilter = new MockZipEntryFilter();
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (ZipCombiner zipCombiner = new ZipCombiner(mockFilter, out)) {
zipCombiner.addZip(sampleZip());
}
assertThat(mockFilter.calls).containsExactly("hello.txt");
}
@Test
public void testDuplicateEntryCallsFilterOnce() throws IOException {
MockZipEntryFilter mockFilter = new MockZipEntryFilter();
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (ZipCombiner zipCombiner = new ZipCombiner(mockFilter, out)) {
zipCombiner.addZip(sampleZip());
zipCombiner.addZip(sampleZip());
}
assertThat(mockFilter.calls).containsExactly("hello.txt");
}
@Test
public void testMergeStrategy() throws IOException {
MockZipEntryFilter mockFilter = new MockZipEntryFilter();
mockFilter.behavior.put("hello.txt", new ConcatenateStrategy());
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (ZipCombiner zipCombiner = new ZipCombiner(mockFilter, out)) {
zipCombiner.addZip(sampleZip());
zipCombiner.addZip(sampleZipWithTwoEntries());
}
assertThat(mockFilter.calls).containsExactly("hello.txt", "hello2.txt").inOrder();
ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
assertEntry(zipInput, "hello2.txt", "Hello World 2!");
assertEntry(zipInput, "hello.txt", "Hello World!\nHello World!");
assertThat(zipInput.getNextEntry()).isNull();
}
@Test
public void testMergeStrategyWithUncompressedFiles() throws IOException {
MockZipEntryFilter mockFilter = new MockZipEntryFilter();
mockFilter.behavior.put("hello.txt", new ConcatenateStrategy());
mockFilter.behavior.put("hello2.txt", SKIP_PLACEHOLDER);
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (ZipCombiner zipCombiner = new ZipCombiner(mockFilter, out)) {
zipCombiner.addZip(sampleZipWithTwoUncompressedEntries());
zipCombiner.addZip(sampleZipWithTwoUncompressedEntries());
}
assertThat(mockFilter.calls).isEqualTo(Arrays.asList("hello.txt", "hello2.txt"));
ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
assertEntry(zipInput, "hello.txt", "Hello World!\nHello World!");
assertThat(zipInput.getNextEntry()).isNull();
}
@Test
public void testMergeStrategyWithSlowCopy() throws IOException {
MockZipEntryFilter mockFilter = new MockZipEntryFilter();
mockFilter.behavior.put("hello.txt", new SlowConcatenateStrategy());
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (ZipCombiner zipCombiner = new ZipCombiner(mockFilter, out)) {
zipCombiner.addZip(sampleZip());
zipCombiner.addZip(sampleZipWithTwoEntries());
}
assertThat(mockFilter.calls).isEqualTo(Arrays.asList("hello.txt", "hello2.txt"));
ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
assertEntry(zipInput, "hello2.txt", "Hello World 2!");
assertEntry(zipInput, "hello.txt", "Hello World!Hello World!");
assertThat(zipInput.getNextEntry()).isNull();
}
@Test
public void testMergeStrategyWithUncompressedFilesAndSlowCopy() throws IOException {
MockZipEntryFilter mockFilter = new MockZipEntryFilter();
mockFilter.behavior.put("hello.txt", new SlowConcatenateStrategy());
mockFilter.behavior.put("hello2.txt", SKIP_PLACEHOLDER);
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (ZipCombiner zipCombiner = new ZipCombiner(mockFilter, out)) {
zipCombiner.addZip(sampleZipWithTwoUncompressedEntries());
zipCombiner.addZip(sampleZipWithTwoUncompressedEntries());
}
assertThat(mockFilter.calls).containsExactly("hello.txt", "hello2.txt").inOrder();
ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
assertEntry(zipInput, "hello.txt", "Hello World!Hello World!");
assertThat(zipInput.getNextEntry()).isNull();
}
private File specialZipWithMinusOne() throws IOException {
ZipFactory factory = new ZipFactory();
factory.addFile("hello.txt", new byte[] {-1});
return writeInputStreamToFile(factory.toInputStream());
}
@Test
public void testMergeStrategyWithSlowCopyAndNegativeBytes() throws IOException {
MockZipEntryFilter mockFilter = new MockZipEntryFilter();
mockFilter.behavior.put("hello.txt", new SlowConcatenateStrategy());
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (ZipCombiner zipCombiner = new ZipCombiner(mockFilter, out)) {
zipCombiner.addZip(specialZipWithMinusOne());
}
assertThat(mockFilter.calls).containsExactly("hello.txt");
ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
assertEntry(zipInput, "hello.txt", new byte[] { -1 });
assertThat(zipInput.getNextEntry()).isNull();
}
@Test
public void testCopyDateHandling() throws IOException {
final Date date = new GregorianCalendar(2009, 8, 2, 0, 0, 0).getTime();
ZipEntryFilter mockFilter =
new ZipEntryFilter() {
@Override
public void accept(String filename, StrategyCallback callback) throws IOException {
assertThat(filename).isEqualTo("hello.txt");
callback.copy(date);
}
};
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (ZipCombiner zipCombiner = new ZipCombiner(mockFilter, out)) {
zipCombiner.addZip(sampleZip());
}
ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
assertEntry(zipInput, "hello.txt", date, "Hello World!");
assertThat(zipInput.getNextEntry()).isNull();
}
@Test
public void testMergeDateHandling() throws IOException {
MockZipEntryFilter mockFilter = new MockZipEntryFilter();
mockFilter.behavior.put("hello.txt", new ConcatenateStrategy());
mockFilter.date = new GregorianCalendar(2009, 8, 2, 0, 0, 0).getTime();
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (ZipCombiner zipCombiner = new ZipCombiner(mockFilter, out)) {
zipCombiner.addZip(sampleZip());
zipCombiner.addZip(sampleZipWithTwoEntries());
}
assertThat(mockFilter.calls).containsExactly("hello.txt", "hello2.txt").inOrder();
ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
assertEntry(zipInput, "hello2.txt", ZipCombiner.DOS_EPOCH, "Hello World 2!");
assertEntry(zipInput, "hello.txt", mockFilter.date, "Hello World!\nHello World!");
assertThat(zipInput.getNextEntry()).isNull();
}
@Test
public void testDuplicateCallThrowsException() throws IOException {
ZipEntryFilter badFilter = new ZipEntryFilter() {
@Override
public void accept(String filename, StrategyCallback callback) throws IOException {
// Duplicate callback call.
callback.skip();
callback.copy(null);
}
};
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (ZipCombiner zipCombiner = new ZipCombiner(badFilter, out)) {
assertThrows(IllegalStateException.class, () -> zipCombiner.addZip(sampleZip()));
}
}
@Test
public void testNoCallThrowsException() throws IOException {
ZipEntryFilter badFilter = new ZipEntryFilter() {
@Override
public void accept(String filename, StrategyCallback callback) {
// No callback call.
}
};
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (ZipCombiner zipCombiner = new ZipCombiner(badFilter, out)) {
assertThrows(IllegalStateException.class, () -> zipCombiner.addZip(sampleZip()));
}
}
// This test verifies that if an entry A is renamed as A (identy mapping),
// then subsequent entries named A are still subject to filtering.
// Note: this is different from a copy, where subsequent entries are skipped.
@Test
public void testRenameIdentityMapping() throws IOException {
MockZipEntryFilter mockFilter = new MockZipEntryFilter();
mockFilter.behavior.put("hello.txt", COPY_PLACEHOLDER);
mockFilter.behavior.put("hello2.txt", COPY_PLACEHOLDER);
mockFilter.renameMap.put("hello.txt", "hello.txt"); // identity rename, not copy
mockFilter.renameMap.put("hello2.txt", "hello2.txt"); // identity rename, not copy
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (ZipCombiner zipCombiner = new ZipCombiner(mockFilter, out)) {
zipCombiner.addZip(sampleZipWithTwoEntries());
zipCombiner.addZip(sampleZipWithTwoEntries());
}
assertThat(mockFilter.calls)
.containsExactly("hello.txt", "hello2.txt", "hello.txt", "hello2.txt")
.inOrder();
ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
assertEntry(zipInput, "hello.txt", "Hello World!");
assertEntry(zipInput, "hello2.txt", "Hello World 2!");
assertThat(zipInput.getNextEntry()).isNull();
}
// This test verifies that multiple entries with the same name can be
// renamed to unique names.
@Test
public void testRenameNoConflictMapping() throws IOException {
MockZipEntryFilter mockFilter = new MockZipEntryFilter();
mockFilter.behavior.put("hello.txt", COPY_PLACEHOLDER);
mockFilter.behavior.put("hello2.txt", COPY_PLACEHOLDER);
mockFilter.renameMap.putAll("hello.txt", Arrays.asList("hello1.txt", "hello2.txt"));
mockFilter.renameMap.putAll("hello2.txt", Arrays.asList("world1.txt", "world2.txt"));
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (ZipCombiner zipCombiner = new ZipCombiner(mockFilter, out)) {
zipCombiner.addZip(sampleZipWithTwoEntries());
zipCombiner.addZip(sampleZipWithTwoEntries());
}
assertThat(mockFilter.calls)
.containsExactly("hello.txt", "hello2.txt", "hello.txt", "hello2.txt")
.inOrder();
ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
assertEntry(zipInput, "hello1.txt", "Hello World!");
assertEntry(zipInput, "world1.txt", "Hello World 2!");
assertEntry(zipInput, "hello2.txt", "Hello World!");
assertEntry(zipInput, "world2.txt", "Hello World 2!");
assertThat(zipInput.getNextEntry()).isNull();
}
// This tests verifies that an attempt to rename an entry to a
// name already written, results in the entry being skipped, after
// calling the filter.
@Test
public void testRenameSkipUsedName() throws IOException {
MockZipEntryFilter mockFilter = new MockZipEntryFilter();
mockFilter.behavior.put("hello.txt", COPY_PLACEHOLDER);
mockFilter.behavior.put("hello2.txt", COPY_PLACEHOLDER);
mockFilter.renameMap.putAll("hello.txt",
Arrays.asList("hello1.txt", "hello2.txt", "hello3.txt"));
mockFilter.renameMap.put("hello2.txt", "hello2.txt");
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (ZipCombiner zipCombiner = new ZipCombiner(mockFilter, out)) {
zipCombiner.addZip(sampleZipWithTwoEntries());
zipCombiner.addZip(sampleZipWithTwoEntries());
zipCombiner.addZip(sampleZipWithTwoEntries());
}
assertThat(mockFilter.calls)
.containsExactly(
"hello.txt", "hello2.txt", "hello.txt", "hello2.txt", "hello.txt", "hello2.txt")
.inOrder();
ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
assertEntry(zipInput, "hello1.txt", "Hello World!");
assertEntry(zipInput, "hello2.txt", "Hello World 2!");
assertEntry(zipInput, "hello3.txt", "Hello World!");
assertThat(zipInput.getNextEntry()).isNull();
}
// This tests verifies that if an entry has been copied, then
// further entries of the same name are skipped (filter not invoked),
// and entries renamed to the same name are skipped (after calling filter).
@Test
public void testRenameAndCopy() throws IOException {
MockZipEntryFilter mockFilter = new MockZipEntryFilter();
mockFilter.behavior.put("hello.txt", COPY_PLACEHOLDER);
mockFilter.behavior.put("hello2.txt", COPY_PLACEHOLDER);
mockFilter.renameMap.putAll("hello.txt",
Arrays.asList("hello1.txt", "hello2.txt", "hello3.txt"));
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (ZipCombiner zipCombiner = new ZipCombiner(mockFilter, out)) {
zipCombiner.addZip(sampleZipWithTwoEntries());
zipCombiner.addZip(sampleZipWithTwoEntries());
zipCombiner.addZip(sampleZipWithTwoEntries());
}
assertThat(mockFilter.calls)
.containsExactly("hello.txt", "hello2.txt", "hello.txt", "hello.txt")
.inOrder();
ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
assertEntry(zipInput, "hello1.txt", "Hello World!");
assertEntry(zipInput, "hello2.txt", "Hello World 2!");
assertEntry(zipInput, "hello3.txt", "Hello World!");
assertThat(zipInput.getNextEntry()).isNull();
}
// This tests verifies that if an entry has been skipped, then
// further entries of the same name are skipped (filter not invoked),
// and entries renamed to the same name are skipped (after calling filter).
@Test
public void testRenameAndSkip() throws IOException {
MockZipEntryFilter mockFilter = new MockZipEntryFilter();
mockFilter.behavior.put("hello.txt", COPY_PLACEHOLDER);
mockFilter.behavior.put("hello2.txt", SKIP_PLACEHOLDER);
mockFilter.renameMap.putAll("hello.txt",
Arrays.asList("hello1.txt", "hello2.txt", "hello3.txt"));
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (ZipCombiner zipCombiner = new ZipCombiner(mockFilter, out)) {
zipCombiner.addZip(sampleZipWithTwoEntries());
zipCombiner.addZip(sampleZipWithTwoEntries());
zipCombiner.addZip(sampleZipWithTwoEntries());
}
assertThat(mockFilter.calls)
.containsExactly("hello.txt", "hello2.txt", "hello.txt", "hello.txt")
.inOrder();
ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
assertEntry(zipInput, "hello1.txt", "Hello World!");
assertEntry(zipInput, "hello3.txt", "Hello World!");
assertThat(zipInput.getNextEntry()).isNull();
}
// This test verifies that renaming works when input and output
// disagree on compression method. This is the simple case, where
// content is read and rewritten, and no header repair is needed.
@Test
public void testRenameWithUncompressedFiles () throws IOException {
MockZipEntryFilter mockFilter = new MockZipEntryFilter();
mockFilter.behavior.put("hello.txt", COPY_PLACEHOLDER);
mockFilter.behavior.put("hello2.txt", COPY_PLACEHOLDER);
mockFilter.renameMap.putAll("hello.txt",
Arrays.asList("hello1.txt", "hello2.txt", "hello3.txt"));
mockFilter.renameMap.put("hello2.txt", "hello2.txt");
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (ZipCombiner zipCombiner = new ZipCombiner(mockFilter, out)) {
zipCombiner.addZip(sampleZipWithTwoUncompressedEntries());
zipCombiner.addZip(sampleZipWithTwoUncompressedEntries());
zipCombiner.addZip(sampleZipWithTwoUncompressedEntries());
}
assertThat(mockFilter.calls)
.containsExactly(
"hello.txt", "hello2.txt", "hello.txt", "hello2.txt", "hello.txt", "hello2.txt")
.inOrder();
ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
assertEntry(zipInput, "hello1.txt", "Hello World!");
assertEntry(zipInput, "hello2.txt", "Hello World 2!");
assertEntry(zipInput, "hello3.txt", "Hello World!");
assertThat(zipInput.getNextEntry()).isNull();
}
// The next two tests check that ZipCombiner can handle a ZIP with an data
// descriptor marker in the compressed data, i.e. that it does not scan for
// the data descriptor marker. It's unfortunately a bit tricky to create such
// a ZIP.
private static final int LOCAL_FILE_HEADER_MARKER = 0x04034b50;
// Create a ZIP with a partial entry.
private InputStream zipWithPartialEntry() {
ByteBuffer out = ByteBuffer.wrap(new byte[32]).order(ByteOrder.LITTLE_ENDIAN);
out.clear();
// file header
out.putInt(LOCAL_FILE_HEADER_MARKER); // file header signature
out.putShort((short) 6); // version to extract
out.putShort((short) 0); // general purpose bit flag
out.putShort((short) ZipOutputStream.STORED); // compression method
out.putShort((short) 0); // mtime (00:00:00)
out.putShort((short) 0x21); // mdate (1.1.1980)
out.putInt(0); // crc32
out.putInt(10); // compressed size
out.putInt(10); // uncompressed size
out.putShort((short) 1); // file name length
out.putShort((short) 0); // extra field length
out.put((byte) 'a'); // file name
// file contents
out.put((byte) 0x01);
// Unexpected end of file.
return new ByteArrayInputStream(out.array());
}
@Test
public void testBadZipFilePartialEntry() throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (ZipCombiner zipCombiner = new ZipCombiner(out)) {
thrown.expect(ZipException.class);
thrown.expectMessage("It does not contain an end of central directory record.");
zipCombiner.addZip(writeInputStreamToFile(zipWithPartialEntry()));
}
}
@Test
public void testZipCombinerAgainstJavaUtil() throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (JarOutputStream jarOut = new JarOutputStream(out)) {
ZipEntry entry;
entry = new ZipEntry("META-INF/");
entry.setTime(ZipCombiner.DOS_EPOCH.getTime());
entry.setMethod(JarOutputStream.STORED);
entry.setSize(0);
entry.setCompressedSize(0);
entry.setCrc(0);
jarOut.putNextEntry(entry);
entry = new ZipEntry("META-INF/MANIFEST.MF");
entry.setTime(ZipCombiner.DOS_EPOCH.getTime());
entry.setMethod(JarOutputStream.DEFLATED);
jarOut.putNextEntry(entry);
jarOut.write(new byte[] {1, 2, 3, 4});
}
File javaFile = writeInputStreamToFile(new ByteArrayInputStream(out.toByteArray()));
out.reset();
try (ZipCombiner zipcombiner = new ZipCombiner(out)) {
zipcombiner.addDirectory("META-INF/", ZipCombiner.DOS_EPOCH,
new ExtraData[] {new ExtraData((short) 0xCAFE, new byte[0])});
zipcombiner.addFile("META-INF/MANIFEST.MF", ZipCombiner.DOS_EPOCH,
new ByteArrayInputStream(new byte[] {1, 2, 3, 4}));
}
File zipCombinerFile = writeInputStreamToFile(new ByteArrayInputStream(out.toByteArray()));
byte[] zipCombinerRaw = out.toByteArray();
new ZipTester(zipCombinerRaw).validate();
assertZipFilesEquivalent(zipCombinerFile, javaFile);
}
void assertZipFilesEquivalent(File a, File b) throws IOException {
try (ZipReader x = new ZipReader(a);
ZipReader y = new ZipReader(b)) {
Collection<ZipFileEntry> xEntries = x.entries();
Collection<ZipFileEntry> yEntries = y.entries();
assertThat(xEntries).hasSize(yEntries.size());
Iterator<ZipFileEntry> xIter = xEntries.iterator();
Iterator<ZipFileEntry> yIter = yEntries.iterator();
for (int i = 0; i < xEntries.size(); i++) {
assertZipEntryEquivalent(xIter.next(), yIter.next());
}
}
}
void assertZipEntryEquivalent(ZipFileEntry x, ZipFileEntry y) {
assertThat(x.getComment()).isEqualTo(y.getComment());
assertThat(x.getCompressedSize()).isEqualTo(y.getCompressedSize());
assertThat(x.getCrc()).isEqualTo(y.getCrc());
assertThat(x.getExternalAttributes()).isEqualTo(y.getExternalAttributes());
// The JDK adds different extra data to zip files on different platforms, so we don't compare
// the extra data.
assertThat(x.getInternalAttributes()).isEqualTo(y.getInternalAttributes());
assertThat(x.getMethod()).isEqualTo(y.getMethod());
assertThat(x.getName()).isEqualTo(y.getName());
assertThat(x.getSize()).isEqualTo(y.getSize());
assertThat(x.getTime()).isEqualTo(y.getTime());
assertThat(x.getVersion()).isEqualTo(y.getVersion());
assertThat(x.getVersionNeeded()).isEqualTo(y.getVersionNeeded());
// Allow general purpose bit 3 (data descriptor) used in jdk7 to differ.
// Allow general purpose bit 11 (UTF-8 encoding) used in jdk7 to differ.
assertThat(x.getFlags() | (1 << 3) | (1 << 11))
.isEqualTo(y.getFlags() | (1 << 3) | (1 << 11));
}
/**
* Ensures that the code that grows the central directory and the code that patches it is not
* obviously broken.
*/
@Test
public void testLotsOfFiles() throws IOException {
int fileCount = 100;
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (ZipCombiner zipCombiner =
new ZipCombiner(OutputMode.DONT_CARE, new CopyEntryFilter(), out)) {
for (int i = 0; i < fileCount; i++) {
zipCombiner.addFile("hello" + i, ZipCombiner.DOS_EPOCH, asStream("Hello " + i + "!"));
}
}
ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
for (int i = 0; i < fileCount; i++) {
assertEntry(zipInput, "hello" + i, "Hello " + i + "!");
}
assertThat(zipInput.getNextEntry()).isNull();
new ZipTester(out.toByteArray()).validate();
}
}