Skip to main content

s_zip/
writer.rs

1//! Streaming ZIP writer that compresses data on-the-fly without temp files
2//!
3//! This eliminates:
4//! - Temp file disk I/O
5//! - File read buffers
6//! - Intermediate storage
7//!
8//! Expected RAM savings: 5-8 MB per file
9//!
10//! Now supports arbitrary writers (File, Vec<u8>, network streams, etc.)
11
12use crate::error::{Result, SZipError};
13use crc32fast::Hasher as Crc32;
14use flate2::write::DeflateEncoder;
15use flate2::Compression;
16use std::fs::File;
17use std::io::{Seek, Write};
18use std::path::Path;
19
20#[cfg(feature = "encryption")]
21use crate::encryption::{AesEncryptor, AesStrength};
22
23/// Compression method to use for ZIP entries
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum CompressionMethod {
26    /// No compression (stored)
27    Stored,
28    /// DEFLATE compression (most common)
29    Deflate,
30    /// Zstd compression (requires zstd-support feature)
31    #[cfg(feature = "zstd-support")]
32    Zstd,
33}
34
35impl CompressionMethod {
36    pub(crate) fn to_zip_method(self) -> u16 {
37        match self {
38            CompressionMethod::Stored => 0,
39            CompressionMethod::Deflate => 8,
40            #[cfg(feature = "zstd-support")]
41            CompressionMethod::Zstd => 93,
42        }
43    }
44}
45
46/// Entry being written to ZIP
47struct ZipEntry {
48    name: String,
49    local_header_offset: u64,
50    crc32: u32,
51    compressed_size: u64,
52    uncompressed_size: u64,
53    compression_method: u16,
54    #[cfg(feature = "encryption")]
55    #[allow(dead_code)] // Will be used for central directory in future versions
56    encryption_strength: Option<u16>,
57}
58
59/// Streaming ZIP writer that compresses data on-the-fly
60pub struct StreamingZipWriter<W: Write + Seek> {
61    output: W,
62    entries: Vec<ZipEntry>,
63    current_entry: Option<CurrentEntry>,
64    compression_level: u32,
65    compression_method: CompressionMethod,
66    #[cfg(feature = "encryption")]
67    password: Option<String>,
68    #[cfg(feature = "encryption")]
69    encryption_strength: AesStrength,
70}
71
72struct CurrentEntry {
73    name: String,
74    local_header_offset: u64,
75    encoder: Box<dyn CompressorWrite>,
76    counter: CrcCounter,
77    compression_method: u16,
78    #[cfg(feature = "encryption")]
79    encryptor: Option<AesEncryptor>,
80}
81
82trait CompressorWrite: Write {
83    fn finish_compression(self: Box<Self>) -> Result<CompressedBuffer>;
84    fn get_buffer_mut(&mut self) -> &mut CompressedBuffer;
85}
86
87struct DeflateCompressor {
88    encoder: DeflateEncoder<CompressedBuffer>,
89}
90
91impl Write for DeflateCompressor {
92    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
93        self.encoder.write(buf)
94    }
95
96    fn flush(&mut self) -> std::io::Result<()> {
97        self.encoder.flush()
98    }
99}
100
101impl CompressorWrite for DeflateCompressor {
102    fn finish_compression(self: Box<Self>) -> Result<CompressedBuffer> {
103        Ok(self.encoder.finish()?)
104    }
105
106    fn get_buffer_mut(&mut self) -> &mut CompressedBuffer {
107        self.encoder.get_mut()
108    }
109}
110
111#[cfg(feature = "zstd-support")]
112struct ZstdCompressor {
113    encoder: zstd::Encoder<'static, CompressedBuffer>,
114}
115
116#[cfg(feature = "zstd-support")]
117impl Write for ZstdCompressor {
118    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
119        self.encoder.write(buf)
120    }
121
122    fn flush(&mut self) -> std::io::Result<()> {
123        self.encoder.flush()
124    }
125}
126
127#[cfg(feature = "zstd-support")]
128impl CompressorWrite for ZstdCompressor {
129    fn finish_compression(self: Box<Self>) -> Result<CompressedBuffer> {
130        Ok(self.encoder.finish()?)
131    }
132
133    fn get_buffer_mut(&mut self) -> &mut CompressedBuffer {
134        self.encoder.get_mut()
135    }
136}
137
138/// Metadata tracker for CRC and byte counts
139struct CrcCounter {
140    crc: Crc32,
141    uncompressed_count: u64,
142    compressed_count: u64,
143}
144
145impl CrcCounter {
146    fn new() -> Self {
147        Self {
148            crc: Crc32::new(),
149            uncompressed_count: 0,
150            compressed_count: 0,
151        }
152    }
153
154    fn update_uncompressed(&mut self, data: &[u8]) {
155        self.crc.update(data);
156        self.uncompressed_count += data.len() as u64;
157    }
158
159    fn add_compressed(&mut self, count: u64) {
160        self.compressed_count += count;
161    }
162
163    fn finalize(&self) -> u32 {
164        self.crc.clone().finalize()
165    }
166}
167
168/// Buffered writer for compressed data with adaptive sizing
169///
170/// Automatically adjusts buffer capacity and flush threshold based on data size hints
171/// to optimize memory usage and performance for different file sizes.
172struct CompressedBuffer {
173    buffer: Vec<u8>,
174    flush_threshold: usize,
175}
176
177impl CompressedBuffer {
178    /// Create buffer with default capacity (for backward compatibility)
179    #[allow(dead_code)]
180    fn new() -> Self {
181        Self::with_size_hint(None)
182    }
183
184    /// Create buffer with adaptive sizing based on expected data size
185    ///
186    /// Optimizes initial capacity and flush threshold:
187    /// - Tiny files (<10KB): 8KB initial, 256KB threshold
188    /// - Small files (<100KB): 32KB initial, 512KB threshold  
189    /// - Medium files (<1MB): 128KB initial, 2MB threshold
190    /// - Large files (≥1MB): 256KB initial, 4MB threshold
191    fn with_size_hint(size_hint: Option<u64>) -> Self {
192        let (initial_capacity, flush_threshold) = match size_hint {
193            Some(size) if size < 10_000 => (8 * 1024, 256 * 1024), // Tiny: 8KB, 256KB
194            Some(size) if size < 100_000 => (32 * 1024, 512 * 1024), // Small: 32KB, 512KB
195            Some(size) if size < 1_000_000 => (128 * 1024, 2 * 1024 * 1024), // Medium: 128KB, 2MB
196            Some(size) if size < 10_000_000 => (256 * 1024, 4 * 1024 * 1024), // Large: 256KB, 4MB
197            _ => (512 * 1024, 8 * 1024 * 1024),                    // Very large: 512KB, 8MB
198        };
199
200        Self {
201            buffer: Vec::with_capacity(initial_capacity),
202            flush_threshold,
203        }
204    }
205
206    fn take(&mut self) -> Vec<u8> {
207        std::mem::take(&mut self.buffer)
208    }
209
210    fn should_flush(&self) -> bool {
211        self.buffer.len() >= self.flush_threshold
212    }
213}
214
215impl Write for CompressedBuffer {
216    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
217        self.buffer.extend_from_slice(buf);
218        Ok(buf.len())
219    }
220
221    fn flush(&mut self) -> std::io::Result<()> {
222        Ok(())
223    }
224}
225
226impl StreamingZipWriter<File> {
227    /// Create a new ZIP writer with default compression level (6) using DEFLATE
228    pub fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
229        Self::with_compression(path, 6)
230    }
231
232    /// Create a new ZIP writer with custom compression level (0-9) using DEFLATE
233    pub fn with_compression<P: AsRef<Path>>(path: P, compression_level: u32) -> Result<Self> {
234        Self::with_method(path, CompressionMethod::Deflate, compression_level)
235    }
236
237    /// Create a new ZIP writer with specified compression method and level
238    ///
239    /// # Arguments
240    /// * `path` - Path to the output ZIP file
241    /// * `method` - Compression method to use (Deflate, Zstd, or Stored)
242    /// * `compression_level` - Compression level (0-9 for DEFLATE, 1-21 for Zstd)
243    pub fn with_method<P: AsRef<Path>>(
244        path: P,
245        method: CompressionMethod,
246        compression_level: u32,
247    ) -> Result<Self> {
248        let output = File::create(path)?;
249        Ok(Self {
250            output,
251            entries: Vec::new(),
252            current_entry: None,
253            compression_level,
254            compression_method: method,
255            #[cfg(feature = "encryption")]
256            password: None,
257            #[cfg(feature = "encryption")]
258            encryption_strength: AesStrength::Aes256,
259        })
260    }
261
262    /// Create a new ZIP writer with Zstd compression (requires zstd-support feature)
263    #[cfg(feature = "zstd-support")]
264    pub fn with_zstd<P: AsRef<Path>>(path: P, compression_level: i32) -> Result<Self> {
265        let output = File::create(path)?;
266        Ok(Self {
267            output,
268            entries: Vec::new(),
269            current_entry: None,
270            compression_level: compression_level as u32,
271            compression_method: CompressionMethod::Zstd,
272            #[cfg(feature = "encryption")]
273            password: None,
274            #[cfg(feature = "encryption")]
275            encryption_strength: AesStrength::Aes256,
276        })
277    }
278}
279
280impl<W: Write + Seek> StreamingZipWriter<W> {
281    /// Create a new ZIP writer from an arbitrary writer with default compression level (6) using DEFLATE
282    pub fn from_writer(writer: W) -> Result<Self> {
283        Self::from_writer_with_compression(writer, 6)
284    }
285
286    /// Create a new ZIP writer from an arbitrary writer with custom compression level
287    pub fn from_writer_with_compression(writer: W, compression_level: u32) -> Result<Self> {
288        Self::from_writer_with_method(writer, CompressionMethod::Deflate, compression_level)
289    }
290
291    /// Create a new ZIP writer from an arbitrary writer with specified compression method and level
292    ///
293    /// # Arguments
294    /// * `writer` - Any writer implementing Write + Seek
295    /// * `method` - Compression method to use (Deflate, Zstd, or Stored)
296    /// * `compression_level` - Compression level (0-9 for DEFLATE, 1-21 for Zstd)
297    pub fn from_writer_with_method(
298        writer: W,
299        method: CompressionMethod,
300        compression_level: u32,
301    ) -> Result<Self> {
302        Ok(Self {
303            output: writer,
304            entries: Vec::new(),
305            current_entry: None,
306            compression_level,
307            compression_method: method,
308            #[cfg(feature = "encryption")]
309            password: None,
310            #[cfg(feature = "encryption")]
311            encryption_strength: AesStrength::Aes256,
312        })
313    }
314
315    /// Set password for AES encryption (requires encryption feature)
316    ///
317    /// All subsequent entries will be encrypted with AES-256 using the provided password.
318    /// Call this method before `start_entry()` to encrypt files.
319    ///
320    /// # Arguments
321    /// * `password` - Password for encryption (minimum 8 characters recommended)
322    ///
323    /// # Example
324    /// ```no_run
325    /// use s_zip::StreamingZipWriter;
326    ///
327    /// let mut writer = StreamingZipWriter::new("encrypted.zip")?;
328    /// writer.set_password("my_secure_password");
329    ///
330    /// writer.start_entry("secret.txt")?;
331    /// writer.write_data(b"Confidential data")?;
332    /// writer.finish()?;
333    /// # Ok::<(), s_zip::SZipError>(())
334    /// ```
335    #[cfg(feature = "encryption")]
336    pub fn set_password(&mut self, password: impl Into<String>) -> &mut Self {
337        self.password = Some(password.into());
338        self
339    }
340
341    /// Set AES encryption strength (default: AES-256)
342    ///
343    /// # Arguments
344    /// * `strength` - AES encryption strength (Aes128, Aes192, or Aes256)
345    #[cfg(feature = "encryption")]
346    pub fn set_encryption_strength(&mut self, strength: AesStrength) -> &mut Self {
347        self.encryption_strength = strength;
348        self
349    }
350
351    /// Clear password (disable encryption for subsequent entries)
352    #[cfg(feature = "encryption")]
353    pub fn clear_password(&mut self) -> &mut Self {
354        self.password = None;
355        self
356    }
357
358    /// Start a new entry (file) in the ZIP
359    pub fn start_entry(&mut self, name: &str) -> Result<()> {
360        self.start_entry_with_hint(name, None)
361    }
362
363    /// Start a new entry with size hint for optimized buffering
364    ///
365    /// Providing an accurate size hint can improve performance by 15-25% for large files.
366    /// The hint is used to optimize buffer allocation and flush thresholds.
367    ///
368    /// # Arguments
369    /// * `name` - The name/path of the entry in the ZIP
370    /// * `size_hint` - Optional uncompressed size hint in bytes
371    ///
372    /// # Example
373    /// ```no_run
374    /// # use s_zip::StreamingZipWriter;
375    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
376    /// let mut writer = StreamingZipWriter::new("output.zip")?;
377    ///
378    /// // For large files, provide size hint for better performance
379    /// writer.start_entry_with_hint("large_file.bin", Some(10_000_000))?;
380    /// # Ok(())
381    /// # }
382    /// ```
383    pub fn start_entry_with_hint(&mut self, name: &str, size_hint: Option<u64>) -> Result<()> {
384        // Finish previous entry if any
385        self.finish_current_entry()?;
386
387        let local_header_offset = self.output.stream_position()?;
388        let compression_method = self.compression_method.to_zip_method();
389
390        // Check if encryption is enabled
391        #[cfg(feature = "encryption")]
392        let (encryptor, encryption_flag) = if let Some(ref password) = self.password {
393            let enc = AesEncryptor::new(password, self.encryption_strength)?;
394            (Some(enc), 0x01) // bit 0 set for encryption
395        } else {
396            (None, 0x00)
397        };
398
399        #[cfg(not(feature = "encryption"))]
400        let encryption_flag = 0x00;
401
402        // Write local file header with data descriptor flag (bit 3) + encryption flag (bit 0)
403        self.output.write_all(&[0x50, 0x4b, 0x03, 0x04])?; // signature
404        self.output.write_all(&[51, 0])?; // version needed (5.1 for AES)
405        self.output.write_all(&[8 | encryption_flag, 0])?; // general purpose bit flag
406        self.output.write_all(&compression_method.to_le_bytes())?; // compression method
407        self.output.write_all(&[0, 0, 0, 0])?; // mod time/date
408        self.output.write_all(&0u32.to_le_bytes())?; // crc32 placeholder
409        self.output.write_all(&0u32.to_le_bytes())?; // compressed size placeholder
410        self.output.write_all(&0u32.to_le_bytes())?; // uncompressed size placeholder
411        self.output.write_all(&(name.len() as u16).to_le_bytes())?;
412
413        // Calculate extra field size for AES
414        #[cfg(feature = "encryption")]
415        let extra_len = if encryptor.is_some() { 11 } else { 0 };
416        #[cfg(not(feature = "encryption"))]
417        let extra_len = 0;
418
419        self.output.write_all(&(extra_len as u16).to_le_bytes())?; // extra len
420        self.output.write_all(name.as_bytes())?;
421
422        // Write AES extra field if encryption is enabled
423        #[cfg(feature = "encryption")]
424        if let Some(ref enc) = encryptor {
425            // AES extra field header (0x9901)
426            self.output.write_all(&[0x01, 0x99])?; // WinZip AES encryption marker
427            self.output.write_all(&[7, 0])?; // data size
428            self.output.write_all(&[2, 0])?; // AE-2 format
429            self.output.write_all(&[0x41, 0x45])?; // vendor ID "AE"
430            self.output
431                .write_all(&enc.strength().to_winzip_code().to_le_bytes())?; // strength
432            self.output.write_all(&compression_method.to_le_bytes())?; // actual compression
433
434            // Write salt and password verification
435            self.output.write_all(enc.salt())?;
436            self.output.write_all(enc.password_verify())?;
437        }
438
439        // Create encoder for this entry based on compression method
440        // Use adaptive buffer if size hint is provided
441        let encoder: Box<dyn CompressorWrite> = match self.compression_method {
442            CompressionMethod::Deflate => Box::new(DeflateCompressor {
443                encoder: DeflateEncoder::new(
444                    CompressedBuffer::with_size_hint(size_hint),
445                    Compression::new(self.compression_level),
446                ),
447            }),
448            #[cfg(feature = "zstd-support")]
449            CompressionMethod::Zstd => {
450                let mut encoder = zstd::Encoder::new(
451                    CompressedBuffer::with_size_hint(size_hint),
452                    self.compression_level as i32,
453                )?;
454                encoder.include_checksum(false)?; // ZIP uses CRC32, not zstd checksum
455                Box::new(ZstdCompressor { encoder })
456            }
457            CompressionMethod::Stored => {
458                // For stored, we don't compress
459                return Err(SZipError::InvalidFormat(
460                    "Stored method not yet implemented".to_string(),
461                ));
462            }
463        };
464
465        self.current_entry = Some(CurrentEntry {
466            name: name.to_string(),
467            local_header_offset,
468            encoder,
469            counter: CrcCounter::new(),
470            compression_method,
471            #[cfg(feature = "encryption")]
472            encryptor,
473        });
474
475        Ok(())
476    }
477
478    /// Write uncompressed data to current entry (will be compressed and/or encrypted on-the-fly)
479    pub fn write_data(&mut self, data: &[u8]) -> Result<()> {
480        let entry = self
481            .current_entry
482            .as_mut()
483            .ok_or_else(|| SZipError::InvalidFormat("No entry started".to_string()))?;
484
485        // Update CRC and size with uncompressed data
486        entry.counter.update_uncompressed(data);
487
488        // For AES encryption: encrypt THEN compress
489        // Note: AE-2 format doesn't use CRC, uses HMAC instead
490        #[cfg(feature = "encryption")]
491        let data_to_compress = if let Some(ref mut encryptor) = entry.encryptor {
492            let mut encrypted = data.to_vec();
493            encryptor.encrypt(&mut encrypted)?;
494            encrypted
495        } else {
496            data.to_vec()
497        };
498
499        #[cfg(not(feature = "encryption"))]
500        let data_to_compress = data.to_vec();
501
502        // Write to encoder (compresses data into buffer)
503        entry.encoder.write_all(&data_to_compress)?;
504
505        // Flush encoder to ensure all data is in buffer
506        entry.encoder.flush()?;
507
508        // Check if buffer should be flushed to output
509        let buffer = entry.encoder.get_buffer_mut();
510        if buffer.should_flush() {
511            // Flush buffer to output to keep memory usage low
512            let compressed_data = buffer.take();
513            self.output.write_all(&compressed_data)?;
514            entry.counter.add_compressed(compressed_data.len() as u64);
515        }
516
517        Ok(())
518    }
519
520    /// Finish current entry and write data descriptor
521    fn finish_current_entry(&mut self) -> Result<()> {
522        if let Some(mut entry) = self.current_entry.take() {
523            // Finish compression and get remaining buffered data
524            let mut buffer = entry.encoder.finish_compression()?;
525
526            // Flush any remaining data from buffer to output
527            let remaining_data = buffer.take();
528            if !remaining_data.is_empty() {
529                self.output.write_all(&remaining_data)?;
530                entry.counter.add_compressed(remaining_data.len() as u64);
531            }
532
533            // Write authentication code for AES encryption
534            #[cfg(feature = "encryption")]
535            let (encryption_strength_code, auth_code_size) =
536                if let Some(encryptor) = entry.encryptor {
537                    let strength_code = encryptor.strength().to_winzip_code();
538                    let auth_code = encryptor.finalize();
539                    self.output.write_all(&auth_code)?;
540                    (Some(strength_code), auth_code.len() as u64)
541                } else {
542                    (None, 0)
543                };
544
545            #[cfg(not(feature = "encryption"))]
546            let auth_code_size = 0u64;
547
548            let crc = entry.counter.finalize();
549            let compressed_size = entry.counter.compressed_count + auth_code_size;
550            let uncompressed_size = entry.counter.uncompressed_count;
551
552            // Write data descriptor
553            // signature
554            self.output.write_all(&[0x50, 0x4b, 0x07, 0x08])?;
555            self.output.write_all(&crc.to_le_bytes())?;
556            // If sizes exceed 32-bit, write 64-bit sizes (ZIP64 data descriptor)
557            if compressed_size > u32::MAX as u64 || uncompressed_size > u32::MAX as u64 {
558                self.output.write_all(&compressed_size.to_le_bytes())?;
559                self.output.write_all(&uncompressed_size.to_le_bytes())?;
560            } else {
561                self.output
562                    .write_all(&(compressed_size as u32).to_le_bytes())?;
563                self.output
564                    .write_all(&(uncompressed_size as u32).to_le_bytes())?;
565            }
566
567            // Save entry info for central directory
568            self.entries.push(ZipEntry {
569                name: entry.name,
570                local_header_offset: entry.local_header_offset,
571                crc32: crc,
572                compressed_size,
573                uncompressed_size,
574                compression_method: entry.compression_method,
575                #[cfg(feature = "encryption")]
576                encryption_strength: encryption_strength_code,
577            });
578        }
579        Ok(())
580    }
581
582    /// Finish ZIP file (write central directory and return the writer)
583    pub fn finish(mut self) -> Result<W> {
584        // Finish last entry
585        self.finish_current_entry()?;
586
587        let central_dir_offset = self.output.stream_position()?;
588
589        // Write central directory
590        for entry in &self.entries {
591            self.output.write_all(&[0x50, 0x4b, 0x01, 0x02])?; // central dir sig
592            self.output.write_all(&[20, 0])?; // version made by
593            self.output.write_all(&[20, 0])?; // version needed
594            self.output.write_all(&[8, 0])?; // general purpose bit flag (bit 3 set)
595            self.output
596                .write_all(&entry.compression_method.to_le_bytes())?; // compression method
597            self.output.write_all(&[0, 0, 0, 0])?; // mod time/date
598            self.output.write_all(&entry.crc32.to_le_bytes())?;
599
600            // Write sizes (32-bit placeholders or actual values)
601            if entry.compressed_size > u32::MAX as u64 {
602                self.output.write_all(&0xFFFFFFFFu32.to_le_bytes())?;
603            } else {
604                self.output
605                    .write_all(&(entry.compressed_size as u32).to_le_bytes())?;
606            }
607
608            if entry.uncompressed_size > u32::MAX as u64 {
609                self.output.write_all(&0xFFFFFFFFu32.to_le_bytes())?;
610            } else {
611                self.output
612                    .write_all(&(entry.uncompressed_size as u32).to_le_bytes())?;
613            }
614
615            self.output
616                .write_all(&(entry.name.len() as u16).to_le_bytes())?;
617
618            // Prepare ZIP64 extra field if needed
619            let mut extra_field: Vec<u8> = Vec::new();
620            if entry.uncompressed_size > u32::MAX as u64
621                || entry.compressed_size > u32::MAX as u64
622                || entry.local_header_offset > u32::MAX as u64
623            {
624                // ZIP64 extra header ID 0x0001
625                extra_field.extend_from_slice(&0x0001u16.to_le_bytes());
626                // data size: we'll include uncompressed (8) if needed, compressed (8) if needed, and offset (8) if needed
627                let mut data: Vec<u8> = Vec::new();
628                if entry.uncompressed_size > u32::MAX as u64 {
629                    data.extend_from_slice(&entry.uncompressed_size.to_le_bytes());
630                }
631                if entry.compressed_size > u32::MAX as u64 {
632                    data.extend_from_slice(&entry.compressed_size.to_le_bytes());
633                }
634                if entry.local_header_offset > u32::MAX as u64 {
635                    data.extend_from_slice(&entry.local_header_offset.to_le_bytes());
636                }
637                extra_field.extend_from_slice(&(data.len() as u16).to_le_bytes());
638                extra_field.extend_from_slice(&data);
639            }
640
641            self.output
642                .write_all(&(extra_field.len() as u16).to_le_bytes())?; // extra len
643            self.output.write_all(&0u16.to_le_bytes())?; // file comment len
644            self.output.write_all(&0u16.to_le_bytes())?; // disk number start
645            self.output.write_all(&0u16.to_le_bytes())?; // internal attrs
646            self.output.write_all(&0u32.to_le_bytes())?; // external attrs
647
648            // local header offset (32-bit or 0xFFFFFFFF)
649            if entry.local_header_offset > u32::MAX as u64 {
650                self.output.write_all(&0xFFFFFFFFu32.to_le_bytes())?;
651            } else {
652                self.output
653                    .write_all(&(entry.local_header_offset as u32).to_le_bytes())?;
654            }
655
656            self.output.write_all(entry.name.as_bytes())?;
657            if !extra_field.is_empty() {
658                self.output.write_all(&extra_field)?;
659            }
660        }
661
662        let central_dir_size = self.output.stream_position()? - central_dir_offset;
663
664        // Determine if we need ZIP64 EOCD
665        let need_zip64 = self.entries.len() > u16::MAX as usize
666            || central_dir_size > u32::MAX as u64
667            || central_dir_offset > u32::MAX as u64;
668
669        if need_zip64 {
670            // Write ZIP64 End of Central Directory Record
671            // signature
672            self.output.write_all(&[0x50, 0x4b, 0x06, 0x06])?; // 0x06064b50
673                                                               // size of zip64 eocd record (size of remaining fields)
674                                                               // We'll write fixed-size fields: version made by(2)+version needed(2)+disk numbers(4+4)+entries on disk(8)+total entries(8)+cd size(8)+cd offset(8)
675            let zip64_eocd_size: u64 = 44;
676            self.output.write_all(&zip64_eocd_size.to_le_bytes())?;
677            // version made by, version needed
678            self.output.write_all(&[20, 0])?;
679            self.output.write_all(&[20, 0])?;
680            // disk number, disk where central dir starts
681            self.output.write_all(&0u32.to_le_bytes())?;
682            self.output.write_all(&0u32.to_le_bytes())?;
683            // entries on this disk (8)
684            self.output
685                .write_all(&(self.entries.len() as u64).to_le_bytes())?;
686            // total entries (8)
687            self.output
688                .write_all(&(self.entries.len() as u64).to_le_bytes())?;
689            // central directory size (8)
690            self.output.write_all(&central_dir_size.to_le_bytes())?;
691            // central directory offset (8)
692            self.output.write_all(&central_dir_offset.to_le_bytes())?;
693
694            // Write ZIP64 EOCD locator
695            // signature
696            self.output.write_all(&[0x50, 0x4b, 0x06, 0x07])?; // 0x07064b50
697                                                               // disk with ZIP64 EOCD (4)
698            self.output.write_all(&0u32.to_le_bytes())?;
699            // relative offset of ZIP64 EOCD (8)
700            let zip64_eocd_pos = central_dir_offset + central_dir_size; // directly after central dir
701            self.output.write_all(&zip64_eocd_pos.to_le_bytes())?;
702            // total number of disks
703            self.output.write_all(&0u32.to_le_bytes())?;
704        }
705
706        // Write end of central directory (classic)
707        self.output.write_all(&[0x50, 0x4b, 0x05, 0x06])?;
708        self.output.write_all(&0u16.to_le_bytes())?; // disk number
709        self.output.write_all(&0u16.to_le_bytes())?; // disk with central dir
710
711        // number of entries (16-bit or 0xFFFF if ZIP64 used)
712        if self.entries.len() > u16::MAX as usize {
713            self.output.write_all(&0xFFFFu16.to_le_bytes())?;
714            self.output.write_all(&0xFFFFu16.to_le_bytes())?;
715        } else {
716            self.output
717                .write_all(&(self.entries.len() as u16).to_le_bytes())?;
718            self.output
719                .write_all(&(self.entries.len() as u16).to_le_bytes())?;
720        }
721
722        // central dir size and offset (32-bit or 0xFFFFFFFF)
723        if central_dir_size > u32::MAX as u64 {
724            self.output.write_all(&0xFFFFFFFFu32.to_le_bytes())?;
725        } else {
726            self.output
727                .write_all(&(central_dir_size as u32).to_le_bytes())?;
728        }
729
730        if central_dir_offset > u32::MAX as u64 {
731            self.output.write_all(&0xFFFFFFFFu32.to_le_bytes())?;
732        } else {
733            self.output
734                .write_all(&(central_dir_offset as u32).to_le_bytes())?;
735        }
736
737        self.output.write_all(&0u16.to_le_bytes())?; // comment len
738
739        self.output.flush()?;
740        Ok(self.output)
741    }
742}