aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/java_tools/singlejar/javatests
diff options
context:
space:
mode:
Diffstat (limited to 'src/java_tools/singlejar/javatests')
-rw-r--r--src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/ConcatenateStrategyTest.java75
-rw-r--r--src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/CopyEntryFilterTest.java39
-rw-r--r--src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/DefaultJarEntryFilterTest.java101
-rw-r--r--src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/FakeZipFile.java265
-rw-r--r--src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/MockSimpleFileSystem.java88
-rw-r--r--src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/OptionFileExpanderTest.java87
-rw-r--r--src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/PrefixListPathFilterTest.java54
-rw-r--r--src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/RecordingCallback.java56
-rw-r--r--src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/SingleJarTest.java634
-rw-r--r--src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/SingleJarTests.java27
-rw-r--r--src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/SlowConcatenateStrategy.java45
-rw-r--r--src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/ZipCombinerTest.java936
-rw-r--r--src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/ZipFactory.java106
-rw-r--r--src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/ZipTester.java412
14 files changed, 2925 insertions, 0 deletions
diff --git a/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/ConcatenateStrategyTest.java b/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/ConcatenateStrategyTest.java
new file mode 100644
index 0000000000..af03729111
--- /dev/null
+++ b/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/ConcatenateStrategyTest.java
@@ -0,0 +1,75 @@
+// Copyright 2015 Google Inc. 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 java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+/**
+ * Unit tests for {@link ConcatenateStrategy}.
+ */
+@RunWith(JUnit4.class)
+public class ConcatenateStrategyTest {
+
+ private String merge(String... inputs) throws IOException {
+ return mergeInternal(true, inputs);
+ }
+
+ private String mergeNoNewLine(String... inputs) throws IOException {
+ return mergeInternal(false, inputs);
+ }
+
+ private String mergeInternal(boolean appendNewLine, String... inputs) throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ConcatenateStrategy strategy = new ConcatenateStrategy(appendNewLine);
+ for (String input : inputs) {
+ strategy.merge(new ByteArrayInputStream(input.getBytes(UTF_8)), out);
+ }
+ strategy.finish(out);
+ return new String(out.toByteArray(), UTF_8);
+ }
+
+ @Test
+ public void testSingleInput() throws IOException {
+ assertEquals("a", merge("a"));
+ assertEquals("a", mergeNoNewLine("a"));
+ }
+
+ @Test
+ public void testTwoInputs() throws IOException {
+ assertEquals("a\nb", merge("a\n", "b"));
+ assertEquals("a\nb", mergeNoNewLine("a\n", "b"));
+ }
+
+ @Test
+ public void testAutomaticNewline() throws IOException {
+ assertEquals("a\nb", merge("a", "b"));
+ assertEquals("ab", mergeNoNewLine("a", "b"));
+ }
+
+ @Test
+ public void testAutomaticNewlineAndEmptyFile() throws IOException {
+ assertEquals("a\nb", merge("a", "", "b"));
+ assertEquals("ab", mergeNoNewLine("a", "", "b"));
+ }
+}
diff --git a/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/CopyEntryFilterTest.java b/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/CopyEntryFilterTest.java
new file mode 100644
index 0000000000..8eeb8d077f
--- /dev/null
+++ b/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/CopyEntryFilterTest.java
@@ -0,0 +1,39 @@
+// Copyright 2015 Google Inc. 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 org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+/**
+ * Unit tests for {@link CopyEntryFilter}.
+ */
+@RunWith(JUnit4.class)
+public class CopyEntryFilterTest {
+
+ @Test
+ public void testSingleInput() throws IOException {
+ RecordingCallback callback = new RecordingCallback();
+ new CopyEntryFilter().accept("abc", callback);
+ assertEquals(Arrays.asList("copy"), callback.calls);
+ }
+
+}
diff --git a/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/DefaultJarEntryFilterTest.java b/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/DefaultJarEntryFilterTest.java
new file mode 100644
index 0000000000..6240cfb58f
--- /dev/null
+++ b/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/DefaultJarEntryFilterTest.java
@@ -0,0 +1,101 @@
+// Copyright 2015 Google Inc. 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 org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.jar.JarFile;
+
+/**
+ * Unit tests for {@link DefaultJarEntryFilter}.
+ */
+@RunWith(JUnit4.class)
+public class DefaultJarEntryFilterTest {
+
+ private static final Date DOS_EPOCH = ZipCombiner.DOS_EPOCH;
+
+ @Test
+ public void testSingleInput() throws IOException {
+ RecordingCallback callback = new RecordingCallback();
+ new DefaultJarEntryFilter().accept("abc", callback);
+ assertEquals(Arrays.asList("copy"), callback.calls);
+ assertEquals(Arrays.asList(DOS_EPOCH), callback.dates);
+ }
+
+ @Test
+ public void testProtobufExtensionsInput() throws IOException {
+ RecordingCallback callback = new RecordingCallback();
+ new DefaultJarEntryFilter().accept("protobuf.meta", callback);
+ assertEquals(Arrays.asList("customMerge"), callback.calls);
+ assertEquals(Arrays.asList(DOS_EPOCH), callback.dates);
+ }
+
+ @Test
+ public void testManifestInput() throws IOException {
+ RecordingCallback callback = new RecordingCallback();
+ new DefaultJarEntryFilter().accept(JarFile.MANIFEST_NAME, callback);
+ assertEquals(Arrays.asList("skip"), callback.calls);
+ }
+
+ @Test
+ public void testServiceInput() throws IOException {
+ RecordingCallback callback = new RecordingCallback();
+ new DefaultJarEntryFilter().accept("META-INF/services/any.service", callback);
+ assertEquals(Arrays.asList("customMerge"), callback.calls);
+ assertEquals(Arrays.asList(DOS_EPOCH), callback.dates);
+ }
+
+ @Test
+ public void testSpringHandlers() throws IOException {
+ RecordingCallback callback = new RecordingCallback();
+ new DefaultJarEntryFilter().accept("META-INF/spring.handlers", callback);
+ assertEquals(Arrays.asList("customMerge"), callback.calls);
+ assertEquals(Arrays.asList(DOS_EPOCH), callback.dates);
+ }
+
+ @Test
+ public void testSpringSchemas() throws IOException {
+ RecordingCallback callback = new RecordingCallback();
+ new DefaultJarEntryFilter().accept("META-INF/spring.schemas", callback);
+ assertEquals(Arrays.asList("customMerge"), callback.calls);
+ assertEquals(Arrays.asList(DOS_EPOCH), callback.dates);
+ }
+
+ @Test
+ public void testClassInput() throws IOException {
+ RecordingCallback callback = new RecordingCallback();
+ new DefaultJarEntryFilter().accept("a.class", callback);
+ assertEquals(Arrays.asList("copy"), callback.calls);
+ assertEquals(Arrays.asList(DefaultJarEntryFilter.DOS_EPOCH_PLUS_2_SECONDS), callback.dates);
+ }
+
+ @Test
+ public void testOtherSkippedInputs() throws IOException {
+ RecordingCallback callback = new RecordingCallback();
+ ZipEntryFilter filter = new DefaultJarEntryFilter();
+ filter.accept("a.SF", callback);
+ filter.accept("a.DSA", callback);
+ filter.accept("a.RSA", callback);
+ assertEquals(Arrays.asList("skip", "skip", "skip"), callback.calls);
+ assertEquals(Arrays.<Date>asList(), callback.dates);
+ }
+}
diff --git a/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/FakeZipFile.java b/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/FakeZipFile.java
new file mode 100644
index 0000000000..0156cbcd20
--- /dev/null
+++ b/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/FakeZipFile.java
@@ -0,0 +1,265 @@
+// Copyright 2015 Google Inc. 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 java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertArrayEquals;
+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 com.google.common.base.Receiver;
+import com.google.devtools.build.singlejar.SingleJarTest.EntryMode;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+/**
+ * A fake zip file to assert that a given {@link ZipInputStream} contains
+ * specified entries in a specified order. Just for unit testing.
+ */
+public final class FakeZipFile {
+
+ private static void assertSameByteArray(byte[] expected, byte[] actual) {
+ if (expected == null) {
+ assertNull(actual);
+ } else {
+ assertArrayEquals(expected, actual);
+ }
+ }
+
+ private static byte[] readZipEntryContent(ZipInputStream zipInput) throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ byte[] buffer = new byte[1024];
+ int bytesCopied;
+ while ((bytesCopied = zipInput.read(buffer)) != -1) {
+ out.write(buffer, 0, bytesCopied);
+ }
+ return out.toByteArray();
+ }
+
+ private static final class PlainByteValidator implements Receiver<byte[]> {
+ private final byte[] expected;
+
+ private PlainByteValidator(String expected) {
+ this.expected = expected == null ? new byte[0] : expected.getBytes(UTF_8);
+ }
+
+ @Override
+ public void accept(byte[] object) {
+ assertSameByteArray(expected, object);
+ }
+
+ }
+
+ private static final class FakeZipEntry {
+
+ private final String name;
+ private final Receiver<byte[]> content;
+ private final Date date;
+ private final byte[] extra;
+ private final EntryMode mode;
+
+ private FakeZipEntry(String name, Date date, String content, byte[] extra, EntryMode mode) {
+ this.name = name;
+ this.date = date;
+ this.content = new PlainByteValidator(content);
+ this.extra = extra;
+ this.mode = mode;
+ }
+
+ private FakeZipEntry(String name, Date date, Receiver<byte[]> content, byte[] extra,
+ EntryMode mode) {
+ this.name = name;
+ this.date = date;
+ this.content = content;
+ this.extra = extra;
+ this.mode = mode;
+ }
+
+ public void assertNext(ZipInputStream zipInput) throws IOException {
+ ZipEntry zipEntry = zipInput.getNextEntry();
+ assertNotNull(zipEntry);
+ switch (mode) {
+ case EXPECT_DEFLATE:
+ assertEquals(ZipEntry.DEFLATED, zipEntry.getMethod());
+ break;
+ case EXPECT_STORED:
+ assertEquals(ZipEntry.STORED, zipEntry.getMethod());
+ break;
+ default:
+ // we don't care.
+ break;
+ }
+ assertEquals(name, zipEntry.getName());
+ if (date != null) {
+ assertEquals(date.getTime(), zipEntry.getTime());
+ }
+ assertSameByteArray(extra, zipEntry.getExtra());
+ content.accept(readZipEntryContent(zipInput));
+ }
+ }
+
+ private final List<FakeZipEntry> entries = new ArrayList<>();
+
+ public FakeZipFile addEntry(String name, String content) {
+ entries.add(new FakeZipEntry(name, null, content, null, EntryMode.DONT_CARE));
+ return this;
+ }
+
+ public FakeZipFile addEntry(String name, String content, boolean compressed) {
+ entries.add(new FakeZipEntry(name, null, content, null,
+ compressed ? EntryMode.EXPECT_DEFLATE : EntryMode.EXPECT_STORED));
+ return this;
+ }
+
+ public FakeZipFile addEntry(String name, Date date, String content) {
+ entries.add(new FakeZipEntry(name, date, content, null, EntryMode.DONT_CARE));
+ return this;
+ }
+
+ public FakeZipFile addEntry(String name, Date date, String content, boolean compressed) {
+ entries.add(new FakeZipEntry(name, date, content, null,
+ compressed ? EntryMode.EXPECT_DEFLATE : EntryMode.EXPECT_STORED));
+ return this;
+ }
+
+ public FakeZipFile addEntry(String name, Receiver<byte[]> content) {
+ entries.add(new FakeZipEntry(name, null, content, null, EntryMode.DONT_CARE));
+ return this;
+ }
+
+ public FakeZipFile addEntry(String name, Receiver<byte[]> content, boolean compressed) {
+ entries.add(new FakeZipEntry(name, null, content, null,
+ compressed ? EntryMode.EXPECT_DEFLATE : EntryMode.EXPECT_STORED));
+ return this;
+ }
+
+ public FakeZipFile addEntry(String name, Date date, Receiver<byte[]> content) {
+ entries.add(new FakeZipEntry(name, date, content, null, EntryMode.DONT_CARE));
+ return this;
+ }
+
+ public FakeZipFile addEntry(String name, Date date, Receiver<byte[]> content,
+ boolean compressed) {
+ entries.add(new FakeZipEntry(name, date, content, null,
+ compressed ? EntryMode.EXPECT_DEFLATE : EntryMode.EXPECT_STORED));
+ return this;
+ }
+
+ public FakeZipFile addEntry(String name, byte[] extra) {
+ entries.add(new FakeZipEntry(name, null, (String) null, extra, EntryMode.DONT_CARE));
+ return this;
+ }
+
+ public FakeZipFile addEntry(String name, byte[] extra, boolean compressed) {
+ entries.add(new FakeZipEntry(name, null, (String) null, extra,
+ compressed ? EntryMode.EXPECT_DEFLATE : EntryMode.EXPECT_STORED));
+ return this;
+ }
+
+ private byte[] preamble = null;
+
+ public FakeZipFile addPreamble(byte[] contents) {
+ preamble = Arrays.copyOf(contents, contents.length);
+ return this;
+ }
+
+ private int getUnsignedShort(byte[] source, int offset) {
+ int a = source[offset + 0] & 0xff;
+ int b = source[offset + 1] & 0xff;
+ return (b << 8) | a;
+ }
+
+ public void assertSame(byte[] data) throws IOException {
+ int offset = 0;
+ int length = data.length;
+ if (preamble != null) {
+ offset += preamble.length;
+ length -= offset;
+ byte[] maybePreamble = Arrays.copyOfRange(data, 0, offset);
+ assertTrue(Arrays.equals(preamble, maybePreamble));
+ }
+ ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(data, offset, length));
+ for (FakeZipEntry entry : entries) {
+ entry.assertNext(zipInput);
+ }
+ assertNull(zipInput.getNextEntry());
+ // Verify that the end of central directory data is correct.
+ // This assumes that the end of directory is at the end of input and that there is no zip file
+ // comment.
+ int count = getUnsignedShort(data, data.length-14);
+ assertEquals(entries.size(), count);
+ count = getUnsignedShort(data, data.length-12);
+ assertEquals(entries.size(), count);
+ }
+
+ /**
+ * Assert that {@code expected} is the same zip file as {@code actual}. It is similar to
+ * {@link org.junit.Assert#assertArrayEquals(byte[], byte[])} but should use a more
+ * helpful error message.
+ */
+ public static void assertSame(byte[] expected, byte[] actual) throws IOException {
+ // First parse the zip files, then compare to have explicit comparison messages.
+ ZipInputStream expectedZip = new ZipInputStream(new ByteArrayInputStream(expected));
+ ZipInputStream actualZip = new ZipInputStream(new ByteArrayInputStream(actual));
+ StringBuffer actualFileList = new StringBuffer();
+ StringBuffer expectedFileList = new StringBuffer();
+ Map<String, ZipEntry> actualEntries = new HashMap<String, ZipEntry>();
+ Map<String, ZipEntry> expectedEntries = new HashMap<String, ZipEntry>();
+ Map<String, byte[]> actualEntryContents = new HashMap<String, byte[]>();
+ Map<String, byte[]> expectedEntryContents = new HashMap<String, byte[]>();
+ parseZipEntry(expectedZip, expectedFileList, expectedEntries, expectedEntryContents);
+ parseZipEntry(actualZip, actualFileList, actualEntries, actualEntryContents);
+ // Compare the ordered file list first.
+ assertEquals(expectedFileList.toString(), actualFileList.toString());
+
+ // Then compare each entry.
+ for (String name : expectedEntries.keySet()) {
+ ZipEntry expectedEntry = expectedEntries.get(name);
+ ZipEntry actualEntry = actualEntries.get(name);
+ assertEquals("Time differs for " + name, expectedEntry.getTime(), actualEntry.getTime());
+ assertArrayEquals("Extraneous content differs for " + name,
+ expectedEntry.getExtra(), actualEntry.getExtra());
+ assertArrayEquals("Content differs for " + name,
+ expectedEntryContents.get(name), actualEntryContents.get(name));
+ }
+
+ // Finally do a binary array comparison to be sure that test fails if files are different in
+ // some way we don't test.
+ assertArrayEquals(expected, actual);
+ }
+
+ private static void parseZipEntry(ZipInputStream expectedZip, StringBuffer expectedFileList,
+ Map<String, ZipEntry> expectedEntries, Map<String, byte[]> expectedEntryContents)
+ throws IOException {
+ ZipEntry expectedEntry;
+ while ((expectedEntry = expectedZip.getNextEntry()) != null) {
+ expectedFileList.append(expectedEntry.getName()).append("\n");
+ expectedEntries.put(expectedEntry.getName(), expectedEntry);
+ expectedEntryContents.put(expectedEntry.getName(), readZipEntryContent(expectedZip));
+ }
+ }
+}
diff --git a/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/MockSimpleFileSystem.java b/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/MockSimpleFileSystem.java
new file mode 100644
index 0000000000..8fec585fe0
--- /dev/null
+++ b/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/MockSimpleFileSystem.java
@@ -0,0 +1,88 @@
+// Copyright 2015 Google Inc. 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 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 java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * FileSystem for testing. FileSystem supports exactly one one OutputStream for filename
+ * specified in constructor.
+ * Workflow for using this class in tests are following:
+ * <ul>
+ * <li> Construct with exactly one outputFile. </li>
+ * <li> add some input files using method addFile </li>
+ * <li> check content of outputFile calling toByteArray </li>
+ * </ul>
+ */
+public final class MockSimpleFileSystem implements SimpleFileSystem {
+
+ private final String outputFileName;
+ private ByteArrayOutputStream out;
+ private final Map<String, byte[]> files = new HashMap<>();
+
+ public MockSimpleFileSystem(String outputFileName) {
+ this.outputFileName = outputFileName;
+ }
+
+ public void addFile(String name, byte[] content) {
+ files.put(name, content);
+ }
+
+ public void addFile(String name, String content) {
+ files.put(name, content.getBytes(UTF_8));
+ }
+
+ @Override
+ public OutputStream getOutputStream(String filename) {
+ assertEquals(outputFileName, filename);
+ assertNull(out);
+ out = new ByteArrayOutputStream();
+ return out;
+ }
+
+ @Override
+ public InputStream getInputStream(String filename) throws IOException {
+ byte[] data = files.get(filename);
+ if (data == null) {
+ throw new FileNotFoundException();
+ }
+ return new ByteArrayInputStream(data);
+ }
+
+ @Override
+ public boolean delete(String filename) {
+ assertEquals(outputFileName, filename);
+ assertNotNull(out);
+ out = null;
+ return true;
+ }
+
+ public byte[] toByteArray() {
+ assertNotNull(out);
+ return out.toByteArray();
+ }
+}
diff --git a/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/OptionFileExpanderTest.java b/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/OptionFileExpanderTest.java
new file mode 100644
index 0000000000..eea87a5796
--- /dev/null
+++ b/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/OptionFileExpanderTest.java
@@ -0,0 +1,87 @@
+// Copyright 2015 Google Inc. 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 java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import com.google.devtools.build.singlejar.OptionFileExpander.OptionFileProvider;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.ByteArrayInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Unit tests for {@link OptionFileExpander}.
+ */
+@RunWith(JUnit4.class)
+public class OptionFileExpanderTest {
+
+ private static class StoredOptionFileProvider implements OptionFileProvider {
+
+ private Map<String, byte[]> availableFiles = new HashMap<>();
+
+ void addFile(String filename, String content) {
+ availableFiles.put(filename, content.getBytes(UTF_8));
+ }
+
+ @Override
+ public InputStream getInputStream(String filename) throws IOException {
+ byte[] result = availableFiles.get(filename);
+ if (result == null) {
+ throw new FileNotFoundException();
+ }
+ return new ByteArrayInputStream(result);
+ }
+ }
+
+ @Test
+ public void testNoExpansion() throws IOException {
+ OptionFileExpander expander = new OptionFileExpander(new StoredOptionFileProvider());
+ assertEquals(Arrays.asList("--some", "option", "list"),
+ expander.expandArguments(Arrays.asList("--some", "option", "list")));
+ }
+
+ @Test
+ public void testExpandSimpleOptionsFile() throws IOException {
+ StoredOptionFileProvider provider = new StoredOptionFileProvider();
+ provider.addFile("options", "--some option list");
+ OptionFileExpander expander = new OptionFileExpander(provider);
+ assertEquals(Arrays.asList("--some", "option", "list"),
+ expander.expandArguments(Arrays.asList("@options")));
+ }
+
+ @Test
+ public void testIllegalOptionsFile() {
+ StoredOptionFileProvider provider = new StoredOptionFileProvider();
+ provider.addFile("options", "'missing apostrophe");
+ OptionFileExpander expander = new OptionFileExpander(provider);
+ try {
+ expander.expandArguments(Arrays.asList("@options"));
+ fail();
+ } catch (IOException e) {
+ // Expected exception.
+ }
+ }
+}
diff --git a/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/PrefixListPathFilterTest.java b/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/PrefixListPathFilterTest.java
new file mode 100644
index 0000000000..32b5eaeb19
--- /dev/null
+++ b/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/PrefixListPathFilterTest.java
@@ -0,0 +1,54 @@
+// Copyright 2015 Google Inc. 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 org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.singlejar.DefaultJarEntryFilter.PathFilter;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests {@link PrefixListPathFilter}.
+ */
+@RunWith(JUnit4.class)
+public class PrefixListPathFilterTest {
+ private PathFilter filter;
+
+ @Test
+ public void testPrefixList() {
+ filter = new PrefixListPathFilter(ImmutableList.of("dir1", "dir/subdir"));
+ assertIncluded("dir1/file1");
+ assertExcluded("dir2/file1");
+ assertIncluded("dir/subdir/file1");
+ assertExcluded("dir2/subdir/file1");
+ assertExcluded("dir/othersub/file1");
+ assertExcluded("dir3/file1");
+ }
+
+ private void assertExcluded(String path) {
+ assertFalse(path + " should have been excluded, but was included",
+ filter.allowed(path));
+ }
+
+ private void assertIncluded(String path) {
+ assertTrue(path + " should have been included but was not",
+ filter.allowed(path));
+ }
+}
diff --git a/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/RecordingCallback.java b/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/RecordingCallback.java
new file mode 100644
index 0000000000..a3f69ff434
--- /dev/null
+++ b/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/RecordingCallback.java
@@ -0,0 +1,56 @@
+// Copyright 2015 Google Inc. 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 com.google.devtools.build.singlejar.ZipEntryFilter.CustomMergeStrategy;
+import com.google.devtools.build.singlejar.ZipEntryFilter.StrategyCallback;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * A helper implementation of {@link StrategyCallback} that records callback
+ * invocations as string.
+ */
+public final class RecordingCallback implements StrategyCallback {
+
+ public final List<String> calls = new ArrayList<>();
+ public final List<Date> dates = new ArrayList<>();
+
+ @Override
+ public void copy(Date date) {
+ calls.add("copy");
+ dates.add(date);
+ }
+
+ @Override
+ public void rename(String filename, Date date) {
+ calls.add("rename");
+ dates.add(date);
+ }
+
+ @Override
+ public void customMerge(Date date, CustomMergeStrategy strategy) {
+ calls.add("customMerge");
+ dates.add(date);
+ }
+
+ @Override
+ public void skip() {
+ calls.add("skip");
+ }
+}
diff --git a/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/SingleJarTest.java b/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/SingleJarTest.java
new file mode 100644
index 0000000000..dbff1553cb
--- /dev/null
+++ b/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/SingleJarTest.java
@@ -0,0 +1,634 @@
+// Copyright 2015 Google Inc. 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 org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Receiver;
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.jar.JarFile;
+
+/**
+ * Unit tests for {@link SingleJar}.
+ */
+@RunWith(JUnit4.class)
+public class SingleJarTest {
+
+ public static final byte[] EXTRA_FOR_META_INF = new byte[] {(byte) 0xFE, (byte) 0xCA, 0x00, 0x00};
+
+ static final Joiner LINE_JOINER = Joiner.on("\r\n");
+ static final Joiner LINEFEED_JOINER = Joiner.on("\n");
+
+ static enum EntryMode {
+ DONT_CARE, EXPECT_DEFLATE, EXPECT_STORED;
+ }
+
+ public static final class BuildInfoValidator implements Receiver<byte[]> {
+ private final List<String> buildInfoLines;
+
+ public BuildInfoValidator(List<String> buildInfoLines) {
+ this.buildInfoLines = buildInfoLines;
+ }
+
+ @Override
+ public void accept(byte[] content) {
+ String actualBuildInfo = new String(content, StandardCharsets.UTF_8);
+ List<String> expectedBuildInfos = new ArrayList<>();
+ for (String line : buildInfoLines) { // the character : is escaped
+ expectedBuildInfos.add(line.replace(":", "\\:"));
+ }
+ Collections.sort(expectedBuildInfos);
+ String[] actualBuildInfos = actualBuildInfo.split("\n");
+ Arrays.sort(actualBuildInfos);
+ assertEquals(LINEFEED_JOINER.join(expectedBuildInfos),
+ LINEFEED_JOINER.join(actualBuildInfos));
+ }
+
+ }
+
+ // Manifest file line ordering is dependent of the ordering in HashMap (Attributes class) so
+ // we do a sorted comparison for Manifest.
+ public static final class ManifestValidator implements Receiver<byte[]> {
+ private final List<String> manifestLines;
+
+ public ManifestValidator(List<String> manifestLines) {
+ this.manifestLines = new ArrayList<String>(manifestLines);
+ Collections.sort(this.manifestLines);
+ }
+
+ public ManifestValidator(String... manifestLines) {
+ this.manifestLines = Arrays.asList(manifestLines);
+ Collections.sort(this.manifestLines);
+ }
+
+ @Override
+ public void accept(byte[] content) {
+ String actualManifest = new String(content, StandardCharsets.UTF_8);
+ String[] actualManifestLines = actualManifest.trim().split("\r\n");
+ Arrays.sort(actualManifestLines);
+ assertEquals(LINEFEED_JOINER.join(manifestLines), LINEFEED_JOINER.join(actualManifestLines));
+ }
+
+ }
+
+ private BuildInfoValidator redactedBuildData(String outputJar) {
+ return new BuildInfoValidator(ImmutableList.of("build.target=" + outputJar));
+ }
+
+ private BuildInfoValidator redactedBuildData(String outputJar, String mainClass) {
+ return new BuildInfoValidator(
+ ImmutableList.of("build.target=" + outputJar, "main.class=" + mainClass));
+ }
+
+ static List<String> getBuildInfo() {
+ return ImmutableList.of("build.build_id=11111-222-33333",
+ "build.version=12659499",
+ "build.location=user@machine.domain.com:/home/user/source",
+ "build.target=output.jar",
+ "build.time=Fri Jan 2 02:17:36 1970 (123456)",
+ "build.timestamp=Fri Jan 2 02:17:36 1970 (123456)",
+ "build.timestamp.as.int=123456"
+ );
+ }
+
+ private byte[] sampleZip() {
+ ZipFactory factory = new ZipFactory();
+ factory.addFile("hello.txt", "Hello World!");
+ return factory.toByteArray();
+ }
+
+ private byte[] sampleUncompressedZip() {
+ ZipFactory factory = new ZipFactory();
+ factory.addFile("hello.txt", "Hello World!", false);
+ return factory.toByteArray();
+ }
+
+ private byte[] sampleZipWithSF() {
+ ZipFactory factory = new ZipFactory();
+ factory.addFile("hello.SF", "Hello World!");
+ return factory.toByteArray();
+ }
+
+ private byte[] sampleZipWithSubdirs() {
+ ZipFactory factory = new ZipFactory();
+ factory.addFile("dir1/file1", "contents11");
+ factory.addFile("dir1/file2", "contents12");
+ factory.addFile("dir2/file1", "contents21");
+ factory.addFile("dir3/file1", "contents31");
+ return factory.toByteArray();
+ }
+
+ private void assertStripFirstLine(String expected, String testCase) {
+ byte[] result = SingleJar.stripFirstLine(testCase.getBytes(StandardCharsets.UTF_8));
+ assertEquals(expected, new String(result));
+ }
+
+ @Test
+ public void testStripFirstLine() {
+ assertStripFirstLine("", "");
+ assertStripFirstLine("", "no linefeed");
+ assertStripFirstLine(LINEFEED_JOINER.join("toto", "titi"),
+ LINEFEED_JOINER.join("# timestamp comment", "toto", "titi"));
+ assertStripFirstLine(LINE_JOINER.join("toto", "titi"),
+ LINE_JOINER.join("# timestamp comment", "toto", "titi"));
+ }
+
+ @Test
+ public void testEmptyJar() throws IOException {
+ MockSimpleFileSystem mockFs = new MockSimpleFileSystem("output.jar");
+ SingleJar singleJar = new SingleJar(mockFs);
+ singleJar.run(ImmutableList.of("--output", "output.jar"));
+ FakeZipFile expectedResult = new FakeZipFile()
+ .addEntry("META-INF/", EXTRA_FOR_META_INF)
+ .addEntry(JarFile.MANIFEST_NAME, new ManifestValidator(
+ "Manifest-Version: 1.0",
+ "Created-By: blaze-singlejar"))
+ .addEntry("build-data.properties", redactedBuildData("output.jar"));
+ expectedResult.assertSame(mockFs.toByteArray());
+ }
+
+ // Test that two identical calls at different time actually returns identical results
+ @Test
+ public void testDeterministicJar() throws IOException, InterruptedException {
+ MockSimpleFileSystem mockFs1 = new MockSimpleFileSystem("output.jar");
+ SingleJar singleJar1 = new SingleJar(mockFs1);
+ singleJar1.run(ImmutableList.of("--output", "output.jar", "--extra_build_info", "toto=titi",
+ "--normalize"));
+ Thread.sleep(1000); // ensure that we are not at the same seconds
+
+ MockSimpleFileSystem mockFs2 = new MockSimpleFileSystem("output.jar");
+ SingleJar singleJar2 = new SingleJar(mockFs2);
+ singleJar2.run(ImmutableList.of("--output", "output.jar", "--extra_build_info", "toto=titi",
+ "--normalize"));
+
+ FakeZipFile.assertSame(mockFs1.toByteArray(), mockFs2.toByteArray());
+ }
+
+ @Test
+ public void testExtraManifestContent() throws IOException {
+ MockSimpleFileSystem mockFs = new MockSimpleFileSystem("output.jar");
+ SingleJar singleJar = new SingleJar(mockFs);
+ singleJar.run(ImmutableList.of("--output", "output.jar", "--deploy_manifest_lines",
+ "Main-Class: SomeClass", "X-Other: Duh"));
+ FakeZipFile expectedResult = new FakeZipFile()
+ .addEntry("META-INF/", EXTRA_FOR_META_INF)
+ .addEntry(JarFile.MANIFEST_NAME, new ManifestValidator(
+ "Manifest-Version: 1.0",
+ "Created-By: blaze-singlejar",
+ "Main-Class: SomeClass",
+ "X-Other: Duh"))
+ .addEntry("build-data.properties", redactedBuildData("output.jar"));
+ expectedResult.assertSame(mockFs.toByteArray());
+ }
+
+ @Test
+ public void testMultipleExtraManifestContent() throws IOException {
+ MockSimpleFileSystem mockFs = new MockSimpleFileSystem("output.jar");
+ SingleJar singleJar = new SingleJar(mockFs);
+ singleJar.run(ImmutableList.of("--deploy_manifest_lines", "X-Other: Duh",
+ "--output", "output.jar",
+ "--deploy_manifest_lines", "Main-Class: SomeClass"));
+ FakeZipFile expectedResult = new FakeZipFile()
+ .addEntry("META-INF/", EXTRA_FOR_META_INF)
+ .addEntry(JarFile.MANIFEST_NAME, new ManifestValidator(
+ "Manifest-Version: 1.0",
+ "Created-By: blaze-singlejar",
+ "Main-Class: SomeClass",
+ "X-Other: Duh"))
+ .addEntry("build-data.properties", redactedBuildData("output.jar"));
+ expectedResult.assertSame(mockFs.toByteArray());
+ }
+
+ @Test
+ public void testMainClass() throws IOException {
+ MockSimpleFileSystem mockFs = new MockSimpleFileSystem("output.jar");
+ SingleJar singleJar = new SingleJar(mockFs);
+ singleJar.run(ImmutableList.of("--output", "output.jar", "--main_class", "SomeClass"));
+ FakeZipFile expectedResult = new FakeZipFile()
+ .addEntry("META-INF/", EXTRA_FOR_META_INF)
+ .addEntry(JarFile.MANIFEST_NAME, new ManifestValidator(
+ "Manifest-Version: 1.0",
+ "Created-By: blaze-singlejar",
+ "Main-Class: SomeClass"))
+ .addEntry("build-data.properties", redactedBuildData("output.jar", "SomeClass"));
+ expectedResult.assertSame(mockFs.toByteArray());
+ }
+
+ // These four tests test all combinations of compressed/uncompressed input and output.
+ @Test
+ public void testSimpleZip() throws IOException {
+ MockSimpleFileSystem mockFs = new MockSimpleFileSystem("output.jar");
+ mockFs.addFile("test.jar", sampleZip());
+ SingleJar singleJar = new SingleJar(mockFs);
+ singleJar.run(ImmutableList.of("--output", "output.jar", "--sources", "test.jar"));
+ FakeZipFile expectedResult = new FakeZipFile()
+ .addEntry("META-INF/", EXTRA_FOR_META_INF, false)
+ .addEntry(JarFile.MANIFEST_NAME, new ManifestValidator(
+ "Manifest-Version: 1.0",
+ "Created-By: blaze-singlejar"), false)
+ .addEntry("build-data.properties", redactedBuildData("output.jar"), false)
+ .addEntry("hello.txt", "Hello World!", false);
+ expectedResult.assertSame(mockFs.toByteArray());
+ }
+
+ @Test
+ public void testSimpleZipExpectCompressedOutput() throws IOException {
+ MockSimpleFileSystem mockFs = new MockSimpleFileSystem("output.jar");
+ mockFs.addFile("test.jar", sampleZip());
+ SingleJar singleJar = new SingleJar(mockFs);
+ singleJar.run(ImmutableList.of("--output", "output.jar", "--sources", "test.jar",
+ "--compression"));
+ FakeZipFile expectedResult = new FakeZipFile()
+ .addEntry("META-INF/", EXTRA_FOR_META_INF, false)
+ .addEntry(JarFile.MANIFEST_NAME, new ManifestValidator(
+ "Manifest-Version: 1.0",
+ "Created-By: blaze-singlejar"), true)
+ .addEntry("build-data.properties", redactedBuildData("output.jar"), true)
+ .addEntry("hello.txt", "Hello World!", true);
+ expectedResult.assertSame(mockFs.toByteArray());
+ }
+
+ @Test
+ public void testSimpleUncompressedZip() throws IOException {
+ MockSimpleFileSystem mockFs = new MockSimpleFileSystem("output.jar");
+ mockFs.addFile("test.jar", sampleUncompressedZip());
+ SingleJar singleJar = new SingleJar(mockFs);
+ singleJar.run(ImmutableList.of("--output", "output.jar", "--sources", "test.jar"));
+ FakeZipFile expectedResult = new FakeZipFile()
+ .addEntry("META-INF/", EXTRA_FOR_META_INF, false)
+ .addEntry(JarFile.MANIFEST_NAME, new ManifestValidator(ImmutableList.of(
+ "Manifest-Version: 1.0",
+ "Created-By: blaze-singlejar")), false)
+ .addEntry("build-data.properties", redactedBuildData("output.jar"), false)
+ .addEntry("hello.txt", "Hello World!", false);
+ expectedResult.assertSame(mockFs.toByteArray());
+ }
+
+ @Test
+ public void testSimpleUncompressedZipExpectCompressedOutput() throws IOException {
+ MockSimpleFileSystem mockFs = new MockSimpleFileSystem("output.jar");
+ mockFs.addFile("test.jar", sampleUncompressedZip());
+ SingleJar singleJar = new SingleJar(mockFs);
+ singleJar.run(ImmutableList.of("--output", "output.jar", "--sources", "test.jar",
+ "--compression"));
+ FakeZipFile expectedResult = new FakeZipFile()
+ .addEntry("META-INF/", EXTRA_FOR_META_INF, false)
+ .addEntry(JarFile.MANIFEST_NAME, new ManifestValidator(
+ "Manifest-Version: 1.0",
+ "Created-By: blaze-singlejar"), true)
+ .addEntry("build-data.properties", redactedBuildData("output.jar"), true)
+ .addEntry("hello.txt", "Hello World!", true);
+ expectedResult.assertSame(mockFs.toByteArray());
+ }
+
+ // Integration test for option file expansion.
+ @Test
+ public void testOptionFile() throws IOException {
+ MockSimpleFileSystem mockFs = new MockSimpleFileSystem("output.jar");
+ mockFs.addFile("input.jar", sampleZip());
+ mockFs.addFile("options", "--output output.jar --sources input.jar");
+ SingleJar singleJar = new SingleJar(mockFs);
+ singleJar.run(ImmutableList.of("@options"));
+ FakeZipFile expectedResult = new FakeZipFile()
+ .addEntry("META-INF/", EXTRA_FOR_META_INF)
+ .addEntry(JarFile.MANIFEST_NAME, new ManifestValidator(
+ "Manifest-Version: 1.0",
+ "Created-By: blaze-singlejar"))
+ .addEntry("build-data.properties", redactedBuildData("output.jar"))
+ .addEntry("hello.txt", "Hello World!");
+ expectedResult.assertSame(mockFs.toByteArray());
+ }
+
+ @Test
+ public void testSkipsSignatureFiles() throws IOException {
+ MockSimpleFileSystem mockFs = new MockSimpleFileSystem("output.jar");
+ mockFs.addFile("input.jar", sampleZipWithSF());
+ SingleJar singleJar = new SingleJar(mockFs);
+ singleJar.run(ImmutableList.of("--output", "output.jar", "--sources", "input.jar"));
+ FakeZipFile expectedResult = new FakeZipFile()
+ .addEntry("META-INF/", EXTRA_FOR_META_INF)
+ .addEntry(JarFile.MANIFEST_NAME, new ManifestValidator(
+ "Manifest-Version: 1.0",
+ "Created-By: blaze-singlejar"))
+ .addEntry("build-data.properties", redactedBuildData("output.jar"));
+ expectedResult.assertSame(mockFs.toByteArray());
+ }
+
+ @Test
+ public void testSkipsUsingInputPrefixes() throws IOException {
+ MockSimpleFileSystem mockFs = new MockSimpleFileSystem("output.jar");
+ mockFs.addFile("input.jar", sampleZipWithSubdirs());
+ SingleJar singleJar = new SingleJar(mockFs);
+ singleJar.run(ImmutableList.of("--output", "output.jar", "--sources",
+ "input.jar", "--include_prefixes", "dir1", "dir2"));
+
+ FakeZipFile expectedResult = new FakeZipFile()
+ .addEntry("META-INF/", EXTRA_FOR_META_INF)
+ .addEntry(JarFile.MANIFEST_NAME, new ManifestValidator(
+ "Manifest-Version: 1.0",
+ "Created-By: blaze-singlejar"))
+ .addEntry("build-data.properties", redactedBuildData("output.jar"))
+ .addEntry("dir1/file1", "contents11")
+ .addEntry("dir1/file2", "contents12")
+ .addEntry("dir2/file1", "contents21");
+
+ expectedResult.assertSame(mockFs.toByteArray());
+ }
+
+ @Test
+ public void testSkipsUsingMultipleInputPrefixes() throws IOException {
+ MockSimpleFileSystem mockFs = new MockSimpleFileSystem("output.jar");
+ mockFs.addFile("input.jar", sampleZipWithSubdirs());
+ SingleJar singleJar = new SingleJar(mockFs);
+ singleJar.run(ImmutableList.of("--output", "output.jar", "--include_prefixes", "dir2",
+ "--sources", "input.jar", "--include_prefixes", "dir1"));
+
+ FakeZipFile expectedResult = new FakeZipFile()
+ .addEntry("META-INF/", EXTRA_FOR_META_INF)
+ .addEntry(JarFile.MANIFEST_NAME, new ManifestValidator(
+ "Manifest-Version: 1.0",
+ "Created-By: blaze-singlejar"))
+ .addEntry("build-data.properties", redactedBuildData("output.jar"))
+ .addEntry("dir1/file1", "contents11")
+ .addEntry("dir1/file2", "contents12")
+ .addEntry("dir2/file1", "contents21");
+
+ expectedResult.assertSame(mockFs.toByteArray());
+ }
+
+ @Test
+ public void testNormalize() throws IOException {
+ MockSimpleFileSystem mockFs = new MockSimpleFileSystem("output.jar");
+ mockFs.addFile("input.jar", sampleZip());
+ SingleJar singleJar = new SingleJar(mockFs);
+ singleJar.run(ImmutableList.of("--output", "output.jar", "--sources", "input.jar",
+ "--normalize"));
+ FakeZipFile expectedResult = new FakeZipFile()
+ .addEntry("META-INF/", EXTRA_FOR_META_INF, false)
+ .addEntry(JarFile.MANIFEST_NAME, ZipCombiner.DOS_EPOCH, new ManifestValidator(
+ "Manifest-Version: 1.0", "Created-By: blaze-singlejar"), false)
+ .addEntry("build-data.properties", ZipCombiner.DOS_EPOCH,
+ redactedBuildData("output.jar"), false)
+ .addEntry("hello.txt", ZipCombiner.DOS_EPOCH, "Hello World!", false);
+ expectedResult.assertSame(mockFs.toByteArray());
+ }
+
+ @Test
+ public void testNormalizeAndCompress() throws IOException {
+ MockSimpleFileSystem mockFs = new MockSimpleFileSystem("output.jar");
+ mockFs.addFile("input.jar", sampleZip());
+ SingleJar singleJar = new SingleJar(mockFs);
+ singleJar.run(ImmutableList.of("--output", "output.jar", "--sources", "input.jar",
+ "--normalize", "--compression"));
+ FakeZipFile expectedResult = new FakeZipFile()
+ .addEntry("META-INF/", EXTRA_FOR_META_INF, false)
+ .addEntry(JarFile.MANIFEST_NAME, ZipCombiner.DOS_EPOCH, new ManifestValidator(
+ "Manifest-Version: 1.0", "Created-By: blaze-singlejar"), true)
+ .addEntry("build-data.properties", ZipCombiner.DOS_EPOCH,
+ redactedBuildData("output.jar"), true)
+ .addEntry("hello.txt", ZipCombiner.DOS_EPOCH, "Hello World!", true);
+ expectedResult.assertSame(mockFs.toByteArray());
+ }
+
+ @Test
+ public void testAddBuildInfoProperties() throws IOException {
+ List<String> buildInfo = getBuildInfo();
+ FakeZipFile expectedResult = new FakeZipFile()
+ .addEntry("META-INF/", EXTRA_FOR_META_INF, false)
+ .addEntry(JarFile.MANIFEST_NAME, new ManifestValidator(
+ "Manifest-Version: 1.0", "Created-By: blaze-singlejar"), false)
+ .addEntry("build-data.properties", new BuildInfoValidator(buildInfo),
+ false);
+
+ MockSimpleFileSystem mockFs = new MockSimpleFileSystem("output.jar");
+ SingleJar singleJar = new SingleJar(mockFs);
+ List<String> args = new ArrayList<String>();
+ args.add("--output");
+ args.add("output.jar");
+ args.addAll(infoPropertyArguments(buildInfo));
+ singleJar.run(args);
+ expectedResult.assertSame(mockFs.toByteArray());
+ }
+
+ private static List<String> infoPropertyArguments(List<String> buildInfoLines) {
+ List<String> args = new ArrayList<>();
+ for (String s : buildInfoLines) {
+ if (!s.isEmpty()) {
+ args.add("--extra_build_info");
+ args.add(s);
+ }
+ }
+ return args;
+ }
+
+ @Test
+ public void testAddBuildInfoPropertiesFile() throws IOException {
+ MockSimpleFileSystem mockFs = new MockSimpleFileSystem("output.jar");
+ SingleJar singleJar = new SingleJar(mockFs);
+ doTestAddBuildInfoPropertiesFile(mockFs, "output.jar", singleJar);
+ }
+
+ public static void doTestAddBuildInfoPropertiesFile(MockSimpleFileSystem mockFs, String target,
+ SingleJar singleJar) throws IOException {
+ List<String> buildInfo = getBuildInfo();
+ mockFs.addFile("my.properties", makePropertyFileFromBuildInfo(buildInfo));
+ singleJar.run(ImmutableList.of("--output", target, "--build_info_file", "my.properties"));
+
+ FakeZipFile expectedResult = new FakeZipFile()
+ .addEntry("META-INF/", EXTRA_FOR_META_INF, false)
+ .addEntry(JarFile.MANIFEST_NAME,
+ new ManifestValidator("Manifest-Version: 1.0", "Created-By: blaze-singlejar"), false)
+ .addEntry("build-data.properties", new BuildInfoValidator(buildInfo),
+ false);
+ expectedResult.assertSame(mockFs.toByteArray());
+ }
+
+ private static String makePropertyFileFromBuildInfo(List<String> buildInfo) {
+ return LINEFEED_JOINER.join(buildInfo).replace(":", "\\:");
+ }
+
+ @Test
+ public void testAddBuildInfoPropertiesFiles() throws IOException {
+ MockSimpleFileSystem mockFs = new MockSimpleFileSystem("output.jar");
+ SingleJar singleJar = new SingleJar(mockFs);
+ doTestAddBuildInfoPropertiesFiles(mockFs, "output.jar", singleJar);
+ }
+
+ public static void doTestAddBuildInfoPropertiesFiles(MockSimpleFileSystem mockFs, String target,
+ SingleJar singleJar) throws IOException {
+ List<String> buildInfo = getBuildInfo();
+
+ mockFs.addFile("my1.properties", makePropertyFileFromBuildInfo(buildInfo.subList(0, 4)));
+ mockFs.addFile("my2.properties",
+ makePropertyFileFromBuildInfo(buildInfo.subList(4, buildInfo.size())));
+ singleJar.run(ImmutableList.of("--output", target,
+ "--build_info_file", "my1.properties",
+ "--build_info_file", "my2.properties"));
+
+ FakeZipFile expectedResult = new FakeZipFile()
+ .addEntry("META-INF/", EXTRA_FOR_META_INF, false)
+ .addEntry(JarFile.MANIFEST_NAME,
+ new ManifestValidator("Manifest-Version: 1.0", "Created-By: blaze-singlejar"), false)
+ .addEntry("build-data.properties", new BuildInfoValidator(buildInfo),
+ false);
+ expectedResult.assertSame(mockFs.toByteArray());
+ }
+
+ @Test
+ public void testAddBuildInfoPropertiesAndFiles() throws IOException {
+ MockSimpleFileSystem mockFs = new MockSimpleFileSystem("output.jar");
+ SingleJar singleJar = new SingleJar(mockFs);
+ doTestAddBuildInfoPropertiesAndFiles(mockFs, "output.jar", singleJar);
+ }
+
+ public static void doTestAddBuildInfoPropertiesAndFiles(MockSimpleFileSystem mockFs,
+ String target, SingleJar singleJar) throws IOException {
+ List<String> buildInfo = getBuildInfo();
+
+ mockFs.addFile("my1.properties", makePropertyFileFromBuildInfo(buildInfo.subList(0, 4)));
+ mockFs.addFile("my2.properties", makePropertyFileFromBuildInfo(
+ buildInfo.subList(4, buildInfo.size())));
+ List<String> args = ImmutableList.<String>builder()
+ .add("--output").add(target)
+ .add("--build_info_file").add("my1.properties")
+ .add("--build_info_file").add("my2.properties")
+ .addAll(infoPropertyArguments(buildInfo.subList(4, buildInfo.size())))
+ .build();
+
+ singleJar.run(args);
+ FakeZipFile expectedResult = new FakeZipFile()
+ .addEntry("META-INF/", EXTRA_FOR_META_INF, false)
+ .addEntry(JarFile.MANIFEST_NAME,
+ new ManifestValidator("Manifest-Version: 1.0", "Created-By: blaze-singlejar"), false)
+ .addEntry("build-data.properties", new BuildInfoValidator(buildInfo),
+ false);
+ expectedResult.assertSame(mockFs.toByteArray());
+ }
+
+
+ @Test
+ public void testExcludeBuildData() throws IOException {
+ MockSimpleFileSystem mockFs = new MockSimpleFileSystem("output.jar");
+ SingleJar singleJar = new SingleJar(mockFs);
+ doTestExcludeBuildData(mockFs, "output.jar", singleJar);
+ }
+
+ public static void doTestExcludeBuildData(MockSimpleFileSystem mockFs, String target,
+ SingleJar singleJar) throws IOException {
+ singleJar.run(ImmutableList.of("--output", target, "--exclude_build_data"));
+ FakeZipFile expectedResult = new FakeZipFile()
+ .addEntry("META-INF/", EXTRA_FOR_META_INF)
+ .addEntry(JarFile.MANIFEST_NAME, new ManifestValidator(
+ "Manifest-Version: 1.0",
+ "Created-By: blaze-singlejar"));
+ expectedResult.assertSame(mockFs.toByteArray());
+ }
+
+ @Test
+ public void testResourceMapping() throws IOException {
+ MockSimpleFileSystem mockFs = new MockSimpleFileSystem("output.jar");
+ mockFs.addFile("a/b/c", "Test");
+ SingleJar singleJar = new SingleJar(mockFs);
+ singleJar.run(ImmutableList.of("--output", "output.jar", "--exclude_build_data",
+ "--resources", "a/b/c:c/b/a"));
+ FakeZipFile expectedResult = new FakeZipFile()
+ .addEntry("META-INF/", EXTRA_FOR_META_INF)
+ .addEntry(JarFile.MANIFEST_NAME, new ManifestValidator(
+ "Manifest-Version: 1.0",
+ "Created-By: blaze-singlejar"))
+ .addEntry("c/b/a", "Test");
+ expectedResult.assertSame(mockFs.toByteArray());
+ }
+
+ @Test
+ public void testResourceMappingIdentity() throws IOException {
+ MockSimpleFileSystem mockFs = new MockSimpleFileSystem("output.jar");
+ mockFs.addFile("a/b/c", "Test");
+ SingleJar singleJar = new SingleJar(mockFs);
+ singleJar.run(ImmutableList.of("--output", "output.jar", "--exclude_build_data",
+ "--resources", "a/b/c"));
+ FakeZipFile expectedResult = new FakeZipFile()
+ .addEntry("META-INF/", EXTRA_FOR_META_INF)
+ .addEntry(JarFile.MANIFEST_NAME, new ManifestValidator(
+ "Manifest-Version: 1.0",
+ "Created-By: blaze-singlejar"))
+ .addEntry("a/b/c", "Test");
+ expectedResult.assertSame(mockFs.toByteArray());
+ }
+
+ @Test
+ public void testResourceMappingDuplicateError() throws IOException {
+ MockSimpleFileSystem mockFs = new MockSimpleFileSystem("output.jar");
+ mockFs.addFile("a/b/c", "Test");
+ SingleJar singleJar = new SingleJar(mockFs);
+ try {
+ singleJar.run(ImmutableList.of("--output", "output.jar", "--exclude_build_data",
+ "--resources", "a/b/c", "a/b/c"));
+ fail();
+ } catch (IllegalStateException e) {
+ assertTrue(e.getMessage().contains("already contains a file named a/b/c"));
+ }
+ }
+
+ @Test
+ public void testResourceMappingDuplicateWarning() throws IOException {
+ MockSimpleFileSystem mockFs = new MockSimpleFileSystem("output.jar");
+ mockFs.addFile("a/b/c", "Test");
+ SingleJar singleJar = new SingleJar(mockFs);
+ singleJar.run(ImmutableList.of("--output", "output.jar", "--exclude_build_data",
+ "--warn_duplicate_resources", "--resources", "a/b/c", "a/b/c"));
+ FakeZipFile expectedResult = new FakeZipFile()
+ .addEntry("META-INF/", EXTRA_FOR_META_INF)
+ .addEntry(JarFile.MANIFEST_NAME, new ManifestValidator(
+ "Manifest-Version: 1.0",
+ "Created-By: blaze-singlejar"))
+ .addEntry("a/b/c", "Test");
+ expectedResult.assertSame(mockFs.toByteArray());
+ }
+
+ @Test
+ public void testCanAddPreamble() throws IOException {
+ MockSimpleFileSystem mockFs = new MockSimpleFileSystem("output.jar");
+ String preamble = "WeThePeople";
+ mockFs.addFile(preamble, preamble.getBytes());
+ SingleJar singleJar = new SingleJar(mockFs);
+ singleJar.run(ImmutableList.of("--output", "output.jar",
+ "--java_launcher", preamble,
+ "--main_class", "SomeClass"));
+ FakeZipFile expectedResult = new FakeZipFile()
+ .addPreamble(preamble.getBytes())
+ .addEntry("META-INF/", EXTRA_FOR_META_INF)
+ .addEntry(JarFile.MANIFEST_NAME, new ManifestValidator(
+ "Manifest-Version: 1.0",
+ "Created-By: blaze-singlejar",
+ "Main-Class: SomeClass"))
+ .addEntry("build-data.properties", redactedBuildData("output.jar", "SomeClass"));
+ expectedResult.assertSame(mockFs.toByteArray());
+ }
+}
diff --git a/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/SingleJarTests.java b/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/SingleJarTests.java
new file mode 100644
index 0000000000..8b68004e72
--- /dev/null
+++ b/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/SingleJarTests.java
@@ -0,0 +1,27 @@
+// Copyright 2015 Google Inc. 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 com.google.devtools.build.lib.testutil.ClasspathSuite;
+
+import org.junit.runner.RunWith;
+
+/**
+ * A test-suite builder for this package.
+ */
+@RunWith(ClasspathSuite.class)
+public class SingleJarTests {
+}
diff --git a/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/SlowConcatenateStrategy.java b/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/SlowConcatenateStrategy.java
new file mode 100644
index 0000000000..d1a50918d2
--- /dev/null
+++ b/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/SlowConcatenateStrategy.java
@@ -0,0 +1,45 @@
+// Copyright 2015 Google Inc. 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 com.google.devtools.build.singlejar.ZipEntryFilter.CustomMergeStrategy;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import javax.annotation.concurrent.NotThreadSafe;
+
+/**
+ * A strategy that merges a set of files by concatenating them. It inserts no
+ * additional characters and copies bytes one by one. Used for testing.
+ */
+@NotThreadSafe
+final class SlowConcatenateStrategy implements CustomMergeStrategy {
+
+ @Override
+ public void merge(InputStream in, OutputStream out) throws IOException {
+ int nextByte;
+ while ((nextByte = in.read()) != -1) {
+ out.write(nextByte);
+ }
+ }
+
+ @Override
+ public void finish(OutputStream out) {
+ // No need to do anything. All the data was already written.
+ }
+}
diff --git a/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/ZipCombinerTest.java b/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/ZipCombinerTest.java
new file mode 100644
index 0000000000..e5345cb1f8
--- /dev/null
+++ b/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/ZipCombinerTest.java
@@ -0,0 +1,936 @@
+// Copyright 2015 Google Inc. 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.base.Preconditions;
+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 org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.jar.JarOutputStream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+import java.util.zip.ZipOutputStream;
+
+/**
+ * Unit tests for {@link ZipCombiner}.
+ */
+@RunWith(JUnit4.class)
+public class ZipCombinerTest {
+
+ private static final Date DOS_EPOCH = ZipCombiner.DOS_EPOCH;
+
+ private InputStream sampleZip() {
+ ZipFactory factory = new ZipFactory();
+ factory.addFile("hello.txt", "Hello World!");
+ return factory.toInputStream();
+ }
+
+ private InputStream sampleZip2() {
+ ZipFactory factory = new ZipFactory();
+ factory.addFile("hello2.txt", "Hello World 2!");
+ return factory.toInputStream();
+ }
+
+ private InputStream sampleZipWithTwoEntries() {
+ ZipFactory factory = new ZipFactory();
+ factory.addFile("hello.txt", "Hello World!");
+ factory.addFile("hello2.txt", "Hello World 2!");
+ return factory.toInputStream();
+ }
+
+ private InputStream sampleZipWithOneUncompressedEntry() {
+ ZipFactory factory = new ZipFactory();
+ factory.addFile("hello.txt", "Hello World!", false);
+ return factory.toInputStream();
+ }
+
+ private InputStream sampleZipWithTwoUncompressedEntries() {
+ ZipFactory factory = new ZipFactory();
+ factory.addFile("hello.txt", "Hello World!", false);
+ factory.addFile("hello2.txt", "Hello World 2!", false);
+ return factory.toInputStream();
+ }
+
+ 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, ZipCombiner.DOS_EPOCH.getTime(), 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 testDateToDosTime() {
+ assertEquals(0x210000, ZipCombiner.dateToDosTime(ZipCombiner.DOS_EPOCH));
+ Calendar calendar = new GregorianCalendar();
+ for (int i = 1980; i <= 2107; i++) {
+ calendar.set(i, 0, 1, 0, 0, 0);
+ int result = ZipCombiner.dateToDosTime(calendar.getTime());
+ assertEquals(i - 1980, result >>> 25);
+ assertEquals(1, (result >> 21) & 0xf);
+ assertEquals(1, (result >> 16) & 0x1f);
+ assertEquals(0, result & 0xffff);
+ }
+ }
+
+ @Test
+ public void testDateToDosTimeFailsForBadValues() {
+ try {
+ Calendar calendar = new GregorianCalendar();
+ calendar.set(1979, 0, 1, 0, 0, 0);
+ ZipCombiner.dateToDosTime(calendar.getTime());
+ fail();
+ } catch (IllegalArgumentException e) {
+ /* Expected exception. */
+ }
+ try {
+ Calendar calendar = new GregorianCalendar();
+ calendar.set(2108, 0, 1, 0, 0, 0);
+ ZipCombiner.dateToDosTime(calendar.getTime());
+ fail();
+ } catch (IllegalArgumentException e) {
+ /* Expected exception. */
+ }
+ }
+
+ @Test
+ public void testCompressedDontCare() throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ZipCombiner singleJar = new ZipCombiner(out);
+ singleJar.addZip(sampleZip());
+ singleJar.close();
+ FakeZipFile expectedResult = new FakeZipFile()
+ .addEntry("hello.txt", "Hello World!", true);
+ expectedResult.assertSame(out.toByteArray());
+ }
+
+ @Test
+ public void testCompressedForceDeflate() throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ZipCombiner singleJar = new ZipCombiner(OutputMode.FORCE_DEFLATE, out);
+ singleJar.addZip(sampleZip());
+ singleJar.close();
+ FakeZipFile expectedResult = new FakeZipFile()
+ .addEntry("hello.txt", "Hello World!", true);
+ expectedResult.assertSame(out.toByteArray());
+ }
+
+ @Test
+ public void testCompressedForceStored() throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ZipCombiner singleJar = new ZipCombiner(OutputMode.FORCE_STORED, out);
+ singleJar.addZip(sampleZip());
+ singleJar.close();
+ FakeZipFile expectedResult = new FakeZipFile()
+ .addEntry("hello.txt", "Hello World!", false);
+ expectedResult.assertSame(out.toByteArray());
+ }
+
+ @Test
+ public void testUncompressedDontCare() throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ZipCombiner singleJar = new ZipCombiner(out);
+ singleJar.addZip(sampleZipWithOneUncompressedEntry());
+ singleJar.close();
+ FakeZipFile expectedResult = new FakeZipFile()
+ .addEntry("hello.txt", "Hello World!", false);
+ expectedResult.assertSame(out.toByteArray());
+ }
+
+ @Test
+ public void testUncompressedForceDeflate() throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ZipCombiner singleJar = new ZipCombiner(OutputMode.FORCE_DEFLATE, out);
+ singleJar.addZip(sampleZipWithOneUncompressedEntry());
+ singleJar.close();
+ FakeZipFile expectedResult = new FakeZipFile()
+ .addEntry("hello.txt", "Hello World!", true);
+ expectedResult.assertSame(out.toByteArray());
+ }
+
+ @Test
+ public void testUncompressedForceStored() throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ZipCombiner singleJar = new ZipCombiner(OutputMode.FORCE_STORED, out);
+ singleJar.addZip(sampleZipWithOneUncompressedEntry());
+ singleJar.close();
+ FakeZipFile expectedResult = new FakeZipFile()
+ .addEntry("hello.txt", "Hello World!", false);
+ expectedResult.assertSame(out.toByteArray());
+ }
+
+ @Test
+ public void testCopyTwoEntries() throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ZipCombiner singleJar = new ZipCombiner(out);
+ singleJar.addZip(sampleZipWithTwoEntries());
+ singleJar.close();
+ 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();
+ ZipCombiner singleJar = new ZipCombiner(out);
+ singleJar.addZip(sampleZipWithTwoUncompressedEntries());
+ singleJar.close();
+ 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();
+ ZipCombiner singleJar = new ZipCombiner(out);
+ singleJar.addZip(sampleZip());
+ singleJar.addZip(sampleZip2());
+ singleJar.close();
+ 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();
+ ZipCombiner singleJar = new ZipCombiner(out);
+ singleJar.addZip(sampleZip());
+ singleJar.addZip(sampleZip());
+ singleJar.close();
+ ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
+ assertEntry(zipInput, "hello.txt", "Hello World!");
+ assertNull(zipInput.getNextEntry());
+ }
+
+ // Returns an input stream that can only read one byte at a time.
+ private InputStream slowRead(final InputStream in) {
+ return new InputStream() {
+ @Override
+ public int read() throws IOException {
+ return in.read();
+ }
+ @Override
+ public int read(byte b[], int off, int len) throws IOException {
+ Preconditions.checkArgument(b != null);
+ Preconditions.checkArgument((len >= 0) && (off >= 0));
+ Preconditions.checkArgument(len <= b.length - off);
+ if (len == 0) {
+ return 0;
+ }
+ int value = read();
+ if (value == -1) {
+ return -1;
+ }
+ b[off] = (byte) value;
+ return 1;
+ }
+ };
+ }
+
+ @Test
+ public void testDuplicateUncompressedEntryWithSlowRead() throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ZipCombiner singleJar = new ZipCombiner(out);
+ singleJar.addZip(slowRead(sampleZipWithOneUncompressedEntry()));
+ singleJar.addZip(slowRead(sampleZipWithOneUncompressedEntry()));
+ singleJar.close();
+ ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
+ assertEntry(zipInput, "hello.txt", "Hello World!");
+ assertNull(zipInput.getNextEntry());
+ }
+
+ @Test
+ public void testDuplicateEntryWithSlowRead() throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ZipCombiner singleJar = new ZipCombiner(out);
+ singleJar.addZip(slowRead(sampleZip()));
+ singleJar.addZip(slowRead(sampleZip()));
+ singleJar.close();
+ 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();
+ ZipCombiner singleJar = new ZipCombiner(out);
+ singleJar.addZip(new ByteArrayInputStream(new byte[] { 1, 2, 3, 4 }));
+ singleJar.close();
+ 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();
+ ZipCombiner singleJar = new ZipCombiner(out);
+ singleJar.addFile("hello.txt", DOS_EPOCH, asStream("Hello World!"));
+ singleJar.close();
+ 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();
+ ZipCombiner singleJar = new ZipCombiner(out);
+ singleJar.addFile("hello.txt", DOS_EPOCH, asStream("Hello World!"));
+ singleJar.addZip(sampleZip());
+ singleJar.close();
+ 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 = 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();
+ ZipCombiner singleJar = new ZipCombiner(mockFilter, out);
+ singleJar.addZip(sampleZip());
+ singleJar.close();
+ assertEquals(Arrays.asList("hello.txt"), mockFilter.calls);
+ }
+
+ @Test
+ public void testDuplicateEntryCallsFilterOnce() throws IOException {
+ MockZipEntryFilter mockFilter = new MockZipEntryFilter();
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ZipCombiner singleJar = new ZipCombiner(mockFilter, out);
+ singleJar.addZip(sampleZip());
+ singleJar.addZip(sampleZip());
+ singleJar.close();
+ 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();
+ ZipCombiner singleJar = new ZipCombiner(mockFilter, out);
+ singleJar.addZip(sampleZip());
+ singleJar.addZip(sampleZipWithTwoEntries());
+ singleJar.close();
+ 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();
+ ZipCombiner singleJar = new ZipCombiner(mockFilter, out);
+ singleJar.addZip(sampleZipWithTwoUncompressedEntries());
+ singleJar.addZip(sampleZipWithTwoUncompressedEntries());
+ singleJar.close();
+ 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 testMergeStrategyWithUncompressedEntriesAndSlowRead() throws IOException {
+ MockZipEntryFilter mockFilter = new MockZipEntryFilter();
+ mockFilter.behavior.put("hello.txt", new ConcatenateStrategy());
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ZipCombiner singleJar = new ZipCombiner(mockFilter, out);
+ singleJar.addZip(slowRead(sampleZipWithOneUncompressedEntry()));
+ singleJar.addZip(slowRead(sampleZipWithTwoUncompressedEntries()));
+ singleJar.close();
+ 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 testMergeStrategyWithSlowCopy() throws IOException {
+ MockZipEntryFilter mockFilter = new MockZipEntryFilter();
+ mockFilter.behavior.put("hello.txt", new SlowConcatenateStrategy());
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ZipCombiner singleJar = new ZipCombiner(mockFilter, out);
+ singleJar.addZip(sampleZip());
+ singleJar.addZip(sampleZipWithTwoEntries());
+ singleJar.close();
+ 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();
+ ZipCombiner singleJar = new ZipCombiner(mockFilter, out);
+ singleJar.addZip(sampleZipWithTwoUncompressedEntries());
+ singleJar.addZip(sampleZipWithTwoUncompressedEntries());
+ singleJar.close();
+ 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 InputStream specialZipWithMinusOne() {
+ ZipFactory factory = new ZipFactory();
+ factory.addFile("hello.txt", new byte[] {-1});
+ return factory.toInputStream();
+ }
+
+ @Test
+ public void testMergeStrategyWithSlowCopyAndNegativeBytes() throws IOException {
+ MockZipEntryFilter mockFilter = new MockZipEntryFilter();
+ mockFilter.behavior.put("hello.txt", new SlowConcatenateStrategy());
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ZipCombiner singleJar = new ZipCombiner(mockFilter, out);
+ singleJar.addZip(specialZipWithMinusOne());
+ singleJar.close();
+ 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();
+ ZipCombiner singleJar = new ZipCombiner(mockFilter, out);
+ singleJar.addZip(sampleZip());
+ singleJar.close();
+ 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();
+ ZipCombiner singleJar = new ZipCombiner(mockFilter, out);
+ singleJar.addZip(sampleZip());
+ singleJar.addZip(sampleZipWithTwoEntries());
+ singleJar.close();
+ assertEquals(Arrays.asList("hello.txt", "hello2.txt"), mockFilter.calls);
+ ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
+ assertEntry(zipInput, "hello2.txt", 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 singleJar = new ZipCombiner(badFilter, out)) {
+ singleJar.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 singleJar = new ZipCombiner(badFilter, out)) {
+ singleJar.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();
+ ZipCombiner singleJar = new ZipCombiner(mockFilter, out);
+ singleJar.addZip(sampleZipWithTwoEntries());
+ singleJar.addZip(sampleZipWithTwoEntries());
+ singleJar.close();
+ 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();
+ ZipCombiner singleJar = new ZipCombiner(mockFilter, out);
+ singleJar.addZip(sampleZipWithTwoEntries());
+ singleJar.addZip(sampleZipWithTwoEntries());
+ singleJar.close();
+ 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();
+ ZipCombiner singleJar = new ZipCombiner(mockFilter, out);
+ singleJar.addZip(sampleZipWithTwoEntries());
+ singleJar.addZip(sampleZipWithTwoEntries());
+ singleJar.addZip(sampleZipWithTwoEntries());
+ singleJar.close();
+ 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();
+ ZipCombiner singleJar = new ZipCombiner(mockFilter, out);
+ singleJar.addZip(sampleZipWithTwoEntries());
+ singleJar.addZip(sampleZipWithTwoEntries());
+ singleJar.addZip(sampleZipWithTwoEntries());
+ singleJar.close();
+ 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();
+ ZipCombiner singleJar = new ZipCombiner(mockFilter, out);
+ singleJar.addZip(sampleZipWithTwoEntries());
+ singleJar.addZip(sampleZipWithTwoEntries());
+ singleJar.addZip(sampleZipWithTwoEntries());
+ singleJar.close();
+ 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();
+ ZipCombiner singleJar = new ZipCombiner(mockFilter, out);
+ singleJar.addZip(sampleZipWithTwoUncompressedEntries());
+ singleJar.addZip(sampleZipWithTwoUncompressedEntries());
+ singleJar.addZip(sampleZipWithTwoUncompressedEntries());
+ singleJar.close();
+ 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;
+ private static final int DATA_DESCRIPTOR_MARKER = 0x08074b50;
+ private static final byte[] DATA_DESCRIPTOR_MARKER_AS_BYTES = new byte[] {
+ 0x50, 0x4b, 0x07, 0x08
+ };
+
+ // Create a ZIP with an data descriptor marker in the DEFLATE content of a
+ // file. To do that, we build the ZIP byte by byte.
+ private InputStream zipWithUnexpectedDataDescriptorMarker() {
+ ByteBuffer out = ByteBuffer.wrap(new byte[200]).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) 8); // general purpose bit flag
+ out.putShort((short) ZipOutputStream.DEFLATED); // compression method
+ out.putShort((short) 0); // mtime (00:00:00)
+ out.putShort((short) 0x21); // mdate (1.1.1980)
+ out.putInt(0); // crc32
+ out.putInt(0); // compressed size
+ out.putInt(0); // 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); // deflated content block is last block and uncompressed
+ out.putShort((short) 4); // uncompressed block length
+ out.putShort((short) ~4); // negated uncompressed block length
+ out.putInt(DATA_DESCRIPTOR_MARKER); // 4 bytes uncompressed data
+
+ // data descriptor
+ out.putInt(DATA_DESCRIPTOR_MARKER); // data descriptor with marker
+ out.putInt((int) ZipFactory.calculateCrc32(DATA_DESCRIPTOR_MARKER_AS_BYTES));
+ out.putInt(9);
+ out.putInt(4);
+ // We omit the central directory here. It's currently not used by
+ // ZipCombiner or by java.util.zip.ZipInputStream, so that shouldn't be a
+ // problem.
+ return new ByteArrayInputStream(out.array());
+ }
+
+ // Check that the created ZIP is correct.
+ @Test
+ public void testZipWithUnexpectedDataDescriptorMarkerIsCorrect() throws IOException {
+ ZipInputStream zipInput = new ZipInputStream(zipWithUnexpectedDataDescriptorMarker());
+ assertEntry(zipInput, "a", DATA_DESCRIPTOR_MARKER_AS_BYTES);
+ assertNull(zipInput.getNextEntry());
+ }
+
+ // Check that ZipCombiner handles the ZIP correctly.
+ @Test
+ public void testZipWithUnexpectedDataDescriptorMarker() throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ZipCombiner singleJar = new ZipCombiner(out);
+ singleJar.addZip(zipWithUnexpectedDataDescriptorMarker());
+ singleJar.close();
+ ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(out.toByteArray()));
+ assertEntry(zipInput, "a", DATA_DESCRIPTOR_MARKER_AS_BYTES);
+ assertNull(zipInput.getNextEntry());
+ }
+
+ // 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 singleJar = new ZipCombiner(out)) {
+ singleJar.addZip(zipWithPartialEntry());
+ fail();
+ } catch (EOFException e) {
+ // Expected exception.
+ }
+ }
+
+ @Test
+ public void testSimpleJarAgainstJavaUtil() throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ JarOutputStream jarOut = new JarOutputStream(out);
+ ZipEntry entry;
+ entry = new ZipEntry("META-INF/");
+ entry.setTime(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(DOS_EPOCH.getTime());
+ entry.setMethod(JarOutputStream.DEFLATED);
+ jarOut.putNextEntry(entry);
+ jarOut.write(new byte[] { 1, 2, 3, 4 });
+ jarOut.close();
+ byte[] javaFile = out.toByteArray();
+ out.reset();
+
+ ZipCombiner singleJar = new ZipCombiner(out);
+ singleJar.addDirectory("META-INF/", DOS_EPOCH,
+ new ExtraData[] { new ExtraData((short) 0xCAFE, new byte[0]) });
+ singleJar.addFile("META-INF/MANIFEST.MF", DOS_EPOCH,
+ new ByteArrayInputStream(new byte[] { 1, 2, 3, 4 }));
+ singleJar.close();
+ byte[] singlejarFile = out.toByteArray();
+
+ new ZipTester(singlejarFile).validate();
+ assertZipFilesEquivalent(singlejarFile, javaFile);
+ }
+
+ void assertZipFilesEquivalent(byte[] x, byte[] y) {
+ assertEquals(x.length, y.length);
+
+ for (int i = 0; i < x.length; i++) {
+ if (x[i] != y[i]) {
+ // Allow general purpose bit 11 (UTF-8 encoding) used in jdk7 to differ
+ assertEquals("at position " + i, 0x08, x[i] ^ y[i]);
+ // Check that x[i] is the second byte of a general purpose bit flag.
+ // Phil Katz, you will never be forgotten.
+ assertTrue(
+ // Local header
+ x[i-7] == 'P' && x[i-6] == 'K' && x[i-5] == 3 && x[i-4] == 4 ||
+ // Central directory header
+ x[i-9] == 'P' && x[i-8] == 'K' && x[i-7] == 1 && x[i-6] == 2);
+ }
+ }
+ }
+
+ /**
+ * 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;
+ for (int blockSize : new int[] { 1, 2, 3, 4, 10, 1000 }) {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ZipCombiner zipCombiner = new ZipCombiner(
+ OutputMode.DONT_CARE, new CopyEntryFilter(), out, blockSize);
+ for (int i = 0; i < fileCount; i++) {
+ zipCombiner.addFile("hello" + i, DOS_EPOCH, asStream("Hello " + i + "!"));
+ }
+ zipCombiner.close();
+ 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();
+ }
+ }
+}
diff --git a/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/ZipFactory.java b/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/ZipFactory.java
new file mode 100644
index 0000000000..a6474fa704
--- /dev/null
+++ b/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/ZipFactory.java
@@ -0,0 +1,106 @@
+// Copyright 2015 Google Inc. 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 java.nio.charset.StandardCharsets.ISO_8859_1;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.zip.CRC32;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+/**
+ * A helper class to create zip files for testing.
+ */
+public class ZipFactory {
+
+ static class Entry {
+ private final String name;
+ private final byte[] content;
+ private final boolean compressed;
+ private Entry(String name, byte[] content, boolean compressed) {
+ this.name = name;
+ this.content = content;
+ this.compressed = compressed;
+ }
+ }
+
+ private final List<Entry> entries = new ArrayList<>();
+
+ // Assumes that content was created locally. Does not perform a defensive copy!
+ private void addEntry(String name, byte[] content, boolean compressed) {
+ entries.add(new Entry(name, content, compressed));
+ }
+
+ public ZipFactory addFile(String name, String content) {
+ addEntry(name, content.getBytes(ISO_8859_1), true);
+ return this;
+ }
+
+ public ZipFactory addFile(String name, byte[] content) {
+ addEntry(name, content.clone(), true);
+ return this;
+ }
+
+ public ZipFactory addFile(String name, String content, boolean compressed) {
+ addEntry(name, content.getBytes(ISO_8859_1), compressed);
+ return this;
+ }
+
+ public ZipFactory addFile(String name, byte[] content, boolean compressed) {
+ addEntry(name, content.clone(), compressed);
+ return this;
+ }
+
+ public byte[] toByteArray() {
+ try {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ZipOutputStream zipper = new ZipOutputStream(out);
+ for (Entry entry : entries) {
+ ZipEntry zipEntry = new ZipEntry(entry.name);
+ if (entry.compressed) {
+ zipEntry.setMethod(ZipEntry.DEFLATED);
+ } else {
+ zipEntry.setMethod(ZipEntry.STORED);
+ zipEntry.setSize(entry.content.length);
+ zipEntry.setCrc(calculateCrc32(entry.content));
+ }
+ zipEntry.setTime(ZipCombiner.DOS_EPOCH.getTime());
+ zipper.putNextEntry(zipEntry);
+ zipper.write(entry.content);
+ zipper.closeEntry();
+ }
+ zipper.close();
+ return out.toByteArray();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public InputStream toInputStream() {
+ return new ByteArrayInputStream(toByteArray());
+ }
+
+ public static long calculateCrc32(byte[] content) {
+ CRC32 crc = new CRC32();
+ crc.update(content);
+ return crc.getValue();
+ }
+}
diff --git a/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/ZipTester.java b/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/ZipTester.java
new file mode 100644
index 0000000000..c1293d6f92
--- /dev/null
+++ b/src/java_tools/singlejar/javatests/com/google/devtools/build/singlejar/ZipTester.java
@@ -0,0 +1,412 @@
+// Copyright 2015 Google Inc. 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 java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.zip.CRC32;
+import java.util.zip.DataFormatException;
+import java.util.zip.Inflater;
+
+/**
+ * A helper class to validate zip files and provide reasonable diagnostics (better than what zip
+ * does). We might want to make this into a fully-fledged binary some day.
+ */
+final class ZipTester {
+
+ // The following constants are ZIP-specific.
+ private static final int LOCAL_FILE_HEADER_MARKER = 0x04034b50;
+ private static final int DATA_DESCRIPTOR_MARKER = 0x08074b50;
+ private static final int CENTRAL_DIRECTORY_MARKER = 0x02014b50;
+ private static final int END_OF_CENTRAL_DIRECTORY_MARKER = 0x06054b50;
+
+ private static final int FILE_HEADER_BUFFER_SIZE = 26; // without marker
+ private static final int DATA_DESCRIPTOR_BUFFER_SIZE = 12; // without marker
+
+ private static final int DIRECTORY_ENTRY_BUFFER_SIZE = 42; // without marker
+ private static final int END_OF_CENTRAL_DIRECTORY_BUFFER_SIZE = 18; // without marker
+
+ // Set if the size, compressed size and CRC are set to zero, and present in
+ // the data descriptor after the data.
+ private static final int SIZE_MASKED_FLAG = 1 << 3;
+
+ private static final int STORED_METHOD = 0;
+ private static final int DEFLATE_METHOD = 8;
+
+ private static final int VERSION_STORED = 10; // Version 1.0
+ private static final int VERSION_DEFLATE = 20; // Version 2.0
+
+ private static class Entry {
+ private final long pos;
+ private final String name;
+ private final int flags;
+ private final int method;
+ private final int dosTime;
+ Entry(long pos, String name, int flags, int method, int dosTime) {
+ this.pos = pos;
+ this.name = name;
+ this.flags = flags;
+ this.method = method;
+ this.dosTime = dosTime;
+ }
+ }
+
+ private final InputStream in;
+ private final byte[] buffer = new byte[1024];
+ private int bufferLength;
+ private int bufferOffset;
+ private long pos;
+
+ private List<Entry> entries = new ArrayList<Entry>();
+
+ public ZipTester(InputStream in) {
+ this.in = in;
+ }
+
+ public ZipTester(byte[] data) {
+ this(new ByteArrayInputStream(data));
+ }
+
+ private void warn(String msg) {
+ System.err.println("WARNING: " + msg);
+ }
+
+ private void readMoreData(String action) throws IOException {
+ if ((bufferLength > 0) && (bufferOffset > 0)) {
+ System.arraycopy(buffer, bufferOffset, buffer, 0, bufferLength);
+ }
+ if (bufferLength >= buffer.length) {
+ // The buffer size is specifically chosen to avoid this situation.
+ throw new AssertionError("Internal error: buffer overrun.");
+ }
+ bufferOffset = 0;
+ int bytesRead = in.read(buffer, bufferLength, buffer.length - bufferLength);
+ if (bytesRead <= 0) {
+ throw new IOException("Unexpected end of file, while " + action);
+ }
+ bufferLength += bytesRead;
+ }
+
+ private int readByte(String action) throws IOException {
+ if (bufferLength == 0) {
+ readMoreData(action);
+ }
+ byte result = buffer[bufferOffset];
+ bufferOffset++; bufferLength--;
+ pos++;
+ return result & 0xff;
+ }
+
+ private long getUnsignedInt(String action) throws IOException {
+ int a = readByte(action);
+ int b = readByte(action);
+ int c = readByte(action);
+ int d = readByte(action);
+ return ((d << 24) | (c << 16) | (b << 8) | a) & 0xffffffffL;
+ }
+
+ private void readFully(byte[] buffer, String action) throws IOException {
+ for (int i = 0; i < buffer.length; i++) {
+ buffer[i] = (byte) readByte(action);
+ }
+ }
+
+ private void skip(long length, String action) throws IOException {
+ for (long i = 0; i < length; i++) {
+ readByte(action);
+ }
+ }
+
+ private int getUnsignedShort(byte[] source, int offset) {
+ int a = source[offset + 0] & 0xff;
+ int b = source[offset + 1] & 0xff;
+ return (b << 8) | a;
+ }
+
+ private long getUnsignedInt(byte[] source, int offset) {
+ int a = source[offset + 0] & 0xff;
+ int b = source[offset + 1] & 0xff;
+ int c = source[offset + 2] & 0xff;
+ int d = source[offset + 3] & 0xff;
+ return ((d << 24) | (c << 16) | (b << 8) | a) & 0xffffffffL;
+ }
+
+ private class DeflateInputStream extends InputStream {
+
+ private final byte[] singleByteBuffer = new byte[1];
+ private int consumedBytes;
+ private final Inflater inflater = new Inflater(true);
+ private long totalBytesRead;
+
+ private int inflateData(byte[] dest, int off, int len)
+ throws IOException {
+ consumedBytes = 0;
+ int bytesProduced = 0;
+ int bytesConsumed = 0;
+ while ((bytesProduced == 0) && !inflater.finished()) {
+ inflater.setInput(buffer, bufferOffset + bytesConsumed, bufferLength - bytesConsumed);
+ int remainingBefore = inflater.getRemaining();
+ try {
+ bytesProduced = inflater.inflate(dest, off, len);
+ } catch (DataFormatException e) {
+ throw new IOException("Invalid deflate stream in ZIP file.", e);
+ }
+ bytesConsumed += remainingBefore - inflater.getRemaining();
+ consumedBytes = bytesConsumed;
+ if (bytesProduced == 0) {
+ if (inflater.needsDictionary()) {
+ // The DEFLATE algorithm as used in the ZIP file format does not
+ // require an additional dictionary.
+ throw new AssertionError("Inflater unexpectedly requires a dictionary.");
+ } else if (inflater.needsInput()) {
+ readMoreData("need more data for deflate");
+ } else if (inflater.finished()) {
+ return 0;
+ } else {
+ // According to the Inflater specification, this cannot happen.
+ throw new AssertionError("Inflater unexpectedly produced no output.");
+ }
+ }
+ }
+ return bytesProduced;
+ }
+
+ @Override
+ public int read(byte b[], int off, int len) throws IOException {
+ if (inflater.finished()) {
+ return -1;
+ }
+ int length = inflateData(b, off, len);
+ totalBytesRead += consumedBytes;
+ bufferLength -= consumedBytes;
+ bufferOffset += consumedBytes;
+ pos += consumedBytes;
+ return length == 0 ? -1 : length;
+ }
+
+ @Override
+ public int read() throws IOException {
+ int bytesRead = read(singleByteBuffer, 0, 1);
+ return (bytesRead == -1) ? -1 : (singleByteBuffer[0] & 0xff);
+ }
+ }
+
+ private void readEntry() throws IOException {
+ long entrypos = pos - 4;
+ String entryDesc = "file entry at " + Long.toHexString(entrypos);
+ byte[] entryBuffer = new byte[FILE_HEADER_BUFFER_SIZE];
+ readFully(entryBuffer, "reading file header");
+ int versionToExtract = getUnsignedShort(entryBuffer, 0);
+ int flags = getUnsignedShort(entryBuffer, 2);
+ int method = getUnsignedShort(entryBuffer, 4);
+ int dosTime = (int) getUnsignedInt(entryBuffer, 6);
+ int crc32 = (int) getUnsignedInt(entryBuffer, 10);
+ long compressedSize = getUnsignedInt(entryBuffer, 14);
+ long uncompressedSize = getUnsignedInt(entryBuffer, 18);
+ int filenameLength = getUnsignedShort(entryBuffer, 22);
+ int extraLength = getUnsignedShort(entryBuffer, 24);
+
+ byte[] filename = new byte[filenameLength];
+ readFully(filename, "reading file name");
+ skip(extraLength, "skipping extra data");
+
+ String name = new String(filename, "UTF-8");
+ for (int i = 0; i < filename.length; i++) {
+ if ((filename[i] < ' ') || (filename[i] > 127)) {
+ warn(entryDesc + ": file name has unexpected non-ascii characters");
+ }
+ }
+ entryDesc = "file entry '" + name + "' at " + Long.toHexString(entrypos);
+
+ if ((method != STORED_METHOD) && (method != DEFLATE_METHOD)) {
+ throw new IOException(entryDesc + ": unknown method " + method);
+ }
+ if ((flags != 0) && (flags != SIZE_MASKED_FLAG)) {
+ throw new IOException(entryDesc + ": unknown flags " + flags);
+ }
+ if ((method == STORED_METHOD) && (versionToExtract != VERSION_STORED)) {
+ warn(entryDesc + ": unexpected version to extract for stored entry " + versionToExtract);
+ }
+ if ((method == DEFLATE_METHOD) && (versionToExtract != VERSION_DEFLATE)) {
+// warn(entryDesc + ": unexpected version to extract for deflated entry " + versionToExtract);
+ }
+
+ if (method == STORED_METHOD) {
+ if (compressedSize != uncompressedSize) {
+ throw new IOException(entryDesc + ": stored entries should have identical compressed and "
+ + "uncompressed sizes");
+ }
+ skip(compressedSize, entryDesc + "skipping data");
+ } else {
+ // No OS resources are actually allocated.
+ @SuppressWarnings("resource") DeflateInputStream deflater = new DeflateInputStream();
+ long generatedBytes = 0;
+ byte[] deflated = new byte[1024];
+ int readBytes;
+ CRC32 crc = new CRC32();
+ while ((readBytes = deflater.read(deflated)) > 0) {
+ crc.update(deflated, 0, readBytes);
+ generatedBytes += readBytes;
+ }
+ int actualCrc32 = (int) crc.getValue();
+ long consumedBytes = deflater.totalBytesRead;
+ if (flags == SIZE_MASKED_FLAG) {
+ long id = getUnsignedInt("reading footer marker");
+ if (id != DATA_DESCRIPTOR_MARKER) {
+ throw new IOException(entryDesc + ": expected footer at " + Long.toHexString(pos - 4)
+ + ", but found " + Long.toHexString(id));
+ }
+ byte[] footer = new byte[DATA_DESCRIPTOR_BUFFER_SIZE];
+ readFully(footer, "reading footer");
+ crc32 = (int) getUnsignedInt(footer, 0);
+ compressedSize = getUnsignedInt(footer, 4);
+ uncompressedSize = getUnsignedInt(footer, 8);
+ }
+
+ if (consumedBytes != compressedSize) {
+ throw new IOException(entryDesc + ": amount of compressed data does not match value "
+ + "specified in the zip (specified: " + compressedSize + ", actual: " + consumedBytes
+ + ")");
+ }
+ if (generatedBytes != uncompressedSize) {
+ throw new IOException(entryDesc + ": amount of uncompressed data does not match value "
+ + "specified in the zip (specified: " + uncompressedSize + ", actual: "
+ + generatedBytes + ")");
+ }
+ if (crc32 != actualCrc32) {
+ throw new IOException(entryDesc + ": specified crc checksum does not match actual check "
+ + "sum");
+ }
+ }
+ entries.add(new Entry(entrypos, name, flags, method, dosTime));
+ }
+
+ @SuppressWarnings("unused") // A couple of unused local variables.
+ private void validateCentralDirectoryEntry(Entry entry) throws IOException {
+ long entrypos = pos - 4;
+ String entryDesc = "file directory entry '" + entry.name + "' at " + Long.toHexString(entrypos);
+
+ byte[] entryBuffer = new byte[DIRECTORY_ENTRY_BUFFER_SIZE];
+ readFully(entryBuffer, "reading central directory entry");
+ int versionMadeBy = getUnsignedShort(entryBuffer, 0);
+ int versionToExtract = getUnsignedShort(entryBuffer, 2);
+ int flags = getUnsignedShort(entryBuffer, 4);
+ int method = getUnsignedShort(entryBuffer, 6);
+ int dosTime = (int) getUnsignedInt(entryBuffer, 8);
+ int crc32 = (int) getUnsignedInt(entryBuffer, 12);
+ long compressedSize = getUnsignedInt(entryBuffer, 16);
+ long uncompressedSize = getUnsignedInt(entryBuffer, 20);
+ int filenameLength = getUnsignedShort(entryBuffer, 24);
+ int extraLength = getUnsignedShort(entryBuffer, 26);
+ int commentLength = getUnsignedShort(entryBuffer, 28);
+ int diskNumberStart = getUnsignedShort(entryBuffer, 30);
+ int internalAttributes = getUnsignedShort(entryBuffer, 32);
+ int externalAttributes = (int) getUnsignedInt(entryBuffer, 34);
+ long offset = getUnsignedInt(entryBuffer, 38);
+
+ byte[] filename = new byte[filenameLength];
+ readFully(filename, "reading file name");
+ skip(extraLength, "skipping extra data");
+ String name = new String(filename, "UTF-8");
+
+ if (!name.equals(entry.name)) {
+ throw new IOException(entryDesc + ": file name in central directory does not match original "
+ + "name");
+ }
+ if (offset != entry.pos) {
+ throw new IOException(entryDesc);
+ }
+ if (flags != entry.flags) {
+ throw new IOException(entryDesc);
+ }
+ if (method != entry.method) {
+ throw new IOException(entryDesc);
+ }
+ if (dosTime != entry.dosTime) {
+ throw new IOException(entryDesc);
+ }
+ }
+
+ private void validateCentralDirectory() throws IOException {
+ boolean first = true;
+ for (Entry entry : entries) {
+ if (first) {
+ first = false;
+ } else {
+ long id = getUnsignedInt("reading marker");
+ if (id != CENTRAL_DIRECTORY_MARKER) {
+ throw new IOException();
+ }
+ }
+ validateCentralDirectoryEntry(entry);
+ }
+ }
+
+ @SuppressWarnings("unused") // A couple of unused local variables.
+ private void validateEndOfCentralDirectory() throws IOException {
+ long id = getUnsignedInt("expecting end of central directory");
+ byte[] entryBuffer = new byte[END_OF_CENTRAL_DIRECTORY_BUFFER_SIZE];
+ readFully(entryBuffer, "reading end of central directory");
+ int diskNumber = getUnsignedShort(entryBuffer, 0);
+ int startDiskNumber = getUnsignedShort(entryBuffer, 2);
+ int numEntries = getUnsignedShort(entryBuffer, 4);
+ int numTotalEntries = getUnsignedShort(entryBuffer, 6);
+ long centralDirectorySize = getUnsignedInt(entryBuffer, 8);
+ long centralDirectoryOffset = getUnsignedInt(entryBuffer, 12);
+ int commentLength = getUnsignedShort(entryBuffer, 16);
+ if (diskNumber != 0) {
+ throw new IOException(String.format("diskNumber=%d", diskNumber));
+ }
+ if (startDiskNumber != 0) {
+ throw new IOException(String.format("startDiskNumber=%d", diskNumber));
+ }
+ if (numEntries != numTotalEntries) {
+ throw new IOException(String.format("numEntries=%d numTotalEntries=%d",
+ numEntries, numTotalEntries));
+ }
+ if (numEntries != (entries.size() % 0x10000)) {
+ throw new IOException("bad number of entries in central directory footer");
+ }
+ if (numTotalEntries != (entries.size() % 0x10000)) {
+ throw new IOException("bad number of entries in central directory footer");
+ }
+ if (commentLength != 0) {
+ throw new IOException("Zip file comment is unexpected");
+ }
+ if (id != END_OF_CENTRAL_DIRECTORY_MARKER) {
+ throw new IOException("Expected end of central directory marker");
+ }
+ }
+
+ public void validate() throws IOException {
+ while (true) {
+ long id = getUnsignedInt("reading marker");
+ if (id == LOCAL_FILE_HEADER_MARKER) {
+ readEntry();
+ } else if (id == CENTRAL_DIRECTORY_MARKER) {
+ validateCentralDirectory();
+ validateEndOfCentralDirectory();
+ return;
+ } else {
+ throw new IOException("unexpected result for marker: "
+ + Long.toHexString(id) + " at position " + Long.toHexString(pos - 4));
+ }
+ }
+ }
+}