Skip to content

Commit 1917e1e

Browse files
Camille Vienotwilkinsona
Camille Vienot
authored andcommitted
Support zip64 jars
See gh-16091
1 parent d5fc324 commit 1917e1e

File tree

2 files changed

+150
-17
lines changed

2 files changed

+150
-17
lines changed

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

Lines changed: 135 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.boot.loader.jar;
1818

1919
import java.io.IOException;
20+
import java.util.Optional;
2021

2122
import org.springframework.boot.loader.data.RandomAccessData;
2223

@@ -25,6 +26,7 @@
2526
*
2627
* @author Phillip Webb
2728
* @author Andy Wilkinson
29+
* @author Camille Vienot
2830
* @see <a href="https://en.wikipedia.org/wiki/Zip_%28file_format%29">Zip File Format</a>
2931
*/
3032
class CentralDirectoryEndRecord {
@@ -33,6 +35,8 @@ class CentralDirectoryEndRecord {
3335

3436
private static final int MAXIMUM_COMMENT_LENGTH = 0xFFFF;
3537

38+
private static final int ZIP64_MAGICCOUNT = 0xFFFF;
39+
3640
private static final int MAXIMUM_SIZE = MINIMUM_SIZE + MAXIMUM_COMMENT_LENGTH;
3741

3842
private static final int SIGNATURE = 0x06054b50;
@@ -41,6 +45,8 @@ class CentralDirectoryEndRecord {
4145

4246
private static final int READ_BLOCK_SIZE = 256;
4347

48+
private final Optional<Zip64End> zip64End;
49+
4450
private byte[] block;
4551

4652
private int offset;
@@ -69,6 +75,9 @@ class CentralDirectoryEndRecord {
6975
}
7076
this.offset = this.block.length - this.size;
7177
}
78+
int startOfCentralDirectoryEndRecord = (int) (data.getSize() - this.size);
79+
this.zip64End = Optional.ofNullable(
80+
isZip64() ? new Zip64End(data, startOfCentralDirectoryEndRecord) : null);
7281
}
7382

7483
private byte[] createBlockFromEndOfData(RandomAccessData data, int size) throws IOException {
@@ -95,7 +104,10 @@ private boolean isValid() {
95104
long getStartOfArchive(RandomAccessData data) {
96105
long length = Bytes.littleEndianValue(this.block, this.offset + 12, 4);
97106
long specifiedOffset = Bytes.littleEndianValue(this.block, this.offset + 16, 4);
98-
long actualOffset = data.getSize() - this.size - length;
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;
99111
return actualOffset - specifiedOffset;
100112
}
101113

@@ -106,21 +118,136 @@ long getStartOfArchive(RandomAccessData data) {
106118
* @return the central directory data
107119
*/
108120
RandomAccessData getCentralDirectory(RandomAccessData data) {
109-
long offset = Bytes.littleEndianValue(this.block, this.offset + 16, 4);
110-
long length = Bytes.littleEndianValue(this.block, this.offset + 12, 4);
111-
return data.getSubsection(offset, length);
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);
128+
}
112129
}
113130

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

126253
String getComment() {

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

Lines changed: 15 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,15 @@ 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+
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();
154+
}
150155
}
151156

152157
@Test
@@ -166,11 +171,12 @@ void nestedZip64ArchivesAreHandledGracefully() throws IOException {
166171
output.closeEntry();
167172
output.close();
168173
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();
174+
Archive nestedArchive = jarFileArchive.getNestedArchive(getEntriesMap(jarFileArchive).get("nested/zip64.jar"));
175+
Iterator<Entry> it = nestedArchive.iterator();
176+
for (int i = 0; i < 65537; i++) {
177+
assertThat(it.hasNext()).as(i + "nth file is present").isTrue();
178+
it.next();
179+
}
174180
}
175181

176182
private byte[] writeZip64Jar() throws IOException {

0 commit comments

Comments
 (0)