// 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.io.ByteStreams;
import com.google.devtools.build.zip.ZipFileEntry.Compression;
import com.google.devtools.build.zip.ZipFileEntry.Feature;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.zip.CRC32;
import java.util.zip.Deflater;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipOutputStream;
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;

@RunWith(JUnit4.class)
public class ZipReaderTest {
  @Rule public TemporaryFolder tmp = new TemporaryFolder();
  @Rule public ExpectedException thrown = ExpectedException.none();

  private File test;

  private void assertDateWithin(Date testDate, Date start, Date end) {
    if (testDate.before(start) || testDate.after(end)) {
      fail();
    }
  }

  private void assertDateAboutNow(Date testDate) {
    Date now = new Date();
    Calendar cal = Calendar.getInstance();
    cal.setTime(now);
    cal.add(Calendar.MINUTE, -30);
    Date start = cal.getTime();
    cal.add(Calendar.HOUR, 1);
    Date end = cal.getTime();
    assertDateWithin(testDate, start, end);
  }

  @Before public void setup() throws IOException {
    test = tmp.newFile("test.zip");
  }

  @Test public void testMalformed_Empty() throws IOException {
    try (FileOutputStream out = new FileOutputStream(test)) {
    }
    thrown.expect(ZipException.class);
    thrown.expectMessage("is malformed. It does not contain an end of central directory record.");
    new ZipReader(test, UTF_8).close();
  }

  @Test public void testMalformed_ShorterThanSignature() throws IOException {
    try (FileOutputStream out = new FileOutputStream(test)) {
      out.write(new byte[] { 1, 2, 3 });
    }
    thrown.expect(ZipException.class);
    thrown.expectMessage("is malformed. It does not contain an end of central directory record.");
    new ZipReader(test, UTF_8).close();
  }

  @Test public void testMalformed_SignatureLength() throws IOException {
    try (FileOutputStream out = new FileOutputStream(test)) {
      out.write(new byte[] { 1, 2, 3, 4 });
    }
    thrown.expect(ZipException.class);
    thrown.expectMessage("is malformed. It does not contain an end of central directory record.");
    new ZipReader(test, UTF_8).close();
  }

  @Test public void testEmpty() throws IOException {
    try (ZipOutputStream zout = new ZipOutputStream(new FileOutputStream(test))) {
    }
    try (ZipReader reader = new ZipReader(test, UTF_8)) {
      assertThat(reader.entries()).isEmpty();
    }
  }

  @Test public void testFileComment() throws IOException {
    try (ZipOutputStream zout = new ZipOutputStream(new FileOutputStream(test))) {
      zout.setComment("test comment");
    }
    try (ZipReader reader = new ZipReader(test, UTF_8)) {
      assertThat(reader.entries()).isEmpty();
      assertThat(reader.getComment()).isEqualTo("test comment");
    }
  }

  @Test public void testFileCommentWithSignature() throws IOException {
    try (ZipOutputStream zout = new ZipOutputStream(new FileOutputStream(test))) {
      zout.setComment("test comment\u0050\u004b\u0005\u0006abcdefghijklmnopqrstuvwxyz");
    }
    try (ZipReader reader = new ZipReader(test, UTF_8)) {
      assertThat(reader.entries()).isEmpty();
      assertThat(reader.getComment())
          .isEqualTo("test comment\u0050\u004b\u0005\u0006abcdefghijklmnopqrstuvwxyz");
    }
  }

  @Test public void testSingleEntry() throws IOException {
    try (ZipOutputStream zout = new ZipOutputStream(new FileOutputStream(test))) {
      zout.putNextEntry(new ZipEntry("test"));
      zout.write("foo".getBytes(UTF_8));
      zout.closeEntry();
    }
    try (ZipReader reader = new ZipReader(test, UTF_8)) {
      assertThat(reader.entries()).hasSize(1);
    }
  }

