diff options
Diffstat (limited to 'src/java_tools/singlejar/javatests/com/google/devtools')
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)); + } + } + } +} |