Open source tests for android/ziputils.

--
MOS_MIGRATED_REVID=97677526
diff --git a/src/test/java/BUILD b/src/test/java/BUILD
index 0e9e922..8605d28 100644
--- a/src/test/java/BUILD
+++ b/src/test/java/BUILD
@@ -511,6 +511,22 @@
 )
 
 java_test(
+    name = "ziputils-tests",
+    srcs = glob(["com/google/devtools/build/android/ziputils/*.java"]),
+    args = ["com.google.devtools.build.android.ziputils.AllTests"],
+    tags = ["ziputils"],
+    deps = [
+        ":testutil",
+        "//src/tools/android/java/com/google/devtools/build/android/ziputils:splitter_lib",
+        "//src/tools/android/java/com/google/devtools/build/android/ziputils:ziputils_lib",
+        "//third_party:guava",
+        "//third_party:jsr305",
+        "//third_party:junit4",
+        "//third_party:truth",
+    ],
+)
+
+java_test(
     name = "common-rules-tests",
     srcs = glob(["com/google/devtools/build/lib/rules/filegroup/*.java"]),
     args = ["com.google.devtools.build.lib.AllTests"],
@@ -531,6 +547,7 @@
 )
 
 TEST_SUITES = [
+    "ziputils",
     "rules",
     "analysis",
     "foundations",
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 0000000..a69b95d
--- /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 0000000..6a51756
--- /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 0000000..e582b52
--- /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 0000000..24b0c85
--- /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 0000000..12bbe22
--- /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 0000000..55a315d
--- /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 0000000..77b9b3e
--- /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 0000000..0a29957
--- /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 0000000..cfec2dc
--- /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 0000000..2f6be47
--- /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 0000000..a10c9b1
--- /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 0000000..7b791c0
--- /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 0000000..8eeabba
--- /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 0000000..ae574c8
--- /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 0000000..9ce75d5
--- /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 0000000..1b9cf39
--- /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);
+    }
+  }
+}