  @Test public void testMultipleEntries() throws IOException {
    String[] names = new String[] { "test", "foo", "bar", "baz" };
    try (ZipOutputStream zout = new ZipOutputStream(new FileOutputStream(test))) {
      for (String name : names) {
        zout.putNextEntry(new ZipEntry(name));
        zout.write(name.getBytes(UTF_8));
        zout.closeEntry();
      }
    }

    try (ZipReader reader = new ZipReader(test, UTF_8)) {
      assertThat(reader.entries()).hasSize(names.length);
      int i = 0;
      for (ZipFileEntry entry : reader.entries()) {
        assertThat(entry.getName()).isEqualTo(names[i++]);
      }
      assertThat(i).isEqualTo(names.length);
    }
  }

  @Test public void testZipEntryFields() throws IOException {
    CRC32 crc = new CRC32();
    Deflater deflater = new Deflater(Deflater.DEFAULT_COMPRESSION, true);
    long date = 791784306000L; // 2/3/1995 04:05:06
    byte[] extra = new ExtraData((short) 0xaa, new byte[] { (byte) 0xbb, (byte) 0xcd }).getBytes();
    byte[] tmp = new byte[128];
    try (ZipOutputStream zout = new ZipOutputStream(new FileOutputStream(test))) {

      ZipEntry foo = new ZipEntry("foo");
      foo.setComment("foo comment.");
      foo.setMethod(ZipEntry.DEFLATED);
      foo.setTime(date);
      foo.setExtra(extra);
      zout.putNextEntry(foo);
      zout.write("foo".getBytes(UTF_8));
      zout.closeEntry();

      ZipEntry bar = new ZipEntry("bar");
      bar.setComment("bar comment.");
      bar.setMethod(ZipEntry.STORED);
      bar.setSize("bar".length());
      bar.setCompressedSize("bar".length());
      crc.reset();
      crc.update("bar".getBytes(UTF_8));
      bar.setCrc(crc.getValue());
      zout.putNextEntry(bar);
      zout.write("bar".getBytes(UTF_8));
      zout.closeEntry();
    }

    try (ZipReader reader = new ZipReader(test, UTF_8)) {
      ZipFileEntry fooEntry = reader.getEntry("foo");
      assertThat(fooEntry.getName()).isEqualTo("foo");
      assertThat(fooEntry.getComment()).isEqualTo("foo comment.");
      assertThat(fooEntry.getMethod()).isEqualTo(Compression.DEFLATED);
      assertThat(fooEntry.getVersion()).isEqualTo(Compression.DEFLATED.getMinVersion());
      assertThat(fooEntry.getTime()).isEqualTo(date);
      assertThat(fooEntry.getSize()).isEqualTo("foo".length());
      deflater.reset();
      deflater.setInput("foo".getBytes(UTF_8));
      deflater.finish();
      assertThat(fooEntry.getCompressedSize()).isEqualTo(deflater.deflate(tmp));
      crc.reset();
      crc.update("foo".getBytes(UTF_8));
      assertThat(fooEntry.getCrc()).isEqualTo(crc.getValue());
      assertThat(fooEntry.getExtra().getBytes()).isEqualTo(extra);

      ZipFileEntry barEntry = reader.getEntry("bar");
      assertThat(barEntry.getName()).isEqualTo("bar");
      assertThat(barEntry.getComment()).isEqualTo("bar comment.");
      assertThat(barEntry.getMethod()).isEqualTo(Compression.STORED);
      assertThat(barEntry.getVersion()).isEqualTo(Compression.STORED.getMinVersion());
      assertDateAboutNow(new Date(barEntry.getTime()));
      assertThat(barEntry.getSize()).isEqualTo("bar".length());
      assertThat(barEntry.getCompressedSize()).isEqualTo("bar".length());
      crc.reset();
      crc.update("bar".getBytes(UTF_8));
      assertThat(barEntry.getCrc()).isEqualTo(crc.getValue());
      assertThat(barEntry.getExtra().getBytes()).isEqualTo(new byte[] {});
    }
  }

