1use std::io::{self, Read, Seek, SeekFrom};
9use std::path::PathBuf;
10
11use crate::bytes::Reader;
12use crate::codec::Decoder;
13use crate::crypto::{read_strong_header, AesInfo, AesReader, StrongAesReader, ZipCryptoReader};
14use crate::{FormatError, ZipCoreError};
15
16const EOCD_SIG: u32 = 0x0605_4b50;
17const CD_HEADER_SIG: u32 = 0x0201_4b50;
18const LFH_SIG: u32 = 0x0403_4b50;
19const ZIP64_EOCD_SIG: u32 = 0x0606_4b50;
20const ARCHIVE_SIG_SIG: u32 = 0x0505_4b50;
22const ZIP64_LOCATOR_SIG: u32 = 0x0706_4b50;
23const ZIP64_EXTRA_ID: u16 = 0x0001;
25const U32_SENTINEL: u32 = 0xFFFF_FFFF;
27const U16_SENTINEL: u16 = 0xFFFF;
29
30const EOCD_MIN: usize = 22;
32const EOCD_SCAN_MAX: usize = EOCD_MIN + u16::MAX as usize;
34const ZIP64_LOCATOR_LEN: usize = 20;
36const LFH_FIXED: usize = 30;
38const MAX_ENTRIES: usize = 16_000_000;
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum CompressionMethod {
45 Stored,
47 Deflated,
49 Deflate64,
51 Bzip2,
53 Lzma,
55 Zstd,
57 Xz,
59 Shrunk,
61 Reduced,
63 Imploded,
65 DclImploded,
67 IbmCmpsc,
69 IbmTerse,
71 IbmLz77,
73 Mp3,
75 Jpeg,
77 WavPack,
79 Ppmd,
81 Unknown(u16),
83}
84
85impl CompressionMethod {
86 pub(crate) fn from_u16(raw: u16) -> Self {
87 match raw {
88 0 => Self::Stored,
89 8 => Self::Deflated,
90 9 => Self::Deflate64,
91 12 => Self::Bzip2,
92 14 => Self::Lzma,
93 93 => Self::Zstd,
94 95 => Self::Xz,
95 1 => Self::Shrunk,
96 2..=5 => Self::Reduced,
97 6 => Self::Imploded,
98 10 => Self::DclImploded,
99 16 => Self::IbmCmpsc,
100 18 => Self::IbmTerse,
101 19 => Self::IbmLz77,
102 94 => Self::Mp3,
103 96 => Self::Jpeg,
104 97 => Self::WavPack,
105 98 => Self::Ppmd,
106 other => Self::Unknown(other),
107 }
108 }
109}
110
111#[derive(Debug, Clone)]
113pub(crate) struct CentralEntry {
114 pub(crate) name: String,
115 pub(crate) method: CompressionMethod,
116 pub(crate) flags: u16,
117 pub(crate) crc32: u32,
118 pub(crate) compressed_size: u64,
119 pub(crate) uncompressed_size: u64,
120 pub(crate) lfh_offset: u64,
121 pub(crate) last_mod_time: u16,
124 pub(crate) aes: Option<AesInfo>,
126 pub(crate) disk_start: u16,
128 pub(crate) extra: ExtraFields,
130}
131
132impl CentralEntry {
133 fn is_dir(&self) -> bool {
134 self.name.ends_with('/') || self.name.ends_with('\\')
135 }
136}
137
138#[derive(Debug, Clone)]
141pub struct ArchiveSummary {
142 pub file_len: u64,
144 pub central_dir_offset: u64,
146 pub central_dir_size: u64,
148 pub eocd_end_offset: u64,
151 pub comment_len: u16,
153 pub disk_number: u32,
155 pub cd_start_disk: u32,
157 pub archive_signature_len: Option<u16>,
160}
161
162pub struct ZipArchive<R> {
164 reader: R,
165 entries: Vec<CentralEntry>,
166 summary: ArchiveSummary,
167}
168
169impl<R: Read + Seek> ZipArchive<R> {
170 pub fn new(mut reader: R) -> Result<Self, ZipCoreError> {
172 let file_len = reader.seek(SeekFrom::End(0))?;
173 let (entries, summary) = parse_central_directory(&mut reader, file_len)?;
174 Ok(Self {
175 reader,
176 entries,
177 summary,
178 })
179 }
180
181 pub fn summary(&self) -> &ArchiveSummary {
183 &self.summary
184 }
185
186 pub fn len(&self) -> usize {
188 self.entries.len()
189 }
190
191 pub fn is_empty(&self) -> bool {
193 self.entries.is_empty()
194 }
195
196 pub fn file_names(&self) -> impl Iterator<Item = &str> {
198 self.entries.iter().map(|e| e.name.as_str())
199 }
200
201 pub fn by_index(&mut self, i: usize) -> Result<ZipFile<'_>, ZipCoreError> {
203 let meta = self
204 .entries
205 .get(i)
206 .ok_or(ZipCoreError::IndexOutOfBounds(i))?
207 .clone();
208 self.open(meta)
209 }
210
211 pub fn by_name(&mut self, name: &str) -> Result<ZipFile<'_>, ZipCoreError> {
213 let meta = self
214 .entries
215 .iter()
216 .find(|e| e.name == name)
217 .ok_or_else(|| ZipCoreError::EntryNotFound(name.to_string()))?
218 .clone();
219 self.open(meta)
220 }
221
222 pub fn by_index_decrypt(
225 &mut self,
226 i: usize,
227 password: &[u8],
228 ) -> Result<ZipFile<'_>, ZipCoreError> {
229 let meta = self
230 .entries
231 .get(i)
232 .ok_or(ZipCoreError::IndexOutOfBounds(i))?
233 .clone();
234 self.open_decrypt(meta, password)
235 }
236
237 pub fn by_name_decrypt(
239 &mut self,
240 name: &str,
241 password: &[u8],
242 ) -> Result<ZipFile<'_>, ZipCoreError> {
243 let meta = self
244 .entries
245 .iter()
246 .find(|e| e.name == name)
247 .ok_or_else(|| ZipCoreError::EntryNotFound(name.to_string()))?
248 .clone();
249 self.open_decrypt(meta, password)
250 }
251
252 pub fn structural_view(&mut self) -> Result<Vec<EntryLayout>, ZipCoreError> {
257 let metas = self.entries.clone();
258 let mut out = Vec::with_capacity(metas.len());
259 for (index, m) in metas.iter().enumerate() {
260 let (local, data_start) = read_lfh_fields(&mut self.reader, m.lfh_offset)?;
261 out.push(EntryLayout {
262 index,
263 lfh_offset: m.lfh_offset,
264 data_start,
265 central: HeaderFields {
266 name: m.name.clone(),
267 method: m.method,
268 flags: m.flags,
269 crc32: m.crc32,
270 compressed_size: m.compressed_size,
271 uncompressed_size: m.uncompressed_size,
272 },
273 local,
274 extra: m.extra.clone(),
275 });
276 }
277 Ok(out)
278 }
279
280 fn open(&mut self, meta: CentralEntry) -> Result<ZipFile<'_>, ZipCoreError> {
281 check_local_disk(&meta, self.summary.disk_number, self.summary.cd_start_disk)?;
282 if meta.flags & 0x0001 != 0 {
283 return Err(ZipCoreError::EncryptedNoPassword(meta.name.clone()));
284 }
285 let (_local, data_start) = read_lfh_fields(&mut self.reader, meta.lfh_offset)?;
286 self.reader.seek(SeekFrom::Start(data_start))?;
287 let limited: Box<dyn Read + '_> = Box::new((&mut self.reader).take(meta.compressed_size));
288 let decoder = Decoder::new(meta.method, meta.uncompressed_size, limited)?;
289 Ok(ZipFile {
290 data_start,
291 decoder,
292 hasher: crc32fast::Hasher::new(),
293 bytes_out: 0,
294 verified: false,
295 verify_crc: true,
296 meta,
297 })
298 }
299
300 fn open_decrypt(
301 &mut self,
302 meta: CentralEntry,
303 password: &[u8],
304 ) -> Result<ZipFile<'_>, ZipCoreError> {
305 check_local_disk(&meta, self.summary.disk_number, self.summary.cd_start_disk)?;
306 if meta.flags & 0x0001 == 0 && meta.aes.is_none() {
308 return self.open(meta);
309 }
310 if meta.flags & 0x2000 != 0 {
313 return Err(ZipCoreError::UnsupportedEncryption {
314 entry: meta.name,
315 reason: "masked / central-directory encryption (GP flag bit 13)".to_string(),
316 });
317 }
318 if meta.aes.is_none() && meta.flags & 0x0040 != 0 {
323 return self.open_strong_decrypt(meta, password);
324 }
325 let (_local, data_start) = read_lfh_fields(&mut self.reader, meta.lfh_offset)?;
326 self.reader.seek(SeekFrom::Start(data_start))?;
327 let take = (&mut self.reader).take(meta.compressed_size);
328 let (reader, method, verify_crc): (Box<dyn Read + '_>, CompressionMethod, bool) =
329 if let Some(aes) = meta.aes {
330 let r = AesReader::new(take, password, aes, meta.compressed_size, &meta.name)?;
331 (
334 Box::new(r),
335 CompressionMethod::from_u16(aes.actual_method),
336 !aes.is_ae2,
337 )
338 } else {
339 let check = zipcrypto_check_byte(meta.flags, meta.crc32, meta.last_mod_time);
342 let r = ZipCryptoReader::new(take, password, check, &meta.name)?;
343 (Box::new(r), meta.method, true)
344 };
345 let decoder = Decoder::new(method, meta.uncompressed_size, reader)?;
346 Ok(ZipFile {
347 data_start,
348 decoder,
349 hasher: crc32fast::Hasher::new(),
350 bytes_out: 0,
351 verified: false,
352 verify_crc,
353 meta,
354 })
355 }
356
357 fn open_strong_decrypt(
364 &mut self,
365 meta: CentralEntry,
366 password: &[u8],
367 ) -> Result<ZipFile<'_>, ZipCoreError> {
368 let (_local, data_start) = read_lfh_fields(&mut self.reader, meta.lfh_offset)?;
369 self.reader.seek(SeekFrom::Start(data_start))?;
370 let mut take = (&mut self.reader).take(meta.compressed_size);
371 let hdr = read_strong_header(
372 &mut take,
373 password,
374 meta.crc32,
375 meta.uncompressed_size,
376 &meta.name,
377 )?;
378 let enc_len = meta.compressed_size.saturating_sub(hdr.header_len);
384 let strong = StrongAesReader::new(take, &hdr.file_key, &hdr.iv, enc_len, &meta.name)?;
385 let reader: Box<dyn Read + '_> = if meta.method == CompressionMethod::Stored {
389 Box::new(strong.take(meta.uncompressed_size))
390 } else {
391 Box::new(strong)
392 };
393 let decoder = Decoder::new(meta.method, meta.uncompressed_size, reader)?;
394 Ok(ZipFile {
395 data_start,
396 decoder,
397 hasher: crc32fast::Hasher::new(),
398 bytes_out: 0,
399 verified: false,
400 verify_crc: true,
401 meta,
402 })
403 }
404}
405
406#[derive(Debug, Clone, PartialEq, Eq)]
409pub struct HeaderFields {
410 pub name: String,
412 pub method: CompressionMethod,
414 pub flags: u16,
416 pub crc32: u32,
418 pub compressed_size: u64,
420 pub uncompressed_size: u64,
422}
423
424#[derive(Debug, Clone)]
427pub struct EntryLayout {
428 pub index: usize,
430 pub lfh_offset: u64,
432 pub data_start: u64,
434 pub central: HeaderFields,
436 pub local: HeaderFields,
438 pub extra: ExtraFields,
440}
441
442#[derive(Debug, Clone, Default, PartialEq, Eq)]
447pub struct ExtraFields {
448 pub ntfs_mtime: Option<u64>,
450 pub ntfs_atime: Option<u64>,
452 pub ntfs_ctime: Option<u64>,
454 pub unix_mtime: Option<i32>,
456 pub unix_atime: Option<i32>,
458 pub unix_ctime: Option<i32>,
460 pub unicode_path: Option<String>,
462 pub unicode_comment: Option<String>,
464}
465
466fn read_lfh_fields<R: Read + Seek>(
470 reader: &mut R,
471 lfh_offset: u64,
472) -> Result<(HeaderFields, u64), ZipCoreError> {
473 reader.seek(SeekFrom::Start(lfh_offset))?;
474 let mut fixed = [0u8; LFH_FIXED];
475 reader.read_exact(&mut fixed)?;
476 let mut r = Reader::new(&fixed);
477 if r.u32()? != LFH_SIG {
478 return Err(FormatError::BadSignature {
479 what: "local file header",
480 offset: lfh_offset,
481 }
482 .into());
483 }
484 let _version_needed = r.u16()?;
485 let flags = r.u16()?;
486 let method = CompressionMethod::from_u16(r.u16()?);
487 let _mod_time = r.u16()?;
488 let _mod_date = r.u16()?;
489 let crc32 = r.u32()?;
490 let compressed_size = u64::from(r.u32()?);
491 let uncompressed_size = u64::from(r.u32()?);
492 let name_len = usize::from(r.u16()?);
493 let extra_len = usize::from(r.u16()?);
494
495 let mut name_buf = vec![0u8; name_len];
496 reader.read_exact(&mut name_buf)?;
497 let name = decode_name(&name_buf, flags);
498 let data_start = lfh_offset + LFH_FIXED as u64 + name_len as u64 + extra_len as u64;
499
500 Ok((
501 HeaderFields {
502 name,
503 method,
504 flags,
505 crc32,
506 compressed_size,
507 uncompressed_size,
508 },
509 data_start,
510 ))
511}
512
513struct Eocd32 {
516 disk_number: u16,
517 cd_start_disk: u16,
518 total_entries: u16,
519 cd_size: u32,
520 cd_offset: u32,
521 comment_len: u16,
522}
523
524fn parse_central_directory<R: Read + Seek>(
525 reader: &mut R,
526 file_len: u64,
527) -> Result<(Vec<CentralEntry>, ArchiveSummary), ZipCoreError> {
528 let scan_len = file_len.min(EOCD_SCAN_MAX as u64);
529 if scan_len < EOCD_MIN as u64 {
530 return Err(FormatError::NoEocd.into());
531 }
532 let scan_start = file_len - scan_len;
533 reader.seek(SeekFrom::Start(scan_start))?;
534 let mut tail = vec![0u8; scan_len as usize];
535 reader.read_exact(&mut tail)?;
536
537 let eocd_rel = find_eocd(&tail).ok_or(FormatError::NoEocd)?;
538 let eocd = parse_eocd(&tail[eocd_rel..])?;
539 let eocd_end_offset =
542 scan_start + eocd_rel as u64 + EOCD_MIN as u64 + u64::from(eocd.comment_len);
543
544 let is_zip64 = eocd.cd_offset == U32_SENTINEL
547 || eocd.cd_size == U32_SENTINEL
548 || eocd.total_entries == U16_SENTINEL;
549 let (cd_offset, cd_size, total_entries, disk_number, cd_start_disk) = if is_zip64 {
550 resolve_zip64_eocd(reader, &tail, eocd_rel)?
551 } else {
552 (
553 u64::from(eocd.cd_offset),
554 u64::from(eocd.cd_size),
555 usize::from(eocd.total_entries),
556 u32::from(eocd.disk_number),
557 u32::from(eocd.cd_start_disk),
558 )
559 };
560
561 let eocd_pos = scan_start + eocd_rel as u64;
570 let prefix = if is_zip64 {
571 0
572 } else {
573 match eocd_pos.checked_sub(cd_offset.saturating_add(cd_size)) {
574 Some(n)
575 if n > 0
576 && !cd_header_at(reader, cd_offset)
577 && cd_header_at(reader, cd_offset + n) =>
578 {
579 n
580 }
581 _ => 0,
582 }
583 };
584 let actual_cd_offset = cd_offset + prefix;
585
586 match actual_cd_offset.checked_add(cd_size) {
587 Some(end) if end <= file_len => {}
588 _ => return Err(FormatError::CentralDirOutOfRange { cd_offset, cd_size }.into()),
589 }
590 if total_entries > MAX_ENTRIES {
591 return Err(FormatError::TooManyEntries(total_entries).into());
592 }
593
594 reader.seek(SeekFrom::Start(actual_cd_offset))?;
595 let mut cd = vec![0u8; cd_size as usize];
596 reader.read_exact(&mut cd)?;
597
598 let (mut entries, cd_consumed) = parse_cd_entries(&cd, total_entries)?;
599 if prefix > 0 {
602 for e in &mut entries {
603 e.lfh_offset += prefix;
604 }
605 }
606
607 let archive_signature_len = {
615 let sig = ARCHIVE_SIG_SIG.to_le_bytes();
616 let trailing = &cd[cd_consumed..];
617 if trailing.len() >= 6 && trailing[..4] == sig {
618 Some(u16::from_le_bytes([trailing[4], trailing[5]]))
619 } else if trailing.is_empty() {
620 let mut hdr = [0u8; 6];
623 match reader.read_exact(&mut hdr) {
624 Ok(()) if hdr[..4] == sig => Some(u16::from_le_bytes([hdr[4], hdr[5]])),
625 _ => None,
626 }
627 } else {
628 None
629 }
630 };
631 let summary = ArchiveSummary {
632 file_len,
633 central_dir_offset: actual_cd_offset,
634 central_dir_size: cd_size,
635 eocd_end_offset,
636 comment_len: eocd.comment_len,
637 disk_number,
638 cd_start_disk,
639 archive_signature_len,
640 };
641 Ok((entries, summary))
642}
643
644fn cd_header_at<R: Read + Seek>(reader: &mut R, offset: u64) -> bool {
647 if reader.seek(SeekFrom::Start(offset)).is_err() {
648 return false; }
650 let mut sig = [0u8; 4];
651 match reader.read_exact(&mut sig) {
652 Ok(()) => u32::from_le_bytes(sig) == CD_HEADER_SIG,
653 Err(_) => false, }
655}
656
657fn find_eocd(tail: &[u8]) -> Option<usize> {
659 if tail.len() < EOCD_MIN {
660 return None; }
662 let sig = EOCD_SIG.to_le_bytes();
663 (0..=tail.len() - EOCD_MIN)
665 .rev()
666 .find(|&i| tail[i..i + 4] == sig)
667}
668
669fn parse_eocd(buf: &[u8]) -> Result<Eocd32, ZipCoreError> {
671 let mut r = Reader::new(buf);
672 if r.u32()? != EOCD_SIG {
673 return Err(FormatError::NoEocd.into()); }
675 let disk_number = r.u16()?;
676 let cd_start_disk = r.u16()?;
677 let _entries_this_disk = r.u16()?;
678 let total_entries = r.u16()?;
679 let cd_size = r.u32()?;
680 let cd_offset = r.u32()?;
681 let comment_len = r.u16()?;
682 Ok(Eocd32 {
683 disk_number,
684 cd_start_disk,
685 total_entries,
686 cd_size,
687 cd_offset,
688 comment_len,
689 })
690}
691
692fn resolve_zip64_eocd<R: Read + Seek>(
696 reader: &mut R,
697 tail: &[u8],
698 eocd_rel: usize,
699) -> Result<(u64, u64, usize, u32, u32), ZipCoreError> {
700 if eocd_rel < ZIP64_LOCATOR_LEN {
701 return Err(FormatError::Zip64Unsupported.into());
702 }
703 let mut loc = Reader::new(&tail[eocd_rel - ZIP64_LOCATOR_LEN..eocd_rel]);
704 if loc.u32()? != ZIP64_LOCATOR_SIG {
705 return Err(FormatError::Zip64Unsupported.into());
706 }
707 let _disk = loc.u32()?;
708 let z64_eocd_offset = loc.u64()?;
709
710 reader.seek(SeekFrom::Start(z64_eocd_offset))?;
711 let mut rec = [0u8; 56];
712 reader.read_exact(&mut rec)?;
713 let mut r = Reader::new(&rec);
714 if r.u32()? != ZIP64_EOCD_SIG {
715 return Err(FormatError::BadSignature {
716 what: "Zip64 EOCD record",
717 offset: z64_eocd_offset,
718 }
719 .into());
720 }
721 let _record_size = r.u64()?;
722 let _version_made_by = r.u16()?;
723 let _version_needed = r.u16()?;
724 let disk_number = r.u32()?;
725 let cd_start_disk = r.u32()?;
726 let _entries_this_disk = r.u64()?;
727 let total_entries = r.u64()?;
728 let cd_size = r.u64()?;
729 let cd_offset = r.u64()?;
730 let total =
731 usize::try_from(total_entries).map_err(|_| FormatError::TooManyEntries(usize::MAX))?;
732 Ok((cd_offset, cd_size, total, disk_number, cd_start_disk))
733}
734
735fn parse_cd_entries(
740 cd: &[u8],
741 total_entries: usize,
742) -> Result<(Vec<CentralEntry>, usize), ZipCoreError> {
743 let mut r = Reader::new(cd);
744 let mut entries = Vec::new();
745 for _ in 0..total_entries {
746 if r.remaining() < 46 {
747 return Err(FormatError::Truncated.into());
748 }
749 if r.u32()? != CD_HEADER_SIG {
750 return Err(FormatError::BadSignature {
751 what: "central directory header",
752 offset: (cd.len() - r.remaining()) as u64,
753 }
754 .into());
755 }
756 let _version_made_by = r.u16()?;
757 let _version_needed = r.u16()?;
758 let flags = r.u16()?;
759 let method_raw = r.u16()?;
760 let method = CompressionMethod::from_u16(method_raw);
761 let last_mod_time = r.u16()?;
762 let _mod_date = r.u16()?;
763 let crc32 = r.u32()?;
764 let compressed_size32 = r.u32()?;
765 let uncompressed_size32 = r.u32()?;
766 let name_len = usize::from(r.u16()?);
767 let extra_len = usize::from(r.u16()?);
768 let comment_len = usize::from(r.u16()?);
769 let disk_start = r.u16()?;
770 let _internal_attrs = r.u16()?;
771 let _external_attrs = r.u32()?;
772 let lfh_offset32 = r.u32()?;
773
774 let name_bytes = r.take(name_len)?;
775 let extra = r.take(extra_len)?;
776 let _comment = r.take(comment_len)?;
777
778 let mut uncompressed_size = u64::from(uncompressed_size32);
782 let mut compressed_size = u64::from(compressed_size32);
783 let mut lfh_offset = u64::from(lfh_offset32);
784 if uncompressed_size32 == U32_SENTINEL
785 || compressed_size32 == U32_SENTINEL
786 || lfh_offset32 == U32_SENTINEL
787 {
788 apply_zip64_extra(
789 extra,
790 uncompressed_size32 == U32_SENTINEL,
791 compressed_size32 == U32_SENTINEL,
792 lfh_offset32 == U32_SENTINEL,
793 &mut uncompressed_size,
794 &mut compressed_size,
795 &mut lfh_offset,
796 )?;
797 }
798
799 let name = decode_name(name_bytes, flags);
803 let aes = if method_raw == 99 {
806 parse_aes_extra(extra)
807 } else {
808 None
809 };
810
811 entries.push(CentralEntry {
812 name,
813 method,
814 flags,
815 crc32,
816 compressed_size,
817 uncompressed_size,
818 lfh_offset,
819 last_mod_time,
820 aes,
821 disk_start,
822 extra: parse_extra_fields(extra),
823 });
824 }
825 let consumed = cd.len() - r.remaining();
826 Ok((entries, consumed))
827}
828
829fn apply_zip64_extra(
834 extra: &[u8],
835 need_uncompressed: bool,
836 need_compressed: bool,
837 need_offset: bool,
838 uncompressed_size: &mut u64,
839 compressed_size: &mut u64,
840 lfh_offset: &mut u64,
841) -> Result<(), ZipCoreError> {
842 let mut r = Reader::new(extra);
843 while r.remaining() >= 4 {
844 let id = r.u16()?;
845 let size = usize::from(r.u16()?);
846 if id == ZIP64_EXTRA_ID {
847 let mut z = Reader::new(r.take(size)?);
848 if need_uncompressed {
849 *uncompressed_size = z.u64()?;
850 }
851 if need_compressed {
852 *compressed_size = z.u64()?;
853 }
854 if need_offset {
855 *lfh_offset = z.u64()?;
856 }
857 return Ok(());
858 }
859 r.skip(size)?;
860 }
861 Err(FormatError::Zip64Inconsistent.into())
862}
863
864fn parse_aes_extra(extra: &[u8]) -> Option<AesInfo> {
867 let mut r = Reader::new(extra);
868 while r.remaining() >= 4 {
869 let id = r.u16().ok()?;
870 let size = usize::from(r.u16().ok()?);
871 if id == 0x9901 {
872 let data = r.take(size).ok()?;
873 let mut d = Reader::new(data);
874 let version = d.u16().ok()?; let _vendor = d.u16().ok()?; let strength = d.take(1).ok()?[0];
877 let actual_method = d.u16().ok()?;
878 return Some(AesInfo {
879 strength,
880 actual_method,
881 is_ae2: version == 2,
882 });
883 }
884 r.skip(size).ok()?;
885 }
886 None
887}
888
889fn parse_extra_fields(extra: &[u8]) -> ExtraFields {
891 let mut out = ExtraFields::default();
892 let mut r = Reader::new(extra);
893 while r.remaining() >= 4 {
894 let (Ok(id), Ok(size)) = (r.u16(), r.u16()) else {
895 break; };
897 let Ok(data) = r.take(usize::from(size)) else {
898 break;
899 };
900 match id {
901 0x000a => parse_ntfs_times(data, &mut out),
902 0x5455 => parse_unix_times(data, &mut out),
903 0x7075 => out.unicode_path = parse_unicode_extra(data),
904 0x6375 => out.unicode_comment = parse_unicode_extra(data),
905 _ => {}
906 }
907 }
908 out
909}
910
911fn parse_ntfs_times(data: &[u8], out: &mut ExtraFields) {
914 let mut r = Reader::new(data);
915 let _ = r.u32(); while r.remaining() >= 4 {
917 let (Ok(tag), Ok(tsize)) = (r.u16(), r.u16()) else {
918 break; };
920 let Ok(tdata) = r.take(usize::from(tsize)) else {
921 break;
922 };
923 if tag == 0x0001 {
924 let mut s = Reader::new(tdata);
925 if let (Ok(m), Ok(a), Ok(c)) = (s.u64(), s.u64(), s.u64()) {
926 out.ntfs_mtime = Some(m);
927 out.ntfs_atime = Some(a);
928 out.ntfs_ctime = Some(c);
929 }
930 }
931 }
932}
933
934fn parse_unix_times(data: &[u8], out: &mut ExtraFields) {
937 let mut r = Reader::new(data);
938 let Ok(flag_byte) = r.take(1) else {
939 return;
940 };
941 let flags = flag_byte[0];
942 if flags & 0x01 != 0 {
943 out.unix_mtime = take_i32le(&mut r);
944 }
945 if flags & 0x02 != 0 {
946 out.unix_atime = take_i32le(&mut r);
947 }
948 if flags & 0x04 != 0 {
949 out.unix_ctime = take_i32le(&mut r);
950 }
951}
952
953fn take_i32le(r: &mut Reader) -> Option<i32> {
954 r.take(4)
955 .ok()
956 .map(|b| i32::from_le_bytes([b[0], b[1], b[2], b[3]]))
957}
958
959fn parse_unicode_extra(data: &[u8]) -> Option<String> {
963 if data.len() < 5 {
964 return None;
965 }
966 String::from_utf8(data[5..].to_vec()).ok()
967}
968
969fn zipcrypto_check_byte(flags: u16, crc32: u32, last_mod_time: u16) -> u8 {
973 if flags & 0x0008 != 0 {
974 (last_mod_time >> 8) as u8
975 } else {
976 (crc32 >> 24) as u8
977 }
978}
979
980fn decode_name(bytes: &[u8], flags: u16) -> String {
983 if flags & 0x0800 != 0 || bytes.is_ascii() {
985 return String::from_utf8_lossy(bytes).into_owned();
986 }
987 bytes.iter().map(|&b| crate::cp437::decode(b)).collect()
988}
989
990pub struct ZipFile<'a> {
993 meta: CentralEntry,
994 data_start: u64,
995 decoder: Decoder<Box<dyn Read + 'a>>,
996 hasher: crc32fast::Hasher,
997 bytes_out: u64,
998 verified: bool,
999 verify_crc: bool,
1002}
1003
1004impl ZipFile<'_> {
1005 pub fn name(&self) -> &str {
1007 &self.meta.name
1008 }
1009
1010 pub fn compression(&self) -> CompressionMethod {
1012 self.meta.method
1013 }
1014
1015 pub fn size(&self) -> u64 {
1017 self.meta.uncompressed_size
1018 }
1019
1020 pub fn compressed_size(&self) -> u64 {
1022 self.meta.compressed_size
1023 }
1024
1025 pub fn crc32(&self) -> u32 {
1027 self.meta.crc32
1028 }
1029
1030 pub fn data_start(&self) -> u64 {
1033 self.data_start
1034 }
1035
1036 pub fn flags(&self) -> u16 {
1038 self.meta.flags
1039 }
1040
1041 pub fn is_dir(&self) -> bool {
1043 self.meta.is_dir()
1044 }
1045
1046 pub fn enclosed_name(&self) -> Option<PathBuf> {
1051 enclosed_name(&self.meta.name)
1052 }
1053}
1054
1055fn check_local_disk(
1062 meta: &CentralEntry,
1063 this_disk: u32,
1064 cd_start_disk: u32,
1065) -> Result<(), ZipCoreError> {
1066 let disk = if meta.disk_start != 0 {
1067 u32::from(meta.disk_start)
1068 } else if cd_start_disk != 0 {
1069 cd_start_disk
1070 } else if this_disk != 0 {
1071 this_disk
1072 } else {
1073 return Ok(());
1074 };
1075 Err(ZipCoreError::SpannedArchive {
1076 entry: meta.name.clone(),
1077 disk,
1078 })
1079}
1080
1081fn enclosed_name(name: &str) -> Option<PathBuf> {
1085 if name.is_empty() || name.contains('\0') {
1086 return None;
1087 }
1088 if name.starts_with('/') || name.starts_with('\\') {
1089 return None; }
1091 let b = name.as_bytes();
1092 if b.len() >= 2 && b[1] == b':' && b[0].is_ascii_alphabetic() {
1093 return None; }
1095 let mut out = PathBuf::new();
1096 for comp in name.split(['/', '\\']) {
1097 match comp {
1098 "" | "." => {}
1099 ".." => return None,
1100 other => out.push(other),
1101 }
1102 }
1103 if out.as_os_str().is_empty() {
1104 return None;
1105 }
1106 Some(out)
1107}
1108
1109impl Read for ZipFile<'_> {
1110 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
1111 let n = self.decoder.read(buf)?;
1112 if n == 0 {
1113 if !self.verified {
1114 self.verified = true;
1115 let actual = self.hasher.clone().finalize();
1116 if self.verify_crc && actual != self.meta.crc32 {
1117 return Err(io::Error::other(ZipCoreError::CrcMismatch {
1118 entry: self.meta.name.clone(),
1119 expected: self.meta.crc32,
1120 actual,
1121 }));
1122 }
1123 }
1124 return Ok(0);
1125 }
1126 self.hasher.update(&buf[..n]);
1127 self.bytes_out += n as u64;
1128 Ok(n)
1129 }
1130}
1131
1132#[cfg(test)]
1133mod tests {
1134 use super::zipcrypto_check_byte;
1135
1136 #[test]
1137 fn check_byte_selects_crc_or_modtime() {
1138 assert_eq!(zipcrypto_check_byte(0x0000, 0xAB12_3456, 0x7890), 0xAB);
1140 assert_eq!(zipcrypto_check_byte(0x0008, 0xAB12_3456, 0xCD90), 0xCD);
1142 }
1143}