Skip to content

Commit 185d9a3

Browse files
committed
Merge pull request #16091 from cvienot
* gh-16091: Polish "Support zip64 jars" Support zip64 jars Closes gh-16091
2 parents d5fc324 + 02ac089 commit 185d9a3

File tree

4 files changed

+207
-13
lines changed

4 files changed

+207
-13
lines changed

spring-boot-project/spring-boot-docs/src/main/asciidoc/deployment.adoc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,10 @@ Currently, some tools do not accept this format, so you may not always be able t
404404
For example, `jar -xf` may silently fail to extract a jar or war that has been made fully executable.
405405
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.
406406

407+
CAUTION: A zip64-format jar file cannot be made fully executable.
408+
Attempting to do so will result in a jar file that is reported as corrupt when executed directly or with `java -jar`.
409+
A standard-format jar file that contains one or more zip64-format nested jars can be fully executable.
410+
407411
To create a '`fully executable`' jar with Maven, use the following plugin configuration:
408412

409413
[source,xml,indent=0,subs="verbatim,quotes,attributes"]

spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java

Lines changed: 121 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
*
2626
* @author Phillip Webb
2727
* @author Andy Wilkinson
28+
* @author Camille Vienot
2829
* @see <a href="https://en.wikipedia.org/wiki/Zip_%28file_format%29">Zip File Format</a>
2930
*/
3031
class CentralDirectoryEndRecord {
@@ -33,6 +34,8 @@ class CentralDirectoryEndRecord {
3334

3435
private static final int MAXIMUM_COMMENT_LENGTH = 0xFFFF;
3536

37+
private static final int ZIP64_MAGICCOUNT = 0xFFFF;
38+
3639
private static final int MAXIMUM_SIZE = MINIMUM_SIZE + MAXIMUM_COMMENT_LENGTH;
3740

3841
private static final int SIGNATURE = 0x06054b50;
@@ -41,6 +44,8 @@ class CentralDirectoryEndRecord {
4144

4245
private static final int READ_BLOCK_SIZE = 256;
4346

47+
private final Zip64End zip64End;
48+
4449
private byte[] block;
4550

4651
private int offset;
@@ -69,6 +74,8 @@ class CentralDirectoryEndRecord {
6974
}
7075
this.offset = this.block.length - this.size;
7176
}
77+
int startOfCentralDirectoryEndRecord = (int) (data.getSize() - this.size);
78+
this.zip64End = isZip64() ? new Zip64End(data, startOfCentralDirectoryEndRecord) : null;
7279
}
7380

7481
private byte[] createBlockFromEndOfData(RandomAccessData data, int size) throws IOException {
@@ -85,6 +92,10 @@ private boolean isValid() {
8592
return this.size == MINIMUM_SIZE + commentLength;
8693
}
8794

95+
private boolean isZip64() {
96+
return (int) Bytes.littleEndianValue(this.block, this.offset + 10, 2) == ZIP64_MAGICCOUNT;
97+
}
98+
8899
/**
89100
* Returns the location in the data that the archive actually starts. For most files
90101
* the archive data will start at 0, however, it is possible to have prefixed bytes
@@ -95,7 +106,9 @@ private boolean isValid() {
95106
long getStartOfArchive(RandomAccessData data) {
96107
long length = Bytes.littleEndianValue(this.block, this.offset + 12, 4);
97108
long specifiedOffset = Bytes.littleEndianValue(this.block, this.offset + 16, 4);
98-
long actualOffset = data.getSize() - this.size - length;
109+
long zip64EndSize = (this.zip64End != null) ? this.zip64End.getSize() : 0L;
110+
int zip64LocSize = (this.zip64End != null) ? Zip64Locator.ZIP64_LOCSIZE : 0;
111+
long actualOffset = data.getSize() - this.size - length - zip64EndSize - zip64LocSize;
99112
return actualOffset - specifiedOffset;
100113
}
101114

@@ -106,6 +119,9 @@ long getStartOfArchive(RandomAccessData data) {
106119
* @return the central directory data
107120
*/
108121
RandomAccessData getCentralDirectory(RandomAccessData data) {
122+
if (this.zip64End != null) {
123+
return this.zip64End.getCentralDirectory(data);
124+
}
109125
long offset = Bytes.littleEndianValue(this.block, this.offset + 16, 4);
110126
long length = Bytes.littleEndianValue(this.block, this.offset + 12, 4);
111127
return data.getSubsection(offset, length);
@@ -116,10 +132,10 @@ RandomAccessData getCentralDirectory(RandomAccessData data) {
116132
* @return the number of records in the zip
117133
*/
118134
int getNumberOfRecords() {
119-
long numberOfRecords = Bytes.littleEndianValue(this.block, this.offset + 10, 2);
120-
if (numberOfRecords == 0xFFFF) {
121-
throw new IllegalStateException("Zip64 archives are not supported");
135+
if (this.zip64End != null) {
136+
return this.zip64End.getNumberOfRecords();
122137
}
138+
long numberOfRecords = Bytes.littleEndianValue(this.block, this.offset + 10, 2);
123139
return (int) numberOfRecords;
124140
}
125141

@@ -129,4 +145,105 @@ String getComment() {
129145
return comment.toString();
130146
}
131147

148+
/**
149+
* A Zip64 end of central directory record.
150+
*
151+
* @see <a href="https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT">Chapter
152+
* 4.3.14 of Zip64 specification</a>
153+
*/
154+
private static final class Zip64End {
155+
156+
private static final int ZIP64_ENDTOT = 32; // total number of entries
157+
158+
private static final int ZIP64_ENDSIZ = 40; // central directory size in bytes
159+
160+
private static final int ZIP64_ENDOFF = 48; // offset of first CEN header
161+
162+
private final Zip64Locator locator;
163+
164+
private final long centralDirectoryOffset;
165+
166+
private final long centralDirectoryLength;
167+
168+
private int numberOfRecords;
169+
170+
private Zip64End(RandomAccessData data, int centratDirectoryEndOffset) throws IOException {
171+
this(data, new Zip64Locator(data, centratDirectoryEndOffset));
172+
}
173+
174+
private Zip64End(RandomAccessData data, Zip64Locator locator) throws IOException {
175+
this.locator = locator;
176+
byte[] block = data.read(locator.getZip64EndOffset(), 56);
177+
this.centralDirectoryOffset = Bytes.littleEndianValue(block, ZIP64_ENDOFF, 8);
178+
this.centralDirectoryLength = Bytes.littleEndianValue(block, ZIP64_ENDSIZ, 8);
179+
this.numberOfRecords = (int) Bytes.littleEndianValue(block, ZIP64_ENDTOT, 8);
180+
}
181+
182+
/**
183+
* Return the size of this zip 64 end of central directory record.
184+
* @return size of this zip 64 end of central directory record
185+
*/
186+
private long getSize() {
187+
return this.locator.getZip64EndSize();
188+
}
189+
190+
/**
191+
* Return the bytes of the "Central directory" based on the offset indicated in
192+
* this record.
193+
* @param data the source data
194+
* @return the central directory data
195+
*/
196+
private RandomAccessData getCentralDirectory(RandomAccessData data) {
197+
return data.getSubsection(this.centralDirectoryOffset, this.centralDirectoryLength);
198+
}
199+
200+
/**
201+
* Return the number of entries in the zip64 archive.
202+
* @return the number of records in the zip
203+
*/
204+
private int getNumberOfRecords() {
205+
return this.numberOfRecords;
206+
}
207+
208+
}
209+
210+
/**
211+
* A Zip64 end of central directory locator.
212+
*
213+
* @see <a href="https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT">Chapter
214+
* 4.3.15 of Zip64 specification</a>
215+
*/
216+
private static final class Zip64Locator {
217+
218+
static final int ZIP64_LOCSIZE = 20; // locator size
219+
static final int ZIP64_LOCOFF = 8; // offset of zip64 end
220+
221+
private final long zip64EndOffset;
222+
223+
private final int offset;
224+
225+
private Zip64Locator(RandomAccessData data, int centralDirectoryEndOffset) throws IOException {
226+
this.offset = centralDirectoryEndOffset - ZIP64_LOCSIZE;
227+
byte[] block = data.read(this.offset, ZIP64_LOCSIZE);
228+
this.zip64EndOffset = Bytes.littleEndianValue(block, ZIP64_LOCOFF, 8);
229+
}
230+
231+
/**
232+
* Return the size of the zip 64 end record located by this zip64 end locator.
233+
* @return size of the zip 64 end record located by this zip64 end locator
234+
*/
235+
private long getZip64EndSize() {
236+
return this.offset - this.zip64EndOffset;
237+
}
238+
239+
/**
240+
* Return the offset to locate {@link Zip64End}.
241+
* @return offset of the Zip64 end of central directory record
242+
*/
243+
private long getZip64EndOffset() {
244+
return this.zip64EndOffset;
245+
}
246+
247+
}
248+
132249
}

spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.io.IOException;
2323
import java.net.URL;
2424
import java.util.HashMap;
25+
import java.util.Iterator;
2526
import java.util.Map;
2627
import java.util.jar.JarEntry;
2728
import java.util.jar.JarOutputStream;
@@ -38,13 +39,13 @@
3839
import org.springframework.util.FileCopyUtils;
3940

4041
import static org.assertj.core.api.Assertions.assertThat;
41-
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
4242

4343
/**
4444
* Tests for {@link JarFileArchive}.
4545
*
4646
* @author Phillip Webb
4747
* @author Andy Wilkinson
48+
* @author Camille Vienot
4849
*/
4950
class JarFileArchiveTests {
5051

@@ -142,11 +143,16 @@ void unpackedLocationsFromSameArchiveShareSameParent() throws Exception {
142143
}
143144

144145
@Test
145-
void zip64ArchivesAreHandledGracefully() throws IOException {
146+
void filesInZip64ArchivesAreAllListed() throws IOException {
146147
File file = new File(this.tempDir, "test.jar");
147148
FileCopyUtils.copy(writeZip64Jar(), file);
148-
assertThatIllegalStateException().isThrownBy(() -> new JarFileArchive(file))
149-
.withMessageContaining("Zip64 archives are not supported");
149+
try (JarFileArchive zip64Archive = new JarFileArchive(file)) {
150+
Iterator<Entry> entries = zip64Archive.iterator();
151+
for (int i = 0; i < 65537; i++) {
152+
assertThat(entries.hasNext()).as(i + "nth file is present").isTrue();
153+
entries.next();
154+
}
155+
}
150156
}
151157

152158
@Test
@@ -166,11 +172,12 @@ void nestedZip64ArchivesAreHandledGracefully() throws IOException {
166172
output.closeEntry();
167173
output.close();
168174
JarFileArchive jarFileArchive = new JarFileArchive(file);
169-
assertThatIllegalStateException().isThrownBy(() -> {
170-
Archive archive = jarFileArchive.getNestedArchive(getEntriesMap(jarFileArchive).get("nested/zip64.jar"));
171-
((JarFileArchive) archive).close();
172-
}).withMessageContaining("Failed to get nested archive for entry nested/zip64.jar");
173-
jarFileArchive.close();
175+
Archive nestedArchive = jarFileArchive.getNestedArchive(getEntriesMap(jarFileArchive).get("nested/zip64.jar"));
176+
Iterator<Entry> it = nestedArchive.iterator();
177+
for (int i = 0; i < 65537; i++) {
178+
assertThat(it.hasNext()).as(i + "nth file is present").isTrue();
179+
it.next();
180+
}
174181
}
175182

176183
private byte[] writeZip64Jar() throws IOException {

spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,26 @@
1616

1717
package org.springframework.boot.loader.jar;
1818

19+
import java.io.ByteArrayOutputStream;
1920
import java.io.File;
2021
import java.io.FileInputStream;
2122
import java.io.FileNotFoundException;
2223
import java.io.FileOutputStream;
2324
import java.io.FilePermission;
25+
import java.io.IOException;
2426
import java.io.InputStream;
2527
import java.net.URL;
2628
import java.net.URLClassLoader;
2729
import java.nio.charset.Charset;
30+
import java.nio.charset.StandardCharsets;
31+
import java.util.Collections;
2832
import java.util.Enumeration;
33+
import java.util.List;
2934
import java.util.jar.JarEntry;
3035
import java.util.jar.JarInputStream;
36+
import java.util.jar.JarOutputStream;
3137
import java.util.jar.Manifest;
38+
import java.util.zip.CRC32;
3239
import java.util.zip.ZipEntry;
3340
import java.util.zip.ZipFile;
3441

@@ -512,6 +519,65 @@ void multiReleaseEntry() throws Exception {
512519
}
513520
}
514521

522+
@Test
523+
void zip64JarCanBeRead() throws Exception {
524+
File zip64Jar = new File(this.tempDir, "zip64.jar");
525+
FileCopyUtils.copy(zip64Jar(), zip64Jar);
526+
try (JarFile zip64JarFile = new JarFile(zip64Jar)) {
527+
List<JarEntry> entries = Collections.list(zip64JarFile.entries());
528+
assertThat(entries).hasSize(65537);
529+
for (int i = 0; i < entries.size(); i++) {
530+
JarEntry entry = entries.get(i);
531+
InputStream entryInput = zip64JarFile.getInputStream(entry);
532+
String contents = StreamUtils.copyToString(entryInput, StandardCharsets.UTF_8);
533+
assertThat(contents).isEqualTo("Entry " + (i + 1));
534+
}
535+
}
536+
}
537+
538+
@Test
539+
void nestedZip64JarCanBeRead() throws Exception {
540+
File outer = new File(this.tempDir, "outer.jar");
541+
try (JarOutputStream jarOutput = new JarOutputStream(new FileOutputStream(outer))) {
542+
JarEntry nestedEntry = new JarEntry("nested-zip64.jar");
543+
byte[] contents = zip64Jar();
544+
nestedEntry.setSize(contents.length);
545+
nestedEntry.setCompressedSize(contents.length);
546+
CRC32 crc32 = new CRC32();
547+
crc32.update(contents);
548+
nestedEntry.setCrc(crc32.getValue());
549+
nestedEntry.setMethod(ZipEntry.STORED);
550+
jarOutput.putNextEntry(nestedEntry);
551+
jarOutput.write(contents);
552+
jarOutput.closeEntry();
553+
}
554+
try (JarFile outerJarFile = new JarFile(outer)) {
555+
try (JarFile nestedZip64JarFile = outerJarFile
556+
.getNestedJarFile(outerJarFile.getJarEntry("nested-zip64.jar"))) {
557+
List<JarEntry> entries = Collections.list(nestedZip64JarFile.entries());
558+
assertThat(entries).hasSize(65537);
559+
for (int i = 0; i < entries.size(); i++) {
560+
JarEntry entry = entries.get(i);
561+
InputStream entryInput = nestedZip64JarFile.getInputStream(entry);
562+
String contents = StreamUtils.copyToString(entryInput, StandardCharsets.UTF_8);
563+
assertThat(contents).isEqualTo("Entry " + (i + 1));
564+
}
565+
}
566+
}
567+
}
568+
569+
private byte[] zip64Jar() throws IOException {
570+
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
571+
JarOutputStream jarOutput = new JarOutputStream(bytes);
572+
for (int i = 0; i < 65537; i++) {
573+
jarOutput.putNextEntry(new JarEntry(i + ".dat"));
574+
jarOutput.write(("Entry " + (i + 1)).getBytes(StandardCharsets.UTF_8));
575+
jarOutput.closeEntry();
576+
}
577+
jarOutput.close();
578+
return bytes.toByteArray();
579+
}
580+
515581
private int getJavaVersion() {
516582
try {
517583
Object runtimeVersion = Runtime.class.getMethod("version").invoke(null);

0 commit comments

Comments
 (0)