  @Test public void testZipEntryInvalidTime() throws IOException {
    long date = 312796800000L; // 11/30/1979 00:00:00, which is also 0 in DOS format
    byte[] extra = new ExtraData((short) 0xaa, new byte[] { (byte) 0xbb, (byte) 0xcd }).getBytes();
    try (ZipOutputStream zout = new ZipOutputStream(new FileOutputStream(test))) {
      ZipEntry foo = new ZipEntry("foo");
      foo.setComment("foo comment.");
      foo.setMethod(ZipEntry.DEFLATED);
      foo.setTime(date);
      foo.setExtra(extra);
      zout.putNextEntry(foo);
      zout.write("foo".getBytes(UTF_8));
      zout.closeEntry();
    }

    try (ZipReader reader = new ZipReader(test, UTF_8)) {
      ZipFileEntry fooEntry = reader.getEntry("foo");
      assertThat(fooEntry.getTime()).isEqualTo(ZipUtil.DOS_EPOCH);
    }
  }

  @Test public void testRawFileData() throws IOException {
    CRC32 crc = new CRC32();
    Deflater deflator = new Deflater(Deflater.DEFAULT_COMPRESSION, true);
    try (ZipOutputStream zout = new ZipOutputStream(new FileOutputStream(test))) {
      ZipEntry foo = new ZipEntry("foo");
      foo.setComment("foo comment.");
      foo.setMethod(ZipEntry.DEFLATED);
      zout.putNextEntry(foo);
      zout.write("foo".getBytes(UTF_8));
      zout.closeEntry();

      ZipEntry bar = new ZipEntry("bar");
      bar.setComment("bar comment.");
      bar.setMethod(ZipEntry.STORED);
      bar.setSize("bar".length());
      bar.setCompressedSize("bar".length());
      crc.reset();
      crc.update("bar".getBytes(UTF_8));
      bar.setCrc(crc.getValue());
      zout.putNextEntry(bar);
      zout.write("bar".getBytes(UTF_8));
      zout.closeEntry();
    }

    try (ZipReader reader = new ZipReader(test, UTF_8)) {
      ZipFileEntry fooEntry = reader.getEntry("foo");
      InputStream fooIn = reader.getRawInputStream(fooEntry);
      byte[] fooData = new byte[10];
      fooIn.read(fooData);
      byte[] expectedFooData = new byte[10];
      deflator.reset();
      deflator.setInput("foo".getBytes(UTF_8));
      deflator.finish();
      deflator.deflate(expectedFooData);
      assertThat(fooData).isEqualTo(expectedFooData);

      ZipFileEntry barEntry = reader.getEntry("bar");
      InputStream barIn = reader.getRawInputStream(barEntry);
      byte[] barData = new byte[3];
      barIn.read(barData);
      byte[] expectedBarData = "bar".getBytes(UTF_8);
      assertThat(barData).isEqualTo(expectedBarData);

      assertThat(barIn.read()).isEqualTo(-1);
      assertThat(barIn.read(barData)).isEqualTo(-1);
      assertThat(barIn.read(barData, 0, 3)).isEqualTo(-1);

      thrown.expect(IOException.class);
      thrown.expectMessage("Reset is not supported on this type of stream.");
      barIn.reset();
    }
  }

