Skip to main content

s_zip/
reader.rs

1//! Streaming ZIP reader - reads ZIP files without loading entire central directory
2//!
3//! This is a minimal ZIP reader that can extract specific files from a ZIP archive
4//! without loading the entire central directory into memory.
5
6use crate::error::{Result, SZipError};
7use crate::format::{
8    find_eocd_in_buffer, find_zip64_eocd_offset, parse_zip64_extra_field,
9    CENTRAL_DIRECTORY_SIGNATURE, END_OF_CENTRAL_DIRECTORY_SIGNATURE, LOCAL_FILE_HEADER_SIGNATURE,
10    MAX_ENTRY_ALLOC, ZIP64_END_OF_CENTRAL_DIRECTORY_SIGNATURE,
11};
12
13#[cfg(feature = "encryption")]
14use crate::format::parse_aes_extra_field_buf;
15use flate2::read::DeflateDecoder;
16use std::fs::File;
17use std::io::{BufReader, Read, Seek, SeekFrom};
18use std::path::Path;
19
20#[cfg(feature = "encryption")]
21use crate::encryption::{AesDecryptor, AesStrength};
22
23// Re-export ZipEntry so existing `use s_zip::reader::ZipEntry` paths still compile.
24pub use crate::format::ZipEntry;
25
26/// Streaming ZIP archive reader with adaptive buffering
27pub struct StreamingZipReader {
28    file: BufReader<File>,
29    entries: Vec<ZipEntry>,
30    #[cfg(feature = "encryption")]
31    password: Option<String>,
32}
33
34impl StreamingZipReader {
35    /// Open a ZIP file and read its central directory with default buffer size
36    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
37        Self::open_with_buffer_size(path, None)
38    }
39
40    /// Open a ZIP file with custom buffer size for optimized reading
41    ///
42    /// Providing a buffer size hint can improve read performance:
43    /// - Small ZIPs (<10MB): 32KB buffer
44    /// - Medium ZIPs (<100MB): 128KB buffer  
45    /// - Large ZIPs (≥100MB): 512KB buffer (default)
46    ///
47    /// # Example
48    /// ```no_run
49    /// # use s_zip::StreamingZipReader;
50    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
51    /// // Optimize for large ZIP files
52    /// let reader = StreamingZipReader::open_with_buffer_size(
53    ///     "large_archive.zip",
54    ///     Some(1024 * 1024) // 1MB buffer for very large files
55    /// )?;
56    /// # Ok(())
57    /// # }
58    /// ```
59    pub fn open_with_buffer_size<P: AsRef<Path>>(
60        path: P,
61        buffer_size: Option<usize>,
62    ) -> Result<Self> {
63        let file = File::open(path)?;
64
65        // Use adaptive buffer size
66        let buf_size = buffer_size.unwrap_or(512 * 1024); // Default 512KB
67        let mut file = BufReader::with_capacity(buf_size, file);
68
69        // Find and read central directory
70        let entries = Self::read_central_directory(&mut file)?;
71
72        Ok(StreamingZipReader {
73            file,
74            entries,
75            #[cfg(feature = "encryption")]
76            password: None,
77        })
78    }
79
80    /// Set password for decrypting encrypted entries
81    #[cfg(feature = "encryption")]
82    pub fn set_password(&mut self, password: impl Into<String>) -> &mut Self {
83        self.password = Some(password.into());
84        self
85    }
86
87    /// Clear password
88    #[cfg(feature = "encryption")]
89    pub fn clear_password(&mut self) -> &mut Self {
90        self.password = None;
91        self
92    }
93
94    /// Get list of all entries in the ZIP
95    pub fn entries(&self) -> &[ZipEntry] {
96        &self.entries
97    }
98
99    /// Find an entry by name
100    pub fn find_entry(&self, name: &str) -> Option<&ZipEntry> {
101        self.entries.iter().find(|e| e.name == name)
102    }
103
104    /// Read an entry's decompressed data into a vector
105    pub fn read_entry(&mut self, entry: &ZipEntry) -> Result<Vec<u8>> {
106        // Seek to local file header
107        self.file.seek(SeekFrom::Start(entry.offset))?;
108
109        // Read and verify local file header
110        let signature = self.read_u32_le()?;
111        if signature != LOCAL_FILE_HEADER_SIGNATURE {
112            return Err(SZipError::InvalidFormat(
113                "Invalid local file header signature".to_string(),
114            ));
115        }
116
117        // Skip version
118        self.file.seek(SeekFrom::Current(2))?;
119
120        // Read flags to check for encryption
121        let flags = self.read_u16_le()?;
122        let is_encrypted = (flags & 0x01) != 0;
123
124        // Read compression method
125        let _compression_method = self.read_u16_le()?;
126
127        // Skip modification time and date, CRC-32
128        self.file.seek(SeekFrom::Current(8))?;
129
130        // Read compressed and uncompressed sizes (already known from central directory)
131        self.file.seek(SeekFrom::Current(8))?;
132
133        // Read filename length and extra field length
134        let filename_len = self.read_u16_le()? as i64;
135        let extra_len = self.read_u16_le()? as usize;
136
137        // Skip filename
138        self.file.seek(SeekFrom::Current(filename_len))?;
139
140        // Check for AES encryption in extra field
141        #[cfg(feature = "encryption")]
142        let encryption_info = if is_encrypted {
143            self.parse_aes_extra_field(extra_len)?
144        } else {
145            // Skip extra field if not encrypted
146            self.file.seek(SeekFrom::Current(extra_len as i64))?;
147            None
148        };
149
150        #[cfg(not(feature = "encryption"))]
151        {
152            if is_encrypted {
153                return Err(SZipError::InvalidFormat(
154                    "Encrypted entry found but encryption feature not enabled".to_string(),
155                ));
156            }
157            // Skip extra field
158            self.file.seek(SeekFrom::Current(extra_len as i64))?;
159        }
160
161        // Calculate actual data size (subtract salt, password verify, and auth code for encrypted entries)
162        #[cfg(feature = "encryption")]
163        let data_size = if let Some((strength, _, _)) = encryption_info {
164            // Subtract salt (already read), password verify (already read), and auth code (10 bytes at end)
165            entry
166                .compressed_size
167                .saturating_sub((strength.salt_size() + 2 + 10) as u64)
168        } else {
169            entry.compressed_size
170        };
171
172        #[cfg(not(feature = "encryption"))]
173        let data_size = entry.compressed_size;
174
175        // Guard against OOM from corrupt/malicious compressed_size values.
176        // Entries larger than 2 GiB must use read_entry_streaming() instead.
177        if data_size > MAX_ENTRY_ALLOC {
178            return Err(SZipError::InvalidFormat(format!(
179                "Entry '{}' is too large to read into memory ({} bytes). \
180                 Use read_entry_streaming() for entries larger than 2 GiB.",
181                entry.name, data_size
182            )));
183        }
184
185        // Now read the compressed data
186        let mut compressed_data = vec![0u8; data_size as usize];
187        self.file.read_exact(&mut compressed_data)?;
188
189        // Read auth code if encrypted
190        #[cfg(feature = "encryption")]
191        let auth_code = if encryption_info.is_some() {
192            let mut ac = vec![0u8; 10];
193            self.file.read_exact(&mut ac)?;
194            Some(ac)
195        } else {
196            None
197        };
198
199        // Decrypt if encrypted (Step 1: Decrypt compressed data)
200        #[cfg(feature = "encryption")]
201        let decryptor_opt = if let Some((strength, salt, pw_verify)) = encryption_info {
202            let password = self.password.as_ref().ok_or_else(|| {
203                SZipError::InvalidFormat("Encrypted entry but no password set".to_string())
204            })?;
205
206            // Create decryptor (password verification happens inside new())
207            let mut decryptor = AesDecryptor::new(password, strength, &salt, &pw_verify)?;
208
209            // Decrypt compressed data in-place
210            decryptor.decrypt(&mut compressed_data)?;
211
212            Some(decryptor)
213        } else {
214            None
215        };
216
217        // Decompress if needed (Step 2: Decompress decrypted data)
218        let data = if entry.compression_method == 8 {
219            // DEFLATE compression
220            let mut decoder = DeflateDecoder::new(&compressed_data[..]);
221            let mut decompressed = Vec::new();
222            decoder.read_to_end(&mut decompressed)?;
223            decompressed
224        } else if entry.compression_method == 0 {
225            // No compression (stored)
226            compressed_data
227        } else if entry.compression_method == 93 {
228            // Zstd compression
229            #[cfg(feature = "zstd-support")]
230            {
231                zstd::decode_all(&compressed_data[..])?
232            }
233            #[cfg(not(feature = "zstd-support"))]
234            {
235                return Err(SZipError::UnsupportedCompression(entry.compression_method));
236            }
237        } else {
238            return Err(SZipError::UnsupportedCompression(entry.compression_method));
239        };
240
241        // Verify HMAC authentication (Step 3: Update HMAC with plaintext and verify)
242        #[cfg(feature = "encryption")]
243        if let Some(mut decryptor) = decryptor_opt {
244            // Update HMAC with decompressed plaintext data
245            decryptor.update_hmac(&data);
246
247            // Verify authentication code
248            if let Some(ac) = auth_code {
249                decryptor.verify_auth_code(&ac)?;
250            }
251        }
252
253        // Verify CRC-32 integrity — catches bit-rot and truncated downloads.
254        // Skip for encrypted entries: the HMAC above already provides stronger
255        // authentication guarantees.
256        #[cfg(not(feature = "encryption"))]
257        {
258            let actual_crc = crc32fast::hash(&data);
259            if entry.crc32 != 0 && actual_crc != entry.crc32 {
260                return Err(SZipError::InvalidFormat(format!(
261                    "CRC-32 mismatch for '{}': expected {:#010x}, got {:#010x}. \
262                     The entry may be corrupt or the download may be incomplete.",
263                    entry.name, entry.crc32, actual_crc
264                )));
265            }
266        }
267        #[cfg(feature = "encryption")]
268        if entry.crc32 != 0 && !entry.is_encrypted {
269            let actual_crc = crc32fast::hash(&data);
270            if actual_crc != entry.crc32 {
271                return Err(SZipError::InvalidFormat(format!(
272                    "CRC-32 mismatch for '{}': expected {:#010x}, got {:#010x}. \
273                     The entry may be corrupt or the download may be incomplete.",
274                    entry.name, entry.crc32, actual_crc
275                )));
276            }
277        }
278
279        Ok(data)
280    }
281
282    /// Read an entry by name
283    pub fn read_entry_by_name(&mut self, name: &str) -> Result<Vec<u8>> {
284        let entry = self
285            .find_entry(name)
286            .ok_or_else(|| SZipError::EntryNotFound(name.to_string()))?
287            .clone();
288
289        self.read_entry(&entry)
290    }
291
292    /// Get a streaming reader for an entry by name (for large files)
293    /// Returns a reader that decompresses data on-the-fly without loading everything into memory
294    pub fn read_entry_streaming_by_name(&mut self, name: &str) -> Result<Box<dyn Read + '_>> {
295        let entry = self
296            .find_entry(name)
297            .ok_or_else(|| SZipError::EntryNotFound(name.to_string()))?
298            .clone();
299
300        self.read_entry_streaming(&entry)
301    }
302
303    /// Get a streaming reader for an entry (for large files).
304    ///
305    /// Returns a `Read` impl that decompresses (and, if encrypted, decrypts)
306    /// data on-the-fly without loading the entire entry into memory.
307    ///
308    /// # Encrypted entries
309    ///
310    /// When the `encryption` feature is enabled and the entry is encrypted,
311    /// this method returns a streaming reader that decrypts on-the-fly.
312    /// **Callers must read all bytes and then call `finish()` on the returned
313    /// reader to verify the HMAC-SHA1 authentication tag.**  Because the
314    /// decompressor sits above the decryptor in the pipeline, the HMAC is
315    /// computed over the *compressed ciphertext* bytes rather than the
316    /// decompressed plaintext — this is a known limitation of the streaming
317    /// path.  For full WinZip AE-2 compliance (HMAC over plaintext) use
318    /// `read_entry()` instead.
319    ///
320    /// # Errors
321    /// Returns `SZipError::EncryptionError` if the entry is encrypted but
322    /// `set_password()` was not called or the password is wrong.
323    pub fn read_entry_streaming(&mut self, entry: &ZipEntry) -> Result<Box<dyn Read + '_>> {
324        // Seek to local file header
325        self.file.seek(SeekFrom::Start(entry.offset))?;
326
327        // Read and verify local file header
328        let signature = self.read_u32_le()?;
329        if signature != LOCAL_FILE_HEADER_SIGNATURE {
330            return Err(SZipError::InvalidFormat(
331                "Invalid local file header signature".to_string(),
332            ));
333        }
334
335        // Skip version, flags, compression method
336        self.file.seek(SeekFrom::Current(6))?;
337
338        // Skip modification time and date, CRC-32
339        self.file.seek(SeekFrom::Current(8))?;
340
341        // Read compressed and uncompressed sizes (use values from central directory)
342        self.file.seek(SeekFrom::Current(8))?;
343
344        // Read filename length and extra field length
345        let filename_len = self.read_u16_le()? as i64;
346        let extra_len = self.read_u16_le()? as usize;
347
348        // Skip filename
349        self.file.seek(SeekFrom::Current(filename_len))?;
350
351        // For encrypted entries: parse AES extra field and decrypt on-the-fly.
352        #[cfg(feature = "encryption")]
353        if entry.is_encrypted {
354            let encryption_info = self.parse_aes_extra_field(extra_len)?;
355
356            if let Some((strength, salt, pw_verify)) = encryption_info {
357                let password = self.password.as_ref().ok_or_else(|| {
358                    SZipError::EncryptionError(
359                        "Encrypted entry but no password set. Call set_password() first."
360                            .to_string(),
361                    )
362                })?;
363
364                // Actual ciphertext size: compressed_size minus (salt + pw_verify + auth_code)
365                let overhead = (strength.salt_size() + 2 + 10) as u64;
366                let cipher_size = entry.compressed_size.saturating_sub(overhead);
367
368                // Read the 10-byte auth code positioned after ciphertext.
369                // We must read it now (by seeking past the ciphertext) then
370                // seek back, because limited_reader borrows self.file mutably.
371                // Instead we store auth_code and pass it to DecryptingReader.
372                //
373                // Offset of auth_code = current_pos + cipher_size
374                let current_pos = self.file.stream_position()?;
375                self.file.seek(SeekFrom::Start(current_pos + cipher_size))?;
376                let mut auth_code = vec![0u8; 10];
377                self.file.read_exact(&mut auth_code)?;
378
379                // Seek back to start of ciphertext
380                self.file.seek(SeekFrom::Start(current_pos))?;
381
382                let limited_reader = (&mut self.file).take(cipher_size);
383
384                use crate::decrypt_reader::sync::DecryptingReader;
385                let decrypt_reader = DecryptingReader::new(
386                    limited_reader,
387                    password,
388                    strength,
389                    &salt,
390                    &pw_verify,
391                    auth_code,
392                )?;
393
394                // Wrap with decompressor
395                return if entry.compression_method == 8 {
396                    Ok(Box::new(DeflateDecoder::new(decrypt_reader)))
397                } else if entry.compression_method == 0 {
398                    Ok(Box::new(decrypt_reader))
399                } else {
400                    Err(SZipError::UnsupportedCompression(entry.compression_method))
401                };
402            } else {
403                // Extra field didn't contain AES info — skip it
404                self.file.seek(SeekFrom::Current(extra_len as i64))?;
405            }
406        }
407
408        // Non-encrypted path: skip extra field
409        #[cfg(feature = "encryption")]
410        if !entry.is_encrypted {
411            self.file.seek(SeekFrom::Current(extra_len as i64))?;
412        }
413
414        #[cfg(not(feature = "encryption"))]
415        self.file.seek(SeekFrom::Current(extra_len as i64))?;
416
417        // Create a reader limited to compressed data size
418        let limited_reader = (&mut self.file).take(entry.compressed_size);
419
420        // Wrap with decompressor if needed
421        if entry.compression_method == 8 {
422            // DEFLATE compression
423            Ok(Box::new(DeflateDecoder::new(limited_reader)))
424        } else if entry.compression_method == 0 {
425            // No compression (stored)
426            Ok(Box::new(limited_reader))
427        } else if entry.compression_method == 93 {
428            // Zstd compression
429            #[cfg(feature = "zstd-support")]
430            {
431                Ok(Box::new(zstd::Decoder::new(limited_reader)?))
432            }
433            #[cfg(not(feature = "zstd-support"))]
434            {
435                Err(SZipError::UnsupportedCompression(entry.compression_method))
436            }
437        } else {
438            Err(SZipError::UnsupportedCompression(entry.compression_method))
439        }
440    }
441
442    /// Get a streaming reader for an entry by name
443    pub fn read_entry_by_name_streaming(&mut self, name: &str) -> Result<Box<dyn Read + '_>> {
444        let entry = self
445            .find_entry(name)
446            .ok_or_else(|| SZipError::EntryNotFound(name.to_string()))?
447            .clone();
448
449        self.read_entry_streaming(&entry)
450    }
451
452    /// Read the central directory from the ZIP file
453    fn read_central_directory(file: &mut BufReader<File>) -> Result<Vec<ZipEntry>> {
454        // Find end of central directory record
455        let eocd_offset = Self::find_eocd(file)?;
456
457        // Seek to EOCD
458        file.seek(SeekFrom::Start(eocd_offset))?;
459
460        // Read EOCD
461        let signature = Self::read_u32_le_static(file)?;
462        if signature != END_OF_CENTRAL_DIRECTORY_SIGNATURE {
463            return Err(SZipError::InvalidFormat(format!(
464                "Invalid end of central directory signature: 0x{:08x}",
465                signature
466            )));
467        }
468
469        // Skip disk number fields (4 bytes)
470        file.seek(SeekFrom::Current(4))?;
471
472        // Read number of entries on this disk (2 bytes)
473        let _entries_on_disk = Self::read_u16_le_static(file)?;
474
475        // Read total number of entries (2 bytes)
476
477        // These values may be placeholder 0xFFFF/0xFFFFFFFF when ZIP64 is used
478        let total_entries_16 = Self::read_u16_le_static(file)?;
479
480        // Read central directory size (4 bytes)
481        let cd_size_32 = Self::read_u32_le_static(file)?;
482
483        // Read central directory offset (4 bytes)
484        let cd_offset_32 = Self::read_u32_le_static(file)? as u64;
485
486        // Promote to u64 and handle ZIP64 if markers present
487        let mut total_entries = total_entries_16 as usize;
488        let mut cd_offset = cd_offset_32;
489        let _cd_size = cd_size_32 as u64;
490
491        if total_entries_16 == 0xFFFF || cd_size_32 == 0xFFFFFFFF || cd_offset_32 == 0xFFFFFFFF {
492            // Need to find ZIP64 EOCD locator and read ZIP64 EOCD record
493            let (zip64_total_entries, zip64_cd_size, zip64_cd_offset) =
494                Self::read_zip64_eocd(file, eocd_offset)?;
495            total_entries = zip64_total_entries as usize;
496            cd_offset = zip64_cd_offset;
497            // _cd_size can be used if needed (zip64_cd_size)
498            let _ = zip64_cd_size;
499        }
500
501        // Seek to central directory
502        file.seek(SeekFrom::Start(cd_offset))?;
503
504        // Read all central directory entries
505        let mut entries = Vec::with_capacity(total_entries);
506        for _ in 0..total_entries {
507            let signature = Self::read_u32_le_static(file)?;
508            if signature != CENTRAL_DIRECTORY_SIGNATURE {
509                break;
510            }
511
512            // Skip version made by, version needed
513            file.seek(SeekFrom::Current(4))?;
514
515            // Read flags (needed for encryption check)
516            #[cfg_attr(not(feature = "encryption"), allow(unused_variables))]
517            let flags = Self::read_u16_le_static(file)?;
518
519            let compression_method = Self::read_u16_le_static(file)?;
520
521            // Read modification time, date, and CRC-32
522            file.seek(SeekFrom::Current(4))?; // mod time + date
523            let crc32 = Self::read_u32_le_static(file)?;
524
525            // Read sizes as 32-bit placeholders (may be 0xFFFFFFFF meaning ZIP64)
526            let compressed_size_32 = Self::read_u32_le_static(file)? as u64;
527            let uncompressed_size_32 = Self::read_u32_le_static(file)? as u64;
528            let filename_len = Self::read_u16_le_static(file)? as usize;
529            let extra_len = Self::read_u16_le_static(file)? as usize;
530            let comment_len = Self::read_u16_le_static(file)? as usize;
531
532            // Skip disk number, internal attributes, external attributes
533            file.seek(SeekFrom::Current(8))?;
534
535            let offset_32 = Self::read_u32_le_static(file)? as u64;
536
537            // Read filename
538            let mut filename_buf = vec![0u8; filename_len];
539            file.read_exact(&mut filename_buf)?;
540            let name = String::from_utf8_lossy(&filename_buf).to_string();
541
542            // Read extra field so we can parse ZIP64 extra if present
543            let mut extra_buf = vec![0u8; extra_len];
544            if extra_len > 0 {
545                file.read_exact(&mut extra_buf)?;
546            }
547
548            // Resolve ZIP64 placeholders using shared pure helper
549            let (uncompressed_size, compressed_size, offset) = if compressed_size_32 == 0xFFFFFFFF
550                || uncompressed_size_32 == 0xFFFFFFFF
551                || offset_32 == 0xFFFFFFFF
552            {
553                parse_zip64_extra_field(
554                    &extra_buf,
555                    compressed_size_32,
556                    uncompressed_size_32,
557                    offset_32,
558                )
559            } else {
560                (uncompressed_size_32, compressed_size_32, offset_32)
561            };
562
563            // Skip comment
564            if comment_len > 0 {
565                file.seek(SeekFrom::Current(comment_len as i64))?;
566            }
567
568            entries.push(ZipEntry {
569                name,
570                compressed_size,
571                uncompressed_size,
572                compression_method,
573                offset,
574                crc32,
575                is_encrypted: (flags & 0x01) != 0,
576            });
577        }
578
579        Ok(entries)
580    }
581
582    /// When EOCD indicates ZIP64 usage, find and read ZIP64 EOCD locator and record
583    fn read_zip64_eocd(file: &mut BufReader<File>, eocd_offset: u64) -> Result<(u64, u64, u64)> {
584        // Search backwards from EOCD for ZIP64 EOCD locator signature (50 4b 06 07)
585        let search_start = eocd_offset.saturating_sub(65557);
586        file.seek(SeekFrom::Start(search_start))?;
587        let mut buffer = Vec::new();
588        file.read_to_end(&mut buffer)?;
589
590        let zip64_eocd_offset = find_zip64_eocd_offset(&buffer)
591            .ok_or_else(|| SZipError::InvalidFormat("ZIP64 EOCD locator not found".to_string()))?;
592
593        // Seek to ZIP64 EOCD record
594        file.seek(SeekFrom::Start(zip64_eocd_offset))?;
595
596        let sig = Self::read_u32_le_static(file)?;
597        if sig != ZIP64_END_OF_CENTRAL_DIRECTORY_SIGNATURE {
598            return Err(SZipError::InvalidFormat(format!(
599                "Invalid ZIP64 EOCD signature: 0x{:08x}",
600                sig
601            )));
602        }
603
604        // size of ZIP64 EOCD record (8 bytes)
605        let _size = {
606            let mut buf = [0u8; 8];
607            file.read_exact(&mut buf)?;
608            u64::from_le_bytes(buf)
609        };
610
611        // skip version made by (2), version needed (2), disk number (4), disk where central dir starts (4)
612        file.seek(SeekFrom::Current(12))?;
613
614        // total number of entries on this disk (8)
615        let total_entries = {
616            let mut buf = [0u8; 8];
617            file.read_exact(&mut buf)?;
618            u64::from_le_bytes(buf)
619        };
620
621        // total number of entries (8) - some implementations write both; ignore the second value
622        {
623            let mut buf = [0u8; 8];
624            file.read_exact(&mut buf)?;
625        }
626
627        // central directory size (8)
628        let cd_size = {
629            let mut buf = [0u8; 8];
630            file.read_exact(&mut buf)?;
631            u64::from_le_bytes(buf)
632        };
633
634        // central directory offset (8)
635        let cd_offset = {
636            let mut buf = [0u8; 8];
637            file.read_exact(&mut buf)?;
638            u64::from_le_bytes(buf)
639        };
640
641        Ok((total_entries, cd_size, cd_offset))
642    }
643
644    /// Find the end of central directory record by scanning from the end of the file
645    fn find_eocd(file: &mut BufReader<File>) -> Result<u64> {
646        let file_size = file.seek(SeekFrom::End(0))?;
647
648        // EOCD is at least 22 bytes, search last 65KB (max comment size + EOCD)
649        let search_start = file_size.saturating_sub(65557);
650        file.seek(SeekFrom::Start(search_start))?;
651
652        let mut buffer = Vec::new();
653        file.read_to_end(&mut buffer)?;
654
655        find_eocd_in_buffer(&buffer, search_start).ok_or_else(|| {
656            SZipError::InvalidFormat("End of central directory not found".to_string())
657        })
658    }
659
660    fn read_u16_le(&mut self) -> Result<u16> {
661        let mut buf = [0u8; 2];
662        self.file.read_exact(&mut buf)?;
663        Ok(u16::from_le_bytes(buf))
664    }
665
666    fn read_u32_le(&mut self) -> Result<u32> {
667        let mut buf = [0u8; 4];
668        self.file.read_exact(&mut buf)?;
669        Ok(u32::from_le_bytes(buf))
670    }
671
672    fn read_u16_le_static(file: &mut BufReader<File>) -> Result<u16> {
673        let mut buf = [0u8; 2];
674        file.read_exact(&mut buf)?;
675        Ok(u16::from_le_bytes(buf))
676    }
677
678    fn read_u32_le_static(file: &mut BufReader<File>) -> Result<u32> {
679        let mut buf = [0u8; 4];
680        file.read_exact(&mut buf)?;
681        Ok(u32::from_le_bytes(buf))
682    }
683
684    /// Parse AES encryption info from extra field
685    #[cfg(feature = "encryption")]
686    #[allow(clippy::type_complexity)]
687    fn parse_aes_extra_field(
688        &mut self,
689        extra_len: usize,
690    ) -> Result<Option<(AesStrength, Vec<u8>, [u8; 2])>> {
691        if extra_len == 0 {
692            return Ok(None);
693        }
694
695        let mut extra_buf = vec![0u8; extra_len];
696        self.file.read_exact(&mut extra_buf)?;
697
698        // Use shared pure helper to find the strength code
699        let strength_code = match parse_aes_extra_field_buf(&extra_buf) {
700            Some(code) => code,
701            None => return Ok(None),
702        };
703
704        let strength = match strength_code {
705            0x03 => AesStrength::Aes256,
706            _ => {
707                return Err(SZipError::InvalidFormat(format!(
708                    "Unsupported AES strength: {}",
709                    strength_code
710                )))
711            }
712        };
713
714        // Read salt and password verification from actual file data (not extra field)
715        // Salt comes after the extra field, before compressed data
716        let salt_size = strength.salt_size();
717
718        let mut salt = vec![0u8; salt_size];
719        self.file.read_exact(&mut salt)?;
720
721        let mut pw_verify = [0u8; 2];
722        self.file.read_exact(&mut pw_verify)?;
723
724        Ok(Some((strength, salt, pw_verify)))
725    }
726}