Skip to content

Commit 1b205bf

Browse files
fix(zip): use the classic 16-byte data descriptor for entries under 4 GiB
Every entry was written with a 24-byte ZIP64 data descriptor, but the local file header carries no ZIP64 extra field, which is what tells a reader to expect 8-byte descriptor sizes (APPNOTE 4.5.3). Streaming readers therefore parsed the descriptor as the classic 16-byte form and failed: Java's ZipInputStream rejected every DEFLATE entry with "invalid entry size (expected 0 but got N bytes)". Both sizes are known by the time the descriptor is written, so emit 4-byte sizes whenever they fit (the form every reader understands) and keep the ZIP64 form only for entries of 4 GiB or larger, matching the size-based heuristic extractors use. Verified that Java ZipInputStream now stream-reads DEFLATE entries, and that unzip, Python zipfile, 7-Zip and Java ZipFile keep accepting the output.
1 parent 2e2de76 commit 1b205bf

1 file changed

Lines changed: 50 additions & 12 deletions

File tree

src/archive.rs

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ impl Default for ZipArchive {
381381
/// Local file header: 30 bytes fixed + `name.len()`.
382382
///
383383
/// CRC-32 and sizes are all zero because GP bit 3 (data descriptor) is set.
384-
/// The real values appear in the ZIP64 data descriptor that follows the file
384+
/// The real values appear in the data descriptor that follows the file
385385
/// data, and in the central directory's ZIP64 extra field.
386386
#[expect(
387387
clippy::cast_possible_truncation,
@@ -410,9 +410,14 @@ fn encode_local_header(
410410
b.put_slice(name);
411411
}
412412

413-
/// ZIP64 data descriptor: 24 bytes.
413+
/// Data descriptor: 16 bytes, or 24 bytes for entries of 4 GiB or larger.
414414
///
415-
/// `sig(4) + crc32(4) + compressed_size(8) + uncompressed_size(8)`
415+
/// `sig(4) + crc32(4) + compressed_size(4|8) + uncompressed_size(4|8)`
416+
///
417+
/// The local header carries no ZIP64 extra field, so readers expect the
418+
/// classic 4-byte sizes (APPNOTE 4.5.3); extractors that do handle 8-byte
419+
/// descriptors detect them from the actual data size. Use 4-byte sizes
420+
/// whenever they fit and the ZIP64 form only when a size needs 8 bytes.
416421
fn encode_data_descriptor(
417422
crc32: u32,
418423
compressed_size: u64,
@@ -422,8 +427,16 @@ fn encode_data_descriptor(
422427
b.reserve(24);
423428
b.put_u32_le(SIG_DATA_DESC);
424429
b.put_u32_le(crc32);
425-
b.put_u64_le(compressed_size);
426-
b.put_u64_le(uncompressed_size);
430+
if let (Ok(compressed), Ok(uncompressed)) = (
431+
u32::try_from(compressed_size),
432+
u32::try_from(uncompressed_size),
433+
) {
434+
b.put_u32_le(compressed);
435+
b.put_u32_le(uncompressed);
436+
} else {
437+
b.put_u64_le(compressed_size);
438+
b.put_u64_le(uncompressed_size);
439+
}
427440
}
428441

429442
/// Central directory file header: 46 bytes fixed + `name.len()` + 28 bytes ZIP64 extra.
@@ -554,7 +567,7 @@ mod tests {
554567
// ── Layout constants ──────────────────────────────────────────────────────
555568
//
556569
// Local header = 30 + name_len (no extra field)
557-
// Data descr. = 24 (sig+crc+comp_size64+orig_size64)
570+
// Data descr. = 16 (sig+crc+comp_size+orig_size; 24 if ≥ 4 GiB)
558571
// CD entry = 46 + name_len + 28 (ZIP64 extra: tag+len+orig+comp+off)
559572
// ZIP64 EOCD = 56
560573
// ZIP64 locator = 20
@@ -638,11 +651,11 @@ mod tests {
638651
h.finalize()
639652
};
640653
assert_eq!(u32le(&zip, dd + 4), expected_crc, "crc32");
641-
assert_eq!(u64le(&zip, dd + 8), content.len() as u64, "comp size64");
642-
assert_eq!(u64le(&zip, dd + 16), content.len() as u64, "orig size64");
654+
assert_eq!(u32le(&zip, dd + 8), content.len() as u32, "comp size");
655+
assert_eq!(u32le(&zip, dd + 12), content.len() as u32, "orig size");
643656

644657
// ── Central directory ─────────────────────────────────────────────
645-
let cd = dd + 24;
658+
let cd = dd + 16;
646659
assert_eq!(u32le(&zip, cd), SIG_CENTRAL);
647660
assert_eq!(u16le(&zip, cd + 4), VERSION_MADE_BY);
648661
assert_eq!(u32le(&zip, cd + 20), 0xFFFF_FFFF, "comp size sentinel");
@@ -697,14 +710,39 @@ mod tests {
697710
});
698711

699712
let name_len = u16le(&zip, 26) as usize;
700-
// local header + 0 payload + 24-byte ZIP64 descriptor
701-
let cd = 30 + name_len + 24;
713+
// local header + 0 payload + 16-byte descriptor
714+
let cd = 30 + name_len + 16;
702715

703716
assert_eq!(u32le(&zip, cd), SIG_CENTRAL, "CD sig");
704717
let ext_attr = u32le(&zip, cd + 38);
705718
assert_eq!(ext_attr >> 16 & 0o170_000, 0o040_000, "S_IFDIR bit");
706719
}
707720

721+
#[test]
722+
fn data_descriptor_widths() {
723+
// Classic 16-byte form when both sizes fit in 32 bits.
724+
let mut b = BytesMut::new();
725+
encode_data_descriptor(0xDEAD_BEEF, 1234, 5678, &mut b);
726+
assert_eq!(b.len(), 16);
727+
assert_eq!(u32le(&b, 0), SIG_DATA_DESC);
728+
assert_eq!(u32le(&b, 4), 0xDEAD_BEEF);
729+
assert_eq!(u32le(&b, 8), 1234);
730+
assert_eq!(u32le(&b, 12), 5678);
731+
732+
// The largest sizes still representable in 4 bytes stay classic.
733+
let max32 = u64::from(u32::MAX);
734+
let mut b = BytesMut::new();
735+
encode_data_descriptor(1, max32, max32, &mut b);
736+
assert_eq!(b.len(), 16);
737+
738+
// ZIP64 24-byte form once either size needs 8 bytes.
739+
let mut b = BytesMut::new();
740+
encode_data_descriptor(1, max32 + 1, 42, &mut b);
741+
assert_eq!(b.len(), 24);
742+
assert_eq!(u64le(&b, 8), max32 + 1);
743+
assert_eq!(u64le(&b, 16), 42);
744+
}
745+
708746
#[test]
709747
fn ms_dos_date_time_validation() {
710748
// Extremes of every valid range are accepted.
@@ -768,7 +806,7 @@ mod tests {
768806

769807
// Compute expected start of second local header.
770808
let name_len_a = u16le(&zip, 26) as usize; // 5 for "a.txt"
771-
let local_b = (30 + name_len_a + 4 + 24) as u64;
809+
let local_b = (30 + name_len_a + 4 + 16) as u64;
772810
assert_eq!(u32le(&zip, local_b as usize), SIG_LOCAL, "second local sig");
773811

774812
// Navigate to ZIP64 EOCD via the locator embedded in the standard EOCD tail.

0 commit comments

Comments
 (0)