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 {