1use crate::error::{Result, SZipError};
7use flate2::read::DeflateDecoder;
8use std::fs::File;
9use std::io::{BufReader, Read, Seek, SeekFrom};
10use std::path::{Component, Path, PathBuf};
11
12#[cfg(feature = "encryption")]
13use crate::encryption::{AesDecryptor, AesStrength};
14
15const LOCAL_FILE_HEADER_SIGNATURE: u32 = 0x04034b50;
17
18const CENTRAL_DIRECTORY_SIGNATURE: u32 = 0x02014b50;
20
21const END_OF_CENTRAL_DIRECTORY_SIGNATURE: u32 = 0x06054b50;
23
24const ZIP64_END_OF_CENTRAL_DIRECTORY_SIGNATURE: u32 = 0x06064b50;
26
27const MAX_ENTRY_ALLOC: u64 = 2 * 1024 * 1024 * 1024; #[derive(Debug, Clone)]
39pub struct ZipEntry {
40 pub name: String,
41 pub compressed_size: u64,
42 pub uncompressed_size: u64,
43 pub compression_method: u16,
44 pub offset: u64,
45 #[cfg(feature = "encryption")]
46 pub is_encrypted: bool,
47}
48
49impl ZipEntry {
50 pub fn safe_path(&self) -> PathBuf {
79 Path::new(&self.name)
80 .components()
81 .filter(|c| matches!(c, Component::Normal(_)))
82 .collect()
83 }
84}
85
86pub struct StreamingZipReader {
88 file: BufReader<File>,
89 entries: Vec<ZipEntry>,
90 #[cfg(feature = "encryption")]
91 password: Option<String>,
92}
93
94impl StreamingZipReader {
95 pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
97 Self::open_with_buffer_size(path, None)
98 }
99
100 pub fn open_with_buffer_size<P: AsRef<Path>>(
120 path: P,
121 buffer_size: Option<usize>,
122 ) -> Result<Self> {
123 let file = File::open(path)?;
124
125 let buf_size = buffer_size.unwrap_or(512 * 1024); let mut file = BufReader::with_capacity(buf_size, file);
128
129 let entries = Self::read_central_directory(&mut file)?;
131
132 Ok(StreamingZipReader {
133 file,
134 entries,
135 #[cfg(feature = "encryption")]
136 password: None,
137 })
138 }
139
140 #[cfg(feature = "encryption")]
142 pub fn set_password(&mut self, password: impl Into<String>) -> &mut Self {
143 self.password = Some(password.into());
144 self
145 }
146
147 #[cfg(feature = "encryption")]
149 pub fn clear_password(&mut self) -> &mut Self {
150 self.password = None;
151 self
152 }
153
154 pub fn entries(&self) -> &[ZipEntry] {
156 &self.entries
157 }
158
159 pub fn find_entry(&self, name: &str) -> Option<&ZipEntry> {
161 self.entries.iter().find(|e| e.name == name)
162 }
163
164 pub fn read_entry(&mut self, entry: &ZipEntry) -> Result<Vec<u8>> {
166 self.file.seek(SeekFrom::Start(entry.offset))?;
168
169 let signature = self.read_u32_le()?;
171 if signature != LOCAL_FILE_HEADER_SIGNATURE {
172 return Err(SZipError::InvalidFormat(
173 "Invalid local file header signature".to_string(),
174 ));
175 }
176
177 self.file.seek(SeekFrom::Current(2))?;
179
180 let flags = self.read_u16_le()?;
182 let is_encrypted = (flags & 0x01) != 0;
183
184 let _compression_method = self.read_u16_le()?;
186
187 self.file.seek(SeekFrom::Current(8))?;
189
190 self.file.seek(SeekFrom::Current(8))?;
192
193 let filename_len = self.read_u16_le()? as i64;
195 let extra_len = self.read_u16_le()? as usize;
196
197 self.file.seek(SeekFrom::Current(filename_len))?;
199
200 #[cfg(feature = "encryption")]
202 let encryption_info = if is_encrypted {
203 self.parse_aes_extra_field(extra_len)?
204 } else {
205 self.file.seek(SeekFrom::Current(extra_len as i64))?;
207 None
208 };
209
210 #[cfg(not(feature = "encryption"))]
211 {
212 if is_encrypted {
213 return Err(SZipError::InvalidFormat(
214 "Encrypted entry found but encryption feature not enabled".to_string(),
215 ));
216 }
217 self.file.seek(SeekFrom::Current(extra_len as i64))?;
219 }
220
221 #[cfg(feature = "encryption")]
223 let data_size = if let Some((strength, _, _)) = encryption_info {
224 entry
226 .compressed_size
227 .saturating_sub((strength.salt_size() + 2 + 10) as u64)
228 } else {
229 entry.compressed_size
230 };
231
232 #[cfg(not(feature = "encryption"))]
233 let data_size = entry.compressed_size;
234
235 if data_size > MAX_ENTRY_ALLOC {
238 return Err(SZipError::InvalidFormat(format!(
239 "Entry '{}' is too large to read into memory ({} bytes). \
240 Use read_entry_streaming() for entries larger than 2 GiB.",
241 entry.name, data_size
242 )));
243 }
244
245 let mut compressed_data = vec![0u8; data_size as usize];
247 self.file.read_exact(&mut compressed_data)?;
248
249 #[cfg(feature = "encryption")]
251 let auth_code = if encryption_info.is_some() {
252 let mut ac = vec![0u8; 10];
253 self.file.read_exact(&mut ac)?;
254 Some(ac)
255 } else {
256 None
257 };
258
259 #[cfg(feature = "encryption")]
261 let decryptor_opt = if let Some((strength, salt, pw_verify)) = encryption_info {
262 let password = self.password.as_ref().ok_or_else(|| {
263 SZipError::InvalidFormat("Encrypted entry but no password set".to_string())
264 })?;
265
266 let mut decryptor = AesDecryptor::new(password, strength, &salt, &pw_verify)?;
268
269 decryptor.decrypt(&mut compressed_data)?;
271
272 Some(decryptor)
273 } else {
274 None
275 };
276
277 let data = if entry.compression_method == 8 {
279 let mut decoder = DeflateDecoder::new(&compressed_data[..]);
281 let mut decompressed = Vec::new();
282 decoder.read_to_end(&mut decompressed)?;
283 decompressed
284 } else if entry.compression_method == 0 {
285 compressed_data
287 } else if entry.compression_method == 93 {
288 #[cfg(feature = "zstd-support")]
290 {
291 zstd::decode_all(&compressed_data[..])?
292 }
293 #[cfg(not(feature = "zstd-support"))]
294 {
295 return Err(SZipError::UnsupportedCompression(entry.compression_method));
296 }
297 } else {
298 return Err(SZipError::UnsupportedCompression(entry.compression_method));
299 };
300
301 #[cfg(feature = "encryption")]
303 if let Some(mut decryptor) = decryptor_opt {
304 decryptor.update_hmac(&data);
306
307 if let Some(ac) = auth_code {
309 decryptor.verify_auth_code(&ac)?;
310 }
311 }
312
313 Ok(data)
314 }
315
316 pub fn read_entry_by_name(&mut self, name: &str) -> Result<Vec<u8>> {
318 let entry = self
319 .find_entry(name)
320 .ok_or_else(|| SZipError::EntryNotFound(name.to_string()))?
321 .clone();
322
323 self.read_entry(&entry)
324 }
325
326 pub fn read_entry_streaming_by_name(&mut self, name: &str) -> Result<Box<dyn Read + '_>> {
329 let entry = self
330 .find_entry(name)
331 .ok_or_else(|| SZipError::EntryNotFound(name.to_string()))?
332 .clone();
333
334 self.read_entry_streaming(&entry)
335 }
336
337 pub fn read_entry_streaming(&mut self, entry: &ZipEntry) -> Result<Box<dyn Read + '_>> {
345 #[cfg(feature = "encryption")]
348 if entry.is_encrypted {
349 return Err(SZipError::EncryptionError(
350 "Streaming read is not supported for encrypted entries. \
351 Use read_entry() instead, which decrypts and authenticates the full entry."
352 .to_string(),
353 ));
354 }
355
356 self.file.seek(SeekFrom::Start(entry.offset))?;
358
359 let signature = self.read_u32_le()?;
361 if signature != LOCAL_FILE_HEADER_SIGNATURE {
362 return Err(SZipError::InvalidFormat(
363 "Invalid local file header signature".to_string(),
364 ));
365 }
366
367 self.file.seek(SeekFrom::Current(6))?;
369
370 self.file.seek(SeekFrom::Current(8))?;
372
373 self.file.seek(SeekFrom::Current(8))?;
375
376 let filename_len = self.read_u16_le()? as i64;
378 let extra_len = self.read_u16_le()? as i64;
379
380 self.file
382 .seek(SeekFrom::Current(filename_len + extra_len))?;
383
384 let limited_reader = (&mut self.file).take(entry.compressed_size);
386
387 if entry.compression_method == 8 {
389 Ok(Box::new(DeflateDecoder::new(limited_reader)))
391 } else if entry.compression_method == 0 {
392 Ok(Box::new(limited_reader))
394 } else if entry.compression_method == 93 {
395 #[cfg(feature = "zstd-support")]
397 {
398 Ok(Box::new(zstd::Decoder::new(limited_reader)?))
399 }
400 #[cfg(not(feature = "zstd-support"))]
401 {
402 Err(SZipError::UnsupportedCompression(entry.compression_method))
403 }
404 } else {
405 Err(SZipError::UnsupportedCompression(entry.compression_method))
406 }
407 }
408
409 pub fn read_entry_by_name_streaming(&mut self, name: &str) -> Result<Box<dyn Read + '_>> {
411 let entry = self
412 .find_entry(name)
413 .ok_or_else(|| SZipError::EntryNotFound(name.to_string()))?
414 .clone();
415
416 self.read_entry_streaming(&entry)
417 }
418
419 fn read_central_directory(file: &mut BufReader<File>) -> Result<Vec<ZipEntry>> {
421 let eocd_offset = Self::find_eocd(file)?;
423
424 file.seek(SeekFrom::Start(eocd_offset))?;
426
427 let signature = Self::read_u32_le_static(file)?;
429 if signature != END_OF_CENTRAL_DIRECTORY_SIGNATURE {
430 return Err(SZipError::InvalidFormat(format!(
431 "Invalid end of central directory signature: 0x{:08x}",
432 signature
433 )));
434 }
435
436 file.seek(SeekFrom::Current(4))?;
438
439 let _entries_on_disk = Self::read_u16_le_static(file)?;
441
442 let total_entries_16 = Self::read_u16_le_static(file)?;
446
447 let cd_size_32 = Self::read_u32_le_static(file)?;
449
450 let cd_offset_32 = Self::read_u32_le_static(file)? as u64;
452
453 let mut total_entries = total_entries_16 as usize;
455 let mut cd_offset = cd_offset_32;
456 let _cd_size = cd_size_32 as u64;
457
458 if total_entries_16 == 0xFFFF || cd_size_32 == 0xFFFFFFFF || cd_offset_32 == 0xFFFFFFFF {
459 let (zip64_total_entries, zip64_cd_size, zip64_cd_offset) =
461 Self::read_zip64_eocd(file, eocd_offset)?;
462 total_entries = zip64_total_entries as usize;
463 cd_offset = zip64_cd_offset;
464 let _ = zip64_cd_size;
466 }
467
468 file.seek(SeekFrom::Start(cd_offset))?;
470
471 let mut entries = Vec::with_capacity(total_entries);
473 for _ in 0..total_entries {
474 let signature = Self::read_u32_le_static(file)?;
475 if signature != CENTRAL_DIRECTORY_SIGNATURE {
476 break;
477 }
478
479 file.seek(SeekFrom::Current(4))?;
481
482 #[cfg_attr(not(feature = "encryption"), allow(unused_variables))]
484 let flags = Self::read_u16_le_static(file)?;
485
486 let compression_method = Self::read_u16_le_static(file)?;
487
488 file.seek(SeekFrom::Current(8))?;
490
491 let compressed_size_32 = Self::read_u32_le_static(file)? as u64;
493 let uncompressed_size_32 = Self::read_u32_le_static(file)? as u64;
494 let filename_len = Self::read_u16_le_static(file)? as usize;
495 let extra_len = Self::read_u16_le_static(file)? as usize;
496 let comment_len = Self::read_u16_le_static(file)? as usize;
497
498 file.seek(SeekFrom::Current(8))?;
500
501 let mut offset = Self::read_u32_le_static(file)? as u64;
502
503 let mut filename_buf = vec![0u8; filename_len];
505 file.read_exact(&mut filename_buf)?;
506 let name = String::from_utf8_lossy(&filename_buf).to_string();
507
508 let mut extra_buf = vec![0u8; extra_len];
510 if extra_len > 0 {
511 file.read_exact(&mut extra_buf)?;
512 }
513
514 let mut compressed_size = compressed_size_32;
516 let mut uncompressed_size = uncompressed_size_32;
517
518 if compressed_size_32 == 0xFFFFFFFF
519 || uncompressed_size_32 == 0xFFFFFFFF
520 || offset == 0xFFFFFFFF
521 {
522 let mut i = 0usize;
524 while i + 4 <= extra_buf.len() {
525 let id = u16::from_le_bytes([extra_buf[i], extra_buf[i + 1]]);
526 let data_len =
527 u16::from_le_bytes([extra_buf[i + 2], extra_buf[i + 3]]) as usize;
528 i += 4;
529 if i + data_len > extra_buf.len() {
530 break;
531 }
532 if id == 0x0001 {
533 let mut cursor = 0usize;
535 if uncompressed_size_32 == 0xFFFFFFFF && cursor + 8 <= data_len {
537 uncompressed_size = u64::from_le_bytes([
538 extra_buf[i + cursor],
539 extra_buf[i + cursor + 1],
540 extra_buf[i + cursor + 2],
541 extra_buf[i + cursor + 3],
542 extra_buf[i + cursor + 4],
543 extra_buf[i + cursor + 5],
544 extra_buf[i + cursor + 6],
545 extra_buf[i + cursor + 7],
546 ]);
547 cursor += 8;
548 }
549 if compressed_size_32 == 0xFFFFFFFF && cursor + 8 <= data_len {
551 compressed_size = u64::from_le_bytes([
552 extra_buf[i + cursor],
553 extra_buf[i + cursor + 1],
554 extra_buf[i + cursor + 2],
555 extra_buf[i + cursor + 3],
556 extra_buf[i + cursor + 4],
557 extra_buf[i + cursor + 5],
558 extra_buf[i + cursor + 6],
559 extra_buf[i + cursor + 7],
560 ]);
561 cursor += 8;
562 }
563 if offset == 0xFFFFFFFF && cursor + 8 <= data_len {
565 offset = u64::from_le_bytes([
566 extra_buf[i + cursor],
567 extra_buf[i + cursor + 1],
568 extra_buf[i + cursor + 2],
569 extra_buf[i + cursor + 3],
570 extra_buf[i + cursor + 4],
571 extra_buf[i + cursor + 5],
572 extra_buf[i + cursor + 6],
573 extra_buf[i + cursor + 7],
574 ]);
575 }
576 break;
578 }
579 i += data_len;
580 }
581 }
582
583 if comment_len > 0 {
585 file.seek(SeekFrom::Current(comment_len as i64))?;
586 }
587
588 entries.push(ZipEntry {
589 name,
590 compressed_size,
591 uncompressed_size,
592 compression_method,
593 offset,
594 #[cfg(feature = "encryption")]
595 is_encrypted: (flags & 0x01) != 0,
596 });
597 }
598
599 Ok(entries)
600 }
601
602 fn read_zip64_eocd(file: &mut BufReader<File>, eocd_offset: u64) -> Result<(u64, u64, u64)> {
604 let search_start = eocd_offset.saturating_sub(65557);
606 file.seek(SeekFrom::Start(search_start))?;
607 let mut buffer = Vec::new();
608 file.read_to_end(&mut buffer)?;
609
610 let mut locator_pos: Option<usize> = None;
611 for i in (0..buffer.len().saturating_sub(3)).rev() {
612 if buffer[i] == 0x50
613 && buffer[i + 1] == 0x4b
614 && buffer[i + 2] == 0x06
615 && buffer[i + 3] == 0x07
616 {
617 locator_pos = Some(i);
618 break;
619 }
620 }
621
622 let locator_pos = locator_pos
623 .ok_or_else(|| SZipError::InvalidFormat("ZIP64 EOCD locator not found".to_string()))?;
624
625 let rel_off_bytes = &buffer[locator_pos + 8..locator_pos + 16];
628 let zip64_eocd_offset = u64::from_le_bytes([
629 rel_off_bytes[0],
630 rel_off_bytes[1],
631 rel_off_bytes[2],
632 rel_off_bytes[3],
633 rel_off_bytes[4],
634 rel_off_bytes[5],
635 rel_off_bytes[6],
636 rel_off_bytes[7],
637 ]);
638
639 file.seek(SeekFrom::Start(zip64_eocd_offset))?;
641
642 let sig = Self::read_u32_le_static(file)?;
643 if sig != ZIP64_END_OF_CENTRAL_DIRECTORY_SIGNATURE {
644 return Err(SZipError::InvalidFormat(format!(
645 "Invalid ZIP64 EOCD signature: 0x{:08x}",
646 sig
647 )));
648 }
649
650 let _size = {
652 let mut buf = [0u8; 8];
653 file.read_exact(&mut buf)?;
654 u64::from_le_bytes(buf)
655 };
656
657 file.seek(SeekFrom::Current(12))?;
659
660 let total_entries = {
662 let mut buf = [0u8; 8];
663 file.read_exact(&mut buf)?;
664 u64::from_le_bytes(buf)
665 };
666
667 {
669 let mut buf = [0u8; 8];
670 file.read_exact(&mut buf)?;
671 }
673
674 let cd_size = {
676 let mut buf = [0u8; 8];
677 file.read_exact(&mut buf)?;
678 u64::from_le_bytes(buf)
679 };
680
681 let cd_offset = {
683 let mut buf = [0u8; 8];
684 file.read_exact(&mut buf)?;
685 u64::from_le_bytes(buf)
686 };
687
688 Ok((total_entries, cd_size, cd_offset))
689 }
690
691 fn find_eocd(file: &mut BufReader<File>) -> Result<u64> {
693 let file_size = file.seek(SeekFrom::End(0))?;
694
695 let search_start = file_size.saturating_sub(65557);
697 file.seek(SeekFrom::Start(search_start))?;
698
699 let mut buffer = Vec::new();
700 file.read_to_end(&mut buffer)?;
701
702 for i in (0..buffer.len().saturating_sub(3)).rev() {
704 if buffer[i] == 0x50
705 && buffer[i + 1] == 0x4b
706 && buffer[i + 2] == 0x05
707 && buffer[i + 3] == 0x06
708 {
709 return Ok(search_start + i as u64);
710 }
711 }
712
713 Err(SZipError::InvalidFormat(
714 "End of central directory not found".to_string(),
715 ))
716 }
717
718 fn read_u16_le(&mut self) -> Result<u16> {
719 let mut buf = [0u8; 2];
720 self.file.read_exact(&mut buf)?;
721 Ok(u16::from_le_bytes(buf))
722 }
723
724 fn read_u32_le(&mut self) -> Result<u32> {
725 let mut buf = [0u8; 4];
726 self.file.read_exact(&mut buf)?;
727 Ok(u32::from_le_bytes(buf))
728 }
729
730 fn read_u16_le_static(file: &mut BufReader<File>) -> Result<u16> {
731 let mut buf = [0u8; 2];
732 file.read_exact(&mut buf)?;
733 Ok(u16::from_le_bytes(buf))
734 }
735
736 fn read_u32_le_static(file: &mut BufReader<File>) -> Result<u32> {
737 let mut buf = [0u8; 4];
738 file.read_exact(&mut buf)?;
739 Ok(u32::from_le_bytes(buf))
740 }
741
742 #[cfg(feature = "encryption")]
744 #[allow(clippy::type_complexity)]
745 fn parse_aes_extra_field(
746 &mut self,
747 extra_len: usize,
748 ) -> Result<Option<(AesStrength, Vec<u8>, [u8; 2])>> {
749 if extra_len == 0 {
750 return Ok(None);
751 }
752
753 let mut extra_buf = vec![0u8; extra_len];
754 self.file.read_exact(&mut extra_buf)?;
755
756 let mut i = 0usize;
758 while i + 4 <= extra_buf.len() {
759 let id = u16::from_le_bytes([extra_buf[i], extra_buf[i + 1]]);
760 let data_len = u16::from_le_bytes([extra_buf[i + 2], extra_buf[i + 3]]) as usize;
761 i += 4;
762
763 if i + data_len > extra_buf.len() {
764 break;
765 }
766
767 if id == 0x9901 {
768 if data_len < 7 {
772 return Err(SZipError::InvalidFormat(
773 "Invalid AES extra field".to_string(),
774 ));
775 }
776
777 let strength_code = extra_buf[i + 4]; let strength = match strength_code {
780 0x03 => AesStrength::Aes256,
781 _ => {
782 return Err(SZipError::InvalidFormat(format!(
783 "Unsupported AES strength: {}",
784 strength_code
785 )))
786 }
787 };
788
789 let salt_size = strength.salt_size();
792
793 let mut salt = vec![0u8; salt_size];
794 self.file.read_exact(&mut salt)?;
795
796 let mut pw_verify = [0u8; 2];
797 self.file.read_exact(&mut pw_verify)?;
798
799 return Ok(Some((strength, salt, pw_verify)));
800 }
801
802 i += data_len;
803 }
804
805 Ok(None)
806 }
807}