Skip to content

Commit 942854b

Browse files
committed
Zip64 support
1 parent e1c1a4e commit 942854b

15 files changed

Lines changed: 821 additions & 83 deletions

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<groupId>software.coley</groupId>
88
<artifactId>lljzip</artifactId>
9-
<version>2.8.2</version>
9+
<version>2.9.0</version>
1010

1111
<name>LL Java ZIP</name>
1212
<description>Lower level ZIP support for Java</description>

src/main/java/software/coley/lljzip/format/ZipPatterns.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,22 @@ public interface ZipPatterns {
4343
* Header for {@link EndOfCentralDirectory}, as a {@code s4/quad/int}
4444
*/
4545
int END_OF_CENTRAL_DIRECTORY_QUAD = 0x06_05_4B_50;
46+
/**
47+
* Header for the ZIP64 end of central directory record.
48+
*/
49+
int[] ZIP64_END_OF_CENTRAL_DIRECTORY = {0x50, 0x4B, 0x06, 0x06};
50+
/**
51+
* Header for the ZIP64 end of central directory record, as a {@code s4/quad/int}
52+
*/
53+
int ZIP64_END_OF_CENTRAL_DIRECTORY_QUAD = 0x06_06_4B_50;
54+
/**
55+
* Header for the ZIP64 end of central directory locator.
56+
*/
57+
int[] ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR = {0x50, 0x4B, 0x06, 0x07};
58+
/**
59+
* Header for the ZIP64 end of central directory locator, as a {@code s4/quad/int}
60+
*/
61+
int ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_QUAD = 0x07_06_4B_50;
4662
/**
4763
* Optional header for the data descriptor section
4864
*/

src/main/java/software/coley/lljzip/format/model/AbstractZipFileHeader.java

Lines changed: 120 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
* @author Matt Coley
1818
*/
1919
public abstract class AbstractZipFileHeader implements ZipPart, ZipRead {
20+
// Extra field ID's can be found at: APPNOTE 4.5.2
21+
protected static final int EXTRA_FID_ZIP64 = 0x0001;
22+
2023
// Zip spec elements, common between central/local file headers
2124
protected int versionNeededToExtract;
2225
protected int generalPurposeBitFlag;
@@ -36,7 +39,8 @@ public abstract class AbstractZipFileHeader implements ZipPart, ZipRead {
3639

3740
// Data source that contents were read from.
3841
protected transient MemorySegment data;
39-
42+
protected transient boolean zip64CompressedSize;
43+
protected transient boolean zip64UncompressedSize;
4044

4145
/**
4246
* @return The associated backing data that this file header was read from.
@@ -74,6 +78,8 @@ public void setVersionNeededToExtract(int versionNeededToExtract) {
7478

7579
/**
7680
* @return Used primarily to expand on details of file compression.
81+
*
82+
* @apiNote APPNOTE 4.4.4 for flag bit purposes.
7783
*/
7884
public int getGeneralPurposeBitFlag() {
7985
return generalPurposeBitFlag;
@@ -169,7 +175,7 @@ public long getCompressedSize() {
169175
* Compressed size of {@link LocalFileHeader#getFileData()}.
170176
*/
171177
public void setCompressedSize(long compressedSize) {
172-
this.compressedSize = compressedSize & 0xFFFFFFFFL;
178+
this.compressedSize = compressedSize;
173179
}
174180

175181
/**
@@ -188,7 +194,7 @@ public long getUncompressedSize() {
188194
* Uncompressed size after {@link LocalFileHeader#decompress(Decompressor)} is used on {@link LocalFileHeader#getFileData()}.
189195
*/
190196
public void setUncompressedSize(long uncompressedSize) {
191-
this.uncompressedSize = uncompressedSize & 0xFFFFFFFFL;
197+
this.uncompressedSize = uncompressedSize;
192198
}
193199

194200
/**
@@ -266,6 +272,117 @@ public String getExtraFieldAsString() {
266272
return MemorySegmentUtil.toString(extraField.get());
267273
}
268274

275+
/**
276+
* @return {@code true} when the compressed-size value was hydrated from the ZIP64 extended information extra field.
277+
*/
278+
protected boolean hasZip64CompressedSize() {
279+
return zip64CompressedSize;
280+
}
281+
282+
/**
283+
* @return {@code true} when the uncompressed-size value was hydrated from the ZIP64 extended information extra field.
284+
*/
285+
protected boolean hasZip64UncompressedSize() {
286+
return zip64UncompressedSize;
287+
}
288+
289+
/**
290+
* Reads ZIP64 extended information from the extra field, if present.
291+
*
292+
* @param readRelativeOffset
293+
* {@code true} to read the relative header offset, {@code false} to skip it.
294+
* @param readDiskStart
295+
* {@code true} to read the disk start number, {@code false} to skip it.
296+
*
297+
* @return ZIP64 extended information, or an empty instance if not present or invalid.
298+
*/
299+
@Nonnull
300+
protected final Zip64ExtendedInfo readZip64ExtendedInfo(boolean readRelativeOffset, boolean readDiskStart) {
301+
zip64CompressedSize = false;
302+
zip64UncompressedSize = false;
303+
if (extraFieldLength <= 0)
304+
return Zip64ExtendedInfo.EMPTY;
305+
306+
MemorySegment extra = extraField.get();
307+
int off = 0;
308+
int len = (int) extra.byteSize();
309+
while (off + 4 <= len) {
310+
// APPNOTE 4.5.3 defines the Zip64 extended information extra field as follows:
311+
// 2: Header ID (0x0001)
312+
// 2: Size of this block
313+
// 8: Size of uncompressed/original data
314+
// 8: Size of compressed data
315+
// 8: Relative header offset (from the start of the first disk)
316+
// 4: Disk start number
317+
int tag = MemorySegmentUtil.readWord(extra, off);
318+
int size = MemorySegmentUtil.readWord(extra, off + 2);
319+
off += 4;
320+
if (off + size > len)
321+
return Zip64ExtendedInfo.EMPTY;
322+
if (tag == EXTRA_FID_ZIP64) {
323+
long pos = off;
324+
long limit = off + size;
325+
long resolvedUncompressedSize = 0L;
326+
long resolvedCompressedSize = 0L;
327+
long resolvedRelativeOffset = 0L;
328+
long resolvedDiskStart = 0L;
329+
boolean hasResolvedUncompressedSize = false;
330+
boolean hasResolvedCompressedSize = false;
331+
boolean hasResolvedRelativeOffset = false;
332+
boolean hasResolvedDiskStart = false;
333+
334+
if (uncompressedSize == 0xFFFFFFFFL) {
335+
if (pos + 8L > limit)
336+
return Zip64ExtendedInfo.EMPTY;
337+
resolvedUncompressedSize = MemorySegmentUtil.readLong(extra, pos);
338+
pos += 8L;
339+
hasResolvedUncompressedSize = true;
340+
}
341+
if (compressedSize == 0xFFFFFFFFL) {
342+
if (pos + 8L > limit)
343+
return Zip64ExtendedInfo.EMPTY;
344+
resolvedCompressedSize = MemorySegmentUtil.readLong(extra, pos);
345+
pos += 8L;
346+
hasResolvedCompressedSize = true;
347+
}
348+
if (readRelativeOffset) {
349+
if (pos + 8L > limit)
350+
return Zip64ExtendedInfo.EMPTY;
351+
resolvedRelativeOffset = MemorySegmentUtil.readLong(extra, pos);
352+
pos += 8L;
353+
hasResolvedRelativeOffset = true;
354+
}
355+
if (readDiskStart) {
356+
if (pos + 4L > limit)
357+
return Zip64ExtendedInfo.EMPTY;
358+
resolvedDiskStart = MemorySegmentUtil.readMaskedLongQuad(extra, pos, 0);
359+
hasResolvedDiskStart = true;
360+
}
361+
362+
if (hasResolvedUncompressedSize)
363+
zip64UncompressedSize = true;
364+
if (hasResolvedCompressedSize)
365+
zip64CompressedSize = true;
366+
367+
return new Zip64ExtendedInfo(
368+
hasResolvedUncompressedSize, resolvedUncompressedSize,
369+
hasResolvedCompressedSize, resolvedCompressedSize,
370+
hasResolvedRelativeOffset, resolvedRelativeOffset,
371+
hasResolvedDiskStart, resolvedDiskStart
372+
);
373+
}
374+
off += size;
375+
}
376+
return Zip64ExtendedInfo.EMPTY;
377+
}
378+
379+
protected record Zip64ExtendedInfo(boolean hasUncompressedSize, long uncompressedSize,
380+
boolean hasCompressedSize, long compressedSize,
381+
boolean hasRelativeOffset, long relativeOffset,
382+
boolean hasDiskStart, long diskStart) {
383+
protected static final Zip64ExtendedInfo EMPTY = new Zip64ExtendedInfo(false, 0L, false, 0L, false, 0L, false, 0L);
384+
}
385+
269386
@Override
270387
public boolean equals(Object o) {
271388
if (this == o) return true;

src/main/java/software/coley/lljzip/format/model/CentralDirectoryFileHeader.java

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ public class CentralDirectoryFileHeader extends AbstractZipFileHeader {
4848
private int internalFileAttributes;
4949
private int externalFileAttributes;
5050
private long relativeOffsetOfLocalHeader;
51+
private transient boolean zip64RelativeOffsetOfLocalHeader;
52+
private transient long zip64DiskNumberStart = -1L;
5153

5254
/**
5355
* @return Copy.
@@ -119,6 +121,28 @@ public void read(@Nonnull MemorySegment data, long offset) throws ZipParseExcept
119121
} catch (Throwable t) {
120122
throw new ZipParseException(t, ZipParseException.Type.OTHER);
121123
}
124+
try {
125+
// Optional zip64 extended information extra field.
126+
// We return an 'empty' model when the field is not present, so we can check for the presence of values.
127+
Zip64ExtendedInfo zip64 = readZip64ExtendedInfo(relativeOffsetOfLocalHeader == 0xFFFFFFFFL, diskNumberStart == 0xFFFF);
128+
zip64RelativeOffsetOfLocalHeader = zip64.hasRelativeOffset();
129+
zip64DiskNumberStart = zip64.hasDiskStart() ? zip64.diskStart() : -1L;
130+
if (zip64.hasCompressedSize())
131+
compressedSize = zip64.compressedSize();
132+
if (zip64.hasUncompressedSize())
133+
uncompressedSize = zip64.uncompressedSize();
134+
if (zip64.hasRelativeOffset())
135+
relativeOffsetOfLocalHeader = zip64.relativeOffset();
136+
if (zip64.hasDiskStart()) {
137+
if (zip64.diskStart() > Integer.MAX_VALUE)
138+
throw new IndexOutOfBoundsException("ZIP64 disk start exceeds supported range");
139+
diskNumberStart = (int) zip64.diskStart();
140+
}
141+
} catch (IndexOutOfBoundsException ex) {
142+
throw new ZipParseException(ex, ZipParseException.Type.IOOBE_FILE_EXTRA);
143+
} catch (Throwable t) {
144+
throw new ZipParseException(t, ZipParseException.Type.OTHER);
145+
}
122146
try {
123147
fileComment = StringData.of(data, offset + 46 + fileNameLength + extraFieldLength, fileCommentLength);
124148
} catch (IndexOutOfBoundsException ex) {
@@ -275,7 +299,21 @@ public long getRelativeOffsetOfLocalHeader() {
275299
* This should also be where the {@link LocalFileHeader} is located. Or {@code 0xFFFFFFFF} for ZIP64.
276300
*/
277301
public void setRelativeOffsetOfLocalHeader(long relativeOffsetOfLocalHeader) {
278-
this.relativeOffsetOfLocalHeader = relativeOffsetOfLocalHeader & 0xFFFFFFFFL;
302+
this.relativeOffsetOfLocalHeader = relativeOffsetOfLocalHeader;
303+
}
304+
305+
/**
306+
* @return {@code true} when the local-header offset was hydrated from the ZIP64 extended information extra field.
307+
*/
308+
public boolean hasZip64RelativeOffsetOfLocalHeader() {
309+
return zip64RelativeOffsetOfLocalHeader;
310+
}
311+
312+
/**
313+
* @return ZIP64 disk-start value when present, otherwise {@code -1}.
314+
*/
315+
public long getZip64DiskNumberStart() {
316+
return zip64DiskNumberStart;
279317
}
280318

281319
/**

src/main/java/software/coley/lljzip/format/model/EndOfCentralDirectory.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ public long getCentralDirectorySize() {
160160
* Size of central directory in bytes, or {@code 0xFFFFFFFF} for ZIP64.
161161
*/
162162
public void setCentralDirectorySize(long centralDirectorySize) {
163-
this.centralDirectorySize = centralDirectorySize & 0xFFFFFFFFL;
163+
this.centralDirectorySize = centralDirectorySize;
164164
}
165165

166166
/**
@@ -177,7 +177,7 @@ public long getCentralDirectoryOffset() {
177177
* {@link #getCentralDirectoryStartDisk() starting disk number}, or {@code 0xFFFFFFFF} for ZIP64.
178178
*/
179179
public void setCentralDirectoryOffset(long centralDirectoryOffset) {
180-
this.centralDirectoryOffset = centralDirectoryOffset & 0xFFFFFFFFL;
180+
this.centralDirectoryOffset = centralDirectoryOffset;
181181
}
182182

183183
/**

src/main/java/software/coley/lljzip/format/model/JvmLocalFileHeader.java

Lines changed: 44 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -39,35 +39,16 @@ protected MemorySegmentData readFileData(@Nonnull MemorySegment data, long heade
3939
long relativeDataOffsetEnd;
4040
Long absoluteDataOffsetEnd = offsets.ceiling(offset + relativeDataOffsetStart);
4141

42-
// While we do scan to the next header, we need to consider the optional data-descriptor component.
43-
// If it is present, the end range of the data should be cut by 16 bytes (the size of the data-descriptor)
42+
// When the local header exposes a trustworthy size (including ZIP64 extra values), use it
43+
// directly and leave any trailing data descriptor outside the file-data slice.
4444
//
45-
// Per section 4.3.9 of the zip file spec:
46-
// This descriptor MUST exist if bit 3 of the general purpose bit flag is set.
47-
// It is byte aligned and immediately follows the last byte of compressed data.
48-
// This descriptor SHOULD be used only when it was not possible to
49-
// seek in the output ZIP file (output can be std-out, or non-seekable device)
50-
//
51-
// Thus, we subtract the length of the data descriptor section from the data end offset.
45+
// For descriptor info see APPNOTE 4.3.9
5246
if (absoluteDataOffsetEnd != null && (getGeneralPurposeBitFlag() & 0b1000) == 0b1000) {
53-
// Format:
54-
// data-descriptor-header 4 bytes 08 07 4b 50
55-
// crc-32 4 bytes
56-
// compressed size 4 bytes
57-
// uncompressed size 4 bytes
58-
//
59-
// The JVM technically allows the header to be excluded, so we split the offset fixing
60-
// into two parts.
61-
//
62-
// In some WEIRD cases the bit flag can be set, but the data-descriptor will be missing.
63-
// When this occurs we can validate the range is currently correct by checking if the data end offset
64-
// is the beginning of another file header. If we find the file header, the bit flag is a lie,
65-
// and we do not need to manipulate our data end offset.
66-
if ((MemorySegmentUtil.readWord(data, absoluteDataOffsetEnd) & ZipPatterns.PK_WORD) != ZipPatterns.PK_WORD) {
67-
absoluteDataOffsetEnd -= 12;
68-
if (MemorySegmentUtil.readQuad(data, absoluteDataOffsetEnd) == ZipPatterns.DATA_DESCRIPTOR_QUAD) {
69-
absoluteDataOffsetEnd -= 4;
70-
}
47+
long explicitFileDataLength = getExplicitFileDataLength();
48+
if (explicitFileDataLength >= 0L) {
49+
long candidateDataEnd = offset + relativeDataOffsetStart + explicitFileDataLength;
50+
if (candidateDataEnd <= absoluteDataOffsetEnd)
51+
absoluteDataOffsetEnd = candidateDataEnd;
7152
}
7253
}
7354
relativeDataOffsetEnd = absoluteDataOffsetEnd == null ? relativeDataOffsetStart : absoluteDataOffsetEnd - offset;
@@ -93,6 +74,11 @@ protected MemorySegmentData readFileData(@Nonnull MemorySegment data, long heade
9374
return fileData;
9475
}
9576

77+
private long getExplicitFileDataLength() {
78+
long length = getCompressionMethod() == ZipCompressions.STORED ? getUncompressedSize() : getCompressedSize();
79+
return length > 0L ? length : -1L;
80+
}
81+
9682
@Override
9783
public void adoptLinkedCentralDirectoryValues() {
9884
CentralDirectoryFileHeader directoryFileHeader = linkedDirectoryFileHeader;
@@ -117,33 +103,38 @@ public void adoptLinkedCentralDirectoryValues() {
117103
// compressedSize = directoryFileHeader.compressedSize;
118104
// uncompressedSize = directoryFileHeader.uncompressedSize;
119105
//
120-
// That being said, we want a fallback if no data is found.
121-
// This may occur if something with offset detection fails.
122-
//
123-
// Update file data with new compressed/uncompressed size if it was not able to be found previously
124-
// with only the local data available.
125-
if (!foundData) {
126-
// Extract updated length from CEN values
127-
long fileDataLength;
128-
if (getCompressionMethod() == ZipCompressions.STORED) {
129-
fileDataLength = getUncompressedSize() & 0xFFFFFFFFL;
130-
} else {
131-
fileDataLength = getCompressedSize() & 0xFFFFFFFFL;
132-
}
106+
// Update file data with authoritative CEN sizes when the current range is missing or includes a descriptor.
107+
long fileDataLength;
108+
if (getCompressionMethod() == ZipCompressions.STORED) {
109+
fileDataLength = getUncompressedSize();
110+
} else {
111+
fileDataLength = getCompressedSize();
112+
}
133113

134-
// Compute absolute end offset for data.
135-
long absoluteDataOffsetStart = offset + relativeDataOffsetStart;
136-
long availableDataEnd = data.byteSize();
137-
if (relativeDataOffsetEnd > relativeDataOffsetStart)
138-
availableDataEnd = Math.min(availableDataEnd, offset + relativeDataOffsetEnd);
139-
long absoluteDataOffsetEnd = absoluteDataOffsetStart + fileDataLength;
140-
141-
// Only allow lengths that are within a sensible range.
142-
// Data should not be overflowing into adjacent header entries or beyond the file bounds.
143-
// - If it is, the data here is likely intentionally tampered with to screw with parsers
144-
if (fileDataLength >= 0 && absoluteDataOffsetStart >= 0 && absoluteDataOffsetEnd <= availableDataEnd) {
145-
fileData = MemorySegmentData.of(data, absoluteDataOffsetStart, fileDataLength);
146-
}
114+
// Get the absolute data offsets and bounds for the file data.
115+
// Ensure the data range is within the file.
116+
long absoluteDataOffsetStart = offset + relativeDataOffsetStart;
117+
long availableDataEnd = data.byteSize();
118+
if (relativeDataOffsetEnd > relativeDataOffsetStart)
119+
availableDataEnd = Math.min(availableDataEnd, offset + relativeDataOffsetEnd);
120+
long absoluteDataOffsetEnd = absoluteDataOffsetStart + fileDataLength;
121+
122+
// If we see any of the following conditions:
123+
// - We didn't find any data
124+
// - The general purpose bit flag indicates a data descriptor is present
125+
// - The file data length doesn't match the expected length
126+
// Then we should realign the file data range to match the authoritative CEN sizes,
127+
// as what we have is invalid.
128+
boolean shouldRealignDataRange = !foundData ||
129+
(getGeneralPurposeBitFlag() & 0b1000) == 0b1000 ||
130+
fileData.length() != fileDataLength;
131+
if (shouldRealignDataRange
132+
&& fileDataLength >= 0L
133+
&& absoluteDataOffsetStart >= 0L
134+
&& absoluteDataOffsetEnd <= availableDataEnd) {
135+
fileData = MemorySegmentData.of(data, absoluteDataOffsetStart, fileDataLength);
136+
relativeDataOffsetEnd = relativeDataOffsetStart + fileDataLength;
137+
foundData = fileDataLength != 0L;
147138
}
148139
}
149140
}

0 commit comments

Comments
 (0)