1use std::io::{self, Read, Seek, SeekFrom};
9use std::path::PathBuf;
10
11use crate::bytes::Reader;
12use crate::codec::Decoder;
13use crate::crypto::{AesInfo, AesReader, 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.aes.is_none() && meta.flags & 0x0040 != 0 {
314 return Err(ZipCoreError::UnsupportedEncryption {
315 entry: meta.name,
316 reason: "PKWARE strong encryption (GP flag bit 6)".to_string(),
317 });
318 }
319 if meta.flags & 0x2000 != 0 {
320 return Err(ZipCoreError::UnsupportedEncryption {
321 entry: meta.name,
322 reason: "masked / central-directory encryption (GP flag bit 13)".to_string(),
323 });
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
358#[derive(Debug, Clone, PartialEq, Eq)]
361pub struct HeaderFields {
362 pub name: String,
364 pub method: CompressionMethod,
366 pub flags: u16,
368 pub crc32: u32,
370 pub compressed_size: u64,
372 pub uncompressed_size: u64,
374}
375
376#[derive(Debug, Clone)]
379pub struct EntryLayout {
380 pub index: usize,
382 pub lfh_offset: u64,
384 pub data_start: u64,
386 pub central: HeaderFields,
388 pub local: HeaderFields,
390 pub extra: ExtraFields,
392}
393
394#[derive(Debug, Clone, Default, PartialEq, Eq)]
399pub struct ExtraFields {
400 pub ntfs_mtime: Option<u64>,
402 pub ntfs_atime: Option<u64>,
404 pub ntfs_ctime: Option<u64>,
406 pub unix_mtime: Option<i32>,
408 pub unix_atime: Option<i32>,
410 pub unix_ctime: Option<i32>,
412 pub unicode_path: Option<String>,
414 pub unicode_comment: Option<String>,
416}
417
418fn read_lfh_fields<R: Read + Seek>(
422 reader: &mut R,
423 lfh_offset: u64,
424) -> Result<(HeaderFields, u64), ZipCoreError> {
425 reader.seek(SeekFrom::Start(lfh_offset))?;
426 let mut fixed = [0u8; LFH_FIXED];
427 reader.read_exact(&mut fixed)?;
428 let mut r = Reader::new(&fixed);
429 if r.u32()? != LFH_SIG {
430 return Err(FormatError::BadSignature {
431 what: "local file header",
432 offset: lfh_offset,
433 }
434 .into());
435 }
436 let _version_needed = r.u16()?;
437 let flags = r.u16()?;
438 let method = CompressionMethod::from_u16(r.u16()?);
439 let _mod_time = r.u16()?;
440 let _mod_date = r.u16()?;
441 let crc32 = r.u32()?;
442 let compressed_size = u64::from(r.u32()?);
443 let uncompressed_size = u64::from(r.u32()?);
444 let name_len = usize::from(r.u16()?);
445 let extra_len = usize::from(r.u16()?);
446
447 let mut name_buf = vec![0u8; name_len];
448 reader.read_exact(&mut name_buf)?;
449 let name = decode_name(&name_buf, flags);
450 let data_start = lfh_offset + LFH_FIXED as u64 + name_len as u64 + extra_len as u64;
451
452 Ok((
453 HeaderFields {
454 name,
455 method,
456 flags,
457 crc32,
458 compressed_size,
459 uncompressed_size,
460 },
461 data_start,
462 ))
463}
464
465struct Eocd32 {
468 disk_number: u16,
469 cd_start_disk: u16,
470 total_entries: u16,
471 cd_size: u32,
472 cd_offset: u32,
473 comment_len: u16,
474}
475
476fn parse_central_directory<R: Read + Seek>(
477 reader: &mut R,
478 file_len: u64,
479) -> Result<(Vec<CentralEntry>, ArchiveSummary), ZipCoreError> {
480 let scan_len = file_len.min(EOCD_SCAN_MAX as u64);
481 if scan_len < EOCD_MIN as u64 {
482 return Err(FormatError::NoEocd.into());
483 }
484 let scan_start = file_len - scan_len;
485 reader.seek(SeekFrom::Start(scan_start))?;
486 let mut tail = vec![0u8; scan_len as usize];
487 reader.read_exact(&mut tail)?;
488
489 let eocd_rel = find_eocd(&tail).ok_or(FormatError::NoEocd)?;
490 let eocd = parse_eocd(&tail[eocd_rel..])?;
491 let eocd_end_offset =
494 scan_start + eocd_rel as u64 + EOCD_MIN as u64 + u64::from(eocd.comment_len);
495
496 let is_zip64 = eocd.cd_offset == U32_SENTINEL
499 || eocd.cd_size == U32_SENTINEL
500 || eocd.total_entries == U16_SENTINEL;
501 let (cd_offset, cd_size, total_entries, disk_number, cd_start_disk) = if is_zip64 {
502 resolve_zip64_eocd(reader, &tail, eocd_rel)?
503 } else {
504 (
505 u64::from(eocd.cd_offset),
506 u64::from(eocd.cd_size),
507 usize::from(eocd.total_entries),
508 u32::from(eocd.disk_number),
509 u32::from(eocd.cd_start_disk),
510 )
511 };
512
513 let eocd_pos = scan_start + eocd_rel as u64;
522 let prefix = if is_zip64 {
523 0
524 } else {
525 match eocd_pos.checked_sub(cd_offset.saturating_add(cd_size)) {
526 Some(n)
527 if n > 0
528 && !cd_header_at(reader, cd_offset)
529 && cd_header_at(reader, cd_offset + n) =>
530 {
531 n
532 }
533 _ => 0,
534 }
535 };
536 let actual_cd_offset = cd_offset + prefix;
537
538 match actual_cd_offset.checked_add(cd_size) {
539 Some(end) if end <= file_len => {}
540 _ => return Err(FormatError::CentralDirOutOfRange { cd_offset, cd_size }.into()),
541 }
542 if total_entries > MAX_ENTRIES {
543 return Err(FormatError::TooManyEntries(total_entries).into());
544 }
545
546 reader.seek(SeekFrom::Start(actual_cd_offset))?;
547 let mut cd = vec![0u8; cd_size as usize];
548 reader.read_exact(&mut cd)?;
549
550 let (mut entries, cd_consumed) = parse_cd_entries(&cd, total_entries)?;
551 if prefix > 0 {
554 for e in &mut entries {
555 e.lfh_offset += prefix;
556 }
557 }
558
559 let archive_signature_len = {
567 let sig = ARCHIVE_SIG_SIG.to_le_bytes();
568 let trailing = &cd[cd_consumed..];
569 if trailing.len() >= 6 && trailing[..4] == sig {
570 Some(u16::from_le_bytes([trailing[4], trailing[5]]))
571 } else if trailing.is_empty() {
572 let mut hdr = [0u8; 6];
575 match reader.read_exact(&mut hdr) {
576 Ok(()) if hdr[..4] == sig => Some(u16::from_le_bytes([hdr[4], hdr[5]])),
577 _ => None,
578 }
579 } else {
580 None
581 }
582 };
583 let summary = ArchiveSummary {
584 file_len,
585 central_dir_offset: actual_cd_offset,
586 central_dir_size: cd_size,
587 eocd_end_offset,
588 comment_len: eocd.comment_len,
589 disk_number,
590 cd_start_disk,
591 archive_signature_len,
592 };
593 Ok((entries, summary))
594}
595
596fn cd_header_at<R: Read + Seek>(reader: &mut R, offset: u64) -> bool {
599 if reader.seek(SeekFrom::Start(offset)).is_err() {
600 return false; }
602 let mut sig = [0u8; 4];
603 match reader.read_exact(&mut sig) {
604 Ok(()) => u32::from_le_bytes(sig) == CD_HEADER_SIG,
605 Err(_) => false, }
607}
608
609fn find_eocd(tail: &[u8]) -> Option<usize> {
611 if tail.len() < EOCD_MIN {
612 return None; }
614 let sig = EOCD_SIG.to_le_bytes();
615 (0..=tail.len() - EOCD_MIN)
617 .rev()
618 .find(|&i| tail[i..i + 4] == sig)
619}
620
621fn parse_eocd(buf: &[u8]) -> Result<Eocd32, ZipCoreError> {
623 let mut r = Reader::new(buf);
624 if r.u32()? != EOCD_SIG {
625 return Err(FormatError::NoEocd.into()); }
627 let disk_number = r.u16()?;
628 let cd_start_disk = r.u16()?;
629 let _entries_this_disk = r.u16()?;
630 let total_entries = r.u16()?;
631 let cd_size = r.u32()?;
632 let cd_offset = r.u32()?;
633 let comment_len = r.u16()?;
634 Ok(Eocd32 {
635 disk_number,
636 cd_start_disk,
637 total_entries,
638 cd_size,
639 cd_offset,
640 comment_len,
641 })
642}
643
644fn resolve_zip64_eocd<R: Read + Seek>(
648 reader: &mut R,
649 tail: &[u8],
650 eocd_rel: usize,
651) -> Result<(u64, u64, usize, u32, u32), ZipCoreError> {
652 if eocd_rel < ZIP64_LOCATOR_LEN {
653 return Err(FormatError::Zip64Unsupported.into());
654 }
655 let mut loc = Reader::new(&tail[eocd_rel - ZIP64_LOCATOR_LEN..eocd_rel]);
656 if loc.u32()? != ZIP64_LOCATOR_SIG {
657 return Err(FormatError::Zip64Unsupported.into());
658 }
659 let _disk = loc.u32()?;
660 let z64_eocd_offset = loc.u64()?;
661
662 reader.seek(SeekFrom::Start(z64_eocd_offset))?;
663 let mut rec = [0u8; 56];
664 reader.read_exact(&mut rec)?;
665 let mut r = Reader::new(&rec);
666 if r.u32()? != ZIP64_EOCD_SIG {
667 return Err(FormatError::BadSignature {
668 what: "Zip64 EOCD record",
669 offset: z64_eocd_offset,
670 }
671 .into());
672 }
673 let _record_size = r.u64()?;
674 let _version_made_by = r.u16()?;
675 let _version_needed = r.u16()?;
676 let disk_number = r.u32()?;
677 let cd_start_disk = r.u32()?;
678 let _entries_this_disk = r.u64()?;
679 let total_entries = r.u64()?;
680 let cd_size = r.u64()?;
681 let cd_offset = r.u64()?;
682 let total =
683 usize::try_from(total_entries).map_err(|_| FormatError::TooManyEntries(usize::MAX))?;
684 Ok((cd_offset, cd_size, total, disk_number, cd_start_disk))
685}
686
687fn parse_cd_entries(
692 cd: &[u8],
693 total_entries: usize,
694) -> Result<(Vec<CentralEntry>, usize), ZipCoreError> {
695 let mut r = Reader::new(cd);
696 let mut entries = Vec::new();
697 for _ in 0..total_entries {
698 if r.remaining() < 46 {
699 return Err(FormatError::Truncated.into());
700 }
701 if r.u32()? != CD_HEADER_SIG {
702 return Err(FormatError::BadSignature {
703 what: "central directory header",
704 offset: (cd.len() - r.remaining()) as u64,
705 }
706 .into());
707 }
708 let _version_made_by = r.u16()?;
709 let _version_needed = r.u16()?;
710 let flags = r.u16()?;
711 let method_raw = r.u16()?;
712 let method = CompressionMethod::from_u16(method_raw);
713 let last_mod_time = r.u16()?;
714 let _mod_date = r.u16()?;
715 let crc32 = r.u32()?;
716 let compressed_size32 = r.u32()?;
717 let uncompressed_size32 = r.u32()?;
718 let name_len = usize::from(r.u16()?);
719 let extra_len = usize::from(r.u16()?);
720 let comment_len = usize::from(r.u16()?);
721 let disk_start = r.u16()?;
722 let _internal_attrs = r.u16()?;
723 let _external_attrs = r.u32()?;
724 let lfh_offset32 = r.u32()?;
725
726 let name_bytes = r.take(name_len)?;
727 let extra = r.take(extra_len)?;
728 let _comment = r.take(comment_len)?;
729
730 let mut uncompressed_size = u64::from(uncompressed_size32);
734 let mut compressed_size = u64::from(compressed_size32);
735 let mut lfh_offset = u64::from(lfh_offset32);
736 if uncompressed_size32 == U32_SENTINEL
737 || compressed_size32 == U32_SENTINEL
738 || lfh_offset32 == U32_SENTINEL
739 {
740 apply_zip64_extra(
741 extra,
742 uncompressed_size32 == U32_SENTINEL,
743 compressed_size32 == U32_SENTINEL,
744 lfh_offset32 == U32_SENTINEL,
745 &mut uncompressed_size,
746 &mut compressed_size,
747 &mut lfh_offset,
748 )?;
749 }
750
751 let name = decode_name(name_bytes, flags);
755 let aes = if method_raw == 99 {
758 parse_aes_extra(extra)
759 } else {
760 None
761 };
762
763 entries.push(CentralEntry {
764 name,
765 method,
766 flags,
767 crc32,
768 compressed_size,
769 uncompressed_size,
770 lfh_offset,
771 last_mod_time,
772 aes,
773 disk_start,
774 extra: parse_extra_fields(extra),
775 });
776 }
777 let consumed = cd.len() - r.remaining();
778 Ok((entries, consumed))
779}
780
781fn apply_zip64_extra(
786 extra: &[u8],
787 need_uncompressed: bool,
788 need_compressed: bool,
789 need_offset: bool,
790 uncompressed_size: &mut u64,
791 compressed_size: &mut u64,
792 lfh_offset: &mut u64,
793) -> Result<(), ZipCoreError> {
794 let mut r = Reader::new(extra);
795 while r.remaining() >= 4 {
796 let id = r.u16()?;
797 let size = usize::from(r.u16()?);
798 if id == ZIP64_EXTRA_ID {
799 let mut z = Reader::new(r.take(size)?);
800 if need_uncompressed {
801 *uncompressed_size = z.u64()?;
802 }
803 if need_compressed {
804 *compressed_size = z.u64()?;
805 }
806 if need_offset {
807 *lfh_offset = z.u64()?;
808 }
809 return Ok(());
810 }
811 r.skip(size)?;
812 }
813 Err(FormatError::Zip64Inconsistent.into())
814}
815
816fn parse_aes_extra(extra: &[u8]) -> Option<AesInfo> {
819 let mut r = Reader::new(extra);
820 while r.remaining() >= 4 {
821 let id = r.u16().ok()?;
822 let size = usize::from(r.u16().ok()?);
823 if id == 0x9901 {
824 let data = r.take(size).ok()?;
825 let mut d = Reader::new(data);
826 let version = d.u16().ok()?; let _vendor = d.u16().ok()?; let strength = d.take(1).ok()?[0];
829 let actual_method = d.u16().ok()?;
830 return Some(AesInfo {
831 strength,
832 actual_method,
833 is_ae2: version == 2,
834 });
835 }
836 r.skip(size).ok()?;
837 }
838 None
839}
840
841fn parse_extra_fields(extra: &[u8]) -> ExtraFields {
843 let mut out = ExtraFields::default();
844 let mut r = Reader::new(extra);
845 while r.remaining() >= 4 {
846 let (Ok(id), Ok(size)) = (r.u16(), r.u16()) else {
847 break; };
849 let Ok(data) = r.take(usize::from(size)) else {
850 break;
851 };
852 match id {
853 0x000a => parse_ntfs_times(data, &mut out),
854 0x5455 => parse_unix_times(data, &mut out),
855 0x7075 => out.unicode_path = parse_unicode_extra(data),
856 0x6375 => out.unicode_comment = parse_unicode_extra(data),
857 _ => {}
858 }
859 }
860 out
861}
862
863fn parse_ntfs_times(data: &[u8], out: &mut ExtraFields) {
866 let mut r = Reader::new(data);
867 let _ = r.u32(); while r.remaining() >= 4 {
869 let (Ok(tag), Ok(tsize)) = (r.u16(), r.u16()) else {
870 break; };
872 let Ok(tdata) = r.take(usize::from(tsize)) else {
873 break;
874 };
875 if tag == 0x0001 {
876 let mut s = Reader::new(tdata);
877 if let (Ok(m), Ok(a), Ok(c)) = (s.u64(), s.u64(), s.u64()) {
878 out.ntfs_mtime = Some(m);
879 out.ntfs_atime = Some(a);
880 out.ntfs_ctime = Some(c);
881 }
882 }
883 }
884}
885
886fn parse_unix_times(data: &[u8], out: &mut ExtraFields) {
889 let mut r = Reader::new(data);
890 let Ok(flag_byte) = r.take(1) else {
891 return;
892 };
893 let flags = flag_byte[0];
894 if flags & 0x01 != 0 {
895 out.unix_mtime = take_i32le(&mut r);
896 }
897 if flags & 0x02 != 0 {
898 out.unix_atime = take_i32le(&mut r);
899 }
900 if flags & 0x04 != 0 {
901 out.unix_ctime = take_i32le(&mut r);
902 }
903}
904
905fn take_i32le(r: &mut Reader) -> Option<i32> {
906 r.take(4)
907 .ok()
908 .map(|b| i32::from_le_bytes([b[0], b[1], b[2], b[3]]))
909}
910
911fn parse_unicode_extra(data: &[u8]) -> Option<String> {
915 if data.len() < 5 {
916 return None;
917 }
918 String::from_utf8(data[5..].to_vec()).ok()
919}
920
921fn zipcrypto_check_byte(flags: u16, crc32: u32, last_mod_time: u16) -> u8 {
925 if flags & 0x0008 != 0 {
926 (last_mod_time >> 8) as u8
927 } else {
928 (crc32 >> 24) as u8
929 }
930}
931
932fn decode_name(bytes: &[u8], flags: u16) -> String {
935 if flags & 0x0800 != 0 || bytes.is_ascii() {
937 return String::from_utf8_lossy(bytes).into_owned();
938 }
939 bytes.iter().map(|&b| crate::cp437::decode(b)).collect()
940}
941
942pub struct ZipFile<'a> {
945 meta: CentralEntry,
946 data_start: u64,
947 decoder: Decoder<Box<dyn Read + 'a>>,
948 hasher: crc32fast::Hasher,
949 bytes_out: u64,
950 verified: bool,
951 verify_crc: bool,
954}
955
956impl ZipFile<'_> {
957 pub fn name(&self) -> &str {
959 &self.meta.name
960 }
961
962 pub fn compression(&self) -> CompressionMethod {
964 self.meta.method
965 }
966
967 pub fn size(&self) -> u64 {
969 self.meta.uncompressed_size
970 }
971
972 pub fn compressed_size(&self) -> u64 {
974 self.meta.compressed_size
975 }
976
977 pub fn crc32(&self) -> u32 {
979 self.meta.crc32
980 }
981
982 pub fn data_start(&self) -> u64 {
985 self.data_start
986 }
987
988 pub fn flags(&self) -> u16 {
990 self.meta.flags
991 }
992
993 pub fn is_dir(&self) -> bool {
995 self.meta.is_dir()
996 }
997
998 pub fn enclosed_name(&self) -> Option<PathBuf> {
1003 enclosed_name(&self.meta.name)
1004 }
1005}
1006
1007fn check_local_disk(
1014 meta: &CentralEntry,
1015 this_disk: u32,
1016 cd_start_disk: u32,
1017) -> Result<(), ZipCoreError> {
1018 let disk = if meta.disk_start != 0 {
1019 u32::from(meta.disk_start)
1020 } else if cd_start_disk != 0 {
1021 cd_start_disk
1022 } else if this_disk != 0 {
1023 this_disk
1024 } else {
1025 return Ok(());
1026 };
1027 Err(ZipCoreError::SpannedArchive {
1028 entry: meta.name.clone(),
1029 disk,
1030 })
1031}
1032
1033fn enclosed_name(name: &str) -> Option<PathBuf> {
1037 if name.is_empty() || name.contains('\0') {
1038 return None;
1039 }
1040 if name.starts_with('/') || name.starts_with('\\') {
1041 return None; }
1043 let b = name.as_bytes();
1044 if b.len() >= 2 && b[1] == b':' && b[0].is_ascii_alphabetic() {
1045 return None; }
1047 let mut out = PathBuf::new();
1048 for comp in name.split(['/', '\\']) {
1049 match comp {
1050 "" | "." => {}
1051 ".." => return None,
1052 other => out.push(other),
1053 }
1054 }
1055 if out.as_os_str().is_empty() {
1056 return None;
1057 }
1058 Some(out)
1059}
1060
1061impl Read for ZipFile<'_> {
1062 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
1063 let n = self.decoder.read(buf)?;
1064 if n == 0 {
1065 if !self.verified {
1066 self.verified = true;
1067 let actual = self.hasher.clone().finalize();
1068 if self.verify_crc && actual != self.meta.crc32 {
1069 return Err(io::Error::other(ZipCoreError::CrcMismatch {
1070 entry: self.meta.name.clone(),
1071 expected: self.meta.crc32,
1072 actual,
1073 }));
1074 }
1075 }
1076 return Ok(0);
1077 }
1078 self.hasher.update(&buf[..n]);
1079 self.bytes_out += n as u64;
1080 Ok(n)
1081 }
1082}
1083
1084#[cfg(test)]
1085mod tests {
1086 use super::zipcrypto_check_byte;
1087
1088 #[test]
1089 fn check_byte_selects_crc_or_modtime() {
1090 assert_eq!(zipcrypto_check_byte(0x0000, 0xAB12_3456, 0x7890), 0xAB);
1092 assert_eq!(zipcrypto_check_byte(0x0008, 0xAB12_3456, 0xCD90), 0xCD);
1094 }
1095}