// 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.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; 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 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; 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; /** * 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(); assertNotNull(zipEntry); assertEquals(filename, zipEntry.getName()); assertEquals(time, zipEntry.getTime()); ByteArrayOutputStream out = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int bytesCopied; while ((bytesCopied = zipInput.read(buffer)) != -1) { out.write(buffer, 0, bytesCopied); } assertTrue(Arrays.equals(content, out.toByteArray())); } 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!"); assertNull(zipInput.getNextEntry()); } @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!"); assertNull(zipInput.getNextEntry()); } @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!"); assertNull(zipInput.getNextEntry()); } @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!"); assertNull(zipInput.getNextEntry()); } @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())); assertNull(zipInput.getNextEntry()); } 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!"); assertNull(zipInput.getNextEntry()); } @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!"); assertNull(zipInput.getNextEntry()); } 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()); } assertEquals(Arrays.asList("hello.txt"), mockFilter.calls); } @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()); } assertEquals(Arrays.asList("hello.txt"), mockFilter.calls); } @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()); } assertEquals(Arrays.asList("hello.txt", "hello2.txt"), mockFilter.calls); ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray())); assertEntry(zipInput, "hello2.txt", "Hello World 2!"); assertEntry(zipInput, "hello.txt", "Hello World!\nHello World!"); assertNull(zipInput.getNextEntry()); } @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()); } assertEquals(Arrays.asList("hello.txt", "hello2.txt"), mockFilter.calls); ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray())); assertEntry(zipInput, "hello.txt", "Hello World!\nHello World!"); assertNull(zipInput.getNextEntry()); } @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()); } assertEquals(Arrays.asList("hello.txt", "hello2.txt"), mockFilter.calls); ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray())); assertEntry(zipInput, "hello2.txt", "Hello World 2!"); assertEntry(zipInput, "hello.txt", "Hello World!Hello World!"); assertNull(zipInput.getNextEntry()); } @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()); } assertEquals(Arrays.asList("hello.txt", "hello2.txt"), mockFilter.calls); ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray())); assertEntry(zipInput, "hello.txt", "Hello World!Hello World!"); assertNull(zipInput.getNextEntry()); } 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()); } assertEquals(Arrays.asList("hello.txt"), mockFilter.calls); ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray())); assertEntry(zipInput, "hello.txt", new byte[] { -1 }); assertNull(zipInput.getNextEntry()); } @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 { assertEquals("hello.txt", filename); 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!"); assertNull(zipInput.getNextEntry()); } @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()); } assertEquals(Arrays.asList("hello.txt", "hello2.txt"), mockFilter.calls); 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!"); assertNull(zipInput.getNextEntry()); } @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)) { zipCombiner.addZip(sampleZip()); fail(); } catch (IllegalStateException e) { // Expected exception. } } @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)) { zipCombiner.addZip(sampleZip()); fail(); } catch (IllegalStateException e) { // Expected exception. } } // 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!"); assertNull(zipInput.getNextEntry()); } // 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!"); assertNull(zipInput.getNextEntry()); } // 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!"); assertNull(zipInput.getNextEntry()); } // 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!"); assertNull(zipInput.getNextEntry()); } // 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!"); assertNull(zipInput.getNextEntry()); } // 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!"); assertNull(zipInput.getNextEntry()); } // 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(new ZipReader(zipCombinerFile), new ZipReader(javaFile)); } void assertZipFilesEquivalent(ZipReader x, ZipReader y) { 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 + "!"); } assertNull(zipInput.getNextEntry()); new ZipTester(out.toByteArray()).validate(); } }