/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You 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 org.apache.commons.compress.archivers.zip;

import static org.apache.commons.compress.AbstractTest.getFile;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.nio.file.Files;
import java.util.Enumeration;
import java.util.Random;
import java.util.zip.ZipEntry;

import org.apache.commons.compress.AbstractTest;
import org.apache.commons.io.RandomAccessFileMode;
import org.junit.jupiter.api.Test;

public class Zip64SupportIT {

    interface ZipOutputTest {
        void test(File f, ZipArchiveOutputStream zos) throws IOException;
    }

    private static final long FIVE_BILLION = 5000000000L;
    private static final int ONE_MILLION = 1000000;

    private static final int ONE_HUNDRED_THOUSAND = 100000;

    private static final ZipOutputTest write100KFilesModeNever = (f, zos) -> {
        zos.setUseZip64(Zip64Mode.Never);
        final Zip64RequiredException ex = assertThrows(Zip64RequiredException.class, () -> write100KFilesToStream(zos), "expected a Zip64RequiredException");
        assertEquals(Zip64RequiredException.TOO_MANY_ENTRIES_MESSAGE, ex.getMessage());
    };

    private static final ZipOutputTest write3EntriesCreatingBigArchiveModeNever = (f, zos) -> {
        zos.setUseZip64(Zip64Mode.Never);
        final Zip64RequiredException ex = assertThrows(Zip64RequiredException.class, () -> write3EntriesCreatingBigArchiveToStream(zos),
                "expected a Zip64RequiredException");
        assertEquals(Zip64RequiredException.ARCHIVE_TOO_BIG_MESSAGE, ex.getMessage());
    };

    private static File get100KFileFile() throws Throwable {
        return getFile("100k_Files.zip");
    }

    private static File get100KFileFileGeneratedBy7ZIP() throws Throwable {
        return getFile("100k_Files_7ZIP.zip");
    }

    private static File get100KFileFileGeneratedByJava7Jar() throws Throwable {
        return getFile("100k_Files_jar.zip");
    }

    private static File get100KFileFileGeneratedByPKZip() throws Throwable {
        return getFile("100k_Files_PKZip.zip");
    }

    private static File get100KFileFileGeneratedByWinCF() throws Throwable {
        return getFile("100k_Files_WindowsCompressedFolders.zip");
    }

    private static File get100KFileFileGeneratedByWinZIP() throws Throwable {
        return getFile("100k_Files_WinZIP.zip");
    }

    private static File get5GBZerosFile() throws Throwable {
        return getFile("5GB_of_Zeros.zip");
    }

    private static File get5GBZerosFileGeneratedBy7ZIP() throws Throwable {
        return getFile("5GB_of_Zeros_7ZIP.zip");
    }

    private static File get5GBZerosFileGeneratedByJava7Jar() throws Throwable {
        return getFile("5GB_of_Zeros_jar.zip");
    }

    private static File get5GBZerosFileGeneratedByPKZip() throws Throwable {
        return getFile("5GB_of_Zeros_PKZip.zip");
    }

    private static File get5GBZerosFileGeneratedByWinZIP() throws Throwable {
        return getFile("5GB_of_Zeros_WinZip.zip");
    }

    private static long getLengthAndPositionAtCentralDirectory(final RandomAccessFile a) throws IOException {
        final long end = a.length();
        a.seek(end - 22 - 20);
        final byte[] sig = new byte[4];
        a.readFully(sig);
        if (sig[0] != (byte) 0x50 || sig[1] != (byte) 0x4b || sig[2] != 6 || sig[3] != 7) {
            // not a ZIP64 archive
            return getLengthAndPositionAtCentralDirectory32(a, end);
        }

        final long cdOffsetLoc = end - 22 - 20 - 56 + 48;
        // seek to central directory locator
        a.seek(cdOffsetLoc);
        final byte[] cdOffset = new byte[8];
        a.readFully(cdOffset);
        a.seek(ZipEightByteInteger.getLongValue(cdOffset));
        return end;
    }

    private static long getLengthAndPositionAtCentralDirectory32(final RandomAccessFile a, final long end) throws IOException {
        a.seek(end - 22 + 16);
        final byte[] cdOffset = new byte[4];
        a.readFully(cdOffset);
        a.seek(ZipLong.getValue(cdOffset));
        return end;
    }

    private static File getTempFile(final String testName) throws Throwable {
        final File f = File.createTempFile("commons-compress-" + testName, ".zip");
        f.deleteOnExit();
        return f;
    }

    private static void read100KFilesImpl(final File f) throws IOException {
        try (InputStream fin = Files.newInputStream(f.toPath());
                ZipArchiveInputStream zin = new ZipArchiveInputStream(fin)) {
            int files = 0;
            ZipArchiveEntry zae;
            while ((zae = zin.getNextZipEntry()) != null) {
                if (!zae.isDirectory()) {
                    files++;
                    assertEquals(0, zae.getSize());
                }
            }
            assertEquals(ONE_HUNDRED_THOUSAND, files);
        }
    }

    private static void read100KFilesUsingZipFileImpl(final File f) throws IOException {
        ZipFile zf = null;
        try {
            zf = ZipFile.builder().setFile(f).get();
            int files = 0;
            for (final Enumeration<ZipArchiveEntry> e = zf.getEntries(); e.hasMoreElements();) {
                final ZipArchiveEntry zae = e.nextElement();
                if (!zae.isDirectory()) {
                    files++;
                    assertEquals(0, zae.getSize());
                }
            }
            assertEquals(ONE_HUNDRED_THOUSAND, files);
        } finally {
            ZipFile.closeQuietly(zf);
        }
    }

    private static void read5GBOfZerosImpl(final File f, final String expectedName) throws IOException {
        try (InputStream fin = Files.newInputStream(f.toPath());
                ZipArchiveInputStream zin = new ZipArchiveInputStream(fin)) {
            ZipArchiveEntry zae = zin.getNextZipEntry();
            while (zae.isDirectory()) {
                zae = zin.getNextZipEntry();
            }
            assertEquals(expectedName, zae.getName());
            final byte[] buf = new byte[1024 * 1024];
            long read = 0;
            final Random r = new Random(System.currentTimeMillis());
            int readNow;
            while ((readNow = zin.read(buf, 0, buf.length)) > 0) {
                // testing all bytes for a value of 0 is going to take
                // too long, just pick a few ones randomly
                for (int i = 0; i < 1024; i++) {
                    final int idx = r.nextInt(readNow);
                    assertEquals(0, buf[idx], "testing byte " + (read + idx));
                }
                read += readNow;
            }
            assertEquals(FIVE_BILLION, read);
            assertNull(zin.getNextZipEntry());
            assertEquals(FIVE_BILLION, zae.getSize());
        }
    }

    private static void read5GBOfZerosUsingZipFileImpl(final File f, final String expectedName) throws IOException {
        ZipFile zf = null;
        try {
            zf = ZipFile.builder().setFile(f).get();
            final Enumeration<ZipArchiveEntry> e = zf.getEntries();
            assertTrue(e.hasMoreElements());
            ZipArchiveEntry zae = e.nextElement();
            while (zae.isDirectory()) {
                zae = e.nextElement();
            }
            assertEquals(expectedName, zae.getName());
            assertEquals(FIVE_BILLION, zae.getSize());
            final byte[] buf = new byte[1024 * 1024];
            long read = 0;
            final Random r = new Random(System.currentTimeMillis());
            int readNow;
            try (InputStream zin = zf.getInputStream(zae)) {
                while ((readNow = zin.read(buf, 0, buf.length)) > 0) {
                    // testing all bytes for a value of 0 is going to take
                    // too long, just pick a few ones randomly
                    for (int i = 0; i < 1024; i++) {
                        final int idx = r.nextInt(readNow);
                        assertEquals(0, buf[idx], "testing byte " + (read + idx));
                    }
                    read += readNow;
                }
            }
            assertEquals(FIVE_BILLION, read);
            assertFalse(e.hasMoreElements());
        } finally {
            ZipFile.closeQuietly(zf);
        }
    }

    private static void withTemporaryArchive(final String testName, final ZipOutputTest test, final boolean useRandomAccessFile) throws Throwable {
        withTemporaryArchive(testName, test, useRandomAccessFile, null);
    }

    private static void withTemporaryArchive(final String testName, final ZipOutputTest test, final boolean useRandomAccessFile, final Long splitSize)
            throws Throwable {
        File f = getTempFile(testName);
        File dir = null;
        if (splitSize != null) {
            dir = Files.createTempDirectory("commons-compress-" + testName).toFile();
            dir.deleteOnExit();

            f = new File(dir, "commons-compress-" + testName + ".zip");
        }
        BufferedOutputStream os = null;
        ZipArchiveOutputStream zos = useRandomAccessFile ? new ZipArchiveOutputStream(f)
                : new ZipArchiveOutputStream(os = new BufferedOutputStream(Files.newOutputStream(f.toPath())));
        if (splitSize != null) {
            zos = new ZipArchiveOutputStream(f, splitSize);
        }

        try {
            test.test(f, zos);
        } catch (final IOException ex) {
            System.err.println("Failed to write archive because of: " + ex.getMessage() + " - likely not enough disk space.");
            assumeTrue(false);
        } finally {
            try {
                zos.destroy();
            } finally {
                try {
                    if (os != null) {
                        os.close();
                    }
                    AbstractTest.forceDelete(f);
                } finally {
                    if (dir != null) {
                        final File directory = dir;
                        AbstractTest.forceDelete(directory);
                    }
                }
            }
        }
    }

    private static ZipOutputTest write100KFiles() {
        return write100KFiles(Zip64Mode.AsNeeded);
    }