  @Test public void testFileData() throws IOException {
    CRC32 crc = new CRC32();
    try (ZipOutputStream zout = new ZipOutputStream(new FileOutputStream(test))) {
      ZipEntry foo = new ZipEntry("foo");
      foo.setComment("foo comment.");
      foo.setMethod(ZipEntry.DEFLATED);
      zout.putNextEntry(foo);
      zout.write("foo".getBytes(UTF_8));
      zout.closeEntry();

      ZipEntry bar = new ZipEntry("bar");
      bar.setComment("bar comment.");
      bar.setMethod(ZipEntry.STORED);
      bar.setSize("bar".length());
      bar.setCompressedSize("bar".length());
      crc.reset();
      crc.update("bar".getBytes(UTF_8));
      bar.setCrc(crc.getValue());
      zout.putNextEntry(bar);
      zout.write("bar".getBytes(UTF_8));
      zout.closeEntry();
    }

    try (ZipReader reader = new ZipReader(test, UTF_8)) {
      ZipFileEntry fooEntry = reader.getEntry("foo");
      InputStream fooIn = reader.getInputStream(fooEntry);
      byte[] fooData = new byte[3];
      fooIn.read(fooData);
      byte[] expectedFooData = "foo".getBytes(UTF_8);
      assertThat(fooData).isEqualTo(expectedFooData);

      assertThat(fooIn.read()).isEqualTo(-1);
      assertThat(fooIn.read(fooData)).isEqualTo(-1);
      assertThat(fooIn.read(fooData, 0, 3)).isEqualTo(-1);

      ZipFileEntry barEntry = reader.getEntry("bar");
      InputStream barIn = reader.getInputStream(barEntry);
      byte[] barData = new byte[3];
      barIn.read(barData);
      byte[] expectedBarData = "bar".getBytes(UTF_8);
      assertThat(barData).isEqualTo(expectedBarData);

      assertThat(barIn.read()).isEqualTo(-1);
      assertThat(barIn.read(barData)).isEqualTo(-1);
      assertThat(barIn.read(barData, 0, 3)).isEqualTo(-1);

      thrown.expect(IOException.class);
      thrown.expectMessage("Reset is not supported on this type of stream.");
      barIn.reset();
    }
  }

  @Test public void testSimultaneousReads() throws IOException {
    byte[] expectedFooData = "This is file foo. It contains a foo.".getBytes(UTF_8);
    byte[] expectedBarData = "This is a different file bar. It contains only a bar."
        .getBytes(UTF_8);
    try (ZipOutputStream zout = new ZipOutputStream(new FileOutputStream(test))) {
      ZipEntry foo = new ZipEntry("foo");
      foo.setComment("foo comment.");
      foo.setMethod(ZipEntry.DEFLATED);
      zout.putNextEntry(foo);
      zout.write(expectedFooData);
      zout.closeEntry();

      ZipEntry bar = new ZipEntry("bar");
      bar.setComment("bar comment.");
      bar.setMethod(ZipEntry.DEFLATED);
      zout.putNextEntry(bar);
      zout.write(expectedBarData);
      zout.closeEntry();
    }

    try (ZipReader reader = new ZipReader(test, UTF_8)) {
      ZipFileEntry fooEntry = reader.getEntry("foo");
      ZipFileEntry barEntry = reader.getEntry("bar");
      InputStream fooIn = reader.getInputStream(fooEntry);
      InputStream barIn = reader.getInputStream(barEntry);
      byte[] fooData = new byte[expectedFooData.length];
      byte[] barData = new byte[expectedBarData.length];
      fooIn.read(fooData, 0, 10);
      barIn.read(barData, 0, 10);
      fooIn.read(fooData, 10, 10);
      barIn.read(barData, 10, 10);
      fooIn.read(fooData, 20, fooData.length - 20);
      barIn.read(barData, 20, barData.length - 20);
      assertThat(fooData).isEqualTo(expectedFooData);
      assertThat(barData).isEqualTo(expectedBarData);
    }
  }

