@@ -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.
416421fn 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