// 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.zip; import static com.google.common.truth.Truth.assertThat; import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.fail; import com.google.common.primitives.Bytes; import com.google.devtools.build.zip.ZipFileEntry.Compression; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.util.Calendar; import java.util.Random; import java.util.zip.CRC32; import java.util.zip.Deflater; import java.util.zip.ZipEntry; import java.util.zip.ZipException; import java.util.zip.ZipFile; @RunWith(JUnit4.class) public class ZipWriterTest { @Rule public TemporaryFolder tmp = new TemporaryFolder(); @Rule public ExpectedException thrown = ExpectedException.none(); private Random rand; private Calendar cal; private CRC32 crc; private Deflater deflater; private File test; @Before public void setup() throws IOException { rand = new Random(); cal = Calendar.getInstance(); cal.clear(); cal.set(Calendar.YEAR, rand.nextInt(128) + 1980); // Zip files have 7-bit year resolution. cal.set(Calendar.MONTH, rand.nextInt(12)); cal.set(Calendar.DAY_OF_MONTH, rand.nextInt(29)); cal.set(Calendar.HOUR_OF_DAY, rand.nextInt(24)); cal.set(Calendar.MINUTE, rand.nextInt(60)); cal.set(Calendar.SECOND, rand.nextInt(30) * 2); // Zip files have 2 second resolution. crc = new CRC32(); deflater = new Deflater(Deflater.DEFAULT_COMPRESSION, true); test = tmp.newFile("test.zip"); } @Test public void testEmpty() throws IOException { try (ZipWriter writer = new ZipWriter(new FileOutputStream(test), UTF_8)) { } try (ZipFile zipFile = new ZipFile(test)) { assertThat(zipFile.entries().hasMoreElements()).isFalse(); } } @Test public void testComment() throws IOException { try (ZipWriter writer = new ZipWriter(new FileOutputStream(test), UTF_8)) { writer.setComment("test comment"); } try (ZipFile zipFile = new ZipFile(test)) { assertThat(zipFile.entries().hasMoreElements()).isFalse(); assertThat(zipFile.getComment()).isEqualTo("test comment"); } } @Test public void testFileDataBeforeEntry() throws IOException { try (ZipWriter writer = new ZipWriter(new FileOutputStream(test), UTF_8)) { writer.write(new byte[] { 0xf, 0xa, 0xb }); fail("Expected ZipException"); } catch (ZipException e) { assertThat(e.getMessage()).contains("Cannot write zip contents without first setting a" + " ZipEntry or starting a prefix file."); } try (ZipFile zipFile = new ZipFile(test)) { assertThat(zipFile.entries().hasMoreElements()).isFalse(); } } @Test public void testSingleEntry() throws IOException { ZipWriter writer = new ZipWriter(new FileOutputStream(test), UTF_8); byte[] content = "content".getBytes(UTF_8); crc.update(content); ZipFileEntry entry = new ZipFileEntry("foo"); entry.setSize(content.length); entry.setCompressedSize(content.length); entry.setCrc(crc.getValue()); entry.setTime(cal.getTimeInMillis()); writer.putNextEntry(entry); writer.write(content); writer.closeEntry(); writer.close(); byte[] buf = new byte[128]; try (ZipFile zipFile = new ZipFile(test)) { ZipEntry foo = zipFile.getEntry("foo"); assertThat(foo.getMethod()).isEqualTo(ZipEntry.STORED); assertThat(foo.getSize()).isEqualTo(content.length); assertThat(foo.getCompressedSize()).isEqualTo(content.length); assertThat(foo.getCrc()).isEqualTo(crc.getValue()); assertThat(foo.getTime()).isEqualTo(cal.getTimeInMillis()); zipFile.getInputStream(foo).read(buf); assertThat(Bytes.indexOf(buf, content)).isEqualTo(0); } } @Test public void testMultipleEntry() throws IOException { ZipWriter writer = new ZipWriter(new FileOutputStream(test), UTF_8); writer.setComment("file comment"); byte[] fooContent = "content".getBytes(UTF_8); crc.update(fooContent); long fooCrc = crc.getValue(); ZipFileEntry rawFoo = new ZipFileEntry("foo"); rawFoo.setMethod(Compression.STORED); rawFoo.setSize(fooContent.length); rawFoo.setCompressedSize(fooContent.length); rawFoo.setCrc(crc.getValue()); rawFoo.setTime(cal.getTimeInMillis()); rawFoo.setComment("foo comment"); writer.putNextEntry(rawFoo); writer.write(fooContent); writer.closeEntry(); byte[] barContent = "stuff".getBytes(UTF_8); byte[] deflatedBarContent = new byte[128]; crc.reset(); crc.update(barContent); long barCrc = crc.getValue(); deflater.setInput(barContent); deflater.finish(); int deflatedSize = deflater.deflate(deflatedBarContent); ZipFileEntry rawBar = new ZipFileEntry("bar"); rawBar.setMethod(Compression.DEFLATED); rawBar.setSize(barContent.length); rawBar.setCompressedSize(deflatedSize); rawBar.setCrc(barCrc); rawBar.setTime(cal.getTimeInMillis()); writer.putNextEntry(rawBar); writer.write(deflatedBarContent, 0, deflatedSize); writer.closeEntry(); writer.close(); byte[] buf = new byte[128]; try (ZipFile zipFile = new ZipFile(test)) { assertThat(zipFile.getComment()).isEqualTo("file comment"); ZipEntry foo = zipFile.getEntry("foo"); assertThat(foo.getMethod()).isEqualTo(ZipEntry.STORED); assertThat(foo.getSize()).isEqualTo(fooContent.length); assertThat(foo.getCompressedSize()).isEqualTo(fooContent.length); assertThat(foo.getCrc()).isEqualTo(fooCrc); assertThat(foo.getTime()).isEqualTo(cal.getTimeInMillis()); assertThat(foo.getComment()).isEqualTo("foo comment"); zipFile.getInputStream(foo).read(buf); assertThat(Bytes.indexOf(buf, fooContent)).isEqualTo(0); ZipEntry bar = zipFile.getEntry("bar"); assertThat(bar.getMethod()).isEqualTo(ZipEntry.DEFLATED); assertThat(bar.getSize()).isEqualTo(barContent.length); assertThat(bar.getCompressedSize()).isEqualTo(deflatedSize); assertThat(bar.getCrc()).isEqualTo(barCrc); assertThat(bar.getTime()).isEqualTo(cal.getTimeInMillis()); zipFile.getInputStream(bar).read(buf); assertThat(Bytes.indexOf(buf, barContent)).isEqualTo(0); } } @Test public void testWrongSizeContent() throws IOException { try (ZipWriter writer = new ZipWriter(new FileOutputStream(test), UTF_8)) { byte[] content = "content".getBytes(UTF_8); crc.update(content); ZipFileEntry entry = new ZipFileEntry("foo"); entry.setSize(content.length); entry.setCompressedSize(content.length); entry.setCrc(crc.getValue()); entry.setTime(cal.getTimeInMillis()); writer.putNextEntry(entry); writer.write("some other content".getBytes(UTF_8)); thrown.expect(ZipException.class); thrown.expectMessage("Number of bytes written for the entry"); writer.closeEntry(); } } @Test public void testRawZipEntry() throws IOException { ZipWriter writer = new ZipWriter(new FileOutputStream(test), UTF_8); byte[] content = "content".getBytes(UTF_8); crc.update(content); ZipFileEntry entry = new ZipFileEntry("foo"); entry.setVersion((short) 1); entry.setVersionNeeded((short) 2); entry.setSize(content.length); entry.setCompressedSize(content.length); entry.setCrc(crc.getValue()); entry.setTime(cal.getTimeInMillis()); entry.setFlags(ZipUtil.get16(new byte[]{ 0x08, 0x00 }, 0)); entry.setInternalAttributes(ZipUtil.get16(new byte[]{ 0x34, 0x12 }, 0)); entry.setExternalAttributes(ZipUtil.get32(new byte[]{ 0x0a, 0x09, 0x78, 0x56 }, 0)); entry.setLocalHeaderOffset(rand.nextInt(Integer.MAX_VALUE)); writer.putNextEntry(entry); writer.write(content); writer.closeEntry(); writer.close(); byte[] buf = new byte[128]; try (ZipFile zipFile = new ZipFile(test)) { ZipEntry foo = zipFile.getEntry("foo"); assertThat(foo.getMethod()).isEqualTo(ZipEntry.STORED); assertThat(foo.getSize()).isEqualTo(content.length); assertThat(foo.getCompressedSize()).isEqualTo(content.length); assertThat(foo.getCrc()).isEqualTo(crc.getValue()); assertThat(foo.getTime()).isEqualTo(cal.getTimeInMillis()); zipFile.getInputStream(foo).read(buf); assertThat(Bytes.indexOf(buf, content)).isEqualTo(0); } try (ZipReader zipFile = new ZipReader(test)) { ZipFileEntry foo = zipFile.getEntry("foo"); // Versions should be increased to minimum required for STORED compression. assertThat(foo.getVersion()).isEqualTo((short) 0xa); assertThat(foo.getVersionNeeded()).isEqualTo((short) 0xa); assertThat(foo.getFlags()).isEqualTo((short) 0); // Data descriptor bit should be cleared. assertThat(foo.getInternalAttributes()).isEqualTo((short) 4660); assertThat(foo.getExternalAttributes()).isEqualTo(1450707210); } } @Test public void testPrefixFile() throws IOException, InterruptedException { ZipWriter writer = new ZipWriter(new FileOutputStream(test), UTF_8); writer.startPrefixFile(); writer.write("#!/bin/bash\necho 'hello world'\n".getBytes(UTF_8)); writer.endPrefixFile(); byte[] content = "content".getBytes(UTF_8); crc.update(content); ZipFileEntry entry = new ZipFileEntry("foo"); entry.setSize(content.length); entry.setCompressedSize(content.length); entry.setCrc(crc.getValue()); entry.setTime(cal.getTimeInMillis()); writer.putNextEntry(entry); writer.write(content); writer.closeEntry(); writer.close(); byte[] buf = new byte[128]; try (ZipFile zipFile = new ZipFile(test)) { ZipEntry foo = zipFile.getEntry("foo"); assertThat(foo.getMethod()).isEqualTo(ZipEntry.STORED); assertThat(foo.getSize()).isEqualTo(content.length); assertThat(foo.getCompressedSize()).isEqualTo(content.length); assertThat(foo.getCrc()).isEqualTo(crc.getValue()); assertThat(foo.getTime()).isEqualTo(cal.getTimeInMillis()); zipFile.getInputStream(foo).read(buf); assertThat(Bytes.indexOf(buf, content)).isEqualTo(0); } Process pr = new ProcessBuilder("chmod", "750", test.getAbsolutePath()).start(); pr.waitFor(); pr = new ProcessBuilder(test.getAbsolutePath()).start(); pr.getInputStream().read(buf); pr.waitFor(); assertThat(Bytes.indexOf(buf, "hello world".getBytes(UTF_8))).isEqualTo(0); } @Test public void testPrefixFileAfterZip() throws IOException { try (ZipWriter writer = new ZipWriter(new FileOutputStream(test), UTF_8)) { byte[] content = "content".getBytes(UTF_8); crc.update(content); ZipFileEntry entry = new ZipFileEntry("foo"); entry.setSize(content.length); entry.setCompressedSize(content.length); entry.setCrc(crc.getValue()); entry.setTime(cal.getTimeInMillis()); writer.putNextEntry(entry); thrown.expect(ZipException.class); thrown.expectMessage("Cannot add a prefix file after the zip contents have been started."); writer.startPrefixFile(); writer.write("#!/bin/bash\necho 'hello world'\n".getBytes(UTF_8)); writer.endPrefixFile(); } } @Test public void testPrefixAfterFinish() throws IOException { try (ZipWriter writer = new ZipWriter(new FileOutputStream(test), UTF_8)) { writer.finish(); thrown.expect(IllegalStateException.class); writer.startPrefixFile(); writer.write("#!/bin/bash\necho 'hello world'\n".getBytes(UTF_8)); writer.endPrefixFile(); } } @Test public void testPutEntryAfterFinish() throws IOException { try (ZipWriter writer = new ZipWriter(new FileOutputStream(test), UTF_8)) { writer.finish(); thrown.expect(IllegalStateException.class); writer.putNextEntry(new ZipFileEntry("foo")); } } @Test public void testCloseEntryAfterFinish() throws IOException { try (ZipWriter writer = new ZipWriter(new FileOutputStream(test), UTF_8)) { byte[] content = "content".getBytes(UTF_8); crc.update(content); ZipFileEntry entry = new ZipFileEntry("foo"); entry.setSize(content.length); entry.setCompressedSize(content.length); entry.setCrc(crc.getValue()); entry.setTime(cal.getTimeInMillis()); writer.putNextEntry(entry); writer.write(content); writer.finish(); thrown.expect(IllegalStateException.class); writer.closeEntry(); } } @Test public void testFinishAfterFinish() throws IOException { try (ZipWriter writer = new ZipWriter(new FileOutputStream(test), UTF_8)) { writer.finish(); thrown.expect(IllegalStateException.class); writer.finish(); } } @Test public void testWriteAfterFinish() throws IOException { try (ZipWriter writer = new ZipWriter(new FileOutputStream(test), UTF_8)) { writer.finish(); thrown.expect(IllegalStateException.class); writer.write("content".getBytes(UTF_8)); } } @Test public void testZip64_FileCount_32BitMax() throws IOException { try (ZipWriter writer = new ZipWriter(new FileOutputStream(test), UTF_8, true)) { ZipFileEntry template = new ZipFileEntry("template"); template.setSize(0); template.setCompressedSize(0); template.setCrc(0); template.setTime(cal.getTimeInMillis()); for (int i = 0; i < 0xffff; i++) { ZipFileEntry entry = new ZipFileEntry(template); entry.setName("entry" + i); writer.putNextEntry(entry); } } try (ZipReader reader = new ZipReader(test, UTF_8, true)) { assertThat(reader.size()).isEqualTo(0xffff); } try (ZipReader reader = new ZipReader(test, UTF_8)) { assertThat(reader.size()).isEqualTo(0xffff); } } @Test public void testZip64_FileCount_Zip64Range() throws IOException { try (ZipWriter writer = new ZipWriter(new FileOutputStream(test), UTF_8, true)) { ZipFileEntry template = new ZipFileEntry("template"); template.setSize(0); template.setCompressedSize(0); template.setCrc(0); template.setTime(cal.getTimeInMillis()); for (int i = 0; i < 0x100ff; i++) { ZipFileEntry entry = new ZipFileEntry(template); entry.setName("entry" + i); writer.putNextEntry(entry); } } try (ZipReader reader = new ZipReader(test, UTF_8, true)) { assertThat(reader.size()).isEqualTo(0x100ff); } try (ZipReader reader = new ZipReader(test, UTF_8)) { assertThat(reader.size()).isEqualTo(0x100ff); } } @Test public void testZip64_FileCount_Zip64Range_ForceZip32() throws IOException { try (ZipWriter writer = new ZipWriter(new FileOutputStream(test), UTF_8, false)) { ZipFileEntry template = new ZipFileEntry("template"); template.setSize(0); template.setCompressedSize(0); template.setCrc(0); template.setTime(cal.getTimeInMillis()); for (int i = 0; i < 0x100ff; i++) { ZipFileEntry entry = new ZipFileEntry(template); entry.setName("entry" + i); writer.putNextEntry(entry); } } try (ZipReader reader = new ZipReader(test, UTF_8, true)) { assertThat(reader.size()).isEqualTo(0x00ff); } try (ZipReader reader = new ZipReader(test, UTF_8)) { assertThat(reader.size()).isEqualTo(0x100ff); } } @Test public void testZip64_FileSize_32BitMax() throws IOException { long size = 0xffffffffL; try (ZipWriter writer = new ZipWriter(new FileOutputStream(test), UTF_8, true)) { ZipFileEntry entry = new ZipFileEntry("big"); entry.setCompressedSize(size); entry.setSize(size); entry.setCrc(0); entry.setTime(cal.getTimeInMillis()); writer.putNextEntry(entry); byte[] chunk = new byte[1024]; for (int i = 0; i < size / chunk.length; i++) { writer.write(chunk); } writer.write(chunk, 0, (int) (size % chunk.length)); writer.closeEntry(); } try (ZipFile file = new ZipFile(test)) { ZipEntry entry = file.getEntry("big"); assertThat(entry.getSize()).isEqualTo(size); assertThat(entry.getCompressedSize()).isEqualTo(size); } } @Test public void testZip64_FileSize_Zip64Range() throws IOException { long size = 0x1000000ffL; try (ZipWriter writer = new ZipWriter(new FileOutputStream(test), UTF_8, true)) { ZipFileEntry entry = new ZipFileEntry("big"); entry.setCompressedSize(size); entry.setSize(size); entry.setCrc(0); entry.setTime(cal.getTimeInMillis()); writer.putNextEntry(entry); byte[] chunk = new byte[1024]; for (int i = 0; i < size / chunk.length; i++) { writer.write(chunk); } writer.write(chunk, 0, (int) (size % chunk.length)); writer.closeEntry(); } try (ZipFile file = new ZipFile(test)) { ZipEntry entry = file.getEntry("big"); assertThat(entry.getSize()).isEqualTo(size); assertThat(entry.getCompressedSize()).isEqualTo(size); } } @Test public void testZip64_FileSize_Zip64Range_ForceZip32() throws IOException { long size = 0x1000000ffL; try (ZipWriter writer = new ZipWriter(new FileOutputStream(test), UTF_8, false)) { ZipFileEntry entry = new ZipFileEntry("big"); entry.setCompressedSize(size); entry.setSize(size); entry.setCrc(0); entry.setTime(cal.getTimeInMillis()); thrown.expect(ZipException.class); thrown.expectMessage("Writing an entry of size"); thrown.expectMessage("without Zip64 extensions is not supported."); writer.putNextEntry(entry); } } }