  @Test public void testSlowRead() throws IOException {
    byte[] expectedFooData = "This is file foo. It contains a foo.".getBytes(UTF_8);
    byte[] expectedBarData = "This is a different file bar. It contains only a bar."
        .getBytes(UTF_8);
    try (ZipOutputStream zout = new ZipOutputStream(new FileOutputStream(test))) {
      ZipEntry foo = new ZipEntry("foo");
      foo.setComment("foo comment.");
      foo.setMethod(ZipEntry.DEFLATED);
      zout.putNextEntry(foo);
      zout.write(expectedFooData);
      zout.closeEntry();

      ZipEntry bar = new ZipEntry("bar");
      bar.setComment("bar comment.");
      bar.setMethod(ZipEntry.DEFLATED);
      zout.putNextEntry(bar);
      zout.write(expectedBarData);
      zout.closeEntry();
    }

    try (ZipReader reader = new SlowZipReader(test, UTF_8)) {
      ZipFileEntry fooEntry = reader.getEntry("foo");
      ZipFileEntry barEntry = reader.getEntry("bar");
      InputStream fooIn = reader.getInputStream(fooEntry);
      InputStream barIn = reader.getInputStream(barEntry);
      byte[] fooData = new byte[expectedFooData.length];
      byte[] barData = new byte[expectedBarData.length];
      ZipUtil.readFully(fooIn, fooData, 0, 10);
      ZipUtil.readFully(barIn, barData, 0, 10);
      ZipUtil.readFully(fooIn, fooData, 10, 10);
      ZipUtil.readFully(barIn, barData, 10, 10);
      ZipUtil.readFully(fooIn, fooData, 20, fooData.length - 20);
      ZipUtil.readFully(barIn, barData, 20, barData.length - 20);
      assertThat(fooData).isEqualTo(expectedFooData);
      assertThat(barData).isEqualTo(expectedBarData);
    }
  }

