Skip to content

Commit 02ac089

Browse files
committed
Polish "Support zip64 jars"
See gh-16091
1 parent 1917e1e commit 02ac089

File tree

4 files changed

+115
-54
lines changed

4 files changed

+115
-54
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: 38 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
package org.springframework.boot.loader.jar;
1818

1919
import java.io.IOException;
20-
import java.util.Optional;
2120

2221
import org.springframework.boot.loader.data.RandomAccessData;
2322

@@ -45,7 +44,7 @@ class CentralDirectoryEndRecord {
4544

4645
private static final int READ_BLOCK_SIZE = 256;
4746

48-
private final Optional<Zip64End> zip64End;
47+
private final Zip64End zip64End;
4948

5049
private byte[] block;
5150

@@ -76,8 +75,7 @@ class CentralDirectoryEndRecord {
7675
this.offset = this.block.length - this.size;
7776
}
7877
int startOfCentralDirectoryEndRecord = (int) (data.getSize() - this.size);
79-
this.zip64End = Optional.ofNullable(
80-
isZip64() ? new Zip64End(data, startOfCentralDirectoryEndRecord) : null);
78+
this.zip64End = isZip64() ? new Zip64End(data, startOfCentralDirectoryEndRecord) : null;
8179
}
8280

8381
private byte[] createBlockFromEndOfData(RandomAccessData data, int size) throws IOException {
@@ -94,6 +92,10 @@ private boolean isValid() {
9492
return this.size == MINIMUM_SIZE + commentLength;
9593
}
9694

95+
private boolean isZip64() {
96+
return (int) Bytes.littleEndianValue(this.block, this.offset + 10, 2) == ZIP64_MAGICCOUNT;
97+
}
98+
9799
/**
98100
* Returns the location in the data that the archive actually starts. For most files
99101
* the archive data will start at 0, however, it is possible to have prefixed bytes
@@ -104,10 +106,9 @@ private boolean isValid() {
104106
long getStartOfArchive(RandomAccessData data) {
105107
long length = Bytes.littleEndianValue(this.block, this.offset + 12, 4);
106108
long specifiedOffset = Bytes.littleEndianValue(this.block, this.offset + 16, 4);
107-
long zip64EndSize = this.zip64End.map((x) -> x.getSize()).orElse(0L);
108-
int zip64LocSize = this.zip64End.map((x) -> Zip64Locator.ZIP64_LOCSIZE).orElse(0);
109-
long actualOffset = data.getSize() - this.size - length - zip64EndSize
110-
- zip64LocSize;
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;
111112
return actualOffset - specifiedOffset;
112113
}
113114

@@ -118,34 +119,30 @@ long getStartOfArchive(RandomAccessData data) {
118119
* @return the central directory data
119120
*/
120121
RandomAccessData getCentralDirectory(RandomAccessData data) {
121-
if (isZip64()) {
122-
return this.zip64End.get().getCentratDirectory(data);
123-
}
124-
else {
125-
long offset = Bytes.littleEndianValue(this.block, this.offset + 16, 4);
126-
long length = Bytes.littleEndianValue(this.block, this.offset + 12, 4);
127-
return data.getSubsection(offset, length);
122+
if (this.zip64End != null) {
123+
return this.zip64End.getCentralDirectory(data);
128124
}
125+
long offset = Bytes.littleEndianValue(this.block, this.offset + 16, 4);
126+
long length = Bytes.littleEndianValue(this.block, this.offset + 12, 4);
127+
return data.getSubsection(offset, length);
129128
}
130129

131130
/**
132131
* Return the number of ZIP entries in the file.
133132
* @return the number of records in the zip
134133
*/
135134
int getNumberOfRecords() {
136-
if (isZip64()) {
137-
return this.zip64End.get().getNumberOfRecords();
138-
}
139-
else {
140-
long numberOfRecords = Bytes.littleEndianValue(this.block, this.offset + 10,
141-
2);
142-
return (int) numberOfRecords;
135+
if (this.zip64End != null) {
136+
return this.zip64End.getNumberOfRecords();
143137
}
138+
long numberOfRecords = Bytes.littleEndianValue(this.block, this.offset + 10, 2);
139+
return (int) numberOfRecords;
144140
}
145141

146-
boolean isZip64() {
147-
return (int) Bytes.littleEndianValue(this.block, this.offset + 10,
148-
2) == ZIP64_MAGICCOUNT;
142+
String getComment() {
143+
int commentLength = (int) Bytes.littleEndianValue(this.block, this.offset + COMMENT_LENGTH_OFFSET, 2);
144+
AsciiBytes comment = new AsciiBytes(this.block, this.offset + COMMENT_LENGTH_OFFSET + 2, commentLength);
145+
return comment.toString();
149146
}
150147

151148
/**
@@ -154,11 +151,13 @@ boolean isZip64() {
154151
* @see <a href="https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT">Chapter
155152
* 4.3.14 of Zip64 specification</a>
156153
*/
157-
private static class Zip64End {
154+
private static final class Zip64End {
158155

159-
static final int ZIP64_ENDTOT = 32; // total number of entries
160-
static final int ZIP64_ENDSIZ = 40; // central directory size in bytes
161-
static final int ZIP64_ENDOFF = 48; // offset of first CEN header
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
162161

163162
private final Zip64Locator locator;
164163

@@ -168,12 +167,11 @@ private static class Zip64End {
168167

169168
private int numberOfRecords;
170169

171-
Zip64End(RandomAccessData data, int centratDirectoryEndOffset)
172-
throws IOException {
170+
private Zip64End(RandomAccessData data, int centratDirectoryEndOffset) throws IOException {
173171
this(data, new Zip64Locator(data, centratDirectoryEndOffset));
174172
}
175173

176-
Zip64End(RandomAccessData data, Zip64Locator locator) throws IOException {
174+
private Zip64End(RandomAccessData data, Zip64Locator locator) throws IOException {
177175
this.locator = locator;
178176
byte[] block = data.read(locator.getZip64EndOffset(), 56);
179177
this.centralDirectoryOffset = Bytes.littleEndianValue(block, ZIP64_ENDOFF, 8);
@@ -185,7 +183,7 @@ private static class Zip64End {
185183
* Return the size of this zip 64 end of central directory record.
186184
* @return size of this zip 64 end of central directory record
187185
*/
188-
public long getSize() {
186+
private long getSize() {
189187
return this.locator.getZip64EndSize();
190188
}
191189

@@ -195,16 +193,15 @@ public long getSize() {
195193
* @param data the source data
196194
* @return the central directory data
197195
*/
198-
public RandomAccessData getCentratDirectory(RandomAccessData data) {
199-
return data.getSubsection(this.centralDirectoryOffset,
200-
this.centralDirectoryLength);
196+
private RandomAccessData getCentralDirectory(RandomAccessData data) {
197+
return data.getSubsection(this.centralDirectoryOffset, this.centralDirectoryLength);
201198
}
202199

203200
/**
204201
* Return the number of entries in the zip64 archive.
205202
* @return the number of records in the zip
206203
*/
207-
public int getNumberOfRecords() {
204+
private int getNumberOfRecords() {
208205
return this.numberOfRecords;
209206
}
210207

@@ -216,7 +213,7 @@ public int getNumberOfRecords() {
216213
* @see <a href="https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT">Chapter
217214
* 4.3.15 of Zip64 specification</a>
218215
*/
219-
private static class Zip64Locator {
216+
private static final class Zip64Locator {
220217

221218
static final int ZIP64_LOCSIZE = 20; // locator size
222219
static final int ZIP64_LOCOFF = 8; // offset of zip64 end
@@ -225,8 +222,7 @@ private static class Zip64Locator {
225222

226223
private final int offset;
227224

228-
Zip64Locator(RandomAccessData data, int centralDirectoryEndOffset)
229-
throws IOException {
225+
private Zip64Locator(RandomAccessData data, int centralDirectoryEndOffset) throws IOException {
230226
this.offset = centralDirectoryEndOffset - ZIP64_LOCSIZE;
231227
byte[] block = data.read(this.offset, ZIP64_LOCSIZE);
232228
this.zip64EndOffset = Bytes.littleEndianValue(block, ZIP64_LOCOFF, 8);
@@ -236,24 +232,18 @@ private static class Zip64Locator {
236232
* Return the size of the zip 64 end record located by this zip64 end locator.
237233
* @return size of the zip 64 end record located by this zip64 end locator
238234
*/
239-
public long getZip64EndSize() {
235+
private long getZip64EndSize() {
240236
return this.offset - this.zip64EndOffset;
241237
}
242238

243239
/**
244240
* Return the offset to locate {@link Zip64End}.
245241
* @return offset of the Zip64 end of central directory record
246242
*/
247-
public long getZip64EndOffset() {
243+
private long getZip64EndOffset() {
248244
return this.zip64EndOffset;
249245
}
250246

251247
}
252248

253-
String getComment() {
254-
int commentLength = (int) Bytes.littleEndianValue(this.block, this.offset + COMMENT_LENGTH_OFFSET, 2);
255-
AsciiBytes comment = new AsciiBytes(this.block, this.offset + COMMENT_LENGTH_OFFSET + 2, commentLength);
256-
return comment.toString();
257-
}
258-
259249
}

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

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -143,14 +143,15 @@ void unpackedLocationsFromSameArchiveShareSameParent() throws Exception {
143143
}
144144

145145
@Test
146-
void filesInzip64ArchivesAreAllListed() throws IOException {
146+
void filesInZip64ArchivesAreAllListed() throws IOException {
147147
File file = new File(this.tempDir, "test.jar");
148148
FileCopyUtils.copy(writeZip64Jar(), file);
149-
JarFileArchive zip64Archive = new JarFileArchive(file);
150-
Iterator<Entry> it = zip64Archive.iterator();
151-
for (int i = 0; i < 65537; i++) {
152-
assertThat(it.hasNext()).as(i + "nth file is present").isTrue();
153-
it.next();
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+
}
154155
}
155156
}
156157

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)