From 1917e1eac546c63be8a19ac2f02a4a472fae85d2 Mon Sep 17 00:00:00 2001 From: Camille Vienot Date: Fri, 22 Mar 2019 20:35:02 +0100 Subject: [PATCH 1/2] Support zip64 jars See gh-16091 --- .../loader/jar/CentralDirectoryEndRecord.java | 143 +++++++++++++++++- .../loader/archive/JarFileArchiveTests.java | 24 +-- 2 files changed, 150 insertions(+), 17 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java index 2dba70d0da..ef8917dfa6 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java @@ -17,6 +17,7 @@ package org.springframework.boot.loader.jar; import java.io.IOException; +import java.util.Optional; import org.springframework.boot.loader.data.RandomAccessData; @@ -25,6 +26,7 @@ import org.springframework.boot.loader.data.RandomAccessData; * * @author Phillip Webb * @author Andy Wilkinson + * @author Camille Vienot * @see Zip File Format */ class CentralDirectoryEndRecord { @@ -33,6 +35,8 @@ class CentralDirectoryEndRecord { private static final int MAXIMUM_COMMENT_LENGTH = 0xFFFF; + private static final int ZIP64_MAGICCOUNT = 0xFFFF; + private static final int MAXIMUM_SIZE = MINIMUM_SIZE + MAXIMUM_COMMENT_LENGTH; private static final int SIGNATURE = 0x06054b50; @@ -41,6 +45,8 @@ class CentralDirectoryEndRecord { private static final int READ_BLOCK_SIZE = 256; + private final Optional zip64End; + private byte[] block; private int offset; @@ -69,6 +75,9 @@ class CentralDirectoryEndRecord { } this.offset = this.block.length - this.size; } + int startOfCentralDirectoryEndRecord = (int) (data.getSize() - this.size); + this.zip64End = Optional.ofNullable( + isZip64() ? new Zip64End(data, startOfCentralDirectoryEndRecord) : null); } private byte[] createBlockFromEndOfData(RandomAccessData data, int size) throws IOException { @@ -95,7 +104,10 @@ class CentralDirectoryEndRecord { long getStartOfArchive(RandomAccessData data) { long length = Bytes.littleEndianValue(this.block, this.offset + 12, 4); long specifiedOffset = Bytes.littleEndianValue(this.block, this.offset + 16, 4); - long actualOffset = data.getSize() - this.size - length; + long zip64EndSize = this.zip64End.map((x) -> x.getSize()).orElse(0L); + int zip64LocSize = this.zip64End.map((x) -> Zip64Locator.ZIP64_LOCSIZE).orElse(0); + long actualOffset = data.getSize() - this.size - length - zip64EndSize + - zip64LocSize; return actualOffset - specifiedOffset; } @@ -106,9 +118,14 @@ class CentralDirectoryEndRecord { * @return the central directory data */ RandomAccessData getCentralDirectory(RandomAccessData data) { - long offset = Bytes.littleEndianValue(this.block, this.offset + 16, 4); - long length = Bytes.littleEndianValue(this.block, this.offset + 12, 4); - return data.getSubsection(offset, length); + if (isZip64()) { + return this.zip64End.get().getCentratDirectory(data); + } + else { + long offset = Bytes.littleEndianValue(this.block, this.offset + 16, 4); + long length = Bytes.littleEndianValue(this.block, this.offset + 12, 4); + return data.getSubsection(offset, length); + } } /** @@ -116,11 +133,121 @@ class CentralDirectoryEndRecord { * @return the number of records in the zip */ int getNumberOfRecords() { - long numberOfRecords = Bytes.littleEndianValue(this.block, this.offset + 10, 2); - if (numberOfRecords == 0xFFFF) { - throw new IllegalStateException("Zip64 archives are not supported"); + if (isZip64()) { + return this.zip64End.get().getNumberOfRecords(); + } + else { + long numberOfRecords = Bytes.littleEndianValue(this.block, this.offset + 10, + 2); + return (int) numberOfRecords; + } + } + + boolean isZip64() { + return (int) Bytes.littleEndianValue(this.block, this.offset + 10, + 2) == ZIP64_MAGICCOUNT; + } + + /** + * A Zip64 end of central directory record. + * + * @see Chapter + * 4.3.14 of Zip64 specification + */ + private static class Zip64End { + + static final int ZIP64_ENDTOT = 32; // total number of entries + static final int ZIP64_ENDSIZ = 40; // central directory size in bytes + static final int ZIP64_ENDOFF = 48; // offset of first CEN header + + private final Zip64Locator locator; + + private final long centralDirectoryOffset; + + private final long centralDirectoryLength; + + private int numberOfRecords; + + Zip64End(RandomAccessData data, int centratDirectoryEndOffset) + throws IOException { + this(data, new Zip64Locator(data, centratDirectoryEndOffset)); + } + + Zip64End(RandomAccessData data, Zip64Locator locator) throws IOException { + this.locator = locator; + byte[] block = data.read(locator.getZip64EndOffset(), 56); + this.centralDirectoryOffset = Bytes.littleEndianValue(block, ZIP64_ENDOFF, 8); + this.centralDirectoryLength = Bytes.littleEndianValue(block, ZIP64_ENDSIZ, 8); + this.numberOfRecords = (int) Bytes.littleEndianValue(block, ZIP64_ENDTOT, 8); } - return (int) numberOfRecords; + + /** + * Return the size of this zip 64 end of central directory record. + * @return size of this zip 64 end of central directory record + */ + public long getSize() { + return this.locator.getZip64EndSize(); + } + + /** + * Return the bytes of the "Central directory" based on the offset indicated in + * this record. + * @param data the source data + * @return the central directory data + */ + public RandomAccessData getCentratDirectory(RandomAccessData data) { + return data.getSubsection(this.centralDirectoryOffset, + this.centralDirectoryLength); + } + + /** + * Return the number of entries in the zip64 archive. + * @return the number of records in the zip + */ + public int getNumberOfRecords() { + return this.numberOfRecords; + } + + } + + /** + * A Zip64 end of central directory locator. + * + * @see Chapter + * 4.3.15 of Zip64 specification + */ + private static class Zip64Locator { + + static final int ZIP64_LOCSIZE = 20; // locator size + static final int ZIP64_LOCOFF = 8; // offset of zip64 end + + private final long zip64EndOffset; + + private final int offset; + + Zip64Locator(RandomAccessData data, int centralDirectoryEndOffset) + throws IOException { + this.offset = centralDirectoryEndOffset - ZIP64_LOCSIZE; + byte[] block = data.read(this.offset, ZIP64_LOCSIZE); + this.zip64EndOffset = Bytes.littleEndianValue(block, ZIP64_LOCOFF, 8); + } + + /** + * Return the size of the zip 64 end record located by this zip64 end locator. + * @return size of the zip 64 end record located by this zip64 end locator + */ + public long getZip64EndSize() { + return this.offset - this.zip64EndOffset; + } + + /** + * Return the offset to locate {@link Zip64End}. + * @return offset of the Zip64 end of central directory record + */ + public long getZip64EndOffset() { + return this.zip64EndOffset; + } + } String getComment() { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java index 50fc2cdcd0..e4ebfc6302 100755 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java @@ -22,6 +22,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.net.URL; import java.util.HashMap; +import java.util.Iterator; import java.util.Map; import java.util.jar.JarEntry; import java.util.jar.JarOutputStream; @@ -38,13 +39,13 @@ import org.springframework.boot.loader.archive.Archive.Entry; import org.springframework.util.FileCopyUtils; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** * Tests for {@link JarFileArchive}. * * @author Phillip Webb * @author Andy Wilkinson + * @author Camille Vienot */ class JarFileArchiveTests { @@ -142,11 +143,15 @@ class JarFileArchiveTests { } @Test - void zip64ArchivesAreHandledGracefully() throws IOException { + void filesInzip64ArchivesAreAllListed() throws IOException { File file = new File(this.tempDir, "test.jar"); FileCopyUtils.copy(writeZip64Jar(), file); - assertThatIllegalStateException().isThrownBy(() -> new JarFileArchive(file)) - .withMessageContaining("Zip64 archives are not supported"); + JarFileArchive zip64Archive = new JarFileArchive(file); + Iterator it = zip64Archive.iterator(); + for (int i = 0; i < 65537; i++) { + assertThat(it.hasNext()).as(i + "nth file is present").isTrue(); + it.next(); + } } @Test @@ -166,11 +171,12 @@ class JarFileArchiveTests { output.closeEntry(); output.close(); JarFileArchive jarFileArchive = new JarFileArchive(file); - assertThatIllegalStateException().isThrownBy(() -> { - Archive archive = jarFileArchive.getNestedArchive(getEntriesMap(jarFileArchive).get("nested/zip64.jar")); - ((JarFileArchive) archive).close(); - }).withMessageContaining("Failed to get nested archive for entry nested/zip64.jar"); - jarFileArchive.close(); + Archive nestedArchive = jarFileArchive.getNestedArchive(getEntriesMap(jarFileArchive).get("nested/zip64.jar")); + Iterator it = nestedArchive.iterator(); + for (int i = 0; i < 65537; i++) { + assertThat(it.hasNext()).as(i + "nth file is present").isTrue(); + it.next(); + } } private byte[] writeZip64Jar() throws IOException { From 02ac0897679e89f0c99e107e8f5e7cea9f2b0dc0 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 20 Sep 2019 12:42:30 +0100 Subject: [PATCH 2/2] Polish "Support zip64 jars" See gh-16091 --- .../src/main/asciidoc/deployment.adoc | 4 + .../loader/jar/CentralDirectoryEndRecord.java | 86 ++++++++----------- .../loader/archive/JarFileArchiveTests.java | 13 +-- .../boot/loader/jar/JarFileTests.java | 66 ++++++++++++++ 4 files changed, 115 insertions(+), 54 deletions(-) diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/deployment.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/deployment.adoc index 3ac6c26793..a4431c5515 100644 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/deployment.adoc +++ b/spring-boot-project/spring-boot-docs/src/main/asciidoc/deployment.adoc @@ -404,6 +404,10 @@ Currently, some tools do not accept this format, so you may not always be able t For example, `jar -xf` may silently fail to extract a jar or war that has been made fully executable. It is recommended that you make your jar or war fully executable only if you intend to execute it directly, rather than running it with `java -jar`or deploying it to a servlet container. +CAUTION: A zip64-format jar file cannot be made fully executable. +Attempting to do so will result in a jar file that is reported as corrupt when executed directly or with `java -jar`. +A standard-format jar file that contains one or more zip64-format nested jars can be fully executable. + To create a '`fully executable`' jar with Maven, use the following plugin configuration: [source,xml,indent=0,subs="verbatim,quotes,attributes"] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java index ef8917dfa6..1d12c00eb0 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java @@ -17,7 +17,6 @@ package org.springframework.boot.loader.jar; import java.io.IOException; -import java.util.Optional; import org.springframework.boot.loader.data.RandomAccessData; @@ -45,7 +44,7 @@ class CentralDirectoryEndRecord { private static final int READ_BLOCK_SIZE = 256; - private final Optional zip64End; + private final Zip64End zip64End; private byte[] block; @@ -76,8 +75,7 @@ class CentralDirectoryEndRecord { this.offset = this.block.length - this.size; } int startOfCentralDirectoryEndRecord = (int) (data.getSize() - this.size); - this.zip64End = Optional.ofNullable( - isZip64() ? new Zip64End(data, startOfCentralDirectoryEndRecord) : null); + this.zip64End = isZip64() ? new Zip64End(data, startOfCentralDirectoryEndRecord) : null; } private byte[] createBlockFromEndOfData(RandomAccessData data, int size) throws IOException { @@ -94,6 +92,10 @@ class CentralDirectoryEndRecord { return this.size == MINIMUM_SIZE + commentLength; } + private boolean isZip64() { + return (int) Bytes.littleEndianValue(this.block, this.offset + 10, 2) == ZIP64_MAGICCOUNT; + } + /** * Returns the location in the data that the archive actually starts. For most files * the archive data will start at 0, however, it is possible to have prefixed bytes @@ -104,10 +106,9 @@ class CentralDirectoryEndRecord { long getStartOfArchive(RandomAccessData data) { long length = Bytes.littleEndianValue(this.block, this.offset + 12, 4); long specifiedOffset = Bytes.littleEndianValue(this.block, this.offset + 16, 4); - long zip64EndSize = this.zip64End.map((x) -> x.getSize()).orElse(0L); - int zip64LocSize = this.zip64End.map((x) -> Zip64Locator.ZIP64_LOCSIZE).orElse(0); - long actualOffset = data.getSize() - this.size - length - zip64EndSize - - zip64LocSize; + long zip64EndSize = (this.zip64End != null) ? this.zip64End.getSize() : 0L; + int zip64LocSize = (this.zip64End != null) ? Zip64Locator.ZIP64_LOCSIZE : 0; + long actualOffset = data.getSize() - this.size - length - zip64EndSize - zip64LocSize; return actualOffset - specifiedOffset; } @@ -118,14 +119,12 @@ class CentralDirectoryEndRecord { * @return the central directory data */ RandomAccessData getCentralDirectory(RandomAccessData data) { - if (isZip64()) { - return this.zip64End.get().getCentratDirectory(data); - } - else { - long offset = Bytes.littleEndianValue(this.block, this.offset + 16, 4); - long length = Bytes.littleEndianValue(this.block, this.offset + 12, 4); - return data.getSubsection(offset, length); + if (this.zip64End != null) { + return this.zip64End.getCentralDirectory(data); } + long offset = Bytes.littleEndianValue(this.block, this.offset + 16, 4); + long length = Bytes.littleEndianValue(this.block, this.offset + 12, 4); + return data.getSubsection(offset, length); } /** @@ -133,19 +132,17 @@ class CentralDirectoryEndRecord { * @return the number of records in the zip */ int getNumberOfRecords() { - if (isZip64()) { - return this.zip64End.get().getNumberOfRecords(); - } - else { - long numberOfRecords = Bytes.littleEndianValue(this.block, this.offset + 10, - 2); - return (int) numberOfRecords; + if (this.zip64End != null) { + return this.zip64End.getNumberOfRecords(); } + long numberOfRecords = Bytes.littleEndianValue(this.block, this.offset + 10, 2); + return (int) numberOfRecords; } - boolean isZip64() { - return (int) Bytes.littleEndianValue(this.block, this.offset + 10, - 2) == ZIP64_MAGICCOUNT; + String getComment() { + int commentLength = (int) Bytes.littleEndianValue(this.block, this.offset + COMMENT_LENGTH_OFFSET, 2); + AsciiBytes comment = new AsciiBytes(this.block, this.offset + COMMENT_LENGTH_OFFSET + 2, commentLength); + return comment.toString(); } /** @@ -154,11 +151,13 @@ class CentralDirectoryEndRecord { * @see Chapter * 4.3.14 of Zip64 specification */ - private static class Zip64End { + private static final class Zip64End { - static final int ZIP64_ENDTOT = 32; // total number of entries - static final int ZIP64_ENDSIZ = 40; // central directory size in bytes - static final int ZIP64_ENDOFF = 48; // offset of first CEN header + private static final int ZIP64_ENDTOT = 32; // total number of entries + + private static final int ZIP64_ENDSIZ = 40; // central directory size in bytes + + private static final int ZIP64_ENDOFF = 48; // offset of first CEN header private final Zip64Locator locator; @@ -168,12 +167,11 @@ class CentralDirectoryEndRecord { private int numberOfRecords; - Zip64End(RandomAccessData data, int centratDirectoryEndOffset) - throws IOException { + private Zip64End(RandomAccessData data, int centratDirectoryEndOffset) throws IOException { this(data, new Zip64Locator(data, centratDirectoryEndOffset)); } - Zip64End(RandomAccessData data, Zip64Locator locator) throws IOException { + private Zip64End(RandomAccessData data, Zip64Locator locator) throws IOException { this.locator = locator; byte[] block = data.read(locator.getZip64EndOffset(), 56); this.centralDirectoryOffset = Bytes.littleEndianValue(block, ZIP64_ENDOFF, 8); @@ -185,7 +183,7 @@ class CentralDirectoryEndRecord { * Return the size of this zip 64 end of central directory record. * @return size of this zip 64 end of central directory record */ - public long getSize() { + private long getSize() { return this.locator.getZip64EndSize(); } @@ -195,16 +193,15 @@ class CentralDirectoryEndRecord { * @param data the source data * @return the central directory data */ - public RandomAccessData getCentratDirectory(RandomAccessData data) { - return data.getSubsection(this.centralDirectoryOffset, - this.centralDirectoryLength); + private RandomAccessData getCentralDirectory(RandomAccessData data) { + return data.getSubsection(this.centralDirectoryOffset, this.centralDirectoryLength); } /** * Return the number of entries in the zip64 archive. * @return the number of records in the zip */ - public int getNumberOfRecords() { + private int getNumberOfRecords() { return this.numberOfRecords; } @@ -216,7 +213,7 @@ class CentralDirectoryEndRecord { * @see Chapter * 4.3.15 of Zip64 specification */ - private static class Zip64Locator { + private static final class Zip64Locator { static final int ZIP64_LOCSIZE = 20; // locator size static final int ZIP64_LOCOFF = 8; // offset of zip64 end @@ -225,8 +222,7 @@ class CentralDirectoryEndRecord { private final int offset; - Zip64Locator(RandomAccessData data, int centralDirectoryEndOffset) - throws IOException { + private Zip64Locator(RandomAccessData data, int centralDirectoryEndOffset) throws IOException { this.offset = centralDirectoryEndOffset - ZIP64_LOCSIZE; byte[] block = data.read(this.offset, ZIP64_LOCSIZE); this.zip64EndOffset = Bytes.littleEndianValue(block, ZIP64_LOCOFF, 8); @@ -236,7 +232,7 @@ class CentralDirectoryEndRecord { * Return the size of the zip 64 end record located by this zip64 end locator. * @return size of the zip 64 end record located by this zip64 end locator */ - public long getZip64EndSize() { + private long getZip64EndSize() { return this.offset - this.zip64EndOffset; } @@ -244,16 +240,10 @@ class CentralDirectoryEndRecord { * Return the offset to locate {@link Zip64End}. * @return offset of the Zip64 end of central directory record */ - public long getZip64EndOffset() { + private long getZip64EndOffset() { return this.zip64EndOffset; } } - String getComment() { - int commentLength = (int) Bytes.littleEndianValue(this.block, this.offset + COMMENT_LENGTH_OFFSET, 2); - AsciiBytes comment = new AsciiBytes(this.block, this.offset + COMMENT_LENGTH_OFFSET + 2, commentLength); - return comment.toString(); - } - } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java index e4ebfc6302..a7f4e1a8ff 100755 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java @@ -143,14 +143,15 @@ class JarFileArchiveTests { } @Test - void filesInzip64ArchivesAreAllListed() throws IOException { + void filesInZip64ArchivesAreAllListed() throws IOException { File file = new File(this.tempDir, "test.jar"); FileCopyUtils.copy(writeZip64Jar(), file); - JarFileArchive zip64Archive = new JarFileArchive(file); - Iterator it = zip64Archive.iterator(); - for (int i = 0; i < 65537; i++) { - assertThat(it.hasNext()).as(i + "nth file is present").isTrue(); - it.next(); + try (JarFileArchive zip64Archive = new JarFileArchive(file)) { + Iterator entries = zip64Archive.iterator(); + for (int i = 0; i < 65537; i++) { + assertThat(entries.hasNext()).as(i + "nth file is present").isTrue(); + entries.next(); + } } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java index 67b1404775..9ec200da7e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java @@ -16,19 +16,26 @@ package org.springframework.boot.loader.jar; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FilePermission; +import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.net.URLClassLoader; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Collections; import java.util.Enumeration; +import java.util.List; import java.util.jar.JarEntry; import java.util.jar.JarInputStream; +import java.util.jar.JarOutputStream; import java.util.jar.Manifest; +import java.util.zip.CRC32; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; @@ -512,6 +519,65 @@ class JarFileTests { } } + @Test + void zip64JarCanBeRead() throws Exception { + File zip64Jar = new File(this.tempDir, "zip64.jar"); + FileCopyUtils.copy(zip64Jar(), zip64Jar); + try (JarFile zip64JarFile = new JarFile(zip64Jar)) { + List entries = Collections.list(zip64JarFile.entries()); + assertThat(entries).hasSize(65537); + for (int i = 0; i < entries.size(); i++) { + JarEntry entry = entries.get(i); + InputStream entryInput = zip64JarFile.getInputStream(entry); + String contents = StreamUtils.copyToString(entryInput, StandardCharsets.UTF_8); + assertThat(contents).isEqualTo("Entry " + (i + 1)); + } + } + } + + @Test + void nestedZip64JarCanBeRead() throws Exception { + File outer = new File(this.tempDir, "outer.jar"); + try (JarOutputStream jarOutput = new JarOutputStream(new FileOutputStream(outer))) { + JarEntry nestedEntry = new JarEntry("nested-zip64.jar"); + byte[] contents = zip64Jar(); + nestedEntry.setSize(contents.length); + nestedEntry.setCompressedSize(contents.length); + CRC32 crc32 = new CRC32(); + crc32.update(contents); + nestedEntry.setCrc(crc32.getValue()); + nestedEntry.setMethod(ZipEntry.STORED); + jarOutput.putNextEntry(nestedEntry); + jarOutput.write(contents); + jarOutput.closeEntry(); + } + try (JarFile outerJarFile = new JarFile(outer)) { + try (JarFile nestedZip64JarFile = outerJarFile + .getNestedJarFile(outerJarFile.getJarEntry("nested-zip64.jar"))) { + List entries = Collections.list(nestedZip64JarFile.entries()); + assertThat(entries).hasSize(65537); + for (int i = 0; i < entries.size(); i++) { + JarEntry entry = entries.get(i); + InputStream entryInput = nestedZip64JarFile.getInputStream(entry); + String contents = StreamUtils.copyToString(entryInput, StandardCharsets.UTF_8); + assertThat(contents).isEqualTo("Entry " + (i + 1)); + } + } + } + } + + private byte[] zip64Jar() throws IOException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + JarOutputStream jarOutput = new JarOutputStream(bytes); + for (int i = 0; i < 65537; i++) { + jarOutput.putNextEntry(new JarEntry(i + ".dat")); + jarOutput.write(("Entry " + (i + 1)).getBytes(StandardCharsets.UTF_8)); + jarOutput.closeEntry(); + } + jarOutput.close(); + return bytes.toByteArray(); + } + private int getJavaVersion() { try { Object runtimeVersion = Runtime.class.getMethod("version").invoke(null);