| // 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(); |
| } |
| } |