  @Test public void testZip64() throws IOException {
    // Generated with: 'echo "foo" > entry; zip -fz -q out.zip entry'
    byte[] data = new byte[] {
        0x50, 0x4b, 0x03, 0x04, 0x2d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5d, (byte) 0x86, (byte) 0xa6,
        0x46, (byte) 0xa8, 0x65, 0x32, 0x7e, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
        (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, 0x05, 0x00, 0x30, 0x00, 0x65, 0x6e,
        0x74, 0x72, 0x79, 0x55, 0x54, 0x09, 0x00, 0x03, (byte) 0xb2, 0x7e, 0x4a, 0x55, (byte) 0xb2,
        0x7e, 0x4a, 0x55, 0x75, 0x78, 0x0b, 0x00, 0x01, 0x04, 0x46, 0x3a, 0x04, 0x00, 0x04,
        (byte) 0x88, 0x13, 0x00, 0x00, 0x01, 0x00, 0x10, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x66, 0x6f, 0x6f, 0x0a, 0x50,
        0x4b, 0x01, 0x02, 0x1e, 0x03, 0x2d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5d, (byte) 0x86,
        (byte) 0xa6, 0x46, (byte) 0xa8, 0x65, 0x32, 0x7e, 0x04, 0x00, 0x00, 0x00, (byte) 0xff,
        (byte) 0xff, (byte) 0xff, (byte) 0xff, 0x05, 0x00, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
        0x00, 0x00, 0x00, (byte) 0xa0, (byte) 0x81, 0x00, 0x00, 0x00, 0x00, 0x65, 0x6e, 0x74, 0x72,
        0x79, 0x55, 0x54, 0x05, 0x00, 0x03, (byte) 0xb2, 0x7e, 0x4a, 0x55, 0x75, 0x78, 0x0b, 0x00,
        0x01, 0x04, 0x46, 0x3a, 0x04, 0x00, 0x04, (byte) 0x88, 0x13, 0x00, 0x00, 0x01, 0x00, 0x08,
        0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x4b, 0x06, 0x06, 0x2c, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1e, 0x03, 0x2d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x57, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x57, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x4b, 0x06, 0x07, 0x00, 0x00, 0x00, 0x00, (byte) 0xae,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x50, 0x4b, 0x05, 0x06,
        0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x57, 0x00, 0x00, 0x00, (byte) 0xff,
        (byte) 0xff, (byte) 0xff, (byte) 0xff, 0x00, 0x00
      };

    try (FileOutputStream out = new FileOutputStream(test)) {
      out.write(data);
    }
    String foo = "foo\n";
    byte[] expectedFooData = foo.getBytes(UTF_8);
    ExtraDataList extras = new ExtraDataList();
    extras.add(new ExtraData((short) 0x0001, ZipUtil.longToLittleEndian(expectedFooData.length)));
    byte[] extra = extras.getBytes();
    CRC32 crc = new CRC32();
    crc.reset();
    crc.update(expectedFooData);
    try (ZipReader reader = new ZipReader(test, UTF_8)) {
      ZipFileEntry fooEntry = reader.getEntry("entry");
      InputStream fooIn = reader.getInputStream(fooEntry);
      byte[] fooData = new byte[expectedFooData.length];
      fooIn.read(fooData);
      assertThat(fooData).isEqualTo(expectedFooData);
      assertThat(fooEntry.getName()).isEqualTo("entry");
      assertThat(fooEntry.getComment()).isEmpty();
      assertThat(fooEntry.getMethod()).isEqualTo(Compression.STORED);
      assertThat(fooEntry.getVersionNeeded()).isEqualTo(Feature.ZIP64_SIZE.getMinVersion());
      assertThat(fooEntry.getSize()).isEqualTo(expectedFooData.length);
      assertThat(fooEntry.getCompressedSize()).isEqualTo(expectedFooData.length);
      assertThat(fooEntry.getCrc()).isEqualTo(crc.getValue());
      assertThat(fooEntry.getExtra().get((short) 0x0001).getBytes()).isEqualTo(extra);
    }
  }

  @Test public void testZip64_Potential() 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(ZipUtil.DOS_EPOCH);
      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)) {
      Collection<ZipFileEntry> entries = reader.entries();
      assertThat(entries).hasSize(0xffff);
    }
  }

  @Test public void testZip64_NumFiles() 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(ZipUtil.DOS_EPOCH);
      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)) {
      Collection<ZipFileEntry> entries = reader.entries();
      assertThat(entries).hasSize(0x100ff);
    }
  }

  @Test public void testZip64_Max32BitSizeFile() throws IOException {
    File bigFile = tmp.newFile("big");
    try (RandomAccessFile bigOut = new RandomAccessFile(bigFile, "rw")) {
      bigOut.setLength(0xffffffffL);
    }
    try (ZipWriter writer = new ZipWriter(new FileOutputStream(test), UTF_8, true)) {
      ZipFileEntry bigEntry = new ZipFileEntry(bigFile.getName());
      bigEntry.setSize(0xffffffffL);
      bigEntry.setCompressedSize(0xffffffffL);
      bigEntry.setCrc(0);
      bigEntry.setTime(ZipUtil.DOS_EPOCH);
      writer.putNextEntry(bigEntry);
      ByteStreams.copy(new BufferedInputStream(new FileInputStream(bigFile)), writer);
    }
    try (ZipReader reader = new ZipReader(test, UTF_8)) {
      Collection<ZipFileEntry> entries = reader.entries();
      assertThat(entries).hasSize(1);
      ZipFileEntry bigEntry = reader.getEntry(bigFile.getName());
      assertThat(bigEntry.getSize()).isEqualTo(0xffffffffL);
    }
  }

  @Test public void testZip64_Zip64SizeFile() throws IOException {
    File biggerFile = tmp.newFile("big");
    try (RandomAccessFile biggerOut = new RandomAccessFile(biggerFile, "rw")) {
      biggerOut.setLength(0x1000000ffL);
    }
    try (ZipWriter writer = new ZipWriter(new FileOutputStream(test), UTF_8, true)) {
      ZipFileEntry bigEntry = new ZipFileEntry(biggerFile.getName());
      bigEntry.setSize(0x1000000ffL);
      bigEntry.setCompressedSize(0x1000000ffL);
      bigEntry.setCrc(0);
      bigEntry.setTime(ZipUtil.DOS_EPOCH);
      writer.putNextEntry(bigEntry);
      ByteStreams.copy(new BufferedInputStream(new FileInputStream(biggerFile)), writer);
    }
    try (ZipReader reader = new ZipReader(test, UTF_8)) {
      Collection<ZipFileEntry> entries = reader.entries();
      assertThat(entries).hasSize(1);
      ZipFileEntry bigEntry = reader.getEntry(biggerFile.getName());
      assertThat(bigEntry.getSize()).isEqualTo(0x1000000ffL);
    }
  }

  @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(ZipUtil.DOS_EPOCH);
      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)) {
      assertThat(reader.size()).isEqualTo(0x100ff);
    }
    try (ZipReader reader = new ZipReader(test, UTF_8, true)) {
      assertThat(reader.size()).isEqualTo(0x00ff);
    }
  }

  @Test
  public void testEocdSignatureNearBufferBoundary() throws IOException {
    // Create a standard valid zip file to be manipulated below into a self-extracting (SFX)
    // archive.
    File zipFile = tmp.newFile("valid.zip");
    byte[] content = "hello".getBytes(UTF_8);
    CRC32 crc = new CRC32();
    crc.update(content);
    try (ZipWriter writer = new ZipWriter(new FileOutputStream(zipFile), UTF_8, false)) {
      ZipFileEntry entry = new ZipFileEntry("foo.txt");
      entry.setTime(ZipUtil.DOS_EPOCH);
      entry.setCrc(crc.getValue());
      entry.setSize(content.length);
      entry.setCompressedSize(content.length);
      writer.putNextEntry(entry);
      writer.write(content);
    }

    byte[] zipBytes = Files.readAllBytes(zipFile.toPath());
    // Prepend 5 bytes (simulating an executable stub in a self-extracting zip archive) to push the
    // EOCD signature position in the 64-byte buffer to index 47, causing
    // signatureLocation + COMMENT_LENGTH_OFFSET (47 + 20 = 67) to cross the 64-byte buffer limit.
    int preambleLength = 5;
    byte[] sfxBytes = new byte[preambleLength + zipBytes.length];
    System.arraycopy(zipBytes, 0, sfxBytes, preambleLength, zipBytes.length);

    File sfxFile = tmp.newFile("sfx.exe");
    Files.write(sfxFile.toPath(), sfxBytes);

    // Initializing ZipReader parses the EOCD header. This verifies that signatureLocation + 20
    // falling across the 64-byte buffer boundary is handled gracefully without throwing an
    // ArrayIndexOutOfBoundsException.
    try (ZipReader reader = new ZipReader(sfxFile, UTF_8)) {
      assertThat(reader.entries()).hasSize(1);
      ZipFileEntry entry = reader.getEntry("foo.txt");
      assertThat(entry).isNotNull();
      assertThat(entry.getCrc()).isEqualTo(crc.getValue());
      assertThat(entry.getSize()).isEqualTo(content.length);
    }
  }

  @Test
  public void testEocdWithZipCommentInSmallBuffer() throws IOException {
    File zipFile = tmp.newFile("comment.zip");
    byte[] content = "hello".getBytes(UTF_8);
    CRC32 crc = new CRC32();
    crc.update(content);
    byte[] comment = new byte[120];
    Arrays.fill(comment, (byte) 'a');
    String commentString = new String(comment, UTF_8);
    try (ZipWriter writer = new ZipWriter(new FileOutputStream(zipFile), UTF_8, false)) {
      ZipFileEntry entry = new ZipFileEntry("foo.txt");
      entry.setTime(ZipUtil.DOS_EPOCH);
      entry.setCrc(crc.getValue());
      entry.setSize(content.length);
      entry.setCompressedSize(content.length);
      writer.putNextEntry(entry);
      writer.write(content);
      writer.setComment(commentString);
    }

    try (ZipReader reader = new ZipReader(zipFile, UTF_8)) {
      assertThat(reader.entries()).hasSize(1);
      assertThat(reader.getComment()).isEqualTo(commentString);
    }
  }
}