    private static ZipOutputTest write100KFiles(final Zip64Mode mode) {
        return (f, zos) -> {
            if (mode != Zip64Mode.AsNeeded) {
                zos.setUseZip64(mode);
            }
            write100KFilesToStream(zos);
            try (RandomAccessFile a = RandomAccessFileMode.READ_ONLY.create(f)) {
                final long end = a.length();

                // validate "end of central directory" is at
                // the end of the file and contains the magic
                // value 0xFFFF as "number of entries".
                a.seek(end - 22 /* length of EOCD without file comment */);
                final byte[] eocd = new byte[12];
                a.readFully(eocd);
                assertArrayEquals(new byte[] {
                        // sig
                        (byte) 0x50, (byte) 0x4b, 5, 6,
                        // disk numbers
                        0, 0, 0, 0,
                        // entries
                        (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, }, eocd);

                // validate "Zip64 end of central directory
                // locator" is right in front of the EOCD and
                // the location of the "Zip64 end of central
                // directory record" seems correct
                final long expectedZ64EocdOffset = end - 22 /* eocd.length */
                        - 20 /* z64 eocd locator.length */
                        - 56 /* z64 eocd without extensible data sector */;
                final byte[] loc = ZipEightByteInteger.getBytes(expectedZ64EocdOffset);
                a.seek(end - 22 - 20);
                final byte[] z64EocdLoc = new byte[20];
                a.readFully(z64EocdLoc);
                assertArrayEquals(new byte[] {
                        // sig
                        (byte) 0x50, (byte) 0x4b, 6, 7,
                        // disk numbers
                        0, 0, 0, 0,
                        // location of Zip64 EOCD,
                        loc[0], loc[1], loc[2], loc[3], loc[4], loc[5], loc[6], loc[7],
                        // total number of disks
                        1, 0, 0, 0, }, z64EocdLoc);

                // validate "Zip64 end of central directory
                // record" is where it is supposed to be, the
                // known values are fine and read the location
                // of the central directory from it
                a.seek(expectedZ64EocdOffset);
                final byte[] z64EocdStart = new byte[40];
                a.readFully(z64EocdStart);
                assertArrayEquals(new byte[] {
                        // sig
                        (byte) 0x50, (byte) 0x4b, 6, 6,
                        // size of z64 EOCD
                        44, 0, 0, 0, 0, 0, 0, 0,
                        // version made by
                        45, 0,
                        // version needed to extract
                        45, 0,
                        // disk numbers
                        0, 0, 0, 0, 0, 0, 0, 0,
                        // number of entries 100k = 0x186A0
                        (byte) 0xA0, (byte) 0x86, 1, 0, 0, 0, 0, 0, (byte) 0xA0, (byte) 0x86, 1, 0, 0, 0, 0, 0, }, z64EocdStart);
                a.seek(expectedZ64EocdOffset + 48 /* skip size */);
                final byte[] cdOffset = new byte[8];
                a.readFully(cdOffset);
                final long cdLoc = ZipEightByteInteger.getLongValue(cdOffset);

                // finally verify there really is a central
                // directory entry where the Zip64 EOCD claims
                a.seek(cdLoc);
                final byte[] sig = new byte[4];
                a.readFully(sig);
                assertArrayEquals(new byte[] { (byte) 0x50, (byte) 0x4b, 1, 2, }, sig);
            }
        };
    }

    private static void write100KFilesToStream(final ZipArchiveOutputStream zos) throws IOException {
        for (int i = 0; i < ONE_HUNDRED_THOUSAND; i++) {
            final ZipArchiveEntry zae = new ZipArchiveEntry(String.valueOf(i));
            zae.setSize(0);
            zos.putArchiveEntry(zae);
            zos.closeArchiveEntry();
        }
        zos.close();
    }

    private static ZipOutputTest write3EntriesCreatingBigArchive() {
        return write3EntriesCreatingBigArchive(Zip64Mode.AsNeeded);
    }

    private static ZipOutputTest write3EntriesCreatingBigArchive(final Zip64Mode mode) {
        return write3EntriesCreatingBigArchive(mode, false);
    }

    /*
     * Individual sizes don't require ZIP64 but the offset of the third entry is bigger than 0xFFFFFFFF so a ZIP64 extended information is needed inside the
     * central directory.
     *
     * Creates a temporary archive of approx 5GB in size
     */
    private static ZipOutputTest write3EntriesCreatingBigArchive(final Zip64Mode mode, final boolean isSplitArchive) {
        return (f, zos) -> {
            if (mode != Zip64Mode.AsNeeded) {
                zos.setUseZip64(mode);
            }
            write3EntriesCreatingBigArchiveToStream(zos);

            try (RandomAccessFile a = RandomAccessFileMode.READ_ONLY.create(f)) {
                getLengthAndPositionAtCentralDirectory(a);
                // skip first two entries
                a.skipBytes(2 * 47 /*
                                    * CD entry of file with file name length 1 and no extra data
                                    */
                        + 2 * (mode == Zip64Mode.Always ? 32 : 0)
                /* ZIP64 extra fields if mode is Always */
                );

                // grab third entry, verify offset is
                // 0xFFFFFFFF, and it has a ZIP64 extended
                // information extra field
                final byte[] header = new byte[12];
                a.readFully(header);
                assertArrayEquals(new byte[] {
                        // sig
                        (byte) 0x50, (byte) 0x4b, 1, 2,
                        // version made by
                        45, 0,
                        // version needed to extract
                        45, 0,
                        // GPB (EFS bit)
                        0, 8,
                        // method
                        0, 0 }, header, "CDH start");
                // ignore timestamp, CRC, compressed size
                a.skipBytes(12);
                // Original Size
                final byte[] originalSize = new byte[4];
                a.readFully(originalSize);
                if (mode == Zip64Mode.Always) {
                    assertArrayEquals(new byte[] { (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, }, originalSize, "CDH original size");
                } else {
                    assertArrayEquals(new byte[] { 1, 0, 0, 0 }, originalSize, "CDH original size");
                }
                final byte[] rest = new byte[19];
                a.readFully(rest);
                assertArrayEquals(new byte[] {
                        // file name length
                        1, 0,
                        // extra field length
                        (byte) (mode == Zip64Mode.Always ? 32 : 12), 0,
                        // comment length
                        0, 0,
                        // disk number
                        (byte) (isSplitArchive ? 0xFF : 0), (byte) (isSplitArchive ? 0xFF : 0),
                        // attributes
                        0, 0, 0, 0, 0, 0,
                        // offset
                        (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
                        // file name
                        (byte) '2' }, rest, "CDH rest");
                if (mode == Zip64Mode.Always) {
                    final byte[] extra1 = new byte[12];
                    a.readFully(extra1);
                    assertArrayEquals(new byte[] {
                            // Header-ID
                            1, 0,
                            // size
                            28, 0,
                            // Original Size
                            1, 0, 0, 0, 0, 0, 0, 0, }, extra1, "CDH extra");
                    // skip compressed size
                    a.skipBytes(8);
                } else {
                    final byte[] extra2 = new byte[4];
                    a.readFully(extra2);
                    assertArrayEquals(new byte[] {
                            // Header-ID
                            1, 0,
                            // size
                            8, 0, }, extra2, "CDH extra");
                }

                // read offset of LFH
                final byte[] offset = new byte[8];
                a.readFully(offset);
                // verify there is an LFH where the CD claims it
                a.seek(ZipEightByteInteger.getLongValue(offset));
                final byte[] sig = new byte[4];
                a.readFully(sig);
                assertArrayEquals(new byte[] { (byte) 0x50, (byte) 0x4b, 3, 4, }, sig, "LFH signature");
            }
        };
    }

    private static void write3EntriesCreatingBigArchiveToStream(final ZipArchiveOutputStream zos) throws IOException {
        final byte[] buf = new byte[ONE_MILLION];
        ZipArchiveEntry zae;
        for (int i = 0; i < 2; i++) {
            zae = new ZipArchiveEntry(String.valueOf(i));
            zae.setSize(FIVE_BILLION / 2);
            zae.setMethod(ZipEntry.STORED);
            zae.setCrc(0x8a408f16L);
            zos.putArchiveEntry(zae);
            for (int j = 0; j < FIVE_BILLION / 2 / 1000 / 1000; j++) {
                zos.write(buf);
            }
            zos.closeArchiveEntry();
        }
        zae = new ZipArchiveEntry(String.valueOf(2));
        zae.setSize(1);
        zae.setMethod(ZipEntry.STORED);
        zae.setCrc(0x9b9265bL);
        zos.putArchiveEntry(zae);
        zos.write(new byte[] { 42 });
        zos.closeArchiveEntry();
        zos.close();
    }

    private static File write5GBZerosFile(final String testName) throws Throwable {
        final File f = getTempFile(testName);
        final ZipArchiveOutputStream zos = new ZipArchiveOutputStream(f);
        try {
            zos.setUseZip64(Zip64Mode.Always);
            final byte[] buf = new byte[ONE_MILLION];
            final ZipArchiveEntry zae = new ZipArchiveEntry("5GB_of_Zeros");
            zae.setSize(FIVE_BILLION);
            zae.setMethod(ZipEntry.DEFLATED);
            zae.setCrc(0x8a408f16L);
            zos.putArchiveEntry(zae);
            for (int j = 0; j < FIVE_BILLION / 1000 / 1000; j++) {
                zos.write(buf);
            }
            zos.closeArchiveEntry();
            zos.close();
        } catch (final IOException ex) {
            System.err.println("Failed to write archive because of: " + ex.getMessage() + " - likely not enough disk space.");
            assumeTrue(false);
        } finally {
            zos.destroy();
        }
        return f;
    }

    private static ZipOutputTest writeBigDeflatedEntryToFile(final boolean knownSize) {
        return writeBigDeflatedEntryToFile(knownSize, Zip64Mode.AsNeeded);
    }

    /*
     * One entry of length 5 billion bytes, written with compression to a file.
     *
     * Writing to a file => sizes are stored directly inside the LFH. No Data Descriptor at all.
     *
     * Creates a temporary archive of approx 4MB in size
     */
    private static ZipOutputTest writeBigDeflatedEntryToFile(final boolean knownSize, final Zip64Mode mode) {
        return (f, zos) -> {
            if (mode != Zip64Mode.AsNeeded) {
                zos.setUseZip64(mode);
            }
            final byte[] buf = new byte[ONE_MILLION];
            final ZipArchiveEntry zae = new ZipArchiveEntry("0");
            if (knownSize) {
                zae.setSize(FIVE_BILLION);
            }
            zae.setMethod(ZipEntry.DEFLATED);
            zos.putArchiveEntry(zae);
            for (int j = 0; j < FIVE_BILLION / 1000 / 1000; j++) {
                zos.write(buf);
            }
            zos.closeArchiveEntry();
            zos.close();

            try (RandomAccessFile a = RandomAccessFileMode.READ_ONLY.create(f)) {
                getLengthAndPositionAtCentralDirectory(a);

                // grab first entry, verify
                // sizes are 0xFFFFFFFF and
                // it has a ZIP64 extended
                // information extra field
                byte[] header = new byte[12];
                a.readFully(header);
                assertArrayEquals(new byte[] {
                        // sig
                        (byte) 0x50, (byte) 0x4b, 1, 2,
                        // version made by
                        45, 0,
                        // version needed to extract
                        45, 0,
                        // GPB (EFS + *no* Data Descriptor)
                        0, 8,
                        // method
                        8, 0, }, header, "CDH start");
                // ignore timestamp
                a.skipBytes(4);
                byte[] rest = new byte[26];
                a.readFully(rest);
                assertArrayEquals(new byte[] {
                        // CRC
                        (byte) 0x50, (byte) 0x6F, (byte) 0x31, (byte) 0x5c,
                        // Compressed Size
                        (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
                        // Original Size
                        (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
                        // file name length
                        1, 0,
                        // extra field length
                        (byte) (mode == Zip64Mode.Always ? 32 : 20), 0,
                        // comment length
                        0, 0,
                        // disk number
                        0, 0,
                        // attributes
                        0, 0, 0, 0, 0, 0, }, rest, "CDH rest");
                byte[] offset = new byte[4];
                a.readFully(offset);
                if (mode == Zip64Mode.Always) {
                    assertArrayEquals(new byte[] { (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, }, offset, "offset");
                } else {
                    assertArrayEquals(new byte[] { 0, 0, 0, 0, }, offset, "offset");
                }
                assertEquals('0', a.read());
                byte[] extra = new byte[12];
                a.readFully(extra);
                // 5e9 == 0x12A05F200
                assertArrayEquals(new byte[] {
                        // Header-ID
                        1, 0,
                        // size of extra
                        (byte) (mode == Zip64Mode.Always ? 28 : 16), 0,
                        // original size
                        0, (byte) 0xF2, 5, (byte) 0x2A, 1, 0, 0, 0, }, extra, "CDH extra");
                if (mode == Zip64Mode.Always) {
                    // skip compressed size
                    a.skipBytes(8);
                    offset = new byte[8];
                    a.readFully(offset);
                    assertArrayEquals(new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, }, offset, "extra offset");
                }

                // and now validate local file header
                a.seek(0);
                header = new byte[10];
                a.readFully(header);
                assertArrayEquals(new byte[] {
                        // sig
                        (byte) 0x50, (byte) 0x4b, 3, 4,
                        // version needed to extract
                        45, 0,
                        // GPB (EFS bit, no DD)
                        0, 8,
                        // method
                        8, 0, }, header, "LFH start");
                // ignore timestamp
                a.skipBytes(4);
                rest = new byte[17];
                a.readFully(rest);
                assertArrayEquals(new byte[] {
                        // CRC
                        (byte) 0x50, (byte) 0x6F, (byte) 0x31, (byte) 0x5c,
                        // Compressed Size
                        (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
                        // Original Size
                        (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
                        // file name length
                        1, 0,
                        // extra field length
                        20, 0,
                        // file name
                        (byte) '0' }, rest);
                extra = new byte[12];
                a.readFully(extra);
                assertArrayEquals(new byte[] {
                        // Header-ID
                        1, 0,
                        // size of extra
                        16, 0,
                        // original size
                        0, (byte) 0xF2, 5, (byte) 0x2A, 1, 0, 0, 0,
                        // skip compressed size
                }, extra);
            }
        };
    }

    /*
     * One entry of length 5 billion bytes, written with compression to a file.
     *
     * Writing to a file => sizes are stored directly inside the LFH. No Data Descriptor at all.
     *
     * Creates a temporary archive of approx 4MB in size
     */
    private static ZipOutputTest writeBigDeflatedEntryToFileModeNever(final boolean knownSize) {
        return (f, zos) -> {
            zos.setUseZip64(Zip64Mode.Never);
            final Zip64RequiredException ex = assertThrows(Zip64RequiredException.class, () -> {
                final byte[] buf = new byte[ONE_MILLION];
                final ZipArchiveEntry zae = new ZipArchiveEntry("0");
                if (knownSize) {
                    zae.setSize(FIVE_BILLION);
                }
                zae.setMethod(ZipEntry.DEFLATED);
                zos.putArchiveEntry(zae);
                for (int j = 0; j < FIVE_BILLION / 1000 / 1000; j++) {
                    zos.write(buf);
                }
                zos.closeArchiveEntry();
            }, "expected a Zip64RequiredException");
            assertTrue(ex.getMessage().startsWith("0's size"));
        };
    }

    /*
     * One entry of length 5 billion bytes, written with compression to a stream.
     *
     * Compression + Stream => sizes are set to 0 in LFH and ZIP64 entry, real values are inside the data descriptor.
     *
     * Creates a temporary archive of approx 4MB in size
     */
    private static ZipOutputTest writeBigDeflatedEntryToStream(final boolean knownSize, final Zip64Mode mode) {
        return (f, zos) -> {
            if (mode != Zip64Mode.AsNeeded) {
                zos.setUseZip64(mode);
            }
            final byte[] buf = new byte[ONE_MILLION];
            final ZipArchiveEntry zae = new ZipArchiveEntry("0");
            if (knownSize) {
                zae.setSize(FIVE_BILLION);
            }
            zae.setMethod(ZipEntry.DEFLATED);
            zos.putArchiveEntry(zae);
            for (int j = 0; j < FIVE_BILLION / 1000 / 1000; j++) {
                zos.write(buf);
            }
            zos.closeArchiveEntry();
            zos.close();

            try (RandomAccessFile a = RandomAccessFileMode.READ_ONLY.create(f)) {
                getLengthAndPositionAtCentralDirectory(a);

                final long cfhPos = a.getFilePointer();
                // grab first entry, verify
                // sizes are 0xFFFFFFFF and
                // it has a ZIP64 extended
                // information extra field
                byte[] header = new byte[12];
                a.readFully(header);
                assertArrayEquals(new byte[] {
                        // sig
                        (byte) 0x50, (byte) 0x4b, 1, 2,
                        // version made by
                        45, 0,
                        // version needed to extract
                        45, 0,
                        // GPB (EFS + Data Descriptor)
                        8, 8,
                        // method
                        8, 0, }, header, "CDH start");
                // ignore timestamp
                a.skipBytes(4);
                byte[] rest = new byte[26];
                a.readFully(rest);
                assertArrayEquals(new byte[] {
                        // CRC
                        (byte) 0x50, (byte) 0x6F, (byte) 0x31, (byte) 0x5c,
                        // Compressed Size
                        (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
                        // Original Size
                        (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
                        // file name length
                        1, 0,
                        // extra field length
                        (byte) (mode == Zip64Mode.Always ? 32 : 20), 0,
                        // comment length
                        0, 0,
                        // disk number
                        0, 0,
                        // attributes
                        0, 0, 0, 0, 0, 0, }, rest, "CDH rest");
                byte[] offset = new byte[4];
                a.readFully(offset);
                if (mode == Zip64Mode.Always) {
                    assertArrayEquals(new byte[] { (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, }, offset, "offset");
                } else {
                    assertArrayEquals(new byte[] { 0, 0, 0, 0, }, offset, "offset");
                }
                assertEquals('0', a.read());
                byte[] extra = new byte[12];
                a.readFully(extra);
                // 5e9 == 0x12A05F200
                assertArrayEquals(new byte[] {
                        // Header-ID
                        1, 0,
                        // size of extra
                        (byte) (mode == Zip64Mode.Always ? 28 : 16), 0,
                        // original size
                        0, (byte) 0xF2, 5, (byte) 0x2A, 1, 0, 0, 0, }, extra, "CDH extra");
                if (mode == Zip64Mode.Always) {
                    // skip compressed size
                    a.skipBytes(8);
                    offset = new byte[8];
                    a.readFully(offset);
                    assertArrayEquals(new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, }, offset, "extra offset");
                }

                // validate data descriptor
                a.seek(cfhPos - 24);
                byte[] dd = new byte[8];
                a.readFully(dd);
                assertArrayEquals(new byte[] {
                        // sig
                        (byte) 0x50, (byte) 0x4b, 7, 8,
                        // CRC
                        (byte) 0x50, (byte) 0x6F, (byte) 0x31, (byte) 0x5c, }, dd, "DD");
                // skip compressed size
                a.skipBytes(8);
                dd = new byte[8];
                a.readFully(dd);
                assertArrayEquals(new byte[] {
                        // original size
                        0, (byte) 0xF2, 5, (byte) 0x2A, 1, 0, 0, 0, }, dd, "DD sizes");

                // and now validate local file header
                a.seek(0);
                header = new byte[10];
                a.readFully(header);
                assertArrayEquals(new byte[] {
                        // sig
                        (byte) 0x50, (byte) 0x4b, 3, 4,
                        // version needed to extract
                        45, 0,
                        // GPB (EFS + Data Descriptor)
                        8, 8,
                        // method
                        8, 0, }, header, "LFH start");
                // ignore timestamp
                a.skipBytes(4);
                rest = new byte[17];
                a.readFully(rest);
                assertArrayEquals(new byte[] {
                        // CRC
                        0, 0, 0, 0,
                        // Compressed Size
                        (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
                        // Original Size
                        (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
                        // file name length
                        1, 0,
                        // extra field length
                        20, 0,
                        // file name
                        (byte) '0' }, rest, "LFH rest");
                extra = new byte[20];
                a.readFully(extra);
                assertArrayEquals(new byte[] {
                        // Header-ID
                        1, 0,
                        // size of extra
                        16, 0,
                        // original size
                        0, 0, 0, 0, 0, 0, 0, 0,
                        // compressed size
                        0, 0, 0, 0, 0, 0, 0, 0, }, extra, "LFH extra");
            }
        };
    }

    private static ZipOutputTest writeBigDeflatedEntryUnknownSizeToStream(final Zip64Mode mode) {
        return (f, zos) -> {
            final Zip64RequiredException ex = assertThrows(Zip64RequiredException.class, () -> {
                if (mode != Zip64Mode.AsNeeded) {
                    zos.setUseZip64(mode);
                }
                final byte[] buf = new byte[ONE_MILLION];
                final ZipArchiveEntry zae = new ZipArchiveEntry("0");
                zae.setMethod(ZipEntry.DEFLATED);
                zos.putArchiveEntry(zae);
                for (int j = 0; j < FIVE_BILLION / 1000 / 1000; j++) {
                    zos.write(buf);
                }
                zos.closeArchiveEntry();
            }, "expected a Zip64RequiredException");
            assertTrue(ex.getMessage().startsWith("0's size"));
        };
    }

    private static ZipOutputTest writeBigStoredEntry(final boolean knownSize) {
        return writeBigStoredEntry(knownSize, Zip64Mode.AsNeeded);
    }

    /*
     * One entry of length 5 billion bytes, written without compression.
     *
     * No Compression => sizes are stored directly inside the LFH. No Data Descriptor at all.
     *
     * Creates a temporary archive of approx 5GB in size
     */
    private static ZipOutputTest writeBigStoredEntry(final boolean knownSize, final Zip64Mode mode) {
        return (f, zos) -> {
            if (mode != Zip64Mode.AsNeeded) {
                zos.setUseZip64(mode);
            }
            final byte[] buf = new byte[ONE_MILLION];
            final ZipArchiveEntry zae = new ZipArchiveEntry("0");
            if (knownSize) {
                zae.setSize(FIVE_BILLION);
                zae.setCrc(0x5c316f50L);
            }
            zae.setMethod(ZipEntry.STORED);
            zos.putArchiveEntry(zae);
            for (int j = 0; j < FIVE_BILLION / 1000 / 1000; j++) {
                zos.write(buf);
            }
            zos.closeArchiveEntry();
            zos.close();

            try (RandomAccessFile a = RandomAccessFileMode.READ_ONLY.create(f)) {
                getLengthAndPositionAtCentralDirectory(a);

                // grab first entry, verify sizes are 0xFFFFFFFF
                // and it has a ZIP64 extended information extra
                // field
                byte[] header = new byte[12];
                a.readFully(header);
                assertArrayEquals(new byte[] {
                        // sig
                        (byte) 0x50, (byte) 0x4b, 1, 2,
                        // version made by
                        45, 0,
                        // version needed to extract
                        45, 0,
                        // GPB (EFS bit)
                        0, 8,
                        // method
                        0, 0 }, header, "CDH start");
                // ignore timestamp
                a.skipBytes(4);
                byte[] rest = new byte[26];
                a.readFully(rest);
                assertArrayEquals(new byte[] {
                        // CRC
                        (byte) 0x50, (byte) 0x6F, (byte) 0x31, (byte) 0x5c,
                        // Compressed Size
                        (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
                        // Original Size
                        (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
                        // file name length
                        1, 0,
                        // extra field length
                        (byte) (mode == Zip64Mode.Always ? 32 : 20), 0,
                        // comment length
                        0, 0,
                        // disk number
                        0, 0,
                        // attributes
                        0, 0, 0, 0, 0, 0, }, rest, "CDH rest");
                byte[] offset = new byte[4];
                a.readFully(offset);
                if (mode == Zip64Mode.Always) {
                    assertArrayEquals(new byte[] { (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, }, offset, "offset");
                } else {
                    assertArrayEquals(new byte[] { 0, 0, 0, 0, }, offset, "offset");
                }
                assertEquals('0', a.read());
                final byte[] extra = new byte[20];
                a.readFully(extra);
                // 5e9 == 0x12A05F200
                assertArrayEquals(new byte[] {
                        // Header-ID
                        1, 0,
                        // size of extra
                        (byte) (mode == Zip64Mode.Always ? 28 : 16), 0,
                        // original size
                        0, (byte) 0xF2, 5, (byte) 0x2A, 1, 0, 0, 0,
                        // compressed size
                        0, (byte) 0xF2, 5, (byte) 0x2A, 1, 0, 0, 0, }, extra, "CDH extra");
                if (mode == Zip64Mode.Always) {
                    offset = new byte[8];
                    a.readFully(offset);
                    assertArrayEquals(new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, }, offset, "extra offset");
                }

                // and now validate local file header
                a.seek(0);
                header = new byte[10];
                a.readFully(header);
                assertArrayEquals(new byte[] {
                        // sig
                        (byte) 0x50, (byte) 0x4b, 3, 4,
                        // version needed to extract
                        45, 0,
                        // GPB (EFS bit)
                        0, 8,
                        // method
                        0, 0 }, header, "LFH start");
                // ignore timestamp
                a.skipBytes(4);
                rest = new byte[17];
                a.readFully(rest);
                assertArrayEquals(new byte[] {
                        // CRC
                        (byte) 0x50, (byte) 0x6F, (byte) 0x31, (byte) 0x5c,
                        // Compressed Size
                        (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
                        // Original Size
                        (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
                        // file name length
                        1, 0,
                        // extra field length
                        20, 0,
                        // file name
                        (byte) '0' }, rest, "LFH rest");
                a.readFully(extra);
                // 5e9 == 0x12A05F200
                assertArrayEquals(new byte[] {
                        // Header-ID
                        1, 0,
                        // size of extra
                        16, 0,
                        // original size
                        0, (byte) 0xF2, 5, (byte) 0x2A, 1, 0, 0, 0,
                        // compressed size
                        0, (byte) 0xF2, 5, (byte) 0x2A, 1, 0, 0, 0, }, extra, "LFH extra");
            }
        };
    }

    private static ZipOutputTest writeBigStoredEntryModeNever(final boolean knownSize) {
        return (f, zos) -> {
            zos.setUseZip64(Zip64Mode.Never);
            final Zip64RequiredException ex = assertThrows(Zip64RequiredException.class, () -> {
                final byte[] buf = new byte[ONE_MILLION];
                final ZipArchiveEntry zae = new ZipArchiveEntry("0");
                if (knownSize) {
                    zae.setSize(FIVE_BILLION);
                    zae.setCrc(0x5c316f50L);
                }
                zae.setMethod(ZipEntry.STORED);
                zos.putArchiveEntry(zae);
                for (int j = 0; j < FIVE_BILLION / 1000 / 1000; j++) {
                    zos.write(buf);
                }
                zos.closeArchiveEntry();
            }, "expected a Zip64RequiredException");
            assertTrue(ex.getMessage().startsWith("0's size"));
        };
    }

    private static ZipOutputTest writeSmallDeflatedEntryToFile(final boolean knownSize) {
        return writeSmallDeflatedEntryToFile(knownSize, Zip64Mode.AsNeeded);
    }

    /*
     * One entry of length 1 million bytes, written with compression to a file.
     *
     * Writing to a file => sizes are stored directly inside the LFH. No Data Descriptor at all. Shouldn't contain any ZIP64 extra field if size was known.
     */
    private static ZipOutputTest writeSmallDeflatedEntryToFile(final boolean knownSize, final Zip64Mode mode) {
        return (f, zos) -> {
            if (mode != Zip64Mode.AsNeeded) {
                zos.setUseZip64(mode);
            }
            final byte[] buf = new byte[ONE_MILLION];
            final ZipArchiveEntry zae = new ZipArchiveEntry("0");
            if (knownSize) {
                zae.setSize(ONE_MILLION);
            }
            zae.setMethod(ZipEntry.DEFLATED);
            zos.putArchiveEntry(zae);
            zos.write(buf);
            zos.closeArchiveEntry();
            zos.close();

            try (RandomAccessFile a = RandomAccessFileMode.READ_ONLY.create(f)) {
                getLengthAndPositionAtCentralDirectory(a);

                // grab first CD entry, verify sizes are not
                // 0xFFFFFFFF and it has a no ZIP64 extended
                // information extra field
                byte[] header = new byte[12];
                a.readFully(header);
                assertArrayEquals(new byte[] {
                        // sig
                        (byte) 0x50, (byte) 0x4b, 1, 2,
                        // version made by
                        20, 0,
                        // version needed to extract
                        20, 0,
                        // GPB (EFS + *no* Data Descriptor)
                        0, 8,
                        // method
                        8, 0, }, header);
                // ignore timestamp
                a.skipBytes(4);
                byte[] crc = new byte[4];
                a.readFully(crc);
                assertArrayEquals(new byte[] { (byte) 0x9E, (byte) 0xCB, (byte) 0x79, (byte) 0x12, }, crc);
                // skip compressed size
                a.skipBytes(4);
                byte[] rest = new byte[23];
                a.readFully(rest);
                assertArrayEquals(new byte[] {
                        // Original Size
                        (byte) 0x40, (byte) 0x42, (byte) 0x0F, 0,
                        // file name length
                        1, 0,
                        // extra field length
                        0, 0,
                        // comment length
                        0, 0,
                        // disk number
                        0, 0,
                        // attributes
                        0, 0, 0, 0, 0, 0,
                        // offset
                        0, 0, 0, 0,
                        // file name
                        (byte) '0' }, rest);

                // and now validate local file header
                a.seek(0);
                header = new byte[10];
                a.readFully(header);
                assertArrayEquals(new byte[] {
                        // sig
                        (byte) 0x50, (byte) 0x4b, 3, 4,
                        // version needed to extract
                        20, 0,
                        // GPB (EFS bit, no DD)
                        0, 8,
                        // method
                        8, 0, }, header);
                // ignore timestamp
                a.skipBytes(4);
                crc = new byte[4];
                a.readFully(crc);
                assertArrayEquals(new byte[] { (byte) 0x9E, (byte) 0xCB, (byte) 0x79, (byte) 0x12, }, crc);
                // skip compressed size
                a.skipBytes(4);
                rest = new byte[9];
                a.readFully(rest);

                final boolean hasExtra = mode == Zip64Mode.AsNeeded && !knownSize;

                assertArrayEquals(new byte[] {
                        // Original Size
                        (byte) 0x40, (byte) 0x42, (byte) 0x0F, 0,
                        // file name length
                        1, 0,
                        // extra field length
                        (byte) (!hasExtra ? 0 : 20), 0,
                        // file name
                        (byte) '0' }, rest);
                if (hasExtra) {
                    final byte[] extra = new byte[12];
                    a.readFully(extra);
                    assertArrayEquals(new byte[] {
                            // Header-ID
                            1, 0,
                            // size of extra
                            16, 0,
                            // original size
                            (byte) 0x40, (byte) 0x42, (byte) 0x0F, 0, 0, 0, 0, 0,
                            // don't know the
                            // compressed size,
                            // don't want to
                            // hard-code it
                    }, extra);
                }
            }
        };
    }

    /*
     * One entry of length 1 million bytes, written with compression to a file.
     *
     * Writing to a file => sizes are stored directly inside the LFH. No Data Descriptor at all. Must contain ZIP64 extra field as mode is Always.
     */
    private static ZipOutputTest writeSmallDeflatedEntryToFileModeAlways(final boolean knownSize) {
        return (f, zos) -> {
            zos.setUseZip64(Zip64Mode.Always);
            final byte[] buf = new byte[ONE_MILLION];
            final ZipArchiveEntry zae = new ZipArchiveEntry("0");
            if (knownSize) {
                zae.setSize(ONE_MILLION);
            }
            zae.setMethod(ZipEntry.DEFLATED);
            zos.putArchiveEntry(zae);
            zos.write(buf);
            zos.closeArchiveEntry();
            zos.close();

            try (RandomAccessFile a = RandomAccessFileMode.READ_ONLY.create(f)) {
                getLengthAndPositionAtCentralDirectory(a);

                // grab first CD entry, verify sizes are not
                // 0xFFFFFFFF, and it has an empty ZIP64 extended
                // information extra field
                byte[] header = new byte[12];
                a.readFully(header);
                assertArrayEquals(new byte[] {
                        // sig
                        (byte) 0x50, (byte) 0x4b, 1, 2,
                        // version made by
                        45, 0,
                        // version needed to extract
                        45, 0,
                        // GPB (EFS + *no* Data Descriptor)
                        0, 8,
                        // method
                        8, 0, }, header, "CDH start");
                // ignore timestamp
                a.skipBytes(4);
                byte[] crc = new byte[4];
                a.readFully(crc);
                assertArrayEquals(new byte[] { (byte) 0x9E, (byte) 0xCB, (byte) 0x79, (byte) 0x12, }, crc, "CDH CRC");
                // skip compressed size
                a.skipBytes(4);
                byte[] rest = new byte[23];
                a.readFully(rest);
                assertArrayEquals(new byte[] {
                        // Original Size
                        (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
                        // file name length
                        1, 0,
                        // extra field length
                        32, 0,
                        // comment length
                        0, 0,
                        // disk number
                        0, 0,
                        // attributes
                        0, 0, 0, 0, 0, 0,
                        // offset
                        (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
                        // file name
                        (byte) '0' }, rest, "CDH rest");
                byte[] extra = new byte[12];
                a.readFully(extra);
                assertArrayEquals(new byte[] {
                        // Header-ID
                        1, 0,
                        // size of extra
                        28, 0,
                        // original size
                        (byte) 0x40, (byte) 0x42, (byte) 0x0F, 0, 0, 0, 0, 0, }, extra, "CDH extra");
                // skip compressed size
                a.skipBytes(8);
                final byte[] offset = new byte[8];
                a.readFully(offset);
                assertArrayEquals(new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, }, offset, "extra offset");

                // and now validate local file header
                a.seek(0);
                header = new byte[10];
                a.readFully(header);
                assertArrayEquals(new byte[] {
                        // sig
                        (byte) 0x50, (byte) 0x4b, 3, 4,
                        // version needed to extract
                        45, 0,
                        // GPB (EFS bit, no DD)
                        0, 8,
                        // method
                        8, 0, }, header, "LFH start");
                // ignore timestamp
                a.skipBytes(4);
                crc = new byte[4];
                a.readFully(crc);
                assertArrayEquals(new byte[] { (byte) 0x9E, (byte) 0xCB, (byte) 0x79, (byte) 0x12, }, crc, "LFH CRC");
                rest = new byte[13];
                a.readFully(rest);

                assertArrayEquals(new byte[] {
                        // Compressed Size
                        (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
                        // Original Size
                        (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
                        // file name length
                        1, 0,
                        // extra field length
                        20, 0,
                        // file name
                        (byte) '0' }, rest, "LFH rest");

                extra = new byte[12];
                a.readFully(extra);
                assertArrayEquals(new byte[] {
                        // Header-ID
                        1, 0,
                        // size of extra
                        16, 0,
                        // original size
                        (byte) 0x40, (byte) 0x42, (byte) 0x0F, 0, 0, 0, 0, 0,
                        // don't know the
                        // compressed size,
                        // don't want to
                        // hard-code it
                }, extra, "LFH extra");
            }
        };
    }

    /*
     * One entry of length 1 million bytes, written with compression to a stream.
     *
     * Compression + Stream => sizes are set to 0 in LFH, real values are inside the data descriptor. No ZIP64 extra field at all.
     */
    private static ZipOutputTest writeSmallDeflatedEntryToStream(final boolean knownSize, final Zip64Mode mode) {
        return (f, zos) -> {
            if (mode != Zip64Mode.AsNeeded) {
                zos.setUseZip64(mode);
            }
            final byte[] buf = new byte[ONE_MILLION];
            final ZipArchiveEntry zae = new ZipArchiveEntry("0");
            if (knownSize) {
                zae.setSize(ONE_MILLION);
            }
            zae.setMethod(ZipEntry.DEFLATED);
            zos.putArchiveEntry(zae);
            zos.write(buf);
            zos.closeArchiveEntry();
            zos.close();

            try (RandomAccessFile a = RandomAccessFileMode.READ_ONLY.create(f)) {
                getLengthAndPositionAtCentralDirectory(a);

                final long cfhPos = a.getFilePointer();
                // grab first entry, verify sizes are not
                // 0xFFFFFFF and it has no ZIP64 extended
                // information extra field
                byte[] header = new byte[12];
                a.readFully(header);
                assertArrayEquals(new byte[] {
                        // sig
                        (byte) 0x50, (byte) 0x4b, 1, 2,
                        // version made by
                        20, 0,
                        // version needed to extract
                        20, 0,
                        // GPB (EFS + Data Descriptor)
                        8, 8,
                        // method
                        8, 0, }, header);
                // ignore timestamp
                a.skipBytes(4);
                final byte[] crc = new byte[4];
                a.readFully(crc);
                assertArrayEquals(new byte[] { (byte) 0x9E, (byte) 0xCB, (byte) 0x79, (byte) 0x12, }, crc);
                // skip compressed size
                a.skipBytes(4);
                byte[] rest = new byte[23];
                a.readFully(rest);
                assertArrayEquals(new byte[] {
                        // Original Size
                        (byte) 0x40, (byte) 0x42, (byte) 0x0F, 0,
                        // file name length
                        1, 0,
                        // extra field length
                        0, 0,
                        // comment length
                        0, 0,
                        // disk number
                        0, 0,
                        // attributes
                        0, 0, 0, 0, 0, 0,
                        // offset
                        0, 0, 0, 0,
                        // file name
                        (byte) '0' }, rest);

                // validate data descriptor
                a.seek(cfhPos - 16);
                byte[] dd = new byte[8];
                a.readFully(dd);
                assertArrayEquals(new byte[] {
                        // sig
                        (byte) 0x50, (byte) 0x4b, 7, 8,
                        // CRC
                        (byte) 0x9E, (byte) 0xCB, (byte) 0x79, (byte) 0x12, }, dd);
                // skip uncompressed size
                a.skipBytes(4);
                dd = new byte[4];
                a.readFully(dd);
                assertArrayEquals(new byte[] {
                        // original size
                        (byte) 0x40, (byte) 0x42, (byte) 0x0F, 0, }, dd);

                // and now validate local file header
                a.seek(0);
                header = new byte[10];
                a.readFully(header);
                assertArrayEquals(new byte[] {
                        // sig
                        (byte) 0x50, (byte) 0x4b, 3, 4,
                        // version needed to extract
                        20, 0,
                        // GPB (EFS + Data Descriptor)
                        8, 8,
                        // method
                        8, 0, }, header);
                // ignore timestamp
                a.skipBytes(4);
                rest = new byte[17];
                a.readFully(rest);
                assertArrayEquals(new byte[] {
                        // CRC
                        0, 0, 0, 0,
                        // Compressed Size
                        0, 0, 0, 0,
                        // Original Size
                        0, 0, 0, 0,
                        // file name length
                        1, 0,
                        // extra field length
                        0, 0,
                        // file name
                        (byte) '0' }, rest);
            }
        };

    }

    /*
     * One entry of length 1 million bytes, written with compression to a stream.
     *
     * Compression + Stream => sizes are set to 0 in LFH, real values are inside the data descriptor. ZIP64 extra field as mode is Always.
     */
    private static ZipOutputTest writeSmallDeflatedEntryToStreamModeAlways(final boolean knownSize) {
        return (f, zos) -> {
            zos.setUseZip64(Zip64Mode.Always);
            final byte[] buf = new byte[ONE_MILLION];
            final ZipArchiveEntry zae = new ZipArchiveEntry("0");
            if (knownSize) {
                zae.setSize(ONE_MILLION);
            }
            zae.setMethod(ZipEntry.DEFLATED);
            zos.putArchiveEntry(zae);
            zos.write(buf);
            zos.closeArchiveEntry();
            zos.close();

            try (RandomAccessFile a = RandomAccessFileMode.READ_ONLY.create(f)) {
                getLengthAndPositionAtCentralDirectory(a);

                final long cfhPos = a.getFilePointer();
                // grab first entry, verify sizes are not
                // 0xFFFFFFF and it has an empty ZIP64 extended
                // information extra field
                byte[] header = new byte[12];
                a.readFully(header);
                assertArrayEquals(new byte[] {
                        // sig
                        (byte) 0x50, (byte) 0x4b, 1, 2,
                        // version made by
                        45, 0,
                        // version needed to extract
                        45, 0,
                        // GPB (EFS + Data Descriptor)
                        8, 8,
                        // method
                        8, 0, }, header, "CDH start");
                // ignore timestamp
                a.skipBytes(4);
                final byte[] crc = new byte[4];
                a.readFully(crc);
                assertArrayEquals(new byte[] { (byte) 0x9E, (byte) 0xCB, (byte) 0x79, (byte) 0x12, }, crc);
                // skip compressed size
                a.skipBytes(4);
                byte[] rest = new byte[23];
                a.readFully(rest);
                assertArrayEquals(new byte[] {
                        // Original Size
                        (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
                        // file name length
                        1, 0,
                        // extra field length
                        32, 0,
                        // comment length
                        0, 0,
                        // disk number
                        0, 0,
                        // attributes
                        0, 0, 0, 0, 0, 0,
                        // offset
                        (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
                        // file name
                        (byte) '0' }, rest, "CDH rest");
                byte[] extra = new byte[12];
                a.readFully(extra);
                assertArrayEquals(new byte[] {
                        // Header-ID
                        1, 0,
                        // size of extra
                        28, 0,
                        // original size
                        (byte) 0x40, (byte) 0x42, (byte) 0x0F, 0, 0, 0, 0, 0, }, extra, "CDH extra");
                // skip compressed size
                a.skipBytes(8);
                final byte[] offset = new byte[8];
                a.readFully(offset);
                assertArrayEquals(new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, }, offset, "extra offset");

                // validate data descriptor
                a.seek(cfhPos - 24);
                byte[] dd = new byte[8];
                a.readFully(dd);
                assertArrayEquals(new byte[] {
                        // sig
                        (byte) 0x50, (byte) 0x4b, 7, 8,
                        // CRC
                        (byte) 0x9E, (byte) 0xCB, (byte) 0x79, (byte) 0x12, }, dd, "DD");
                // skip compressed size
                a.skipBytes(8);
                dd = new byte[8];
                a.readFully(dd);
                assertArrayEquals(new byte[] {
                        // original size
                        (byte) 0x40, (byte) 0x42, (byte) 0x0F, 0, 0, 0, 0, 0 }, dd, "DD size");

                // and now validate local file header
                a.seek(0);
                header = new byte[10];
                a.readFully(header);
                assertArrayEquals(new byte[] {
                        // sig
                        (byte) 0x50, (byte) 0x4b, 3, 4,
                        // version needed to extract
                        45, 0,
                        // GPB (EFS + Data Descriptor)
                        8, 8,
                        // method
                        8, 0, }, header, "LFH start");
                // ignore timestamp
                a.skipBytes(4);
                rest = new byte[17];
                a.readFully(rest);
                assertArrayEquals(new byte[] {
                        // CRC
                        0, 0, 0, 0,
                        // Compressed Size
                        (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
                        // Original Size
                        (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
                        // file name length
                        1, 0,
                        // extra field length
                        20, 0,
                        // file name
                        (byte) '0' }, rest, "LFH rest");

                extra = new byte[20];
                a.readFully(extra);
                assertArrayEquals(new byte[] {
                        // Header-ID
                        1, 0,
                        // size of extra
                        16, 0,
                        // original size
                        0, 0, 0, 0, 0, 0, 0, 0,
                        // compressed size
                        0, 0, 0, 0, 0, 0, 0, 0, }, extra, "LFH extra");
            }
        };

    }

    private static ZipOutputTest writeSmallStoredEntry(final boolean knownSize) {
        return writeSmallStoredEntry(knownSize, Zip64Mode.AsNeeded);
    }

    /*
     * One entry of length 1 million bytes, written without compression.
     *
     * No Compression => sizes are stored directly inside the LFH. No Data Descriptor at all. Shouldn't contain any ZIP64 extra field if size was known.
     *
     * Creates a temporary archive of approx 1MB in size
     */
    private static ZipOutputTest writeSmallStoredEntry(final boolean knownSize, final Zip64Mode mode) {
        return (f, zos) -> {
            if (mode != Zip64Mode.AsNeeded) {
                zos.setUseZip64(mode);
            }
            final byte[] buf = new byte[ONE_MILLION];
            final ZipArchiveEntry zae = new ZipArchiveEntry("0");
            if (knownSize) {
                zae.setSize(ONE_MILLION);
                zae.setCrc(0x1279CB9EL);
            }
            zae.setMethod(ZipEntry.STORED);
            zos.putArchiveEntry(zae);
            zos.write(buf);
            zos.closeArchiveEntry();
            zos.close();

            try (RandomAccessFile a = RandomAccessFileMode.READ_ONLY.create(f)) {
                getLengthAndPositionAtCentralDirectory(a);

                // grab first CF entry, verify sizes are 1e6 and it
                // has no ZIP64 extended information extra field
                // at all
                byte[] header = new byte[12];
                a.readFully(header);
                assertArrayEquals(new byte[] {
                        // sig
                        (byte) 0x50, (byte) 0x4b, 1, 2,
                        // version made by
                        20, 0,
                        // version needed to extract
                        10, 0,
                        // GPB (EFS bit)
                        0, 8,
                        // method
                        0, 0 }, header, "CDH start");
                // ignore timestamp
                a.skipBytes(4);
                byte[] rest = new byte[31];
                a.readFully(rest);
                // 1e6 == 0xF4240
                assertArrayEquals(new byte[] {
                        // CRC
                        (byte) 0x9E, (byte) 0xCB, (byte) 0x79, (byte) 0x12,
                        // Compressed Size
                        (byte) 0x40, (byte) 0x42, (byte) 0x0F, 0,
                        // Original Size
                        (byte) 0x40, (byte) 0x42, (byte) 0x0F, 0,
                        // file name length
                        1, 0,
                        // extra field length
                        0, 0,
                        // comment length
                        0, 0,
                        // disk number
                        0, 0,
                        // attributes
                        0, 0, 0, 0, 0, 0,
                        // offset
                        0, 0, 0, 0,
                        // file name
                        (byte) '0' }, rest, "CDH rest");

                // and now validate local file header: this one
                // has a ZIP64 extra field if and only if size was
                // unknown and mode was not Never or the mode was
                // Always (regardless of size)
                final boolean hasExtra = mode == Zip64Mode.Always || mode == Zip64Mode.AsNeeded && !knownSize;
                a.seek(0);
                header = new byte[10];
                a.readFully(header);
                assertArrayEquals(new byte[] {
                        // sig
                        (byte) 0x50, (byte) 0x4b, 3, 4,
                        // version needed to extract
                        10, 0,
                        // GPB (EFS bit)
                        0, 8,
                        // method
                        0, 0 }, header, "LFH start");
                // ignore timestamp
                a.skipBytes(4);
                rest = new byte[17];
                a.readFully(rest);
                // 1e6 == 0xF4240
                assertArrayEquals(new byte[] {
                        // CRC
                        (byte) 0x9E, (byte) 0xCB, (byte) 0x79, (byte) 0x12,
                        // Compressed Size
                        (byte) 0x40, (byte) 0x42, (byte) 0x0F, 0,
                        // Original Size
                        (byte) 0x40, (byte) 0x42, (byte) 0x0F, 0,
                        // file name length
                        1, 0,
                        // extra field length
                        (byte) (!hasExtra ? 0 : 20), 0,
                        // file name
                        (byte) '0' }, rest, "LFH rest");
                if (hasExtra) {
                    final byte[] extra = new byte[20];
                    a.readFully(extra);
                    assertArrayEquals(new byte[] {
                            // Header-ID
                            1, 0,
                            // size of extra
                            16, 0,
                            // original size
                            (byte) 0x40, (byte) 0x42, (byte) 0x0F, 0, 0, 0, 0, 0,
                            // compressed size
                            (byte) 0x40, (byte) 0x42, (byte) 0x0F, 0, 0, 0, 0, 0, }, extra, "ZIP64 extra field");
                }
            }
        };
    }

    /*
     * One entry of length 1 million bytes, written without compression.
     *
     * No Compression => sizes are stored directly inside the LFH. No Data Descriptor at all. Contains ZIP64 extra fields because mode is Always
     *
     * Creates a temporary archive of approx 1MB in size
     */
    private static ZipOutputTest writeSmallStoredEntryModeAlways(final boolean knownSize) {
        return (f, zos) -> {
            zos.setUseZip64(Zip64Mode.Always);
            final byte[] buf = new byte[ONE_MILLION];
            final ZipArchiveEntry zae = new ZipArchiveEntry("0");
            if (knownSize) {
                zae.setSize(ONE_MILLION);
                zae.setCrc(0x1279CB9EL);
            }
            zae.setMethod(ZipEntry.STORED);
            zos.putArchiveEntry(zae);
            zos.write(buf);
            zos.closeArchiveEntry();
            zos.close();

            try (RandomAccessFile a = RandomAccessFileMode.READ_ONLY.create(f)) {
                getLengthAndPositionAtCentralDirectory(a);

                // grab first CF entry, verify sizes are 1e6, and it
                // has an empty ZIP64 extended information extra field
                byte[] header = new byte[12];
                a.readFully(header);
                assertArrayEquals(new byte[] {
                        // sig
                        (byte) 0x50, (byte) 0x4b, 1, 2,
                        // version made by
                        45, 0,
                        // version needed to extract
                        45, 0,
                        // GPB (EFS bit)
                        0, 8,
                        // method
                        0, 0 }, header, "CDH start");
                // ignore timestamp
                a.skipBytes(4);
                byte[] rest = new byte[31];
                a.readFully(rest);
                // 1e6 == 0xF4240
                assertArrayEquals(new byte[] {
                        // CRC
                        (byte) 0x9E, (byte) 0xCB, (byte) 0x79, (byte) 0x12,
                        // Compressed Size
                        (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
                        // Original Size
                        (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
                        // file name length
                        1, 0,
                        // extra field length
                        32, 0,
                        // comment length
                        0, 0,
                        // disk number
                        0, 0,
                        // attributes
                        0, 0, 0, 0, 0, 0,
                        // offset
                        (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
                        // file name
                        (byte) '0' }, rest, "CDH rest");

                byte[] extra = new byte[28];
                a.readFully(extra);
                assertArrayEquals(new byte[] {
                        // Header-ID
                        1, 0,
                        // size of extra
                        28, 0,
                        // original size
                        (byte) 0x40, (byte) 0x42, (byte) 0x0F, 0, 0, 0, 0, 0,
                        // compressed size
                        (byte) 0x40, (byte) 0x42, (byte) 0x0F, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, extra, "CDH extra");

                // and now validate local file header: this one
                // has a ZIP64 extra field as the mode was
                // Always
                a.seek(0);
                header = new byte[10];
                a.readFully(header);
                assertArrayEquals(new byte[] {
                        // sig
                        (byte) 0x50, (byte) 0x4b, 3, 4,
                        // version needed to extract
                        45, 0,
                        // GPB (EFS bit)
                        0, 8,
                        // method
                        0, 0 }, header, "LFH start");
                // ignore timestamp
                a.skipBytes(4);
                rest = new byte[17];
                a.readFully(rest);
                // 1e6 == 0xF4240
                assertArrayEquals(new byte[] {
                        // CRC
                        (byte) 0x9E, (byte) 0xCB, (byte) 0x79, (byte) 0x12,
                        // Compressed Size
                        (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
                        // Original Size
                        (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
                        // file name length
                        1, 0,
                        // extra field length
                        20, 0,
                        // file name
                        (byte) '0' }, rest, "LFH rest");

                extra = new byte[20];
                a.readFully(extra);
                assertArrayEquals(new byte[] {
                        // Header-ID
                        1, 0,
                        // size of extra
                        16, 0,
                        // original size
                        (byte) 0x40, (byte) 0x42, (byte) 0x0F, 0, 0, 0, 0, 0,
                        // compressed size
                        (byte) 0x40, (byte) 0x42, (byte) 0x0F, 0, 0, 0, 0, 0, }, extra, "LFH extra");
            }
        };
    }

    private File buildZipWithZip64Mode(final String fileName, final Zip64Mode zip64Mode, final File inputFile) throws Throwable {
        final File outputFile = getTempFile(fileName);
        outputFile.createNewFile();
        try (ZipArchiveOutputStream zipArchiveOutputStream = new ZipArchiveOutputStream(new BufferedOutputStream(new FileOutputStream(outputFile)))) {
            zipArchiveOutputStream.setUseZip64(zip64Mode);
            zipArchiveOutputStream.setCreateUnicodeExtraFields(ZipArchiveOutputStream.UnicodeExtraFieldPolicy.ALWAYS);

            zipArchiveOutputStream.putArchiveEntry(new ZipArchiveEntry("input.bin"));

            Files.copy(inputFile.toPath(), zipArchiveOutputStream);

            zipArchiveOutputStream.closeArchiveEntry();
        }

        return outputFile;
    }

    @Test
    public void testRead100KFilesGeneratedBy7ZIPUsingInputStream() throws Throwable {
        read100KFilesImpl(get100KFileFileGeneratedBy7ZIP());
    }

    @Test
    public void testRead100KFilesGeneratedBy7ZIPUsingZipFile() throws Throwable {
        read100KFilesUsingZipFileImpl(get100KFileFileGeneratedBy7ZIP());
    }

    @Test
    public void testRead100KFilesGeneratedByJava7JarUsingInputStream() throws Throwable {
        read100KFilesImpl(get100KFileFileGeneratedByJava7Jar());
    }

    @Test
    public void testRead100KFilesGeneratedByJava7JarUsingZipFile() throws Throwable {
        read100KFilesUsingZipFileImpl(get100KFileFileGeneratedByJava7Jar());
    }

    @Test
    public void testRead100KFilesGeneratedByPKZipUsingInputStream() throws Throwable {
        read100KFilesImpl(get100KFileFileGeneratedByPKZip());
    }

    @Test
    public void testRead100KFilesGeneratedByPKZipUsingZipFile() throws Throwable {
        read100KFilesUsingZipFileImpl(get100KFileFileGeneratedByPKZip());
    }

    @Test
    public void testRead100KFilesGeneratedByWinCFUsingInputStream() throws Throwable {
        read100KFilesImpl(get100KFileFileGeneratedByWinCF());
    }

    @Test
    public void testRead100KFilesGeneratedByWinCFUsingZipFile() throws Throwable {
        read100KFilesUsingZipFileImpl(get100KFileFileGeneratedByWinCF());
    }

    @Test
    public void testRead100KFilesGeneratedByWinZIPUsingInputStream() throws Throwable {
        read100KFilesImpl(get100KFileFileGeneratedByWinZIP());
    }

    @Test
    public void testRead100KFilesGeneratedByWinZIPUsingZipFile() throws Throwable {
        read100KFilesUsingZipFileImpl(get100KFileFileGeneratedByWinZIP());
    }

    @Test
    public void testRead100KFilesUsingInputStream() throws Throwable {
        read100KFilesImpl(get100KFileFile());
    }

    @Test
    public void testRead100KFilesUsingZipFile() throws Throwable {
        read100KFilesUsingZipFileImpl(get100KFileFile());
    }

    @Test
    public void testRead3EntriesCreatingBigArchiveFileUsingZipFile() throws Throwable {
        withTemporaryArchive("read3EntriesCreatingBigArchiveFileUsingZipFile", (f, zos) -> {
            write3EntriesCreatingBigArchiveToStream(zos);
            ZipFile zf = null;
            try {
                zf = ZipFile.builder().setFile(f).get();
                int idx = 0;
                for (final Enumeration<ZipArchiveEntry> e = zf.getEntriesInPhysicalOrder(); e.hasMoreElements();) {
                    final ZipArchiveEntry zae = e.nextElement();
                    assertEquals(String.valueOf(idx), zae.getName());
                    if (idx++ < 2) {
                        assertEquals(FIVE_BILLION / 2, zae.getSize());
                    } else {
                        assertEquals(1, zae.getSize());
                        try (InputStream i = zf.getInputStream(zae)) {
                            assertNotNull(i);
                            assertEquals(42, i.read());
                        }
                    }
                }
            } finally {
                ZipFile.closeQuietly(zf);
            }
        }, true);
    }

    @Test
    public void testRead5GBOfZerosGeneratedBy7ZIPUsingInputStream() throws Throwable {
        read5GBOfZerosImpl(get5GBZerosFileGeneratedBy7ZIP(), "5GB_of_Zeros");
    }

    @Test
    public void testRead5GBOfZerosGeneratedBy7ZIPUsingZipFile() throws Throwable {
        read5GBOfZerosUsingZipFileImpl(get5GBZerosFileGeneratedBy7ZIP(), "5GB_of_Zeros");
    }

    @Test
    public void testRead5GBOfZerosGeneratedByJava7JarUsingInputStream() throws Throwable {
        read5GBOfZerosImpl(get5GBZerosFileGeneratedByJava7Jar(), "5GB_of_Zeros");
    }

    @Test
    public void testRead5GBOfZerosGeneratedByJava7JarUsingZipFile() throws Throwable {
        read5GBOfZerosUsingZipFileImpl(get5GBZerosFileGeneratedByJava7Jar(), "5GB_of_Zeros");
    }

    @Test
    public void testRead5GBOfZerosGeneratedByPKZipUsingInputStream() throws Throwable {
        read5GBOfZerosImpl(get5GBZerosFileGeneratedByPKZip(), "zip6/5GB_of_Zeros");
    }

    @Test
    public void testRead5GBOfZerosGeneratedByPKZipUsingZipFile() throws Throwable {
        read5GBOfZerosUsingZipFileImpl(get5GBZerosFileGeneratedByPKZip(), "zip6/5GB_of_Zeros");
    }

    @Test
    public void testRead5GBOfZerosGeneratedByWinZIPUsingInputStream() throws Throwable {
        read5GBOfZerosImpl(get5GBZerosFileGeneratedByWinZIP(), "5GB_of_Zeros");
    }

    @Test
    public void testRead5GBOfZerosGeneratedByWinZIPUsingZipFile() throws Throwable {
        read5GBOfZerosUsingZipFileImpl(get5GBZerosFileGeneratedByWinZIP(), "5GB_of_Zeros");
    }

    @Test
    public void testRead5GBOfZerosUsingInputStream() throws Throwable {
        read5GBOfZerosImpl(get5GBZerosFile(), "5GB_of_Zeros");
    }

    @Test
    public void testRead5GBOfZerosUsingZipFile() throws Throwable {
        read5GBOfZerosUsingZipFileImpl(get5GBZerosFile(), "5GB_of_Zeros");
    }

    @Test
    public void testReadSelfGenerated100KFilesUsingZipFile() throws Throwable {
        withTemporaryArchive("readSelfGenerated100KFilesUsingZipFile()", (f, zos) -> {
            write100KFilesToStream(zos);
            read100KFilesUsingZipFileImpl(f);
        }, true);
    }

    @Test
    public void testWrite100KFilesFile() throws Throwable {
        withTemporaryArchive("write100KFilesFile", write100KFiles(), true);
    }

    @Test
    public void testWrite100KFilesFileModeAlways() throws Throwable {
        withTemporaryArchive("write100KFilesFileModeAlways", write100KFiles(Zip64Mode.Always), true);
    }

    @Test
    public void testWrite100KFilesFileModeNever() throws Throwable {
        withTemporaryArchive("write100KFilesFileModeNever", write100KFilesModeNever, true);
    }

    @Test
    public void testWrite100KFilesStream() throws Throwable {
        withTemporaryArchive("write100KFilesStream", write100KFiles(), false);
    }

    @Test
    public void testWrite100KFilesStreamModeAlways() throws Throwable {
        withTemporaryArchive("write100KFilesStreamModeAlways", write100KFiles(Zip64Mode.Always), false);
    }

    @Test
    public void testWrite100KFilesStreamModeNever() throws Throwable {
        withTemporaryArchive("write100KFilesStreamModeNever", write100KFilesModeNever, false);
    }

    @Test
    public void testWrite3EntriesCreatingBigArchiveFile() throws Throwable {
        withTemporaryArchive("write3EntriesCreatingBigArchiveFile", write3EntriesCreatingBigArchive(), true);
    }

    @Test
    public void testWrite3EntriesCreatingBigArchiveFileModeAlways() throws Throwable {
        withTemporaryArchive("write3EntriesCreatingBigArchiveFileModeAlways", write3EntriesCreatingBigArchive(Zip64Mode.Always), true);
    }

    @Test
    public void testWrite3EntriesCreatingBigArchiveFileModeNever() throws Throwable {
        withTemporaryArchive("write3EntriesCreatingBigArchiveFileModeNever", write3EntriesCreatingBigArchiveModeNever, true);
    }

    @Test
    public void testWrite3EntriesCreatingBigArchiveStream() throws Throwable {
        withTemporaryArchive("write3EntriesCreatingBigArchiveStream", write3EntriesCreatingBigArchive(), false);
    }

    @Test
    public void testWrite3EntriesCreatingBigArchiveStreamModeAlways() throws Throwable {
        withTemporaryArchive("write3EntriesCreatingBigArchiveStreamModeAlways", write3EntriesCreatingBigArchive(Zip64Mode.Always), false);
    }

    @Test
    public void testWrite3EntriesCreatingBigArchiveStreamModeNever() throws Throwable {
        withTemporaryArchive("write3EntriesCreatingBigArchiveStreamModeNever", write3EntriesCreatingBigArchiveModeNever, false);
    }

    @Test
    public void testWrite3EntriesCreatingManySplitArchiveFileModeAlways() throws Throwable {
        // about 76,293 ZIP split segments will be created
        withTemporaryArchive("write3EntriesCreatingManySplitArchiveFileModeAlways", write3EntriesCreatingBigArchive(Zip64Mode.Always, true), true, 65536L);
    }

    @Test
    public void testWrite3EntriesCreatingManySplitArchiveFileModeNever() throws Throwable {
        withTemporaryArchive("write3EntriesCreatingManySplitArchiveFileModeNever", write3EntriesCreatingBigArchiveModeNever, true, 65536L);
    }

    @Test
    public void testWriteAndRead5GBOfZerosUsingZipFile() throws Throwable {
        File f = null;
        try {
            f = write5GBZerosFile("writeAndRead5GBOfZerosUsingZipFile");
            read5GBOfZerosUsingZipFileImpl(f, "5GB_of_Zeros");
        } finally {
            if (f != null) {
                AbstractTest.forceDelete(f);
            }
        }
    }

    @Test
    public void testWriteBigDeflatedEntryKnownSizeToFile() throws Throwable {
        withTemporaryArchive("writeBigDeflatedEntryKnownSizeToFile", writeBigDeflatedEntryToFile(true), true);
    }

    @Test
    public void testWriteBigDeflatedEntryKnownSizeToFileModeAlways() throws Throwable {
        withTemporaryArchive("writeBigDeflatedEntryKnownSizeToFileModeAlways", writeBigDeflatedEntryToFile(true, Zip64Mode.Always), true);
    }

    @Test
    public void testWriteBigDeflatedEntryKnownSizeToFileModeNever() throws Throwable {
        withTemporaryArchive("writeBigDeflatedEntryKnownSizeToFileModeNever", writeBigDeflatedEntryToFileModeNever(true), true);
    }

    @Test
    public void testWriteBigDeflatedEntryKnownSizeToStream() throws Throwable {
        withTemporaryArchive("writeBigDeflatedEntryKnownSizeToStream", writeBigDeflatedEntryToStream(true, Zip64Mode.AsNeeded), false);
    }

    @Test
    public void testWriteBigDeflatedEntryKnownSizeToStreamModeAlways() throws Throwable {
        withTemporaryArchive("writeBigDeflatedEntryKnownSizeToStreamModeAlways", writeBigDeflatedEntryToStream(true, Zip64Mode.Always), false);
    }

    @Test
    public void testWriteBigDeflatedEntryKnownSizeToStreamModeNever() throws Throwable {
        withTemporaryArchive("writeBigDeflatedEntryKnownSizeToStreamModeNever", (f, zos) -> {
            zos.setUseZip64(Zip64Mode.Never);
            final Zip64RequiredException ex = assertThrows(Zip64RequiredException.class, () -> {
                final ZipArchiveEntry zae = new ZipArchiveEntry("0");
                zae.setSize(FIVE_BILLION);
                zae.setMethod(ZipEntry.DEFLATED);
                zos.putArchiveEntry(zae);
            }, "expected a Zip64RequiredException");
            assertTrue(ex.getMessage().startsWith("0's size"));
        }, false);
    }

    @Test
    public void testWriteBigDeflatedEntryUnknownSizeToFile() throws Throwable {
        withTemporaryArchive("writeBigDeflatedEntryUnknownSizeToFile", writeBigDeflatedEntryToFile(false), true);
    }

    @Test
    public void testWriteBigDeflatedEntryUnknownSizeToFileModeAlways() throws Throwable {
        withTemporaryArchive("writeBigDeflatedEntryUnknownSizeToFileModeAlways", writeBigDeflatedEntryToFile(false, Zip64Mode.Always), true);
    }

    @Test
    public void testWriteBigDeflatedEntryUnknownSizeToFileModeNever() throws Throwable {
        withTemporaryArchive("writeBigDeflatedEntryUnknownSizeToFileModeNever", writeBigDeflatedEntryToFileModeNever(false), true);
    }

    @Test
    public void testWriteBigDeflatedEntryUnknownSizeToStream() throws Throwable {
        withTemporaryArchive("writeBigDeflatedEntryUnknownSizeToStream", writeBigDeflatedEntryUnknownSizeToStream(Zip64Mode.AsNeeded), false);
    }

    @Test
    public void testWriteBigDeflatedEntryUnknownSizeToStreamModeAlways() throws Throwable {
        withTemporaryArchive("writeBigDeflatedEntryUnknownSizeToStreamModeAlways", writeBigDeflatedEntryToStream(false, Zip64Mode.Always), false);
    }

    @Test
    public void testWriteBigDeflatedEntryUnknownSizeToStreamModeNever() throws Throwable {
        withTemporaryArchive("writeBigDeflatedEntryUnknownSizeToStreamModeNever", writeBigDeflatedEntryUnknownSizeToStream(Zip64Mode.Never), false);
    }

    @Test
    public void testWriteBigStoredEntryKnownSizeToFile() throws Throwable {
        withTemporaryArchive("writeBigStoredEntryKnownSizeToFile", writeBigStoredEntry(true), true);
    }

    @Test
    public void testWriteBigStoredEntryKnownSizeToFileModeAlways() throws Throwable {
        withTemporaryArchive("writeBigStoredEntryKnownSizeToFileModeAlways", writeBigStoredEntry(true, Zip64Mode.Always), true);
    }

    @Test
    public void testWriteBigStoredEntryKnownSizeToFileModeNever() throws Throwable {
        withTemporaryArchive("writeBigStoredEntryKnownSizeToFileModeNever", writeBigStoredEntryModeNever(true), true);
    }

    /*
     * No Compression + Stream => sizes must be known before data is written.
     */
    @Test
    public void testWriteBigStoredEntryToStream() throws Throwable {
        withTemporaryArchive("writeBigStoredEntryToStream", writeBigStoredEntry(true), false);
    }

    @Test
    public void testWriteBigStoredEntryToStreamModeAlways() throws Throwable {
        withTemporaryArchive("writeBigStoredEntryToStreamModeAlways", writeBigStoredEntry(true, Zip64Mode.Always), false);
    }

    @Test
    public void testWriteBigStoredEntryToStreamModeNever() throws Throwable {
        withTemporaryArchive("writeBigStoredEntryToStreamModeNever", writeBigStoredEntryModeNever(true), false);
    }

    @Test
    public void testWriteBigStoredEntryUnknownSizeToFile() throws Throwable {
        withTemporaryArchive("writeBigStoredEntryUnknownSizeToFile", writeBigStoredEntry(false), true);
    }

    @Test
    public void testWriteBigStoredEntryUnknownSizeToFileModeAlways() throws Throwable {
        withTemporaryArchive("writeBigStoredEntryUnknownSizeToFileModeAlways", writeBigStoredEntry(false, Zip64Mode.Always), true);
    }

    @Test
    public void testWriteBigStoredEntryUnknownSizeToFileModeNever() throws Throwable {
        withTemporaryArchive("writeBigStoredEntryUnknownSizeToFileModeNever", writeBigStoredEntryModeNever(false), true);
    }

    @Test
    public void testWriteSmallDeflatedEntryKnownSizeToFile() throws Throwable {
        withTemporaryArchive("writeSmallDeflatedEntryKnownSizeToFile", writeSmallDeflatedEntryToFile(true), true);
    }

    @Test
    public void testWriteSmallDeflatedEntryKnownSizeToFileModeAlways() throws Throwable {
        withTemporaryArchive("writeSmallDeflatedEntryKnownSizeToFileModeAlways", writeSmallDeflatedEntryToFileModeAlways(true), true);
    }

    @Test
    public void testWriteSmallDeflatedEntryKnownSizeToFileModeNever() throws Throwable {
        withTemporaryArchive("writeSmallDeflatedEntryKnownSizeToFileModeNever", writeSmallDeflatedEntryToFile(true, Zip64Mode.Never), true);
    }

    @Test
    public void testWriteSmallDeflatedEntryKnownSizeToStream() throws Throwable {
        withTemporaryArchive("writeSmallDeflatedEntryKnownSizeToStream", writeSmallDeflatedEntryToStream(true, Zip64Mode.AsNeeded), false);
    }

    @Test
    public void testWriteSmallDeflatedEntryKnownSizeToStreamModeAlways() throws Throwable {
        withTemporaryArchive("writeSmallDeflatedEntryKnownSizeToStreamModeAlways", writeSmallDeflatedEntryToStreamModeAlways(true), false);
    }

    @Test
    public void testWriteSmallDeflatedEntryKnownSizeToStreamModeNever() throws Throwable {
        withTemporaryArchive("writeSmallDeflatedEntryKnownSizeToStreamModeNever", writeSmallDeflatedEntryToStream(true, Zip64Mode.Never), false);
    }

    @Test
    public void testWriteSmallDeflatedEntryUnknownSizeToFile() throws Throwable {
        withTemporaryArchive("writeSmallDeflatedEntryUnknownSizeToFile", writeSmallDeflatedEntryToFile(false), true);
    }

    @Test
    public void testWriteSmallDeflatedEntryUnknownSizeToFileModeAlways() throws Throwable {
        withTemporaryArchive("writeSmallDeflatedEntryUnknownSizeToFileModeAlways", writeSmallDeflatedEntryToFileModeAlways(false), true);
    }

    @Test
    public void testWriteSmallDeflatedEntryUnknownSizeToFileModeNever() throws Throwable {
        withTemporaryArchive("writeSmallDeflatedEntryUnknownSizeToFileModeNever", writeSmallDeflatedEntryToFile(false, Zip64Mode.Never), true);
    }

    @Test
    public void testWriteSmallDeflatedEntryUnknownSizeToStream() throws Throwable {
        withTemporaryArchive("writeSmallDeflatedEntryUnknownSizeToStream", writeSmallDeflatedEntryToStream(false, Zip64Mode.AsNeeded), false);
    }

    @Test
    public void testWriteSmallDeflatedEntryUnknownSizeToStreamModeAlways() throws Throwable {
        withTemporaryArchive("writeSmallDeflatedEntryUnknownSizeToStreamModeAlways", writeSmallDeflatedEntryToStreamModeAlways(false), false);
    }

    @Test
    public void testWriteSmallDeflatedEntryUnknownSizeToStreamModeNever() throws Throwable {
        withTemporaryArchive("writeSmallDeflatedEntryUnknownSizeToStreamModeNever", writeSmallDeflatedEntryToStream(false, Zip64Mode.Never), false);
    }

    @Test
    public void testWriteSmallStoredEntryKnownSizeToFile() throws Throwable {
        withTemporaryArchive("writeSmallStoredEntryKnownSizeToFile", writeSmallStoredEntry(true), true);
    }

    @Test
    public void testWriteSmallStoredEntryKnownSizeToFileModeAlways() throws Throwable {
        withTemporaryArchive("writeSmallStoredEntryKnownSizeToFileModeAlways", writeSmallStoredEntryModeAlways(true), true);
    }

    @Test
    public void testWriteSmallStoredEntryKnownSizeToFileModeNever() throws Throwable {
        withTemporaryArchive("writeSmallStoredEntryKnownSizeToFileModeNever", writeSmallStoredEntry(true, Zip64Mode.Never), true);
    }

    @Test
    public void testWriteSmallStoredEntryToStream() throws Throwable {
        withTemporaryArchive("writeSmallStoredEntryToStream", writeSmallStoredEntry(true), false);
    }

    @Test
    public void testWriteSmallStoredEntryToStreamModeAlways() throws Throwable {
        withTemporaryArchive("writeSmallStoredEntryToStreamModeAlways", writeSmallStoredEntryModeAlways(true), false);
    }

    @Test
    public void testWriteSmallStoredEntryToStreamModeNever() throws Throwable {
        withTemporaryArchive("writeSmallStoredEntryToStreamModeNever", writeSmallStoredEntry(true, Zip64Mode.Never), false);
    }

    @Test
    public void testWriteSmallStoredEntryUnknownSizeToFile() throws Throwable {
        withTemporaryArchive("writeSmallStoredEntryUnknownSizeToFile", writeSmallStoredEntry(false), true);
    }

    @Test
    public void testWriteSmallStoredEntryUnknownSizeToFileModeAlways() throws Throwable {
        withTemporaryArchive("writeSmallStoredEntryUnknownSizeToFileModeAlways", writeSmallStoredEntryModeAlways(false), true);
    }

    @Test
    public void testWriteSmallStoredEntryUnknownSizeToFileModeNever() throws Throwable {
        withTemporaryArchive("writeSmallStoredEntryUnknownSizeToFileModeNever", writeSmallStoredEntry(false, Zip64Mode.Never), true);
    }

    @Test
    public void testZip64ModeAlwaysWithCompatibility() throws Throwable {
        final File inputFile = getFile("test3.xml");

        // with Zip64Mode.AlwaysWithCompatibility, the relative header offset and disk number
        // start will not be set in extra fields
        final File zipUsingModeAlwaysWithCompatibility = buildZipWithZip64Mode("testZip64ModeAlwaysWithCompatibility-output-1",
                Zip64Mode.AlwaysWithCompatibility, inputFile);
        final ZipFile zipFileWithAlwaysWithCompatibility = ZipFile.builder().setFile(zipUsingModeAlwaysWithCompatibility).get();
        ZipArchiveEntry entry = zipFileWithAlwaysWithCompatibility.getEntries().nextElement();
        for (final ZipExtraField extraField : entry.getExtraFields()) {
            if (!(extraField instanceof Zip64ExtendedInformationExtraField)) {
                continue;
            }

            assertNull(((Zip64ExtendedInformationExtraField) extraField).getRelativeHeaderOffset());
            assertNull(((Zip64ExtendedInformationExtraField) extraField).getDiskStartNumber());
        }

        // with Zip64Mode.Always, the relative header offset and disk number start will be
        // set in extra fields
        final File zipUsingModeAlways = buildZipWithZip64Mode("testZip64ModeAlwaysWithCompatibility-output-2", Zip64Mode.Always, inputFile);
        final ZipFile zipFileWithAlways = ZipFile.builder().setFile(zipUsingModeAlways).get();
        entry = zipFileWithAlways.getEntries().nextElement();
        for (final ZipExtraField extraField : entry.getExtraFields()) {
            if (!(extraField instanceof Zip64ExtendedInformationExtraField)) {
                continue;
            }

            assertNotNull(((Zip64ExtendedInformationExtraField) extraField).getRelativeHeaderOffset());
            assertNotNull(((Zip64ExtendedInformationExtraField) extraField).getDiskStartNumber());
        }
    }
}
