// Copyright 2015 The Bazel Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package com.google.devtools.build.android.ziputils; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; 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 java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.fail; import com.google.devtools.build.android.ziputils.ZipIn.ZipEntry; import java.io.IOException; import java.nio.ByteBuffer; import java.util.Arrays; import java.util.List; import java.util.zip.ZipInputStream; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** * 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(); assertWithMessage(subcase + "found").that(result).isNotNull(); 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) { assertWithMessage(subcase + "caught exception") .that(ex.getClass()) .isSameAs(IllegalStateException.class); } // 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) { assertWithMessage(subcase + "caught exception") .that(ex.getClass()) .isSameAs(IndexOutOfBoundsException.class); } // 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) { assertWithMessage(subcase + "caught exception") .that(ex.getClass()) .isSameAs(IndexOutOfBoundsException.class); } // 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) { assertWithMessage(subcase + "caught exception") .that(ex.getClass()) .isSameAs(IllegalArgumentException.class); } 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(); assertWithMessage(subcase + "found").that(result).isNotNull(); assertWithMessage(subcase + "comment").that(result.getComment()).isEqualTo(""); assertWithMessage(subcase + "marker") .that((int) result.get(ENDSIG)) .isEqualTo(ZipInputStream.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); assertWithMessage(subcase + "setup") .that(new String(bytes, bytes.length - commentLen, commentLen, UTF_8)) .isEqualTo(comment); fileSystem.addFile(filename, bytes); zipIn = newZipIn(filename); result = zipIn.endOfCentralDirectory(); assertWithMessage(subcase + "found").that(result).isNotNull(); assertWithMessage(subcase + "comment").that(result.getComment()).isEqualTo(comment); assertWithMessage(subcase + "marker") .that((int) result.get(ENDSIG)) .isEqualTo(ZipInputStream.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(); assertWithMessage(subcase + "found").that(result).isNotNull(); assertWithMessage(subcase + "comment").that(result.getComment()).isEqualTo(""); assertWithMessage(subcase + "marker") .that((int) result.get(ENDSIG)) .isEqualTo(ZipInputStream.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(); assertWithMessage(subcase + "found").that(result).isNotNull(); List list = result.list(); assertWithMessage(subcase + "size").that(list.size()).isEqualTo(count); for (int i = 0; i < list.size(); i++) { assertWithMessage(subcase + "offset check[" + i + "]") .that(list.get(i).get(CENOFF)) .isEqualTo(i); } } /** * 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 { assertThat(in).isSameAs(zipIn); String filename = "pkg/f" + count + ".class"; assertThat(header.getFilename()).isEqualTo(filename); assertThat(dirEntry.getFilename()).isEqualTo(filename); 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) { assertThat(header.getFilename()).isEqualTo(name); count++; offset = (int) header.fileOffset() + 4; } } while(header != null); assertThat(count).isEqualTo(ENTRY_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 list = zipIn.centralDirectory().list(); count = 0; String name; LocalFileHeader header = zipIn.nextHeaderFrom(null); for (DirectoryEntry dirEntry : list) { name = "pkg/f" + count + ".class"; assertThat(dirEntry.getFilename()).isEqualTo(name); assertThat(header.getFilename()).isEqualTo(name); header = zipIn.nextHeaderFrom(dirEntry); count++; } assertThat(header).isNull(); } /** * 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 list = zipIn.centralDirectory().list(); count = 0; String name; LocalFileHeader header; for (DirectoryEntry dirEntry : list) { name = "pkg/f" + count + ".class"; header = zipIn.localHeaderFor(dirEntry); assertThat(dirEntry.getFilename()).isEqualTo(name); assertThat(header.getFilename()).isEqualTo(name); 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 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)); assertThat(dirEntry.getFilename()).isEqualTo(name); assertThat(header.getFilename()).isEqualTo(name); 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) { assertThat(zipEntry.getHeader()).isNotNull(); assertThat(zipEntry.getDirEntry()).isNotNull(); assertThat(zipEntry.getHeader().getFilename()).isEqualTo(name); assertThat(zipEntry.getDirEntry().getFilename()).isEqualTo(name); count++; offset = (int) zipEntry.getHeader().fileOffset() + 4; } } while(zipEntry.getCode() != ZipEntry.Status.ENTRY_NOT_FOUND); assertThat(count).isEqualTo(ENTRY_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 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"; assertThat(zipEntry.getHeader()).isNotNull(); assertThat(zipEntry.getDirEntry()).isNotNull(); assertThat(zipEntry.getHeader().getFilename()).isEqualTo(name); assertThat(zipEntry.getDirEntry().getFilename()).isEqualTo(name); zipEntry = zipIn.nextFrom(dirEntry); count++; } assertThat(count).isEqualTo(ENTRY_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 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"; assertThat(zipEntry.getHeader()).isNotNull(); assertThat(zipEntry.getDirEntry()).isNotNull(); assertThat(zipEntry.getHeader().getFilename()).isEqualTo(name); assertThat(zipEntry.getDirEntry().getFilename()).isEqualTo(name); count++; } assertThat(count).isEqualTo(ENTRY_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); assertThat(zipEntry.getDirEntry()).isNotNull(); assertThat(zipEntry.getHeader()).isSameAs(header); assertThat(zipEntry.getHeader().getFilename()).isEqualTo(name); assertThat(zipEntry.getDirEntry().getFilename()).isEqualTo(name); assertThat(header.getFilename()).isEqualTo(name); count++; offset = (int) header.fileOffset() + 4; } } while(header != null); assertThat(count).isEqualTo(ENTRY_COUNT); } private ZipIn newZipIn(String filename) throws IOException { return new ZipIn(fileSystem.getInputChannel(filename), filename); } }