diff options
author | 2015-07-07 16:13:40 +0000 | |
---|---|---|
committer | 2015-07-07 16:33:30 +0000 | |
commit | 5a9ec720d1224f1360fa2bf732a869a22c17afc1 (patch) | |
tree | 0a5263ae5814f49c6d4099d3176d6b346f3ac5fb /src/test/java/com/google | |
parent | 2fd9960f0bc43eff04b8bc317e635c754a67dd27 (diff) |
Open source tests for android/ziputils.
--
MOS_MIGRATED_REVID=97677526
Diffstat (limited to 'src/test/java/com/google')
16 files changed, 3218 insertions, 0 deletions
diff --git a/src/test/java/com/google/devtools/build/android/ziputils/AllTests.java b/src/test/java/com/google/devtools/build/android/ziputils/AllTests.java new file mode 100644 index 0000000000..a69b95dc10 --- /dev/null +++ b/src/test/java/com/google/devtools/build/android/ziputils/AllTests.java @@ -0,0 +1,24 @@ +// 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.android.ziputils; + +import com.google.devtools.build.lib.testutil.ClasspathSuite; + +import org.junit.runner.RunWith; + +/** + * Test suite for options parsing framework. + */ +@RunWith(ClasspathSuite.class) +public class AllTests {} diff --git a/src/test/java/com/google/devtools/build/android/ziputils/BufferedFileTest.java b/src/test/java/com/google/devtools/build/android/ziputils/BufferedFileTest.java new file mode 100644 index 0000000000..6a51756dd3 --- /dev/null +++ b/src/test/java/com/google/devtools/build/android/ziputils/BufferedFileTest.java @@ -0,0 +1,229 @@ +// 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.android.ziputils; + +import static com.google.common.truth.Truth.assertWithMessage; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; + +/** + * Unit tests for {@link BufferedFile}. + */ +@RunWith(JUnit4.class) +public class BufferedFileTest { + + private static final FakeFileSystem fileSystem = new FakeFileSystem(); + + @Test + public void testBufferedFile() throws Exception { + int fileSize = 64; + String filename = "bytes64"; + byte[] bytes = fileData(fileSize); + fileSystem.addFile(filename, bytes); + FileChannel file = fileSystem.getInputChannel(filename); + int maxAlloc = 16; + int regionOff = 0; + assertException("channel null", + null, regionOff, fileSize, maxAlloc, NullPointerException.class); + assertException("region offset negative", + file, -1, fileSize, maxAlloc, IllegalArgumentException.class); + assertException("region size negative", + file, regionOff, -1, maxAlloc, IllegalArgumentException.class); + assertException("maxAlloc negative", + file, regionOff, fileSize, -1, IllegalArgumentException.class); + assertException("region exceeds file", + file, regionOff, fileSize + 1, maxAlloc, IllegalArgumentException.class); + assertException("region too long from offset", + file, regionOff + 1, fileSize, maxAlloc, IllegalArgumentException.class); + assertException("region short, still too long", + file, regionOff + 50, fileSize - 49, maxAlloc, IllegalArgumentException.class); + + new BufferedFile(file, regionOff, fileSize, 0); // alloc minimal buffers for request + new BufferedFile(file, regionOff, fileSize, fileSize); // alloc for full region + } + + @Test + public void testGetBufferThrows() throws Exception { + BufferedFile instance; + + int fileSize = 64; + String filename = "bytes64"; + byte[] bytes = fileData(fileSize); + fileSystem.addFile(filename, bytes); + FileChannel file = fileSystem.getInputChannel(filename); + int regionOff = 4; + int regionSize = 50; + int maxAlloc = 16; + instance = new BufferedFile(file, regionOff, regionSize, maxAlloc); + assertException("buffer negative size", + instance, regionOff, -1, IllegalArgumentException.class); + assertException("buffer lower bound", + instance, regionOff - 1, regionSize, IllegalArgumentException.class); + assertException("buffer upper bound", + instance, regionOff + regionSize + 1, 1, IllegalArgumentException.class); + assertException("buffer upper bound zero read", + instance, regionOff + regionSize + 1, 0, IllegalArgumentException.class); + assertException("buffer beyond region non zero read", + instance, regionOff + regionSize, 1, IllegalArgumentException.class); + } + + @Test + public void testGetBufferAllocationLimits() throws Exception { + BufferedFile instance; + int fileSize = 64; + String filename = "bytes64"; + byte[] bytes = fileData(fileSize); + fileSystem.addFile(filename, bytes); + FileChannel file = fileSystem.getInputChannel(filename); + int regionOff = 4; + int regionSize = 50; + int maxAlloc = 16; + instance = new BufferedFile(file, regionOff, regionSize, maxAlloc); + assertCase("buffer, empty, start", instance, regionOff, 0, 0, maxAlloc); + assertWithMessage("buffer, empty, start").that(regionOff + regionSize) + .isEqualTo(instance.limit()); + instance = new BufferedFile(file, regionOff, regionSize, maxAlloc); + assertCase("buffer, empty, end", instance, regionOff + regionSize, 0, 0, 0); + instance = new BufferedFile(file, regionOff, regionSize, maxAlloc); + assertCase("buffer, one, end", instance, regionOff + regionSize - 1, 1, 1, 1); + assertWithMessage("buffer, one, end").that(regionOff + regionSize) + .isEqualTo(instance.limit()); + instance = new BufferedFile(file, regionOff, regionSize, maxAlloc); + assertCase("buffer, small, end", instance, regionOff + regionSize - 2, 2, 2, 2); + assertWithMessage("buffer, small, end").that(regionOff + regionSize) + .isEqualTo(instance.limit()); + instance = new BufferedFile(file, regionOff, regionSize, maxAlloc); + assertCase("buffer, small, start", instance, regionOff, 2, 2, maxAlloc); + assertWithMessage("buffer, small, start").that(regionOff + regionSize) + .isEqualTo(instance.limit()); + instance = new BufferedFile(file, regionOff, regionSize, maxAlloc); + assertCase("buffer, all region", instance, regionOff, regionSize, regionSize, regionSize); + assertWithMessage("buffer, all region").that(regionOff + regionSize) + .isEqualTo(instance.limit()); + instance = new BufferedFile(file, regionOff, regionSize, maxAlloc); + assertCase("buffer, request more", + instance, regionOff + 5, regionSize, regionSize - 5, regionSize - 5); + assertWithMessage("buffer, request more").that(regionOff + regionSize) + .isEqualTo(instance.limit()); + } + + @Test + public void testGetBufferInCache() throws Exception { + BufferedFile instance; + int fileSize = 64; + String filename = "bytes64"; + byte[] bytes = fileData(fileSize); + fileSystem.addFile(filename, bytes); + FileChannel file = fileSystem.getInputChannel(filename); + int regionOff = 5; + int regionSize = 50; + int maxAlloc = 20; + int cacheOff = regionOff + 5; + instance = new BufferedFile(file, regionOff, regionSize, maxAlloc); + instance.getBuffer(cacheOff, maxAlloc); + assertCase("Cached zero buf", instance, cacheOff, 0, 0, maxAlloc); + assertCase("Cached at front", instance, cacheOff, 5, 5, maxAlloc); + assertCase("Cached at end", instance, cacheOff + 2, 5, 5, maxAlloc - 2); + assertCase("Cached", instance, cacheOff, maxAlloc, maxAlloc, maxAlloc); + } + + @Test + public void testGetBufferReadMore() throws Exception { + BufferedFile instance; + int fileSize = 64; + String filename = "bytes64"; + byte[] bytes = fileData(fileSize); + fileSystem.addFile(filename, bytes); + FileChannel file = fileSystem.getInputChannel(filename); + int regionOff = 5; + int regionSize = 50; + int maxAlloc = 20; + int cacheOff = regionOff + 5; + int initialRead = maxAlloc / 2; + instance = new BufferedFile(file, regionOff, regionSize, maxAlloc); + instance.getBuffer(cacheOff, initialRead); + assertCase("Read more overlap", + instance, cacheOff + 1, initialRead, initialRead, maxAlloc - 1); + assertCase("Read more jump", + instance, cacheOff + initialRead + 5, 5, 5, maxAlloc - initialRead - 5); + } + + @Test + public void testGetBufferReallocate() throws Exception { + BufferedFile instance; + int fileSize = 64; + String filename = "bytes64"; + byte[] bytes = fileData(fileSize); + fileSystem.addFile(filename, bytes); + FileChannel file = fileSystem.getInputChannel(filename); + int regionOff = 5; + int regionSize = 50; + int maxAlloc = 20; + int cacheOff = regionOff + 5; + instance = new BufferedFile(file, regionOff, regionSize, maxAlloc); + instance.getBuffer(cacheOff, maxAlloc); + assertCase("Realloc after", instance, cacheOff + maxAlloc, maxAlloc, maxAlloc, maxAlloc); + assertCase("Realloc before", instance, cacheOff, maxAlloc, maxAlloc, maxAlloc); + assertCase("Realloc just after", instance, cacheOff + 5, maxAlloc, maxAlloc, maxAlloc); + assertCase("Realloc just before", instance, cacheOff, maxAlloc, maxAlloc, maxAlloc); + assertCase("Realloc supersize", instance, cacheOff, maxAlloc + 5, maxAlloc + 5, maxAlloc + 5); + } + + void assertException(String msg, FileChannel file, long off, long len, int maxAlloc, + Class<?> expect) { + try { + new BufferedFile(file, off, len, maxAlloc); + fail(msg + " - no exception"); + } catch (Exception ex) { + assertWithMessage(msg + " - exception, ").that(expect).isSameAs(ex.getClass()); + } + } + + void assertException(String msg, BufferedFile instance, long off, int len, Class<?> expect) { + try { + instance.getBuffer(off, len); + fail(msg + " - no exception"); + } catch (Exception ex) { + assertWithMessage(msg + " - exception, ").that(expect).isSameAs(ex.getClass()); + } + } + + void assertCase(String msg, BufferedFile instance, long off, int len, int expectLimit, + int capacityBound) throws IOException { + ByteBuffer buf = instance.getBuffer(off, len); + assertWithMessage(msg + " - position, ").that(0).isEqualTo(buf.position()); + assertWithMessage(msg + " - limit, ").that(expectLimit).isEqualTo(buf.limit()); + assertWithMessage(msg + " - capacity, ").that(buf.capacity()).isAtLeast(expectLimit); + assertWithMessage(msg + " - capacity, ").that(buf.capacity()).isAtMost(capacityBound); + if (len > 0 && expectLimit > 0) { + assertEquals(msg + " - value, ", (byte) off, buf.get(0)); + } + } + + byte[] fileData(int count) { + byte[] bytes = new byte[count]; + for (int i = 0; i < count; i++) { + bytes[i] = (byte) i; + } + return bytes; + } +} diff --git a/src/test/java/com/google/devtools/build/android/ziputils/CentralDirectoryTest.java b/src/test/java/com/google/devtools/build/android/ziputils/CentralDirectoryTest.java new file mode 100644 index 0000000000..e582b527c3 --- /dev/null +++ b/src/test/java/com/google/devtools/build/android/ziputils/CentralDirectoryTest.java @@ -0,0 +1,124 @@ +// 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.android.ziputils; + +import static com.google.devtools.build.android.ziputils.DirectoryEntry.CENTIM; +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * Unit tests for {@link CentralDirectory}. + */ +@RunWith(JUnit4.class) +public class CentralDirectoryTest { + + /** + * Test of viewOf method, of class CentralDirectory. + */ + @Test + public void testViewOf() { + ByteBuffer buffer = ByteBuffer.allocate(100).order(ByteOrder.LITTLE_ENDIAN); + for (int i = 0; i < 100; i++) { + buffer.put((byte) i); + } + buffer.position(20); + buffer.limit(90); + CentralDirectory view = CentralDirectory.viewOf(buffer); + int expPos = 0; + int expLimit = 90; + // expect the buffer to have been reset to 0 (CentralDirectory does NOT slice). + assertEquals("View not at position 0", expPos, view.buffer.position()); + assertEquals("Buffer not at position 0", expPos, buffer.position()); + assertEquals("Buffer limit changed", expLimit, view.buffer.limit()); + assertEquals("Buffer limit changed", expLimit, buffer.limit()); + } + + /** + * Test of parse method, of class CentralDirectory. + */ + @Test + public void testParse() { + // First fill it with some entries + ByteBuffer inputBuffer = ByteBuffer.allocate(10000).order(ByteOrder.LITTLE_ENDIAN); + String comment = null; + byte[] extra = null; + String filename = "pkg/0.txt"; + DirectoryEntry entry = DirectoryEntry.view(inputBuffer, filename, extra , comment); + int expSize = entry.getSize(); + comment = ""; + extra = new byte[]{}; + for (int i = 1; i < 20; i++) { + filename = "pkg/" + i + ".txt"; + entry = DirectoryEntry.view(inputBuffer, filename, extra , comment); + expSize += entry.getSize(); + extra = new byte[extra.length + 1]; + comment = comment + "," + i; + } + // Parse the entries. + CentralDirectory cdir = CentralDirectory.viewOf(inputBuffer).at(0).parse(); + assertEquals("Count", 20, cdir.getCount()); + assertEquals("Position after parse", expSize, cdir.buffer.position()); + assertEquals("Limit after parse", 10000, cdir.buffer.limit()); + cdir.buffer.flip(); + assertEquals("Position after finish", 0, cdir.buffer.position()); + assertEquals("Limit after finish", expSize, cdir.buffer.limit()); + } + + /** + * Test of nextEntry method, of class CentralDirectory. + */ + @Test + public void testNextEntry() { + ByteBuffer outputBuffer = ByteBuffer.allocate(10000).order(ByteOrder.LITTLE_ENDIAN); + CentralDirectory cdir = CentralDirectory.viewOf(outputBuffer); + String comment = null; + byte[] extra = null; + String filename = "pkg/0.txt"; + DirectoryEntry entry = DirectoryEntry.allocate(filename, extra , comment); + cdir.nextEntry(entry).set(CENTIM, 0); + int expSize = entry.getSize(); + comment = ""; + extra = new byte[]{}; + for (int i = 1; i < 20; i++) { + filename = "pkg/" + i + ".txt"; + entry = DirectoryEntry.allocate(filename, extra , comment); + cdir.nextEntry(entry).set(CENTIM, 0); + int size = entry.getSize(); + expSize += size; + extra = new byte[extra.length + 1]; + comment = comment + "," + i; + } + assertEquals("Count", 20, cdir.getCount()); + assertEquals("Position after build", expSize, cdir.buffer.position()); + assertEquals("Limit after build", 10000, cdir.buffer.limit()); + cdir.buffer.flip(); + assertEquals("Position after finish build", 0, cdir.buffer.position()); + assertEquals("Limit after finish build", expSize, cdir.buffer.limit()); + + // now try to parse the directory we just created. + cdir.at(0).parse(); + assertEquals("Count", 20, cdir.getCount()); + assertEquals("Position after re-parse", expSize, cdir.buffer.position()); + assertEquals("Limit after re-parse", expSize, cdir.buffer.limit()); + cdir.buffer.flip(); + assertEquals("Position after finish parse", 0, cdir.buffer.position()); + assertEquals("Limit after finish parse", expSize, cdir.buffer.limit()); + } +} diff --git a/src/test/java/com/google/devtools/build/android/ziputils/DataDescriptorTest.java b/src/test/java/com/google/devtools/build/android/ziputils/DataDescriptorTest.java new file mode 100644 index 0000000000..24b0c85311 --- /dev/null +++ b/src/test/java/com/google/devtools/build/android/ziputils/DataDescriptorTest.java @@ -0,0 +1,130 @@ +// 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.android.ziputils; + +import static com.google.devtools.build.android.ziputils.DataDescriptor.EXTCRC; +import static com.google.devtools.build.android.ziputils.DataDescriptor.EXTLEN; +import static com.google.devtools.build.android.ziputils.DataDescriptor.EXTSIG; +import static com.google.devtools.build.android.ziputils.DataDescriptor.EXTSIZ; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.zip.ZipInputStream; + +/** + * Unit tests for {@link DataDescriptor}. + */ +@RunWith(JUnit4.class) +public class DataDescriptorTest { + + /** + * Test of viewOf method, of class DataDescriptor. + */ + @Test + public void testViewOf() { + int[] markers = { 12345678, DataDescriptor.SIGNATURE, 0}; + for (int marker : markers) { + ByteBuffer buffer = ByteBuffer.allocate(50).order(ByteOrder.LITTLE_ENDIAN); + for (int i = 0; i < 50; i++) { + buffer.put((byte) i); + } + int offset = 20; + buffer.putInt(offset, marker); + buffer.position(offset); + DataDescriptor view = DataDescriptor.viewOf(buffer); + int expMark = marker == DataDescriptor.SIGNATURE ? (int) ZipInputStream.EXTSIG : -1; + int expSize = marker == DataDescriptor.SIGNATURE ? ZipInputStream.EXTHDR + : ZipInputStream.EXTHDR - 4; + int expPos = 0; + assertEquals("not based at current position[" + marker + "]", expMark, view.get(EXTSIG)); + assertEquals("Not slice with position 0[" + marker + "]", expPos, view.buffer.position()); + assertEquals("Not sized with comment[" + marker + "]", expSize, view.getSize()); + assertEquals("Not limited to size[" + marker + "]", expSize, view.buffer.limit()); + } + } + + /** + * Test of view method, of class DataDescriptor. + */ + @Test + public void testView_0args() { + DataDescriptor view = DataDescriptor.allocate(); + int expSize = ZipInputStream.EXTHDR; + int expPos = 0; + int expMarker = (int) ZipInputStream.EXTSIG; + assertTrue("no marker", view.hasMarker()); + assertEquals("No marker", expMarker, view.get(EXTSIG)); + assertEquals("Not at position 0", expPos, view.buffer.position()); + assertEquals("Not sized correctly", expSize, view.getSize()); + assertEquals("Not limited to size", expSize, view.buffer.limit()); + } + + /** + * Test of view method, of class DataDescriptor. + */ + @Test + public void testView_ByteBuffer() { + ByteBuffer buffer = ByteBuffer.allocate(100).order(ByteOrder.LITTLE_ENDIAN); + for (int i = 0; i < 100; i++) { + buffer.put((byte) i); + } + buffer.position(50); + DataDescriptor view = DataDescriptor.view(buffer); + int expMark = (int) ZipInputStream.EXTSIG; + int expSize = ZipInputStream.EXTHDR; + int expPos = 0; + assertEquals("not based at current position", expMark, view.get(EXTSIG)); + assertEquals("Not slice with position 0", expPos, view.buffer.position()); + assertEquals("Not sized with comment", expSize, view.getSize()); + assertEquals("Not limited to size", expSize, view.buffer.limit()); + } + + /** + * Test of copy method, of class DataDescriptor. + */ + @Test + public void testCopy() { + ByteBuffer buffer = ByteBuffer.allocate(100).order(ByteOrder.LITTLE_ENDIAN); + DataDescriptor view = DataDescriptor.allocate(); + view.copy(buffer); + int expSize = view.getSize(); + assertEquals("buffer not advanced as expected", expSize, buffer.position()); + buffer.position(0); + DataDescriptor clone = DataDescriptor.viewOf(buffer); + assertEquals("Fail to copy mark", view.get(EXTSIG), clone.get(EXTSIG)); + } + + /** + * Test of with and get methods. + */ + @Test + public void testWithAndGetMethods() { + int crc = 0x12345678; + int compressed = 0x357f1d5; + int uncompressed = 0x74813159; + DataDescriptor view = DataDescriptor.allocate() + .set(EXTCRC, crc) + .set(EXTSIZ, compressed) + .set(EXTLEN, uncompressed); + assertEquals("CRC", crc, view.get(EXTCRC)); + assertEquals("Compressed size", compressed, view.get(EXTSIZ)); + assertEquals("Uncompressed size", uncompressed, view.get(EXTLEN)); + } +} diff --git a/src/test/java/com/google/devtools/build/android/ziputils/DirectoryEntryTest.java b/src/test/java/com/google/devtools/build/android/ziputils/DirectoryEntryTest.java new file mode 100644 index 0000000000..12bbe22b5a --- /dev/null +++ b/src/test/java/com/google/devtools/build/android/ziputils/DirectoryEntryTest.java @@ -0,0 +1,188 @@ +// 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.android.ziputils; + +import static com.google.devtools.build.android.ziputils.DirectoryEntry.CENATT; +import static com.google.devtools.build.android.ziputils.DirectoryEntry.CENATX; +import static com.google.devtools.build.android.ziputils.DirectoryEntry.CENCRC; +import static com.google.devtools.build.android.ziputils.DirectoryEntry.CENDSK; +import static com.google.devtools.build.android.ziputils.DirectoryEntry.CENFLG; +import static com.google.devtools.build.android.ziputils.DirectoryEntry.CENHOW; +import static com.google.devtools.build.android.ziputils.DirectoryEntry.CENLEN; +import static com.google.devtools.build.android.ziputils.DirectoryEntry.CENOFF; +import static com.google.devtools.build.android.ziputils.DirectoryEntry.CENSIG; +import static com.google.devtools.build.android.ziputils.DirectoryEntry.CENSIZ; +import static com.google.devtools.build.android.ziputils.DirectoryEntry.CENTIM; +import static com.google.devtools.build.android.ziputils.DirectoryEntry.CENVEM; +import static com.google.devtools.build.android.ziputils.DirectoryEntry.CENVER; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.assertEquals; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.zip.ZipInputStream; + +/** + * Unit tests for {@link DirectoryEntry}. + */ +@RunWith(JUnit4.class) +public class DirectoryEntryTest { + + /** + * Test of viewOf method, of class DirectoryEntry. + */ + @Test + public void testViewOf() { + ByteBuffer buffer = ByteBuffer.allocate(100).order(ByteOrder.LITTLE_ENDIAN); + for (int i = 0; i < 100; i++) { + buffer.put((byte) i); + } + int offset = 20; + int filenameLength = 10; + int extraLength = 6; + int commentLength = 8; + int marker = DirectoryEntry.SIGNATURE; + buffer.putShort(offset + ZipInputStream.CENNAM, (short) filenameLength); // filename length + buffer.putShort(offset + ZipInputStream.CENEXT, (short) extraLength); // extra data length + buffer.putShort(offset + ZipInputStream.CENCOM, (short) commentLength); // comment length + buffer.putInt(20, marker); // any marker + buffer.position(offset); + DirectoryEntry view = DirectoryEntry.viewOf(buffer); + int expMark = (int) ZipInputStream.CENSIG; + int expSize = ZipInputStream.CENHDR + filenameLength + extraLength + commentLength; + int expPos = 0; + assertEquals("not based at current position", expMark, view.get(CENSIG)); + assertEquals("Not slice with position 0", expPos, view.buffer.position()); + assertEquals("Not sized with comment", expSize, view.getSize()); + assertEquals("Not limited to size", expSize, view.buffer.limit()); + } + + /** + * Test of view method, of class DirectoryEntry. + */ + @Test + public void testView_3Args() { + String filename = "pkg/foo.class"; + String comment = "got milk"; + byte[] extraData = { 1, 2, 3, 4, 5, 6, 7, 8}; + int expSize = ZipInputStream.CENHDR + filename.getBytes(UTF_8).length + + extraData.length + comment.getBytes(UTF_8).length; + int expPos = 0; + DirectoryEntry view = DirectoryEntry.allocate(filename, extraData, comment); + assertEquals("Incorrect filename", filename, view.getFilename()); + Assert.assertArrayEquals("Incorrect extra data", extraData, view.getExtraData()); + assertEquals("Incorrect comment", comment, view.getComment()); + assertEquals("Not at position 0", expPos, view.buffer.position()); + assertEquals("Not sized correctly", expSize, view.getSize()); + assertEquals("Not limited to size", expSize, view.buffer.limit()); + } + + /** + * Test of view method, of class DirectoryEntry. + */ + @Test + public void testView_4Args() { + ByteBuffer buffer = ByteBuffer.allocate(100).order(ByteOrder.LITTLE_ENDIAN); + for (int i = 0; i < 100; i++) { + buffer.put((byte) i); + } + int offset = 20; + buffer.position(offset); + String filename = "pkg/foo.class"; + byte[] extraData = { 1, 2, 3, 4, 5}; + String comment = "c"; + int expMark = (int) ZipInputStream.CENSIG; + int expSize = 46 + filename.getBytes(UTF_8).length + extraData.length + + comment.getBytes(UTF_8).length; + int expPos = 0; + DirectoryEntry view = DirectoryEntry.view(buffer, filename, extraData, comment); + assertEquals("not based at current position", expMark, view.get(CENSIG)); + assertEquals("Not slice with position 0", expPos, view.buffer.position()); + assertEquals("Not sized with filename", expSize, view.getSize()); + assertEquals("Not limited to size", expSize, view.buffer.limit()); + assertEquals("Incorrect filename", filename, view.getFilename()); + Assert.assertArrayEquals("Incorrect extra data", extraData, view.getExtraData()); + assertEquals("Incorrect comment", comment, view.getComment()); + } + + /** + * Test of copy method, of class DirectoryEntry. + */ + @Test + public void testCopy() { + String filename = "pkg/foo.class"; + byte[] extraData = {}; + String comment = "always comment!"; + ByteBuffer buffer = ByteBuffer.allocate(100).order(ByteOrder.LITTLE_ENDIAN); + DirectoryEntry view = DirectoryEntry.allocate(filename, extraData, comment); + view.copy(buffer); + int expSize = view.getSize(); + assertEquals("buffer not advanced as expected", expSize, buffer.position()); + buffer.position(0); + DirectoryEntry clone = DirectoryEntry.viewOf(buffer); + assertEquals("Fail to copy mark", view.get(CENSIG), clone.get(CENSIG)); + assertEquals("Fail to copy comment", view.getFilename(), clone.getFilename()); + Assert.assertArrayEquals("Fail to copy comment", view.getExtraData(), clone.getExtraData()); + assertEquals("Fail to copy comment", view.getComment(), clone.getComment()); + } + + /** + * Test of with and get methods. + */ + @Test + public void testWithAndGetMethods() { + int crc = 0x12345678; + int compressed = 0x357f1d5; + int uncompressed = 0x74813159; + short flags = 0x7a61; + short method = 0x3b29; + int time = 0x12312345; + short version = 0x1234; + short versionMadeBy = 0x27a1; + short disk = 0x5a78; + int extAttr = 0x73b27a15; + short intAttr = 0x37cc; + int offset = 0x74c93ac1; + DirectoryEntry view = DirectoryEntry.allocate("pkg/foo.class", null, "") + .set(CENCRC, crc) + .set(CENSIZ, compressed) + .set(CENLEN, uncompressed) + .set(CENFLG, flags) + .set(CENHOW, method) + .set(CENTIM, time) + .set(CENVER, version) + .set(CENVEM, versionMadeBy) + .set(CENDSK, disk) + .set(CENATX, extAttr) + .set(CENATT, intAttr) + .set(CENOFF, offset); + assertEquals("CRC", crc, view.get(CENCRC)); + assertEquals("Compressed size", compressed, view.get(CENSIZ)); + assertEquals("Uncompressed size", uncompressed, view.get(CENLEN)); + assertEquals("Flags", flags, view.get(CENFLG)); + assertEquals("Method", method, view.get(CENHOW)); + assertEquals("Modified time", time, view.get(CENTIM)); + assertEquals("Version needed", version, view.get(CENVER)); + assertEquals("Version made by", versionMadeBy, view.get(CENVEM)); + assertEquals("Disk", disk, view.get(CENDSK)); + assertEquals("External attributes", extAttr, view.get(CENATX)); + assertEquals("Internal attributes", intAttr, view.get(CENATT)); + assertEquals("Offset", offset, view.get(CENOFF)); + } +} diff --git a/src/test/java/com/google/devtools/build/android/ziputils/EndOfCentralDirectoryTest.java b/src/test/java/com/google/devtools/build/android/ziputils/EndOfCentralDirectoryTest.java new file mode 100644 index 0000000000..55a315d7b0 --- /dev/null +++ b/src/test/java/com/google/devtools/build/android/ziputils/EndOfCentralDirectoryTest.java @@ -0,0 +1,133 @@ +// 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.android.ziputils; + +import static com.google.devtools.build.android.ziputils.EndOfCentralDirectory.ENDDCD; +import static com.google.devtools.build.android.ziputils.EndOfCentralDirectory.ENDDSK; +import static com.google.devtools.build.android.ziputils.EndOfCentralDirectory.ENDOFF; +import static com.google.devtools.build.android.ziputils.EndOfCentralDirectory.ENDSIG; +import static com.google.devtools.build.android.ziputils.EndOfCentralDirectory.ENDSIZ; +import static com.google.devtools.build.android.ziputils.EndOfCentralDirectory.ENDSUB; +import static com.google.devtools.build.android.ziputils.EndOfCentralDirectory.ENDTOT; +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.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.zip.ZipInputStream; + +/** + * Unit tests for {@link EndOfCentralDirectory}. + */ +@RunWith(JUnit4.class) +public class EndOfCentralDirectoryTest { + @Test + public void testViewOf() { + ByteBuffer buffer = ByteBuffer.allocate(100).order(ByteOrder.LITTLE_ENDIAN); + for (int i = 0; i < 100; i++) { + buffer.put((byte) i); + } + int offset = 50; + int marker = EndOfCentralDirectory.SIGNATURE; + int comLength = 8; + buffer.putInt(offset, marker); + buffer.putShort(offset + ZipInputStream.ENDCOM, (short) comLength); + buffer.position(offset); + EndOfCentralDirectory view = EndOfCentralDirectory.viewOf(buffer); + int expMark = (int) ZipInputStream.ENDSIG; + int expSize = ZipInputStream.ENDHDR + comLength; // fixed + comment + int expPos = 0; + assertEquals("not based at current position", expMark, view.get(ENDSIG)); + assertEquals("Not slice with position 0", expPos, view.buffer.position()); + assertEquals("Not sized with comment", expSize, view.getSize()); + assertEquals("Not limited to size", expSize, view.buffer.limit()); + } + + @Test + public void testView_String() { + String[] comments = { "hello world", "", null}; + + for (String comment : comments) { + String expComment = comment != null ? comment : ""; + EndOfCentralDirectory view = EndOfCentralDirectory.allocate(comment); + String commentResult = view.getComment(); + assertEquals("Incorrect comment", expComment, commentResult); + int expSize = ZipInputStream.ENDHDR + (comment != null ? comment.getBytes(UTF_8).length : 0); + int expPos = 0; + assertEquals("Not at position 0", expPos, view.buffer.position()); + assertEquals("Not sized correctly", expSize, view.getSize()); + assertEquals("Not limited to size", expSize, view.buffer.limit()); + } + } + + @Test + public void testView_ByteBuffer_String() { + ByteBuffer buffer = ByteBuffer.allocate(100).order(ByteOrder.LITTLE_ENDIAN); + for (int i = 0; i < 100; i++) { + buffer.put((byte) i); + } + int offset = 50; + buffer.position(offset); + String comment = "this is a comment"; + EndOfCentralDirectory view = EndOfCentralDirectory.view(buffer, comment); + int expMark = (int) ZipInputStream.ENDSIG; + int expSize = ZipInputStream.ENDHDR + comment.length(); + int expPos = 0; + assertEquals("not based at current position", expMark, view.get(ENDSIG)); + assertEquals("Not slice with position 0", expPos, view.buffer.position()); + assertEquals("Not sized with comment", expSize, view.getSize()); + assertEquals("Not limited to size", expSize, view.buffer.limit()); + assertEquals("Incorrect comment", comment, view.getComment()); + } + + @Test + public void testCopy() { + ByteBuffer buffer = ByteBuffer.allocate(100).order(ByteOrder.LITTLE_ENDIAN); + EndOfCentralDirectory view = EndOfCentralDirectory.allocate("comment"); + view.copy(buffer); + int expSize = view.getSize(); + assertEquals("buffer not advanced as expected", expSize, buffer.position()); + buffer.position(0); + EndOfCentralDirectory clone = EndOfCentralDirectory.viewOf(buffer); + assertEquals("Fail to copy mark", view.get(ENDSIG), clone.get(ENDSIG)); + assertEquals("Fail to copy comment", view.getComment(), clone.getComment()); + } + + @Test + public void testWithAndGetMethods() { + short cdDisk = (short) 0x36c2; + int cdOffset = 0x924ac255; + int cdSize = 0x138ca234; + short disk = (short) 0x5c12; + short local = (short) 0x4ae1; + short total = (short) 0x63be; + EndOfCentralDirectory view = EndOfCentralDirectory.allocate("Hello World!") + .set(ENDDCD, cdDisk) + .set(ENDOFF, cdOffset) + .set(ENDSIZ, cdSize) + .set(ENDDSK, disk) + .set(ENDSUB, local) + .set(ENDTOT, total); + assertEquals("Central directory start disk", cdDisk, view.get(ENDDCD)); + assertEquals("Central directory file offset", cdOffset, view.get(ENDOFF)); + assertEquals("Central directory size", cdSize, view.get(ENDSIZ)); + assertEquals("This disk number", disk, view.get(ENDDSK)); + assertEquals("Number of records on this disk", local, view.get(ENDSUB)); + assertEquals("Total number of central directory records", total, view.get(ENDTOT)); + } +} diff --git a/src/test/java/com/google/devtools/build/android/ziputils/FakeFileSystem.java b/src/test/java/com/google/devtools/build/android/ziputils/FakeFileSystem.java new file mode 100644 index 0000000000..77b9b3e76f --- /dev/null +++ b/src/test/java/com/google/devtools/build/android/ziputils/FakeFileSystem.java @@ -0,0 +1,312 @@ +// 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.android.ziputils; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.util.HashMap; +import java.util.Map; + +/** + * Simple in-memory test file system. + */ +class FakeFileSystem extends FileSystem { + + private final Map<String, byte[]> files = new HashMap<>(); + + public FakeFileSystem() { + FileSystem.fileSystem = this; + } + + 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)); + } + + public String content(String filename) throws IOException { + byte[] data = files.get(filename); + if (data == null) { + throw new FileNotFoundException(); + } + return new String(data, UTF_8); + } + + public byte[] toByteArray(String filename) throws IOException { + byte[] data = files.get(filename); + if (data == null) { + throw new FileNotFoundException(); + } + return data; + } + + @Override + public FileChannel getInputChannel(String filename) throws IOException { + return new FakeReadChannel(filename); + } + + @Override + public FileChannel getOutputChannel(String filename, boolean append) throws IOException { + return new FakeWriteChannel(filename); + } + + @Override + public InputStream getInputStream(String filename) throws IOException { + byte[] data = files.get(filename); + if (data == null) { + throw new FileNotFoundException(); + } + return new ByteArrayInputStream(data); + } + + class FakeReadChannel extends FileChannel { + + final String name; + byte[] data; + int position; + + public FakeReadChannel(String filename) throws IOException { + this.name = filename; + this.data = toByteArray(filename); + this.position = 0; + } + + @Override + public int read(ByteBuffer dst) throws IOException { + if (position >= data.length) { + return -1; + } + int remaining = data.length - position; + if (dst.remaining() < remaining) { + remaining = dst.remaining(); + } + dst.put(data, position, remaining); + position += remaining; + return remaining; + } + + @Override + public long read(ByteBuffer[] dsts, int offset, int length) throws IOException { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public int write(ByteBuffer src) throws IOException { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public long write(ByteBuffer[] srcs, int offset, int length) throws IOException { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public long position() throws IOException { + return position; + } + + @Override + public FileChannel position(long newPosition) throws IOException { + position = (int) newPosition; + return this; + } + + @Override + public long size() throws IOException { + return data.length; + } + + @Override + public FileChannel truncate(long size) throws IOException { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void force(boolean metaData) throws IOException { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public long transferTo(long position, long count, WritableByteChannel target) + throws IOException { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public long transferFrom(ReadableByteChannel src, long position, long count) + throws IOException { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public int read(ByteBuffer dst, long position) throws IOException { + if (position < 0 || position >= data.length) { + throw new IOException("out of bounds"); + } + int remaining = data.length - (int) position; + if (dst.remaining() < remaining) { + remaining = dst.remaining(); + } + dst.put(data, (int) position, remaining); + return remaining; + } + + @Override + public int write(ByteBuffer src, long position) throws IOException { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public MappedByteBuffer map(FileChannel.MapMode mode, long position, long size) + throws IOException { + throw new UnsupportedOperationException("Not supported yet."); + } + + public ByteBuffer map(long position, long size) { + return ByteBuffer.wrap(data, (int) position, (int) size).slice(); + } + + @Override + public FileLock lock(long position, long size, boolean shared) throws IOException { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public FileLock tryLock(long position, long size, boolean shared) throws IOException { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + protected void implCloseChannel() throws IOException { + } + } + + class FakeWriteChannel extends FileChannel { + + final String name; + final ByteArrayOutputStream buf; + int position; + + public FakeWriteChannel(String filename) { + this.name = filename; + this.buf = new ByteArrayOutputStream(); + this.position = 0; + } + + @Override + public int read(ByteBuffer dst) throws IOException { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public long read(ByteBuffer[] dsts, int offset, int length) throws IOException { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public int write(ByteBuffer src) throws IOException { + byte[] bytes = new byte[src.remaining()]; + src.get(bytes); + buf.write(bytes); + position += bytes.length; + return bytes.length; + } + + @Override + public long write(ByteBuffer[] srcs, int offset, int length) throws IOException { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public long position() throws IOException { + return position; + } + + @Override + public FileChannel position(long newPosition) throws IOException { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public long size() throws IOException { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public FileChannel truncate(long size) throws IOException { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void force(boolean metaData) throws IOException { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public long transferTo(long position, long count, WritableByteChannel target) + throws IOException { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public long transferFrom(ReadableByteChannel src, long position, long count) + throws IOException { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public int read(ByteBuffer dst, long position) throws IOException { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public int write(ByteBuffer src, long position) throws IOException { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public MappedByteBuffer map(FileChannel.MapMode mode, long position, long size) + throws IOException { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public FileLock lock(long position, long size, boolean shared) throws IOException { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public FileLock tryLock(long position, long size, boolean shared) throws IOException { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + protected void implCloseChannel() throws IOException { + files.put(name, buf.toByteArray()); + } + } +} diff --git a/src/test/java/com/google/devtools/build/android/ziputils/FileSystem.java b/src/test/java/com/google/devtools/build/android/ziputils/FileSystem.java new file mode 100644 index 0000000000..0a29957ed5 --- /dev/null +++ b/src/test/java/com/google/devtools/build/android/ziputils/FileSystem.java @@ -0,0 +1,78 @@ +// 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.android.ziputils; + +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.channels.FileChannel; + +/** + * A simple file system abstraction, for testing. This library doesn't itself open files. + * Clients are responsible for opening files and pass in file channels to the API. + * This class is currently here to be able to use an common abstraction for testing the + * library, and tools implemented using this library. This class may be removed in the future. + */ +class FileSystem { + + protected static FileSystem fileSystem; + + /** + * Returns the configured file system implementation. If no filesystem is configured, and + * instance of this class is created and returned. The default filesystem implementation is + * a simple wrapper of the standard Java file system. + */ + public static FileSystem fileSystem() { + if (fileSystem == null) { + fileSystem = new FileSystem(); + } + return fileSystem; + } + + /** + * Opens a file for reading, and returns a file channel. + * + * @param filename name of file to open. + * @return file channel open for reading. + * @throws java.io.IOException + */ + public FileChannel getInputChannel(String filename) throws IOException { + return new FileInputStream(filename).getChannel(); + } + + /** + * Opens a file for writing, and returns a file channel. + * + * @param filename name of file to open. + * @param append whether to open file in append mode. + * @return file channel open for write. + * @throws java.io.IOException + */ + public FileChannel getOutputChannel(String filename, boolean append) throws IOException { + return new FileOutputStream(filename, append).getChannel(); + } + + /** + * Opens a file for reading, and returns an input stream. + * + * @param filename name of file to open. + * @return input stream reading from the specified file. + * @throws java.io.IOException + */ + public InputStream getInputStream(String filename) throws IOException { + return new FileInputStream(filename); + } +} diff --git a/src/test/java/com/google/devtools/build/android/ziputils/LocalFileHeaderTest.java b/src/test/java/com/google/devtools/build/android/ziputils/LocalFileHeaderTest.java new file mode 100644 index 0000000000..cfec2dca84 --- /dev/null +++ b/src/test/java/com/google/devtools/build/android/ziputils/LocalFileHeaderTest.java @@ -0,0 +1,139 @@ +// 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.android.ziputils; + +import static com.google.devtools.build.android.ziputils.LocalFileHeader.LOCCRC; +import static com.google.devtools.build.android.ziputils.LocalFileHeader.LOCFLG; +import static com.google.devtools.build.android.ziputils.LocalFileHeader.LOCHOW; +import static com.google.devtools.build.android.ziputils.LocalFileHeader.LOCLEN; +import static com.google.devtools.build.android.ziputils.LocalFileHeader.LOCSIG; +import static com.google.devtools.build.android.ziputils.LocalFileHeader.LOCSIZ; +import static com.google.devtools.build.android.ziputils.LocalFileHeader.LOCTIM; +import static com.google.devtools.build.android.ziputils.LocalFileHeader.LOCVER; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.assertEquals; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.zip.ZipInputStream; + +/** + * Unit tests for {@link LocalFileHeader}. + */ +@RunWith(JUnit4.class) +public class LocalFileHeaderTest { + + @Test + public void testViewOf() { + ByteBuffer buffer = ByteBuffer.allocate(100).order(ByteOrder.LITTLE_ENDIAN); + for (int i = 0; i < 100; i++) { + buffer.put((byte) i); + } + int offset = 20; + int filenameLength = 10; + int extraLength = 25; + int marker = LocalFileHeader.SIGNATURE; + buffer.putShort(offset + ZipInputStream.LOCNAM, (short) filenameLength); // filename length + buffer.putShort(offset + ZipInputStream.LOCEXT, (short) extraLength); // extra data length + buffer.putInt(offset, marker); // need to zero filename length to have predictable size + buffer.position(offset); + LocalFileHeader view = LocalFileHeader.viewOf(buffer); + int expMark = (int) ZipInputStream.LOCSIG; + int expSize = ZipInputStream.LOCHDR + filenameLength + extraLength; // fixed + comment + int expPos = 0; + assertEquals("not based at current position", expMark, view.get(LOCSIG)); + assertEquals("Not slice with position 0", expPos, view.buffer.position()); + assertEquals("Not sized with comment", expSize, view.getSize()); + assertEquals("Not limited to size", expSize, view.buffer.limit()); + } + + @Test + public void testView_String_byteArr() { + String filename = "pkg/foo.class"; + byte[] extraData = { 1, 2, 3, 4, 5, 6, 7, 8}; + int expSize = ZipInputStream.LOCHDR + filename.getBytes(UTF_8).length + + extraData.length; + int expPos = 0; + LocalFileHeader view = LocalFileHeader.allocate(filename, extraData); + assertEquals("Incorrect filename", filename, view.getFilename()); + Assert.assertArrayEquals("Incorrect extra data", extraData, view.getExtraData()); + assertEquals("Not at position 0", expPos, view.buffer.position()); + assertEquals("Not sized correctly", expSize, view.getSize()); + assertEquals("Not limited to size", expSize, view.buffer.limit()); + } + + @Test + public void testView_3Args() { + ByteBuffer buffer = ByteBuffer.allocate(100).order(ByteOrder.LITTLE_ENDIAN); + for (int i = 0; i < 100; i++) { + buffer.put((byte) i); + } + int offset = 20; + buffer.position(offset); + String filename = "pkg/foo.class"; + int expMark = LocalFileHeader.SIGNATURE; + int expSize = ZipInputStream.LOCHDR + filename.getBytes(UTF_8).length; + int expPos = 0; + LocalFileHeader view = LocalFileHeader.view(buffer, filename, null); + assertEquals("not based at current position", expMark, view.get(LOCSIG)); + assertEquals("Not slice with position 0", expPos, view.buffer.position()); + assertEquals("Not sized with filename", expSize, view.getSize()); + assertEquals("Not limited to size", expSize, view.buffer.limit()); + assertEquals("Incorrect filename", filename, view.getFilename()); + } + + @Test + public void testCopy() { + ByteBuffer buffer = ByteBuffer.allocate(100).order(ByteOrder.LITTLE_ENDIAN); + LocalFileHeader view = LocalFileHeader.allocate("pkg/foo.class", null); + view.copy(buffer); + int expSize = view.getSize(); + assertEquals("buffer not advanced as expected", expSize, buffer.position()); + buffer.position(0); + LocalFileHeader clone = LocalFileHeader.viewOf(buffer); + assertEquals("Fail to copy mark", view.get(LOCSIG), view.get(LOCSIG)); + assertEquals("Fail to copy comment", view.getFilename(), clone.getFilename()); + } + + @Test + public void testWithAndGetMethods() { + int crc = 0x12345678; + int compressed = 0x357f1d5; + int uncompressed = 0x74813159; + short flags = 0x7a61; + short method = 0x3b29; + int time = 0x12c673e1; + short version = 0x1234; + LocalFileHeader view = LocalFileHeader.allocate("pkg/foo.class", null) + .set(LOCCRC, crc) + .set(LOCSIZ, compressed) + .set(LOCLEN, uncompressed) + .set(LOCFLG, flags) + .set(LOCHOW, method) + .set(LOCTIM, time) + .set(LOCVER, version); + assertEquals("CRC", crc, view.get(LOCCRC)); + assertEquals("Compressed size", compressed, view.get(LOCSIZ)); + assertEquals("Uncompressed size", uncompressed, view.get(LOCLEN)); + assertEquals("Flags", flags, view.get(LOCFLG)); + assertEquals("Method", method, view.get(LOCHOW)); + assertEquals("Modified time", time, view.get(LOCTIM)); + assertEquals("Version needed", version, view.get(LOCVER)); + } +} diff --git a/src/test/java/com/google/devtools/build/android/ziputils/ScanUtilTest.java b/src/test/java/com/google/devtools/build/android/ziputils/ScanUtilTest.java new file mode 100644 index 0000000000..2f6be47fd1 --- /dev/null +++ b/src/test/java/com/google/devtools/build/android/ziputils/ScanUtilTest.java @@ -0,0 +1,116 @@ +// 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.android.ziputils; + +import static com.google.common.truth.Truth.assertWithMessage; +import static org.junit.Assert.fail; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +/** + * Unit tests for {@link ScanUtil}. + */ +@RunWith(JUnit4.class) +public class ScanUtilTest { + + @Test + public void testScanTo() { + try { + assertLocation(null, new byte[]{}, -1); + fail("No exception on null target"); + } catch (NullPointerException ex) { + // expected + } + try { + assertLocation(new byte[] {}, null, -1); + fail("No exception on null domain"); + } catch (NullPointerException ex) { + // expected + } + assertLocation(new byte[] {}, new byte[] {}, -1); + assertLocation(new byte[] {}, new byte[] {1}, 0); + assertLocation(new byte[] {}, new byte[] {1, 2, 3, 4}, 0); + assertLocation(new byte[] {1}, new byte[] {}, -1); + assertLocation(new byte[] {1}, new byte[] {1}, 0); + assertLocation(new byte[] {1}, new byte[] {1, 2, 3, 4}, 0); + assertLocation(new byte[] {1}, new byte[] {5, 4, 1, 2, 3, 4}, 2); + assertLocation(new byte[] {1}, new byte[] {4, 2, 3, 1}, 3); + assertLocation(new byte[] {1, 2, 3, 4}, new byte[] {}, -1); + assertLocation(new byte[] {1, 2, 3, 4}, new byte[] {1}, -1); + assertLocation(new byte[] {1, 2, 3, 4}, new byte[] {1, 2, 3, 4}, 0); + assertLocation(new byte[] {1, 2, 3, 4}, new byte[] {1, 2, 3, 4, 1, 2, 3, 4}, 0); + assertLocation(new byte[] {1, 2, 3, 4}, new byte[] {5, 1, 2, 3, 4}, 1); + assertLocation(new byte[] {1, 2, 3, 4}, new byte[] {5, 1, 2, 3, 4, 5}, 1); + assertLocation(new byte[] {1, 2, 3, 4}, new byte[] {5, 5, 1, 2, 3, 4}, 2); + assertLocation(new byte[] {1, 2, 3, 4}, new byte[] {5, 1, 1, 2, 3, 4}, 2); + assertLocation(new byte[] {1, 2, 3, 4}, new byte[] {5, 1, 2, 3, 5, 4}, -1); + } + + @Test + public void testScanBackwardsTo() { + try { + assertBackwardsLocation(null, new byte[]{}, -1); + fail("No exception on null target"); + } catch (NullPointerException ex) { + // expected + } + try { + assertBackwardsLocation(new byte[]{}, null, -1); + fail("No exception on null domain"); + } catch (NullPointerException ex) { + // expected + } + assertBackwardsLocation(new byte[] {}, new byte[] {}, -1); + assertBackwardsLocation(new byte[] {}, new byte[] {1}, 0); + assertBackwardsLocation(new byte[] {}, new byte[] {1, 2, 3, 4}, 3); + assertBackwardsLocation(new byte[] {1}, new byte[] {}, -1); + assertBackwardsLocation(new byte[] {1}, new byte[] {1}, 0); + assertBackwardsLocation(new byte[] {1}, new byte[] {1, 2, 3, 4}, 0); + assertBackwardsLocation(new byte[] {1}, new byte[] {5, 4, 1, 2, 3, 4}, 2); + assertBackwardsLocation(new byte[] {1}, new byte[] {4, 2, 3, 1}, 3); + assertBackwardsLocation(new byte[] {1, 2, 3, 4}, new byte[] {}, -1); + assertBackwardsLocation(new byte[] {1, 2, 3, 4}, new byte[] {1}, -1); + assertBackwardsLocation(new byte[] {1, 2, 3, 4}, new byte[] {1, 2, 3, 4}, 0); + assertBackwardsLocation(new byte[] {1, 2, 3, 4}, new byte[] {1, 2, 3, 4, 1, 2, 3, 4}, 4); + assertBackwardsLocation(new byte[] {1, 2, 3, 4}, new byte[] {1, 2, 3, 4, 1, 2, 3, 4, 1}, 4); + assertBackwardsLocation(new byte[] {1, 2, 3, 4}, new byte[] {5, 1, 2, 3, 4}, 1); + assertBackwardsLocation(new byte[] {1, 2, 3, 4}, new byte[] {5, 1, 2, 3, 4, 5}, 1); + assertBackwardsLocation(new byte[] {1, 2, 3, 4}, new byte[] {5, 5, 1, 2, 3, 4}, 2); + assertBackwardsLocation(new byte[] {1, 2, 3, 4}, new byte[] {5, 1, 1, 2, 3, 4}, 2); + assertBackwardsLocation(new byte[] {1, 2, 3, 4}, new byte[] {5, 1, 2, 3, 5, 4}, -1); + } + + private void assertLocation(byte[] target, byte[] domain, int expected) { + int pos = ScanUtil.scanTo(target, domain != null ? ByteBuffer.wrap(domain) : null); + assertWithMessage("Position of " + Arrays.toString(target) + " in " + Arrays.toString(domain)) + .that(expected).isEqualTo(pos); + } + + private void assertBackwardsLocation(byte[] target, byte[] domain, int expected) { + ByteBuffer buf = null; + if (domain != null) { + buf = ByteBuffer.wrap(domain); + buf.position(buf.limit()); + } + int pos = ScanUtil.scanBackwardsTo(target, buf); + assertWithMessage("Position of " + Arrays.toString(target) + " in " + Arrays.toString(domain) + + ", " + buf.position() + ", " + buf.limit()) + .that(expected).isEqualTo(pos); + } +} diff --git a/src/test/java/com/google/devtools/build/android/ziputils/SplitZipTest.java b/src/test/java/com/google/devtools/build/android/ziputils/SplitZipTest.java new file mode 100644 index 0000000000..a10c9b11df --- /dev/null +++ b/src/test/java/com/google/devtools/build/android/ziputils/SplitZipTest.java @@ -0,0 +1,436 @@ +// 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.android.ziputils; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.Arrays; +import java.util.Date; + +/** + * Unit tests for {@link SplitZip}. + */ +@RunWith(JUnit4.class) +public class SplitZipTest { + private FakeFileSystem fileSystem; + + @Before + public void setUp() { + fileSystem = new FakeFileSystem(); + } + + @Test + public void test() { + SplitZip instance = new SplitZip(); + assertThat(instance.getMainClassListFile()).isNull(); + assertThat(instance.isVerbose()).isFalse(); + assertThat(instance.getEntryDate()).isNull(); + assertThat(instance.getResourceFile()).isNull(); + } + + @Test + public void testSetOutput() { + SplitZip instance = new SplitZip(); + try { + instance.addOutput((String) null); + fail("should have failed"); + } catch (Exception ex) { + assertTrue("NullPointerException expected", ex instanceof NullPointerException); + } + try { + SplitZip result = instance + .addOutput(new ZipOut(fileSystem.getOutputChannel("out/shard1.jar", false), + "out/shard1.jar")) + .addOutput(new ZipOut(fileSystem.getOutputChannel("out/shard2.jar", false), + "out/shard2.jar")); + assertSame(instance, result); + } catch (IOException ex) { + fail("Unexpected exception: " + ex); + } + } + + @Test + public void testSetResourceFile() { + SplitZip instance = new SplitZip(); + String res = "res"; + SplitZip result = instance.setResourceFile(res); + assertSame(instance, result); + } + + @Test + public void testGetResourceFile() { + SplitZip instance = new SplitZip(); + String res = "res"; + assertThat(instance.setResourceFile(res).getResourceFile()).isEqualTo(res); + assertThat(instance.setResourceFile((String) null).getResourceFile()).isNull(); + } + + @Test + public void testSetMainClassListFile() { + SplitZip instance = new SplitZip(); + SplitZip result = instance.setMainClassListFile((String) null); + assertSame(instance, result); + result = instance.setMainClassListFile("no format checks"); + assertSame(instance, result); + } + + @Test + public void testGetMainClassListFile() { + SplitZip instance = new SplitZip(); + String file = "list.txt"; + instance.setMainClassListFile(file); + String result = instance.getMainClassListFile(); + assertThat(result).isEqualTo(file); + } + + // Instance date test. Implementation has little constraints today. + // This should be improved. + @Test + public void testSetEntryDate() { + SplitZip instance = new SplitZip(); + SplitZip result = instance.setEntryDate(null); + assertSame(instance, result); + } + + @Test + public void testGetEntryDate() { + SplitZip instance = new SplitZip(); + Date now = new Date(); + instance.setEntryDate(now); + Date result = instance.getEntryDate(); + assertSame(result, now); + instance.setEntryDate(null); + assertThat(instance.getEntryDate()).isNull(); + } + + @Test + public void testUseDefaultEntryDate() { + SplitZip instance = new SplitZip(); + SplitZip result = instance.useDefaultEntryDate(); + assertSame(instance, result); + Date date = instance.getEntryDate(); + assertThat(date).isEqualTo(DosTime.DOS_EPOCH); + } + + @Test + public void testAddInput() { + try { + SplitZip instance = new SplitZip(); + String noexists = "noexists.zip"; + instance.addInput(noexists); + fail("should not be able to add non existing file: " + noexists); + } catch (IOException ex) { + assertTrue("FileNotFoundException expected", ex instanceof FileNotFoundException); + } + } + + @Test + public void testAddInputs() { + try { + SplitZip instance = new SplitZip(); + String noexists = "noexists.zip"; + instance.addInputs(Arrays.asList(noexists)); + fail("should not be able to add non existing file: " + noexists); + } catch (IOException ex) { + assertTrue("FileNotFoundException expected", ex instanceof FileNotFoundException); + } + } + + @Test + public void testCopyOneDir() { + try { + new ZipFileBuilder() + .add("pkg/test.txt", "hello world") + .create("input.zip"); + byte[] inputBytes = fileSystem.toByteArray("input.zip"); + + new SplitZip() + .addOutput(new ZipOut(fileSystem.getOutputChannel("out/shard1.jar", false), + "out/shard1.jar")) + .setVerbose(true) + .addInput(new ZipIn(fileSystem.getInputChannel("input.zip"), "input.zip")) + .run() + .close(); + + byte[] outputBytes = fileSystem.toByteArray("out/shard1.jar"); + assertThat(inputBytes).isEqualTo(outputBytes); + } catch (IOException e) { + fail("Exception: " + e); + } + } + + @Test + public void testSetDate() { + try { + Date now = new Date(); + new ZipFileBuilder() + .add(new ZipFileBuilder.FileInfo("pkg/test.txt", new DosTime(now).time, "hello world")) + .create("input.zip"); + + new ZipFileBuilder() + .add(new ZipFileBuilder.FileInfo("pkg/test.txt", DosTime.EPOCH.time, "hello world")) + .create("expect.zip"); + byte[] expectBytes = fileSystem.toByteArray("expect.zip"); + + new SplitZip() + .addOutput(new ZipOut(fileSystem.getOutputChannel("out/shard1.jar", false), + "out/shard1.jar")) + .setVerbose(true) + .setEntryDate(DosTime.DOS_EPOCH) + .addInput(new ZipIn(fileSystem.getInputChannel("input.zip"), "input.zip")) + .run() + .close(); + + byte[] outputBytes = fileSystem.toByteArray("out/shard1.jar"); + assertThat(expectBytes).isEqualTo(outputBytes); + } catch (IOException e) { + fail("Exception: " + e); + } + } + + @Test + public void testDuplicatedInput() { + try { + new ZipFileBuilder() + .add("pkg/test.txt", "hello world") + .create("input1.zip"); + + new ZipFileBuilder() + .add("pkg/test.txt", "Goodbye world") + .create("input2.zip"); + + new SplitZip() + .addOutput(new ZipOut(fileSystem.getOutputChannel("out/shard1.jar", false), + "out/shard1.jar")) + .setVerbose(true) + .addInput(new ZipIn(fileSystem.getInputChannel("input1.zip"), "input1.zip")) + .addInput(new ZipIn(fileSystem.getInputChannel("input2.zip"), "input2.zip")) + .run() + .close(); + + new ZipFileBuilder() + .add("pkg/test.txt", "hello world") + .create("expect.zip"); + byte[] expectBytes = fileSystem.toByteArray("expect.zip"); + byte[] outputBytes = fileSystem.toByteArray("out/shard1.jar"); + assertThat(expectBytes).isEqualTo(outputBytes); + } catch (IOException e) { + fail("Exception: " + e); + } + } + + @Test + public void testCopyThreeDir() { + try { + new ZipFileBuilder() + .add("pkg/hello.txt", "hello world") + .add("pkg/greet.txt", "how are you") + .add("pkg/bye.txt", "bye bye") + .create("input.zip"); + byte[] inputBytes = fileSystem.toByteArray("input.zip"); + + new SplitZip() + .addOutput(new ZipOut(fileSystem.getOutputChannel("out/shard1.jar", false), + "out/shard1.jar")) + .setVerbose(true) + .addInput(new ZipIn(fileSystem.getInputChannel("input.zip"), "input.zip")) + .run() + .close(); + + byte[] outputBytes = fileSystem.toByteArray("out/shard1.jar"); + assertThat(inputBytes).isEqualTo(outputBytes); + } catch (IOException e) { + fail("Exception: " + e); + } + } + + @Test + public void testSplitInTwo() { + try { + new ZipFileBuilder() + .add("pkg1/test1.class", "hello world") + .add("pkg2/test1.class", "hello world") + .add("pkg1/test2.class", "how are you") + .add("pkg2/test2.class", "how are you") + .add("pkg1/test3.class", "bye bye") + .add("pkg2/test3.class", "bye bye") + .create("input.jar"); + + new SplitZip() + .addOutput(new ZipOut(fileSystem.getOutputChannel("out/shard1.jar", false), + "out/shard1.jar")) + .addOutput(new ZipOut(fileSystem.getOutputChannel("out/shard2.jar", false), + "out/shard2.jar")) + .setVerbose(true) + .addInput(new ZipIn(fileSystem.getInputChannel("input.jar"), "input.jar")) + .run() + .close(); + + new ZipFileBuilder() + .add("pkg1/test1.class", "hello world") + .add("pkg1/test2.class", "how are you") + .add("pkg1/test3.class", "bye bye") + .create("expected/shard1.jar"); + new ZipFileBuilder() + .add("pkg2/test1.class", "hello world") + .add("pkg2/test2.class", "how are you") + .add("pkg2/test3.class", "bye bye") + .create("expected/shard2.jar"); + + assertThat(fileSystem.toByteArray("out/shard1.jar")) + .isEqualTo(fileSystem.toByteArray("expected/shard1.jar")); + + assertThat(fileSystem.toByteArray("out/shard2.jar")) + .isEqualTo(fileSystem.toByteArray("expected/shard2.jar")); + + } catch (IOException e) { + fail("Exception: " + e); + } + } + + @Test + public void testSeparateResources() { + try { + new ZipFileBuilder() + .add("resources/oil.xml", "oil") + .add("pkg1/test1.class", "hello world") + .add("pkg2/test1.class", "hello world") + .add("pkg1/test2.class", "how are you") + .add("pkg2/test2.class", "how are you") + .add("pkg1/test3.class", "bye bye") + .add("pkg2/test3.class", "bye bye") + .create("input.jar"); + ZipIn input = new ZipIn(fileSystem.getInputChannel("input.jar"), "input.jar"); + + String resources = "out/resources.zip"; + ZipOut resourceOut = new ZipOut(fileSystem.getOutputChannel(resources, false), resources); + new SplitZip() + .addOutput(new ZipOut(fileSystem.getOutputChannel("out/shard1.jar", false), + "out/shard1.jar")) + .addOutput(new ZipOut(fileSystem.getOutputChannel("out/shard2.jar", false), + "out/shard2.jar")) + .setResourceFile(resourceOut) + .setVerbose(true) + .addInput(input) + .run() + .close(); + + new ZipFileBuilder() + .add("pkg1/test1.class", "hello world") + .add("pkg1/test2.class", "how are you") + .add("pkg1/test3.class", "bye bye") + .create("expected/shard1.jar"); + new ZipFileBuilder() + .add("pkg2/test1.class", "hello world") + .add("pkg2/test2.class", "how are you") + .add("pkg2/test3.class", "bye bye") + .create("expected/shard2.jar"); + new ZipFileBuilder() + .add("resources/oil.xml", "oil") + .create("expected/resources.zip"); + + + assertThat(fileSystem.toByteArray("out/shard1.jar")) + .isEqualTo(fileSystem.toByteArray("expected/shard1.jar")); + + assertThat(fileSystem.toByteArray("out/shard2.jar")) + .isEqualTo(fileSystem.toByteArray("expected/shard2.jar")); + + assertThat(fileSystem.toByteArray("out/resources.zip")) + .isEqualTo(fileSystem.toByteArray("expected/resources.zip")); + + } catch (IOException e) { + e.printStackTrace(); + fail("Exception: " + e); + } + } + + @Test + public void testMainClassListFile() { + SplitZip instance = new SplitZip(); + String filename = "x/y/z/foo.txt"; + instance.setMainClassListFile(filename); + String out = instance.getMainClassListFile(); + assertThat(out).isEqualTo(filename); + + instance.setMainClassListFile((String) null); + assertThat(instance.getMainClassListFile()).isNull(); + + try { + new ZipFileBuilder() + .add("pkg1/test1.class", "hello world") + .add("pkg2/test1.class", "hello world") + .add("pkg1/test2.class", "how are you") + .add("pkg2/test2.class", "how are you") + .add("pkg1/test3.class", "bye bye") + .add("pkg2/test3.class", "bye bye") + .create("input.jar"); + + String classFileList = "pkg1/test1.class\npkg2/test2.class\n"; + fileSystem.addFile("main_dex_list.txt", classFileList); + + new SplitZip() + .addOutput(new ZipOut(fileSystem.getOutputChannel("out/shard1.jar", false), + "out/shard1.jar")) + .addOutput(new ZipOut(fileSystem.getOutputChannel("out/shard2.jar", false), + "out/shard2.jar")) + .setMainClassListFile(fileSystem.getInputStream("main_dex_list.txt")) + .addInput(new ZipIn(fileSystem.getInputChannel("input.jar"), "input.jar")) + .run() + .close(); + + new ZipFileBuilder() + .add("pkg1/test1.class", "hello world") + .add("pkg2/test2.class", "how are you") + .create("expected/shard1.jar"); + + // Sorting is used for split calculation, but classes assigned to the same shard are expected + // to be output in the order they appear in input. + new ZipFileBuilder() + .add("pkg2/test1.class", "hello world") + .add("pkg1/test2.class", "how are you") + .add("pkg1/test3.class", "bye bye") + .add("pkg2/test3.class", "bye bye") + .create("expected/shard2.jar"); + + assertThat(fileSystem.toByteArray("out/shard1.jar")) + .isEqualTo(fileSystem.toByteArray("expected/shard1.jar")); + + assertThat(fileSystem.toByteArray("out/shard2.jar")) + .isEqualTo(fileSystem.toByteArray("expected/shard2.jar")); + + } catch (IOException e) { + fail("Exception: " + e); + } + } + + @Test + public void testVerbose() { + SplitZip instance = new SplitZip(); + instance.setVerbose(true); + assertThat(instance.isVerbose()).isTrue(); + instance.setVerbose(false); + assertThat(instance.isVerbose()).isFalse(); + } +} diff --git a/src/test/java/com/google/devtools/build/android/ziputils/SplitterTest.java b/src/test/java/com/google/devtools/build/android/ziputils/SplitterTest.java new file mode 100644 index 0000000000..7b791c0f75 --- /dev/null +++ b/src/test/java/com/google/devtools/build/android/ziputils/SplitterTest.java @@ -0,0 +1,345 @@ +// 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.android.ziputils; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.Range; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Unit tests for {@link Splitter}. + */ +@RunWith(JUnit4.class) +public class SplitterTest { + + private static final String ARCHIVE_DIR_SUFFIX = "/"; + private static final String ARCHIVE_FILE_SEPARATOR = "/"; + private static final String CLASS_SUFFIX = ".class"; + + @Test + public void testAssign() { + + int size = 10; + + Collection<String> input; + ArrayList<String> filter; + Map<String, Integer> output; + + input = genEntries(size, size); + filter = new ArrayList<>(10); + for (int i = 0; i < size; i++) { + filter.add("dir" + i + ARCHIVE_FILE_SEPARATOR + "file" + i + CLASS_SUFFIX); + } + Splitter splitter = new Splitter(size + 1, input.size()); + splitter.assign(filter); + splitter.nextShard(); + output = new LinkedHashMap<>(); + for (String path : input) { + output.put(path, splitter.assign(path)); + } + + for (int i = 0; i < size; i++) { + for (int j = 0; j < size; j++) { + String path = "dir" + i + ARCHIVE_FILE_SEPARATOR + "file" + j + CLASS_SUFFIX; + if (i == j) { + assertThat(output.get(path)).isEqualTo(0); + } else { + assertThat(output.get(path)).isEqualTo(i + 1); + } + } + } + } + + + /** + * Test splitting of single-ile packages. Note, this is also testing for the situation + * where input entries are unordered, and thus appearing to be in different packages, + * to the implementation that only confiders the previous file to determine package + * boundaries. + */ + @Test + public void testSingleFilePackages() { + int[][] params = { + { 1, 1, 1}, // one shard, for one package with one file + {1, 2, 1}, // one shard, for two packages, with one file each + {1, 10, 1}, // one shard, for ten packages, with one file each + {2, 2, 1}, // ... + {2, 10, 1}, + {2, 100, 1}, + {10, 10, 1}, + {10, 15, 1}, + {10, 95, 1}, + {97, 10000, 1}, + }; + comboRunner(params); + } + + /** + * Test cases where the number of shards is less than the number + * of packages. This implies that the package size is less than + * the average shard size. We expect shards to be multiple of + * package size. + */ + @Test + public void testPackageSplit() { + int[][] params = { + {2, 3, 2}, // two shards, for three packages, with two files each + {2, 3, 9}, // ... + {2, 3, 10}, + {2, 3, 11}, + {2, 3, 19}, + + {2, 10, 2}, + {2, 10, 9}, + {2, 10, 10}, + {2, 10, 11}, + {2, 10, 19}, + + {10, 11, 2}, + {10, 11, 9}, + {10, 11, 10}, + {10, 11, 11}, + {10, 11, 19}, + + {10, 111, 2}, + {10, 111, 9}, + {10, 111, 10}, + {10, 111, 11}, + {10, 111, 19}, + + {25, 1000, 8}, + {25, 1000, 10}, + {25, 1000, 19}, + + {250, 10000, 19}, + }; + comboRunner(params); + } + + /** + * Tests situations where the number of shards exceeds the number of + * packages (but not the number of files). That is, the implementation + * must split at least one package. + */ + @Test + public void testForceSplit() { + int[][] params = { + {2, 1, 2}, // two shards, for one package, with two files + {2, 1, 9}, // ... + {2, 1, 10}, + {2, 1, 11}, + + {3, 2, 2}, + {10, 9, 2}, + {10, 2, 9}, + {10, 9, 9}, + {10, 2, 10}, + {10, 9, 10}, + {10, 2, 11}, + {10, 9, 11}, + {10, 2, 111}, + {10, 9, 111}, + + {100, 12, 9}, + {100, 12, 9}, + {100, 10, 10}, + {100, 10, 10}, + {100, 10, 11}, + {100, 20, 111}, + }; + comboRunner(params); + } + + /** + * Tests situation where the number of shards requested exceeds the + * the number of files. Empty shards are expected. + */ + @Test + public void testEmptyShards() { + int[][] params = { + {2, 1, 1}, // two shards, for one package, with one files + {10, 2, 2}, + {100, 10, 9}, + {100, 9, 10}, + }; + comboRunner(params); + } + + /** + * Run multiple test for sets of test specifications consisting of + * "number of shards", "number of packages", "package size". + */ + private void comboRunner(int[][] params) { + + Collection<String> input; + Map<String, Integer> output; + + for (int[] param : params) { + input = genEntries(param[1], param[2]); + output = runOne(param[0], input); + splitAsserts(param[0], param[1], param[2], + commonAsserts(param[0], param[1], param[2], input, output)); + } + } + + private Map<String, Integer> runOne(int shards, Collection<String> entries) { + Splitter splitter = new Splitter(shards, entries.size()); + Map<String, Integer> result = new LinkedHashMap<>(); + for (String entry : entries) { + result.put(entry, splitter.assign(entry)); + } + return result; + } + + private Collection<String> genEntries(int packages, int files) { + List<String> entries = new ArrayList<>(); + for (int dir = 0; dir < packages; dir++) { + for (int file = 0; file < files; file++) { + entries.add("dir" + dir + ARCHIVE_FILE_SEPARATOR + "file" + file + CLASS_SUFFIX); + } + } + return entries; + } + + private int[] assertAndCountMappings(int shards, int packageSize, + Map<String, Integer> output) { + int[] counts = new int[shards + 1]; + String prevPath = null; + int prev = -2; + for (Map.Entry<String, Integer> entry : output.entrySet()) { + String path = entry.getKey(); + int assignment = entry.getValue(); + assertThat(assignment).isIn(Range.closed(0, shards)); + counts[assignment + 1]++; + if (path.endsWith(CLASS_SUFFIX)) { + if (prev == -2) { + assertThat(assignment).isEqualTo(0); + } else if (prev > 0 && prev != assignment) { + String prevDir = prevPath.substring(0, prevPath.lastIndexOf(ARCHIVE_DIR_SUFFIX)); + String dir = path.substring(0, path.lastIndexOf(ARCHIVE_DIR_SUFFIX)); + assertThat(assignment).isEqualTo(prev + 1); // shard index increasing + // package boundary, or partial package + assertThat(!prevDir.equals(dir) || counts[prev + 1] % packageSize != 0).isTrue(); + } + prevPath = path; + } + prev = assignment; + } + return counts; + } + + /** + * Validate that generated mapping maintains input order. + */ + private void assertMaintainOrder(Collection<String> input, Map<String, Integer> output) { + assertThat(output.keySet()).containsExactlyElementsIn(input).inOrder(); + } + + /** + * Verifies that packages have not been unnecessarily split. + */ + private void assertNoSplit(int packageSize, int[] counts) { + for (int i = 1; i < counts.length; i++) { + assertThat(counts[i] % packageSize).isEqualTo(0); + } + } + + /** + * Verifies the presence of package-split in the tailing shards. + */ + private void assertHasSplit(int packageSize, int[] counts) { + for (int i = 1; i < counts.length - 1; i++) { + if (counts[i + 1] <= 1) { + continue; + } + assertThat(counts[i] % packageSize).isEqualTo(0); + } + } + + /** + * Verify the presence of tailing empty shards, if unavoidable. + */ + private void assertHasEmpty(int[] counts, boolean expectEmpty) { + boolean hasEmpty = false; + for (int i = 1; i < counts.length; i++) { + if (counts[i] == 0) { + hasEmpty = true; + } else { + assertThat(!hasEmpty || counts[i] == 0).isTrue(); + } + } + assertThat(hasEmpty).isEqualTo(expectEmpty); + } + + /** + * Validates that each chard meets expected minimal and maximum size requirements, + * to ensure that shards are reasonably evenly sized. + */ + private void assertBalanced(int shards, int packageCount, int packageSize, int entries, + int[] counts) { + int classes = packageSize * packageCount; + int noneClass = entries - counts[0] - classes; + int idealSize = Math.max(1, classes / shards); + int superSize = Math.max(1, entries / shards); + int almostFull = Math.min(Math.min(10, (idealSize + 3) >> 2), (int) Math.log(shards)); + int lowerBound = idealSize - almostFull; + int upperBound = superSize + Math.max(packageSize, (int) (Math.log(shards)) * 10); + for (int i = 1; i < counts.length; i++) { + int adjusted = i == 1 ? counts[i] - noneClass : counts[i]; + if (i < shards && counts[i + 1] > 1) { + assertThat(counts[i]).isIn(Range.closed(packageSize, entries)); + if (noneClass == 0 && counts[0] == 0) { + assertThat(counts[i]).isIn(Range.closed(lowerBound, entries)); + } + } + assertThat(adjusted).isIn(Range.closed(0, upperBound)); + } + } + + /** + * Verifies that packages are only split as expected, and that no unexpected + * empty shards are generated. + */ + private void splitAsserts(int shards, int packageCount, int packageSize, int[] counts) { + boolean emptyExpected = packageCount * packageSize < shards; + boolean splitExpected = shards > packageCount; + if (splitExpected) { + assertHasSplit(packageSize, counts); + } else { + assertNoSplit(packageSize, counts); + } + assertHasEmpty(counts, emptyExpected); + } + + /** + * Checks assert applicable to all tests. + */ + private int[] commonAsserts(int shards, int packageCount, int packageSize, + Collection<String> input, Map<String, Integer> output) { + assertMaintainOrder(input, output); + int[] counts = assertAndCountMappings(shards, packageSize, output); + assertBalanced(shards, packageCount, packageSize, input.size(), counts); + return counts; + } +} diff --git a/src/test/java/com/google/devtools/build/android/ziputils/ViewTest.java b/src/test/java/com/google/devtools/build/android/ziputils/ViewTest.java new file mode 100644 index 0000000000..8eeabbab90 --- /dev/null +++ b/src/test/java/com/google/devtools/build/android/ziputils/ViewTest.java @@ -0,0 +1,191 @@ +// 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.android.ziputils; + +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.assertSame; +import static org.junit.Assert.fail; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; + +/** + * Unit tests for {@link View}. + */ +@RunWith(JUnit4.class) +public class ViewTest { + + private static final FakeFileSystem fileSystem = new FakeFileSystem(); + + @Test + public void testView() { + // View takes ownership of constructor argument! + // Subclasses are responsible for slicing, when needed. + ByteBuffer buffer = ByteBuffer.allocate(100); + TestView instance = new TestView(buffer); + buffer.putInt(12345678); + int fromBuf = buffer.getInt(0); + int fromView = instance.getInt(0); + assertEquals("must assume buffer ownership", fromBuf, fromView); + int posBuf = buffer.position(); + int posView = instance.buffer.position(); + assertEquals("must assume buffer ownership", posBuf, posView); + } + + @Test + public void testAt() { + long fileOffset = 0L; + ByteBuffer buffer = ByteBuffer.allocate(100); + TestView instance = new TestView(buffer); + View<TestView> result = instance.at(fileOffset); + assertSame("didn't return this", instance, result); + + long resultValue = instance.fileOffset(); + assertEquals("didn't return set value", fileOffset, resultValue); + } + + @Test + public void testFileOffset() { + ByteBuffer buffer = ByteBuffer.allocate(100); + TestView instance = new TestView(buffer); + long expResult = -1L; + long result = instance.fileOffset(); + assertEquals("default file offset should be -1", expResult, result); + } + + @Test + public void testFinish() { + ByteBuffer buffer = ByteBuffer.allocate(100); + TestView instance = new TestView(buffer); + int limit = instance.buffer.limit(); + int pos = instance.buffer.position(); + assertEquals("initial limit", 100, limit); + assertEquals("initial position", 0, pos); + instance.putInt(1234); + limit = instance.buffer.limit(); + pos = instance.buffer.position(); + assertEquals("limit unchanged", 100, limit); + assertEquals("position advanced", 4, pos); + instance.buffer.flip(); + int finishedLimit = instance.buffer.limit(); + int finishedPos = instance.buffer.position(); + assertEquals("must set limit to position", pos, finishedLimit); + assertEquals("must set position to 0", 0, finishedPos); + } + + @Test + public void testWriteTo() throws Exception { + FileChannel file = fileSystem.getOutputChannel("hello", false); + byte[] bytes = "hello world".getBytes(UTF_8); + ByteBuffer buffer = ByteBuffer.wrap(bytes); + TestView instance = new TestView(buffer); + int expResult = bytes.length; + instance.buffer.rewind(); + int result = file.write(instance.buffer); + file.close(); + assertEquals("incorrect number of bytes written", expResult, result); + byte[] bytesWritten = fileSystem.toByteArray("hello"); + Assert.assertArrayEquals("incorrect bytes written", bytes, bytesWritten); + } + + @Test + public void testGetBytes() { + int off = 3; + int len = 5; + byte[] bytes = "hello world".getBytes(UTF_8); + ByteBuffer buffer = ByteBuffer.wrap(bytes); + TestView instance = new TestView(buffer); + byte[] expResult = "lo wo".getBytes(UTF_8); + byte[] result = instance.getBytes(off, len); + assertArrayEquals("incorrect bytes returned", expResult, result); + try { + instance.getBytes(bytes.length - len + 1, len); + fail("expected Exception"); + } catch (IndexOutOfBoundsException ex) { + // expected + } + try { + instance.getBytes(-1, len); + fail("expected Exception"); + } catch (IndexOutOfBoundsException ex) { + // expected + } + } + + @Test + public void testGetString() { + int off = 6; + int len = 5; + byte[] bytes = "hello world".getBytes(UTF_8); + ByteBuffer buffer = ByteBuffer.wrap(bytes); + TestView instance = new TestView(buffer); + String expResult = "world"; + String result = instance.getString(off, len); + assertEquals("didn't return this", expResult, result); + try { + instance.getString(off + 1, len); + fail("expected Exception"); + } catch (IndexOutOfBoundsException ex) { + // expected + } + try { + instance.getString(-1, len); + fail("expected Exception"); + } catch (IndexOutOfBoundsException ex) { + // expected + } + } + + @Test + public void testByteOrder() { + byte[] bytes = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + TestView instance = new TestView(ByteBuffer.wrap(bytes)); + int expValue = 0x08070605; + int value = instance.getInt(4); + assertEquals("Byte order incorrect", expValue, value); + } + + static class TestView extends View<TestView> { + TestView(ByteBuffer buffer) { + super(buffer); + } + + // Will advance buffer position + public void putInt(int value) { + buffer.putInt(value); + } + + // Will advance buffer position + public int getInt() { + return buffer.getInt(); + } + + // will not advance buffer position + public void putInt(int index, int value) { + buffer.putInt(index, value); + } + + // will not advance buffer position + public int getInt(int index) { + return buffer.getInt(index); + } + } +} diff --git a/src/test/java/com/google/devtools/build/android/ziputils/ZipFileBuilder.java b/src/test/java/com/google/devtools/build/android/ziputils/ZipFileBuilder.java new file mode 100644 index 0000000000..ae574c824e --- /dev/null +++ b/src/test/java/com/google/devtools/build/android/ziputils/ZipFileBuilder.java @@ -0,0 +1,164 @@ +// 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.android.ziputils; + +import static com.google.devtools.build.android.ziputils.DataDescriptor.EXTLEN; +import static com.google.devtools.build.android.ziputils.DataDescriptor.EXTSIZ; +import static com.google.devtools.build.android.ziputils.DirectoryEntry.CENFLG; +import static com.google.devtools.build.android.ziputils.DirectoryEntry.CENLEN; +import static com.google.devtools.build.android.ziputils.DirectoryEntry.CENOFF; +import static com.google.devtools.build.android.ziputils.DirectoryEntry.CENSIZ; +import static com.google.devtools.build.android.ziputils.DirectoryEntry.CENTIM; +import static com.google.devtools.build.android.ziputils.LocalFileHeader.LOCFLG; +import static com.google.devtools.build.android.ziputils.LocalFileHeader.LOCLEN; +import static com.google.devtools.build.android.ziputils.LocalFileHeader.LOCSIZ; +import static com.google.devtools.build.android.ziputils.LocalFileHeader.LOCTIM; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +/** + * Zip file builder for testing, For now it only supports building + * valid zip files. + */ +class ZipFileBuilder { + private final List<FileInfo> input; + private static final Charset CHARSET = Charset.forName("UTF-8"); + private static final byte[] EMPTY = {}; + + public ZipFileBuilder() { + input = new ArrayList<>(); + } + + public ZipFileBuilder add(String filename, String content) { + input.add(new FileInfo(filename, content.getBytes(Charset.defaultCharset()))); + return this; + } + + public ZipFileBuilder add(FileInfo fileInfo) { + input.add(fileInfo); + return this; + } + + public void create(String filename) throws IOException { + ZipOut out = new ZipOut(FileSystem.fileSystem().getOutputChannel(filename, false), filename); + for (FileInfo info : input) { + int compressed = info.compressedSize(); + int uncompressed = info.uncompressedSize(); + int dirCompressed = info.dirCompressedSize(); + int dirUncompressed = info.dirUncompressedSize(); + short flags = info.flags(); + DirectoryEntry entry = DirectoryEntry.allocate(info.name, info.extra, info.comment); + out.nextEntry(entry) + .set(CENOFF, out.fileOffset()) + .set(CENFLG, flags) + .set(CENTIM, info.date) + .set(CENLEN, dirUncompressed) + .set(CENSIZ, dirCompressed); + LocalFileHeader header = LocalFileHeader.allocate(info.name, null) + .set(LOCFLG, flags) + .set(LOCTIM, info.date) + .set(LOCLEN, uncompressed) + .set(LOCSIZ, compressed); + out.write(header); + out.write(ByteBuffer.wrap(info.data)); + if (flags != 0) { + DataDescriptor desc = DataDescriptor.allocate() + .set(EXTLEN, dirUncompressed) + .set(EXTSIZ, dirCompressed); + out.write(desc); + } + } + out.close(); + } + + public static class FileInfo { + private final String name; + private final short method; + private final int date; + private final int uncompressed; + private final byte[] data; + private final byte[] extra; + private final String comment; + boolean maskSize; + + static final short STORED = 0; + static final short DEFLATED = 8; + + public FileInfo(String filename, String content) { + this(filename, DosTime.EPOCH.time, STORED, 0, + (content == null ? EMPTY : content.getBytes(CHARSET)), null, null); + } + + public FileInfo(String filename, byte[] data) { + this(filename, DosTime.EPOCH.time, STORED, 0, data, null, null); + } + + public FileInfo(String filename, byte[] data, int uncompressed) { + this(filename, DosTime.EPOCH.time, DEFLATED, uncompressed, data, null, null); + } + + public FileInfo(String filename, int dosTime, String content) { + this(filename, dosTime, STORED, 0, + (content == null ? EMPTY : content.getBytes(CHARSET)), null, null); + } + + public FileInfo(String filename, int dosTime, byte[] data) { + this(filename, dosTime, STORED, 0, data, null, null); + } + + public FileInfo(String filename, int dosTime, byte[] data, int uncompressed) { + this(filename, dosTime, DEFLATED, uncompressed, data, null, null); + } + + public FileInfo(String filename, int dosTime, short method, int uncompressed, + byte[] content, byte[] extra, String comment) { + this.name = filename; + this.date = dosTime; + this.method = method; + this.uncompressed = uncompressed; + this.data = content; + this.extra = extra; + this.comment = comment; + maskSize = false; + } + + public void setMaskSize(boolean ignore) { + maskSize = ignore; + } + + int compressedSize() { + return method != 0 && !maskSize ? data.length : 0; + } + + int uncompressedSize() { + return method == 0 ? data.length : maskSize ? 0 : uncompressed; + } + + int dirCompressedSize() { + return method == 0 ? 0 : data.length; + } + + int dirUncompressedSize() { + return method == 0 ? data.length : uncompressed; + } + + short flags() { + return method != 0 && uncompressed == 0 ? LocalFileHeader.SIZE_MASKED_FLAG : 0; + } + } +} diff --git a/src/test/java/com/google/devtools/build/android/ziputils/ZipInTest.java b/src/test/java/com/google/devtools/build/android/ziputils/ZipInTest.java new file mode 100644 index 0000000000..9ce75d528c --- /dev/null +++ b/src/test/java/com/google/devtools/build/android/ziputils/ZipInTest.java @@ -0,0 +1,558 @@ +// 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.android.ziputils; + +import static com.google.devtools.build.android.ziputils.DirectoryEntry.CENHOW; +import static com.google.devtools.build.android.ziputils.DirectoryEntry.CENLEN; +import static com.google.devtools.build.android.ziputils.DirectoryEntry.CENOFF; +import static com.google.devtools.build.android.ziputils.DirectoryEntry.CENSIZ; +import static com.google.devtools.build.android.ziputils.EndOfCentralDirectory.ENDOFF; +import static com.google.devtools.build.android.ziputils.EndOfCentralDirectory.ENDSIG; +import static com.google.devtools.build.android.ziputils.EndOfCentralDirectory.ENDSIZ; +import static com.google.devtools.build.android.ziputils.EndOfCentralDirectory.ENDSUB; +import static com.google.devtools.build.android.ziputils.EndOfCentralDirectory.ENDTOT; +import static com.google.devtools.build.android.ziputils.ZipIn.ZipEntry; +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.assertSame; +import static org.junit.Assert.fail; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.List; +import java.util.zip.ZipInputStream; + +/** + * Unit tests for {@link ZipIn}. + */ +@RunWith(JUnit4.class) +public class ZipInTest { + + private static final int ENTRY_COUNT = 1000; + private FakeFileSystem fileSystem; + + @Before + public void setUp() throws Exception { + fileSystem = new FakeFileSystem(); + } + + /** + * Test of endOfCentralDirectory method, of class ZipIn. + */ + @Test + public void testEndOfCentralDirectory() throws Exception { + + String filename = "test.zip"; + byte[] bytes; + ByteBuffer buffer; + String comment; + int commentLen; + int offset; + ZipIn zipIn; + EndOfCentralDirectory result; + String subcase; + + // Find it, even if it's the only useful thing in the file. + subcase = " EOCD found it, "; + bytes = new byte[] { + 0x50, 0x4b, 0x05, 0x06, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + }; + fileSystem.addFile(filename, bytes); + zipIn = newZipIn(filename); + result = zipIn.endOfCentralDirectory(); + assertNotNull(subcase + "found", result); + + subcase = " EOCD not there at all, "; + bytes = new byte[]{ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + }; + fileSystem.addFile(filename, bytes); + zipIn = newZipIn(filename); + try { + zipIn.endOfCentralDirectory(); + fail(subcase + "expected IllegalStateException"); + } catch (Exception ex) { + assertSame(subcase + "caught exception", IllegalStateException.class, ex.getClass()); + } + + // If we can't read it, it's not there + subcase = " EOCD too late to read, "; + bytes = new byte[] { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0x50, 0x4b, 0x05, 0x06, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + }; + fileSystem.addFile(filename, bytes); + zipIn = newZipIn(filename); + try { + zipIn.endOfCentralDirectory(); + fail(subcase + "expected IndexOutOfBoundsException"); + } catch (Exception ex) { + assertSame(subcase + "caught exception", IndexOutOfBoundsException.class, ex.getClass()); + } + + // Current implementation doesn't know to scan past a bad EOCD record. + // I'm not sure if it should. + subcase = " EOCD good hidden by bad, "; + bytes = new byte[] { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0x50, 0x4b, 0x05, 0x06, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0x50, 0x4b, 0x05, 0x06, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + }; + fileSystem.addFile(filename, bytes); + zipIn = newZipIn(filename); + try { + zipIn.endOfCentralDirectory(); + fail(subcase + "expected IndexOutOfBoundsException"); + } catch (Exception ex) { + assertSame(subcase + "caught exception", IndexOutOfBoundsException.class, ex.getClass()); + } + + // Minimal format checking here, assuming the EndOfDirectoryTest class + // test for that. + + subcase = " EOCD truncated comment, "; + bytes = new byte[100]; + buffer = ByteBuffer.wrap(bytes); + comment = "optional file comment"; + commentLen = comment.getBytes(UTF_8).length; + offset = bytes.length - ZipInputStream.ENDHDR - commentLen; + buffer.position(offset); + EndOfCentralDirectory.view(buffer, comment); + byte[] truncated = Arrays.copyOf(bytes, bytes.length - 5); + fileSystem.addFile(filename, truncated); + zipIn = newZipIn(filename); + try { // not sure this is the exception we want! + zipIn.endOfCentralDirectory(); + fail(subcase + "expected IllegalArgumentException"); + } catch (Exception ex) { + assertSame(subcase + "caught exception", IllegalArgumentException.class, ex.getClass()); + } + + subcase = " EOCD no comment, "; + bytes = new byte[100]; + buffer = ByteBuffer.wrap(bytes); + comment = null; + commentLen = 0; + offset = bytes.length - ZipInputStream.ENDHDR - commentLen; + buffer.position(offset); + EndOfCentralDirectory.view(buffer, comment); + fileSystem.addFile(filename, bytes); + zipIn = newZipIn(filename); + result = zipIn.endOfCentralDirectory(); + assertNotNull(subcase + "found", result); + assertEquals(subcase + "comment", "", result.getComment()); + assertEquals(subcase + "marker", ZipInputStream.ENDSIG, (int) result.get(ENDSIG)); + + subcase = " EOCD comment, "; + bytes = new byte[100]; + buffer = ByteBuffer.wrap(bytes); + comment = "optional file comment"; + commentLen = comment.getBytes(UTF_8).length; + offset = bytes.length - ZipInputStream.ENDHDR - commentLen; + buffer.position(offset); + EndOfCentralDirectory.view(buffer, comment); + assertEquals(subcase + "setup", comment, + new String(bytes, bytes.length - commentLen, commentLen, UTF_8)); + fileSystem.addFile(filename, bytes); + zipIn = newZipIn(filename); + result = zipIn.endOfCentralDirectory(); + assertNotNull(subcase + "found", result); + assertEquals(subcase + "comment", comment, result.getComment()); + assertEquals(subcase + "marker", ZipInputStream.ENDSIG, (int) result.get(ENDSIG)); + + subcase = " EOCD extra data, "; + bytes = new byte[100]; + buffer = ByteBuffer.wrap(bytes); + comment = null; + commentLen = 0; + offset = bytes.length - ZipInputStream.ENDHDR - commentLen - 10; + buffer.position(offset); + EndOfCentralDirectory.view(buffer, comment); + fileSystem.addFile(filename, bytes); + zipIn = newZipIn(filename); + result = zipIn.endOfCentralDirectory(); + assertNotNull(subcase + "found", result); + assertEquals(subcase + "comment", "", result.getComment()); + assertEquals(subcase + "marker", ZipInputStream.ENDSIG, (int) result.get(ENDSIG)); + } + + /** + * Test of centralDirectory method, of class ZipIn. + */ + @Test + public void testCentralDirectory() throws Exception { + String filename = "test.zip"; + ByteBuffer buffer; + int offset; + ZipIn zipIn; + String subcase; + subcase = " EOCD extra data, "; + String commonName = "thisIsNotNormal.txt"; + int filenameLen = commonName.getBytes(UTF_8).length; + int count = ENTRY_COUNT; + int dirEntry = ZipInputStream.CENHDR; + int before = count; + int between = 0; // implementation doesn't tolerate data between dir entries, does the spec? + int after = 20; + int eocd = ZipInputStream.ENDHDR; + int total = before + (count * (dirEntry + filenameLen)) + ((count - 1) * between) + + after + eocd; + byte[] bytes = new byte[total]; + offset = before; + for (int i = 0; i < count; i++) { + if (i > 0) { + offset += between; + } + buffer = ByteBuffer.wrap(bytes, offset, bytes.length - offset); + DirectoryEntry.view(buffer, commonName, null, null) + .set(CENHOW, (short) 8) + .set(CENSIZ, before) + .set(CENLEN, 2 * before) + .set(CENOFF, i); // Not valid of course, but we're only testing central dir parsing. + // and there are currently no checks in the parser to see if offset makes sense. + offset += dirEntry + filenameLen; + } + offset += after; + buffer = ByteBuffer.wrap(bytes, offset, bytes.length - offset); + EndOfCentralDirectory.view(buffer, null) + .set(ENDOFF, before) + .set(ENDSIZ, offset - before - after) + .set(ENDTOT, (short) count) + .set(ENDSUB, (short) count); + + fileSystem.addFile(filename, bytes); + zipIn = newZipIn(filename); + CentralDirectory result = zipIn.centralDirectory(); + assertNotNull(subcase + "found", result); + List<DirectoryEntry> list = result.list(); + assertEquals(subcase + "size", count, list.size()); + for (int i = 0; i < list.size(); i++) { + assertEquals(subcase + "offset check[" + i + "]", i, list.get(i).get(CENOFF)); + } + } + + /** + * Test of scanEntries method, of class ZipIn. + */ + @Test + public void testScanEntries() throws Exception { + int count = ENTRY_COUNT * 100; + String filename = "test.jar"; + + ZipFileBuilder builder = new ZipFileBuilder(); + for (int i = 0; i < count; i++) { + builder.add("pkg/f" + i + ".class", "All day long"); + } + builder.create(filename); + + final ZipIn zipIn = newZipIn(filename); + zipIn.scanEntries(new EntryHandler() { + int count = 0; + @Override + public void handle(ZipIn in, LocalFileHeader header, DirectoryEntry dirEntry, + ByteBuffer data) throws IOException { + assertSame(zipIn, in); + String filename = "pkg/f" + count + ".class"; + assertEquals(filename, header.getFilename()); + assertEquals(filename, dirEntry.getFilename()); + count++; + } + }); + } + + /** + * Test of nextHeaderFrom method, of class ZipIn. + */ + @Test + public void testNextHeaderFrom_long() throws Exception { + int count = ENTRY_COUNT; + String filename = "test.jar"; + ZipFileBuilder builder = new ZipFileBuilder(); + for (int i = 0; i < count; i++) { + builder.add("pkg/f" + i + ".class", "All day long"); + } + builder.create(filename); + final ZipIn zipIn = newZipIn(filename); + zipIn.endOfCentralDirectory(); + count = 0; + int offset = 0; + LocalFileHeader header; + do { + header = zipIn.nextHeaderFrom(offset); + String name = "pkg/f" + count + ".class"; + if (header != null) { + assertEquals(name, header.getFilename()); + count++; + offset = (int) header.fileOffset() + 4; + } + } while(header != null); + assertEquals(ENTRY_COUNT, count); + } + + /** + * Test of nextHeaderFrom method, of class ZipIn. + */ + @Test + public void testNextHeaderFrom_DirectoryEntry() throws Exception { + int count = ENTRY_COUNT; + String filename = "test.jar"; + ZipFileBuilder builder = new ZipFileBuilder(); + for (int i = 0; i < count; i++) { + builder.add("pkg/f" + i + ".class", "All day long"); + } + builder.create(filename); + final ZipIn zipIn = newZipIn(filename); + zipIn.centralDirectory(); + List<DirectoryEntry> list = zipIn.centralDirectory().list(); + count = 0; + String name; + LocalFileHeader header = zipIn.nextHeaderFrom(null); + for (DirectoryEntry dirEntry : list) { + name = "pkg/f" + count + ".class"; + assertEquals(name, dirEntry.getFilename()); + assertEquals(name, header.getFilename()); + header = zipIn.nextHeaderFrom(dirEntry); + count++; + } + assertNull(header); + } + + /** + * Test of localHeaderFor method, of class ZipIn. + */ + @Test + public void testLocalHeaderFor() throws Exception { + int count = ENTRY_COUNT; + String filename = "test.jar"; + ZipFileBuilder builder = new ZipFileBuilder(); + for (int i = 0; i < count; i++) { + builder.add("pkg/f" + i + ".class", "All day long"); + } + builder.create(filename); + final ZipIn zipIn = newZipIn(filename); + zipIn.centralDirectory(); + List<DirectoryEntry> list = zipIn.centralDirectory().list(); + count = 0; + String name; + LocalFileHeader header; + for (DirectoryEntry dirEntry : list) { + name = "pkg/f" + count + ".class"; + header = zipIn.localHeaderFor(dirEntry); + assertEquals(name, dirEntry.getFilename()); + assertEquals(name, header.getFilename()); + count++; + } + } + + /** + * Test of localHeaderAt method, of class ZipIn. + */ + @Test + public void testLocalHeaderAt() throws Exception { + int count = ENTRY_COUNT; + String filename = "test.jar"; + ZipFileBuilder builder = new ZipFileBuilder(); + for (int i = 0; i < count; i++) { + builder.add("pkg/f" + i + ".class", "All day long"); + } + builder.create(filename); + final ZipIn zipIn = newZipIn(filename); + zipIn.centralDirectory(); + List<DirectoryEntry> list = zipIn.centralDirectory().list(); + count = 0; + String name; + LocalFileHeader header; + for (DirectoryEntry dirEntry : list) { + name = "pkg/f" + count + ".class"; + header = zipIn.localHeaderAt(dirEntry.get(CENOFF)); + assertEquals(name, dirEntry.getFilename()); + assertEquals(name, header.getFilename()); + count++; + } + } + + /** + * Test of nextFrom method, of class ZipIn. + */ + @Test + public void testNextFrom_long() throws Exception { + int count = ENTRY_COUNT; + String filename = "test.jar"; + ZipFileBuilder builder = new ZipFileBuilder(); + for (int i = 0; i < count; i++) { + builder.add("pkg/f" + i + ".class", "All day long"); + } + builder.create(filename); + final ZipIn zipIn = newZipIn(filename); + zipIn.centralDirectory(); + count = 0; + int offset = 0; + ZipEntry zipEntry; + do { + zipEntry = zipIn.nextFrom(offset); + String name = "pkg/f" + count + ".class"; + if (zipEntry.getCode() != ZipEntry.Status.ENTRY_NOT_FOUND) { + assertNotNull(zipEntry.getHeader()); + assertNotNull(zipEntry.getDirEntry()); + assertEquals(name, zipEntry.getHeader().getFilename()); + assertEquals(name, zipEntry.getDirEntry().getFilename()); + count++; + offset = (int) zipEntry.getHeader().fileOffset() + 4; + } + } while(zipEntry.getCode() != ZipEntry.Status.ENTRY_NOT_FOUND); + assertEquals(ENTRY_COUNT, count); + } + + /** + * Test of nextFrom method, of class ZipIn. + */ + @Test + public void testNextFrom_DirectoryEntry() throws Exception { + int count = ENTRY_COUNT; + String filename = "test.jar"; + ZipFileBuilder builder = new ZipFileBuilder(); + for (int i = 0; i < count; i++) { + builder.add("pkg/f" + i + ".class", "All day long"); + } + builder.create(filename); + final ZipIn zipIn = newZipIn(filename); + zipIn.centralDirectory(); + List<DirectoryEntry> list = zipIn.centralDirectory().list(); + count = 0; + String name; + ZipEntry zipEntry = zipIn.nextFrom(null); + for (DirectoryEntry dirEntry : list) { + if (zipEntry.getCode() == ZipEntry.Status.ENTRY_NOT_FOUND) { + break; + } + name = "pkg/f" + count + ".class"; + assertNotNull(zipEntry.getHeader()); + assertNotNull(zipEntry.getDirEntry()); + assertEquals(name, zipEntry.getHeader().getFilename()); + assertEquals(name, zipEntry.getDirEntry().getFilename()); + zipEntry = zipIn.nextFrom(dirEntry); + count++; + } + assertEquals(ENTRY_COUNT, count); + } + + /** + * Test of entryAt method, of class ZipIn. + */ + @Test + public void testEntryAt() throws Exception { + int count = ENTRY_COUNT; + String filename = "test.jar"; + ZipFileBuilder builder = new ZipFileBuilder(); + for (int i = 0; i < count; i++) { + builder.add("pkg/f" + i + ".class", "All day long"); + } + builder.create(filename); + final ZipIn zipIn = newZipIn(filename); + zipIn.centralDirectory(); + List<DirectoryEntry> list = zipIn.centralDirectory().list(); + count = 0; + String name; + ZipEntry zipEntry; + for (DirectoryEntry dirEntry : list) { + zipEntry = zipIn.entryAt(dirEntry.get(CENOFF)); + name = "pkg/f" + count + ".class"; + assertNotNull(zipEntry.getHeader()); + assertNotNull(zipEntry.getDirEntry()); + assertEquals(name, zipEntry.getHeader().getFilename()); + assertEquals(name, zipEntry.getDirEntry().getFilename()); + count++; + } + assertEquals(ENTRY_COUNT, count); + } + + /** + * Test of entryWith method, of class ZipIn. + */ + @Test + public void testEntryWith() throws Exception { + int count = ENTRY_COUNT; + String filename = "test.jar"; + ZipFileBuilder builder = new ZipFileBuilder(); + for (int i = 0; i < count; i++) { + builder.add("pkg/f" + i + ".class", "All day long"); + } + builder.create(filename); + final ZipIn zipIn = newZipIn(filename); + zipIn.centralDirectory(); + count = 0; + int offset = 0; + LocalFileHeader header; + do { + header = zipIn.nextHeaderFrom(offset); + String name = "pkg/f" + count + ".class"; + if (header != null) { + ZipEntry zipEntry = zipIn.entryWith(header); + assertNotNull(zipEntry.getDirEntry()); + assertSame(header, zipEntry.getHeader()); + assertEquals(name, zipEntry.getHeader().getFilename()); + assertEquals(name, zipEntry.getDirEntry().getFilename()); + assertEquals(name, header.getFilename()); + count++; + offset = (int) header.fileOffset() + 4; + } + } while(header != null); + assertEquals(ENTRY_COUNT, count); + } + + private ZipIn newZipIn(String filename) throws IOException { + return new ZipIn(fileSystem.getInputChannel(filename), filename); + } +} diff --git a/src/test/java/com/google/devtools/build/android/ziputils/ZipOutTest.java b/src/test/java/com/google/devtools/build/android/ziputils/ZipOutTest.java new file mode 100644 index 0000000000..1b9cf39979 --- /dev/null +++ b/src/test/java/com/google/devtools/build/android/ziputils/ZipOutTest.java @@ -0,0 +1,51 @@ +// 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.android.ziputils; + +import static com.google.devtools.build.android.ziputils.DirectoryEntry.CENTIM; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Unit tests for {@link ZipOut}. + */ +@RunWith(JUnit4.class) +public class ZipOutTest { + + private static final FakeFileSystem fileSystem = new FakeFileSystem(); + + @Test + public void testNextEntry() { + try { + String filename = "out.zip"; + ZipOut instance = new ZipOut(fileSystem.getOutputChannel(filename, false), filename); + + instance.nextEntry(DirectoryEntry.allocate("pgk/a.class", null, null)) + .set(CENTIM, DosTime.EPOCH.time); + + instance.nextEntry(DirectoryEntry.allocate("pgk/b.class", null, null)) + .set(CENTIM, DosTime.EPOCH.time); + + instance.close(); + } catch (IOException ex) { + Logger.getLogger(ZipOutTest.class.getName()).log(Level.SEVERE, null, ex); + } + } +} |