1use std::fs::File;
17use std::io;
18use std::path::Path;
19use std::sync::Arc;
20
21use memchr::memmem;
22
23use crate::crc32::crc32;
24use crate::error::{Error, Result};
25use crate::hash::FastMap;
26
27const LOCAL_HEADER_SIG: [u8; 4] = 0x0403_4b50u32.to_le_bytes();
28const CENTRAL_HEADER_SIG: [u8; 4] = 0x0201_4b50u32.to_le_bytes();
29const END_RECORD_SIG: [u8; 4] = 0x0605_4b50u32.to_le_bytes();
30const ZIP64_END_RECORD_SIG: [u8; 4] = 0x0606_4b50u32.to_le_bytes();
31const ZIP64_LOCATOR_SIG: [u8; 4] = 0x0706_4b50u32.to_le_bytes();
32
33const END_RECORD_LEN: usize = 22;
34const ZIP64_LOCATOR_LEN: usize = 20;
35const ZIP64_END_RECORD_LEN: usize = 56;
36const CENTRAL_HEADER_LEN: usize = 46;
37const LOCAL_HEADER_LEN: usize = 30;
38const WHOLE_FILE_LEN: u64 = 256 * 1024;
40const TAIL_LEN: u64 = 66 * 1024;
43const ZIP64_EXTRA_ID: u16 = 0x0001;
44
45pub const METHOD_STORED: u16 = 0;
47pub const METHOD_DEFLATE: u16 = 8;
49
50pub const FLAG_ENCRYPTED: u16 = 1;
52pub const FLAG_DATA_DESCRIPTOR: u16 = 1 << 3;
54pub const FLAG_UTF8_NAMES: u16 = 1 << 11;
56
57pub trait Source: Send + Sync {
59 fn len(&self) -> u64;
61 fn is_empty(&self) -> bool {
62 self.len() == 0
63 }
64 fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()>;
66 fn as_bytes(&self) -> Option<&[u8]> {
68 None
69 }
70}
71
72impl Source for Vec<u8> {
73 fn len(&self) -> u64 {
74 Vec::len(self) as u64
75 }
76
77 fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()> {
78 let start = usize::try_from(offset).map_err(|_| out_of_bounds())?;
79 let end = start.checked_add(buf.len()).ok_or_else(out_of_bounds)?;
80 let slice = self.get(start..end).ok_or_else(out_of_bounds)?;
81 buf.copy_from_slice(slice);
82 Ok(())
83 }
84
85 fn as_bytes(&self) -> Option<&[u8]> {
86 Some(self)
87 }
88}
89
90pub struct FileSource {
92 file: File,
93 len: u64,
94}
95
96impl FileSource {
97 pub fn new(file: File) -> io::Result<Self> {
98 let len = file.metadata()?.len();
99 Ok(Self { file, len })
100 }
101
102 pub fn open(path: impl AsRef<Path>) -> io::Result<Self> {
103 Self::new(File::open(path)?)
104 }
105}
106
107impl Source for FileSource {
108 fn len(&self) -> u64 {
109 self.len
110 }
111
112 #[cfg(unix)]
113 fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()> {
114 use std::os::unix::fs::FileExt;
115 self.file.read_exact_at(buf, offset)
116 }
117
118 #[cfg(windows)]
119 fn read_at(&self, offset: u64, mut buf: &mut [u8]) -> io::Result<()> {
120 use std::os::windows::fs::FileExt;
121 let mut offset = offset;
122 while !buf.is_empty() {
123 let read = self.file.seek_read(buf, offset)?;
124 if read == 0 {
125 return Err(out_of_bounds());
126 }
127 buf = &mut buf[read..];
128 offset += read as u64;
129 }
130 Ok(())
131 }
132}
133
134fn out_of_bounds() -> io::Error {
135 io::Error::new(io::ErrorKind::UnexpectedEof, "read past end of archive")
136}
137
138#[derive(Clone, Debug)]
140pub struct Entry {
141 pub name: String,
143 pub raw_name: Vec<u8>,
145 pub method: u16,
146 pub flags: u16,
147 pub version_needed: u16,
148 pub crc32: u32,
149 pub compressed_size: u64,
150 pub uncompressed_size: u64,
151 pub header_offset: u64,
153 pub central_offset: u64,
155 pub extra_ids: Vec<u16>,
157 pub has_comment: bool,
159}
160
161impl Entry {
162 pub fn is_directory(&self) -> bool {
163 self.raw_name.last() == Some(&b'/')
164 }
165
166 pub fn is_encrypted(&self) -> bool {
167 self.flags & FLAG_ENCRYPTED != 0
168 }
169
170 pub fn has_data_descriptor(&self) -> bool {
171 self.flags & FLAG_DATA_DESCRIPTOR != 0
172 }
173
174 pub fn names_are_utf8(&self) -> bool {
175 self.flags & FLAG_UTF8_NAMES != 0
176 }
177
178 pub fn has_zip64_extra(&self) -> bool {
179 self.extra_ids.contains(&ZIP64_EXTRA_ID)
180 }
181}
182
183#[derive(Clone, Debug, PartialEq, Eq)]
185pub struct LocalHeader {
186 pub version_needed: u16,
187 pub flags: u16,
188 pub method: u16,
189 pub crc32: u32,
190 pub compressed_size: u32,
191 pub uncompressed_size: u32,
192 pub raw_name: Vec<u8>,
193 pub extra_len: u16,
194 pub data_offset: u64,
196}
197
198#[derive(Clone, Debug, Default)]
200pub struct Layout {
201 pub len: u64,
203 pub end_record_offset: u64,
205 pub zip64: bool,
207 pub offset_shift: u64,
210 pub central_offset: u64,
212 pub central_size: u64,
214 pub declared_entries: u64,
216 pub truncated_central: bool,
218 pub comment: Vec<u8>,
220 pub trailing_garbage: u64,
224 pub reconstructed: bool,
227}
228
229pub struct Archive {
231 source: Arc<dyn Source>,
232 cache: Box<[u8]>,
234 cache_start: u64,
235 entries: Vec<Entry>,
236 index: FastMap<String, usize>,
237 duplicates: Vec<usize>,
238 layout: Layout,
239}
240
241#[derive(Clone, Copy)]
243enum Located {
244 Bytes(usize),
246 Cache(usize),
248 Scratch,
250}
251
252fn locate_range(
255 source: &dyn Source,
256 cache: &[u8],
257 cache_start: u64,
258 offset: u64,
259 len: usize,
260 scratch: &mut Vec<u8>,
261) -> io::Result<Located> {
262 let end = offset.checked_add(len as u64).ok_or_else(out_of_bounds)?;
263 if let Some(bytes) = source.as_bytes() {
264 if end > bytes.len() as u64 {
265 return Err(out_of_bounds());
266 }
267 return Ok(Located::Bytes(offset as usize));
268 }
269 if offset >= cache_start && end <= cache_start + cache.len() as u64 {
270 return Ok(Located::Cache((offset - cache_start) as usize));
271 }
272 scratch.clear();
273 scratch.resize(len, 0);
274 source.read_at(offset, scratch)?;
275 Ok(Located::Scratch)
276}
277
278fn view_range<'a>(
279 source: &'a dyn Source,
280 cache: &'a [u8],
281 located: Located,
282 len: usize,
283 scratch: &'a [u8],
284) -> &'a [u8] {
285 match located {
286 Located::Bytes(start) => &source.as_bytes().unwrap_or_default()[start..start + len],
287 Located::Cache(start) => &cache[start..start + len],
288 Located::Scratch => &scratch[..len],
289 }
290}
291
292impl Archive {
293 pub fn open(source: Arc<dyn Source>) -> Result<Self> {
295 let len = source.len();
296 let tail_len = match len <= WHOLE_FILE_LEN {
297 true => len,
298 false => TAIL_LEN,
299 };
300 let tail_len = usize::try_from(tail_len).map_err(|_| Error::NotZip)?;
301 let tail_start = len - tail_len as u64;
302 let cache: Box<[u8]> = match source.as_bytes() {
303 Some(_) => Box::default(),
304 None => {
305 let mut tail = vec![0u8; tail_len];
306 source.read_at(tail_start, &mut tail)?;
307 tail.into_boxed_slice()
308 }
309 };
310 let cache_start = tail_start;
311 let tail: &[u8] = match source.as_bytes() {
312 Some(bytes) => &bytes[tail_start as usize..],
313 None => &cache,
314 };
315 let Some(end_pos) = find_end_record(tail) else {
316 return Self::reconstruct(source, len);
317 };
318 let end = &tail[end_pos..];
319 let end_record_offset = tail_start + end_pos as u64;
320 let comment_len = usize::from(u16_at(end, 20));
321 let comment = end[END_RECORD_LEN..]
322 .get(..comment_len)
323 .unwrap_or(&end[END_RECORD_LEN..])
324 .to_vec();
325 let after_comment = end.len().saturating_sub(END_RECORD_LEN + comment_len) as u64;
326
327 let mut layout = Layout {
328 len,
329 end_record_offset,
330 comment,
331 trailing_garbage: after_comment,
332 ..Layout::default()
333 };
334 let mut declared_entries = u64::from(u16_at(end, 10));
335 let mut central_size = u64::from(u32_at(end, 12));
336 let mut central_offset = u64::from(u32_at(end, 16));
337 let mut records_start = end_record_offset;
338
339 let locator_pos = end_pos.checked_sub(ZIP64_LOCATOR_LEN);
340 let has_locator = locator_pos.is_some_and(|pos| tail[pos..pos + 4] == ZIP64_LOCATOR_SIG);
341 let needs_zip64 = declared_entries == 0xffff
342 || central_size == 0xffff_ffff
343 || central_offset == 0xffff_ffff;
344 if has_locator && (needs_zip64 || true) {
345 let locator = &tail[locator_pos.unwrap_or(0)..];
346 let declared_z64 = u64_at(locator, 8);
347 let locator_offset = end_record_offset - ZIP64_LOCATOR_LEN as u64;
348 let (z64_pos, shift) = locate_zip64_end(&source, declared_z64, locator_offset)?;
349 let mut record = [0u8; ZIP64_END_RECORD_LEN];
350 source.read_at(z64_pos, &mut record)?;
351 layout.zip64 = true;
352 layout.offset_shift = shift;
353 declared_entries = u64_at(&record, 32);
354 central_size = u64_at(&record, 40);
355 central_offset = u64_at(&record, 48);
356 records_start = z64_pos;
357 } else if needs_zip64 {
358 return Err(Error::zip(
359 end_record_offset,
360 "end record needs a Zip64 record that is missing",
361 ));
362 }
363
364 layout.declared_entries = declared_entries;
365 layout.central_size = central_size;
366 layout.central_offset = central_offset;
367
368 let central_start = match locate_central(
369 &source,
370 central_offset,
371 central_size,
372 records_start,
373 layout.offset_shift,
374 ) {
375 Ok(start) => start,
376 Err(_) => return Self::reconstruct(source, len),
377 };
378 layout.offset_shift = central_start.wrapping_sub(central_offset);
379 let readable = records_start.saturating_sub(central_start);
380 let central_len = usize::try_from(central_size.min(readable))
381 .map_err(|_| Error::zip(central_start, "central directory too large"))?;
382 let mut scratch = Vec::new();
383 let located = locate_range(
384 source.as_ref(),
385 &cache,
386 cache_start,
387 central_start,
388 central_len,
389 &mut scratch,
390 )?;
391 let central = view_range(source.as_ref(), &cache, located, central_len, &scratch);
392 layout.trailing_garbage += readable.saturating_sub(central_size);
393
394 let mut entries =
395 Vec::with_capacity(usize::try_from(declared_entries).unwrap_or(0).min(1 << 16));
396 let mut cursor = 0usize;
397 while cursor + CENTRAL_HEADER_LEN <= central.len()
398 && (entries.len() as u64) < declared_entries
399 {
400 let header = ¢ral[cursor..];
401 if header[..4] != CENTRAL_HEADER_SIG {
402 break;
403 }
404 let name_len = usize::from(u16_at(header, 28));
405 let extra_len = usize::from(u16_at(header, 30));
406 let comment_len = usize::from(u16_at(header, 32));
407 let total = CENTRAL_HEADER_LEN + name_len + extra_len + comment_len;
408 if cursor + total > central.len() {
409 break;
410 }
411 let raw_name = header[CENTRAL_HEADER_LEN..CENTRAL_HEADER_LEN + name_len].to_vec();
412 let extra =
413 &header[CENTRAL_HEADER_LEN + name_len..CENTRAL_HEADER_LEN + name_len + extra_len];
414 let mut entry = Entry {
415 name: String::from_utf8_lossy(&raw_name).into_owned(),
416 raw_name,
417 method: u16_at(header, 10),
418 flags: u16_at(header, 8),
419 version_needed: u16_at(header, 6),
420 crc32: u32_at(header, 16),
421 compressed_size: u64::from(u32_at(header, 20)),
422 uncompressed_size: u64::from(u32_at(header, 24)),
423 header_offset: u64::from(u32_at(header, 42)),
424 central_offset: central_start + cursor as u64,
425 extra_ids: Vec::new(),
426 has_comment: comment_len > 0,
427 };
428 apply_extras(&mut entry, extra);
429 entries.push(entry);
430 cursor += total;
431 }
432 layout.truncated_central = (entries.len() as u64) < declared_entries;
433
434 let mut index = FastMap::default();
435 index.reserve(entries.len());
436 let mut duplicates = Vec::new();
437 for (i, entry) in entries.iter().enumerate() {
438 if index.contains_key(&entry.name) {
439 duplicates.push(i);
440 continue;
441 }
442 index.insert(entry.name.clone(), i);
443 }
444
445 Ok(Self {
446 source,
447 cache,
448 cache_start,
449 entries,
450 index,
451 duplicates,
452 layout,
453 })
454 }
455
456 fn reconstruct(source: Arc<dyn Source>, len: u64) -> Result<Self> {
461 let cache: Box<[u8]> = Box::default();
462 let cache_start = 0;
463 let total = usize::try_from(len).map_err(|_| Error::NotZip)?;
464 let mut data = vec![0u8; total];
465 source.read_at(0, &mut data)?;
466 let positions: Vec<usize> = memmem::find_iter(&data, &LOCAL_HEADER_SIG).collect();
467 if positions.is_empty() {
468 return Err(Error::NotZip);
469 }
470 let mut entries = Vec::with_capacity(positions.len());
471 for (i, &pos) in positions.iter().enumerate() {
472 let Some(header) = data.get(pos..pos + LOCAL_HEADER_LEN) else {
473 break;
474 };
475 let name_len = usize::from(u16_at(header, 26));
476 let extra_len = usize::from(u16_at(header, 28));
477 let data_offset = pos + LOCAL_HEADER_LEN + name_len + extra_len;
478 let Some(raw_name) =
479 data.get(pos + LOCAL_HEADER_LEN..pos + LOCAL_HEADER_LEN + name_len)
480 else {
481 break;
482 };
483 if raw_name.is_empty() || raw_name.contains(&0) || data_offset > data.len() {
484 continue;
485 }
486 let flags = u16_at(header, 6);
487 let next = positions.get(i + 1).copied().unwrap_or_else(|| {
488 memmem::find(&data[data_offset..], &CENTRAL_HEADER_SIG)
489 .map_or(data.len(), |rel| data_offset + rel)
490 });
491 let (crc, compressed_size, uncompressed_size) = match flags & FLAG_DATA_DESCRIPTOR != 0
492 {
493 false => (
494 u32_at(header, 14),
495 u64::from(u32_at(header, 18)),
496 u64::from(u32_at(header, 22)),
497 ),
498 true => match descriptor_before(&data, data_offset, next) {
499 Some(descriptor) => descriptor,
500 None => continue,
501 },
502 };
503 entries.push(Entry {
504 name: String::from_utf8_lossy(raw_name).into_owned(),
505 raw_name: raw_name.to_vec(),
506 method: u16_at(header, 8),
507 flags,
508 version_needed: u16_at(header, 4),
509 crc32: crc,
510 compressed_size,
511 uncompressed_size,
512 header_offset: pos as u64,
513 central_offset: 0,
514 extra_ids: Vec::new(),
515 has_comment: false,
516 });
517 }
518 if entries.is_empty() {
519 return Err(Error::NotZip);
520 }
521 let mut index = FastMap::default();
522 let mut duplicates = Vec::new();
523 for (i, entry) in entries.iter().enumerate() {
524 if index.contains_key(&entry.name) {
525 duplicates.push(i);
526 continue;
527 }
528 index.insert(entry.name.clone(), i);
529 }
530 let layout = Layout {
531 len,
532 declared_entries: entries.len() as u64,
533 reconstructed: true,
534 ..Layout::default()
535 };
536 Ok(Self {
537 source,
538 cache,
539 cache_start,
540 entries,
541 index,
542 duplicates,
543 layout,
544 })
545 }
546
547 pub fn from_bytes(bytes: Vec<u8>) -> Result<Self> {
549 Self::open(Arc::new(bytes))
550 }
551
552 pub fn open_path(path: impl AsRef<Path>) -> Result<Self> {
554 Self::open(Arc::new(FileSource::open(path)?))
555 }
556
557 pub fn source(&self) -> &Arc<dyn Source> {
558 &self.source
559 }
560
561 pub fn entries(&self) -> &[Entry] {
562 &self.entries
563 }
564
565 pub fn entry(&self, name: &str) -> Option<&Entry> {
567 self.index.get(name).map(|&i| &self.entries[i])
568 }
569
570 pub fn get(&self, index: usize) -> Option<&Entry> {
571 self.entries.get(index)
572 }
573
574 pub fn duplicates(&self) -> &[usize] {
576 &self.duplicates
577 }
578
579 pub fn layout(&self) -> &Layout {
580 &self.layout
581 }
582
583 fn locate(&self, offset: u64, len: usize, scratch: &mut Vec<u8>) -> Result<Located> {
586 locate_range(
587 self.source.as_ref(),
588 &self.cache,
589 self.cache_start,
590 offset,
591 len,
592 scratch,
593 )
594 .map_err(|_| Error::zip(offset, "read past the end of the archive"))
595 }
596
597 fn view<'a>(&'a self, located: Located, len: usize, scratch: &'a [u8]) -> &'a [u8] {
598 view_range(self.source.as_ref(), &self.cache, located, len, scratch)
599 }
600
601 fn raw_slice<'a>(&'a self, entry: &Entry, scratch: &'a mut Vec<u8>) -> Result<&'a [u8]> {
604 let offset = entry.header_offset.wrapping_add(self.layout.offset_shift);
605 let available = self
606 .layout
607 .len
608 .checked_sub(offset)
609 .filter(|available| *available >= LOCAL_HEADER_LEN as u64)
610 .ok_or_else(|| Error::zip(offset, "local header out of bounds"))?;
611 let size_hint = match entry.compressed_size > 0 || entry.uncompressed_size == 0 {
612 true => entry.compressed_size,
613 false => 0,
614 };
615 let want = (LOCAL_HEADER_LEN + entry.raw_name.len() + 64) as u64 + size_hint;
616 let want = usize::try_from(want.min(available))
617 .map_err(|_| Error::zip(offset, "entry too large"))?;
618 let first = self.locate(offset, want, scratch)?;
619 let (start, size, fits) = {
620 let block = self.view(first, want, scratch);
621 if block[..4] != LOCAL_HEADER_SIG {
622 return Err(Error::zip(offset, "bad local header signature"));
623 }
624 let start =
625 LOCAL_HEADER_LEN + usize::from(u16_at(block, 26)) + usize::from(u16_at(block, 28));
626 let size = self.compressed_len(entry, offset + start as u64)?;
627 (start, size, start + size <= block.len())
628 };
629 if fits {
630 return Ok(&self.view(first, want, scratch)[start..start + size]);
631 }
632 let second = self.locate(offset + start as u64, size, scratch)?;
633 Ok(self.view(second, size, scratch))
634 }
635
636 pub fn local_header(&self, entry: &Entry) -> Result<LocalHeader> {
638 let offset = entry.header_offset.wrapping_add(self.layout.offset_shift);
639 let mut scratch = Vec::new();
640 let located = self
641 .locate(offset, LOCAL_HEADER_LEN, &mut scratch)
642 .map_err(|_| Error::zip(offset, "local header out of bounds"))?;
643 let mut fixed = [0u8; LOCAL_HEADER_LEN];
644 fixed.copy_from_slice(self.view(located, LOCAL_HEADER_LEN, &scratch));
645 if fixed[..4] != LOCAL_HEADER_SIG {
646 return Err(Error::zip(offset, "bad local header signature"));
647 }
648 let name_len = usize::from(u16_at(&fixed, 26));
649 let extra_len = u16_at(&fixed, 28);
650 let located = self.locate(offset + LOCAL_HEADER_LEN as u64, name_len, &mut scratch)?;
651 let raw_name = self.view(located, name_len, &scratch).to_vec();
652 Ok(LocalHeader {
653 version_needed: u16_at(&fixed, 4),
654 flags: u16_at(&fixed, 6),
655 method: u16_at(&fixed, 8),
656 crc32: u32_at(&fixed, 14),
657 compressed_size: u32_at(&fixed, 18),
658 uncompressed_size: u32_at(&fixed, 22),
659 raw_name,
660 extra_len,
661 data_offset: offset + (LOCAL_HEADER_LEN + name_len + usize::from(extra_len)) as u64,
662 })
663 }
664
665 pub fn read_raw(&self, entry: &Entry, out: &mut Vec<u8>) -> Result<()> {
667 COMPRESSED.with(|cell| {
668 let mut scratch = cell.borrow_mut();
669 let raw = self.raw_slice(entry, &mut scratch)?;
670 out.clear();
671 out.extend_from_slice(raw);
672 Ok(())
673 })
674 }
675
676 pub fn read(&self, entry: &Entry, out: &mut Vec<u8>) -> Result<()> {
678 if entry.is_encrypted() {
679 return Err(Error::Unsupported(format!(
680 "encrypted zip entry {}",
681 entry.name
682 )));
683 }
684 match entry.method {
685 METHOD_STORED => self.read_raw(entry, out),
686 METHOD_DEFLATE => COMPRESSED.with(|cell| {
687 let mut scratch = cell.borrow_mut();
688 let raw = self.raw_slice(entry, &mut scratch)?;
689 out.clear();
690 let expected = usize::try_from(entry.uncompressed_size).unwrap_or(0);
691 inflate(raw, expected, out)
692 }),
693 other => Err(Error::Unsupported(format!(
694 "zip compression method {other} for {}",
695 entry.name
696 ))),
697 }
698 }
699
700 pub fn read_to_vec(&self, entry: &Entry) -> Result<Vec<u8>> {
701 let mut out = Vec::new();
702 self.read(entry, &mut out)?;
703 Ok(out)
704 }
705
706 pub fn crc_matches(entry: &Entry, data: &[u8]) -> bool {
708 crc32(data) == entry.crc32
709 }
710
711 fn compressed_len(&self, entry: &Entry, data_offset: u64) -> Result<usize> {
712 let recorded = entry.compressed_size;
713 let known = recorded > 0 || entry.uncompressed_size == 0;
714 let size = match known {
715 true => recorded,
716 false => self.next_boundary(entry).saturating_sub(data_offset),
717 };
718 let end = data_offset
719 .checked_add(size)
720 .filter(|end| *end <= self.layout.len);
721 if end.is_none() {
722 return Err(Error::zip(
723 data_offset,
724 format!("data of {} runs past the end of the archive", entry.name),
725 ));
726 }
727 usize::try_from(size).map_err(|_| Error::zip(data_offset, "entry too large"))
728 }
729
730 fn next_boundary(&self, entry: &Entry) -> u64 {
731 let shift = self.layout.offset_shift;
732 let start = entry.header_offset.wrapping_add(shift);
733 self.entries
734 .iter()
735 .map(|other| other.header_offset.wrapping_add(shift))
736 .filter(|offset| *offset > start)
737 .min()
738 .unwrap_or(self.layout.central_offset.wrapping_add(shift))
739 }
740}
741
742fn descriptor_before(data: &[u8], data_offset: usize, next: usize) -> Option<(u32, u64, u64)> {
746 for (len, signed) in [(16usize, true), (12, false)] {
747 let Some(start) = next.checked_sub(len) else {
748 continue;
749 };
750 if start < data_offset {
751 continue;
752 }
753 let descriptor = &data[start..next];
754 let body = match signed {
755 true if descriptor[..4] == 0x0807_4b50u32.to_le_bytes() => &descriptor[4..],
756 true => continue,
757 false => descriptor,
758 };
759 let compressed = u64::from(u32_at(body, 4));
760 if compressed == (start - data_offset) as u64 {
761 return Some((u32_at(body, 0), compressed, u64::from(u32_at(body, 8))));
762 }
763 }
764 None
765}
766
767fn find_end_record(tail: &[u8]) -> Option<usize> {
768 let mut candidates =
769 memmem::rfind_iter(tail, &END_RECORD_SIG).filter(|pos| pos + END_RECORD_LEN <= tail.len());
770 let mut fallback = None;
771 for pos in candidates.by_ref() {
772 let comment_len = usize::from(u16_at(&tail[pos..], 20));
773 if pos + END_RECORD_LEN + comment_len == tail.len() {
774 return Some(pos);
775 }
776 fallback.get_or_insert(pos);
777 }
778 fallback
779}
780
781fn locate_zip64_end(
782 source: &Arc<dyn Source>,
783 declared: u64,
784 locator_offset: u64,
785) -> Result<(u64, u64)> {
786 let mut sig = [0u8; 4];
787 if source.read_at(declared, &mut sig).is_ok() && sig == ZIP64_END_RECORD_SIG {
788 return Ok((declared, 0));
789 }
790 let candidate = locator_offset.saturating_sub(ZIP64_END_RECORD_LEN as u64);
791 source.read_at(candidate, &mut sig)?;
792 if sig != ZIP64_END_RECORD_SIG {
793 return Err(Error::zip(
794 declared,
795 "zip64 end of central directory record not found",
796 ));
797 }
798 Ok((candidate, candidate.wrapping_sub(declared)))
799}
800
801fn locate_central(
802 source: &Arc<dyn Source>,
803 declared: u64,
804 size: u64,
805 records_start: u64,
806 shift: u64,
807) -> Result<u64> {
808 let mut sig = [0u8; 4];
809 let shifted = declared.wrapping_add(shift);
810 if size == 0 {
811 return Ok(shifted);
812 }
813 if source.read_at(shifted, &mut sig).is_ok() && sig == CENTRAL_HEADER_SIG {
814 return Ok(shifted);
815 }
816 let candidate = records_start.saturating_sub(size);
817 source
818 .read_at(candidate, &mut sig)
819 .map_err(|_| Error::zip(declared, "central directory out of bounds"))?;
820 if sig != CENTRAL_HEADER_SIG {
821 return Err(Error::zip(declared, "central directory not found"));
822 }
823 Ok(candidate)
824}
825
826fn apply_extras(entry: &mut Entry, mut extra: &[u8]) {
827 while extra.len() >= 4 {
828 let id = u16_at(extra, 0);
829 let len = usize::from(u16_at(extra, 2));
830 let body = extra.get(4..4 + len).unwrap_or(&extra[4..]);
831 entry.extra_ids.push(id);
832 if id == ZIP64_EXTRA_ID {
833 let mut fields = body
834 .as_chunks::<8>()
835 .0
836 .iter()
837 .map(|chunk| u64::from_le_bytes(*chunk));
838 if entry.uncompressed_size == 0xffff_ffff {
839 entry.uncompressed_size = fields.next().unwrap_or(entry.uncompressed_size);
840 }
841 if entry.compressed_size == 0xffff_ffff {
842 entry.compressed_size = fields.next().unwrap_or(entry.compressed_size);
843 }
844 if entry.header_offset == 0xffff_ffff {
845 entry.header_offset = fields.next().unwrap_or(entry.header_offset);
846 }
847 }
848 extra = extra.get(4 + len..).unwrap_or(&[]);
849 }
850}
851
852pub fn inflate(input: &[u8], expected: usize, out: &mut Vec<u8>) -> Result<()> {
854 out.clear();
855 crate::inflate::inflate(input, expected, out)
856}
857
858thread_local! {
859 static COMPRESSED: std::cell::RefCell<Vec<u8>> = const { std::cell::RefCell::new(Vec::new()) };
861}
862
863#[inline]
864fn u16_at(bytes: &[u8], offset: usize) -> u16 {
865 u16::from_le_bytes([bytes[offset], bytes[offset + 1]])
866}
867
868#[inline]
869fn u32_at(bytes: &[u8], offset: usize) -> u32 {
870 u32::from_le_bytes([
871 bytes[offset],
872 bytes[offset + 1],
873 bytes[offset + 2],
874 bytes[offset + 3],
875 ])
876}
877
878#[inline]
879fn u64_at(bytes: &[u8], offset: usize) -> u64 {
880 let mut chunk = [0u8; 8];
881 chunk.copy_from_slice(&bytes[offset..offset + 8]);
882 u64::from_le_bytes(chunk)
883}
884
885#[cfg(test)]
886mod tests {
887 use super::*;
888 use pptxboss_testkit::ZipBuilder;
889
890 fn names(archive: &Archive) -> Vec<&str> {
891 archive
892 .entries()
893 .iter()
894 .map(|entry| entry.name.as_str())
895 .collect()
896 }
897
898 #[test]
899 fn stored_and_deflated_entries_round_trip() {
900 let bytes = ZipBuilder::new()
901 .stored("a.txt", b"hello")
902 .deflated("dir/b.xml", &[b'x'; 5000])
903 .build();
904 let archive = Archive::from_bytes(bytes).unwrap();
905 assert_eq!(names(&archive), ["a.txt", "dir/b.xml"]);
906 let a = archive.entry("a.txt").unwrap();
907 assert_eq!(archive.read_to_vec(a).unwrap(), b"hello");
908 let b = archive.entry("dir/b.xml").unwrap();
909 assert_eq!(b.method, METHOD_DEFLATE);
910 assert_eq!(archive.read_to_vec(b).unwrap(), vec![b'x'; 5000]);
911 assert!(Archive::crc_matches(b, &[b'x'; 5000]));
912 assert_eq!(archive.layout().offset_shift, 0);
913 assert!(!archive.layout().zip64);
914 }
915
916 #[test]
917 fn data_descriptors_with_and_without_signature_are_read() {
918 for builder in [
919 ZipBuilder::new().with_data_descriptors(),
920 ZipBuilder::new().with_descriptor_signature(),
921 ] {
922 let bytes = builder
923 .deflated("a", b"alpha")
924 .deflated("b", b"beta")
925 .build();
926 let archive = Archive::from_bytes(bytes).unwrap();
927 assert!(archive.entry("a").unwrap().has_data_descriptor());
928 assert_eq!(
929 archive.read_to_vec(archive.entry("a").unwrap()).unwrap(),
930 b"alpha"
931 );
932 assert_eq!(
933 archive.read_to_vec(archive.entry("b").unwrap()).unwrap(),
934 b"beta"
935 );
936 }
937 }
938
939 #[test]
940 fn zip64_records_are_followed() {
941 let bytes = ZipBuilder::new()
942 .with_zip64()
943 .deflated("one", b"1")
944 .stored("two", b"22")
945 .build();
946 let archive = Archive::from_bytes(bytes).unwrap();
947 assert!(archive.layout().zip64);
948 assert_eq!(archive.layout().declared_entries, 2);
949 assert!(archive.entry("two").unwrap().has_zip64_extra());
950 assert_eq!(
951 archive.read_to_vec(archive.entry("one").unwrap()).unwrap(),
952 b"1"
953 );
954 assert_eq!(
955 archive.read_to_vec(archive.entry("two").unwrap()).unwrap(),
956 b"22"
957 );
958 }
959
960 #[test]
961 fn junk_before_the_archive_is_compensated() {
962 let bytes = ZipBuilder::new()
963 .with_prefix(b"JUNKJUNKJUNK")
964 .stored("a", b"A")
965 .deflated("b", b"BB")
966 .build();
967 let archive = Archive::from_bytes(bytes).unwrap();
968 assert_eq!(archive.layout().offset_shift, 12);
969 assert_eq!(
970 archive.read_to_vec(archive.entry("a").unwrap()).unwrap(),
971 b"A"
972 );
973 assert_eq!(
974 archive.read_to_vec(archive.entry("b").unwrap()).unwrap(),
975 b"BB"
976 );
977 }
978
979 #[test]
980 fn junk_before_a_zip64_archive_is_compensated() {
981 let bytes = ZipBuilder::new()
982 .with_zip64()
983 .with_prefix(b"xx")
984 .stored("a", b"A")
985 .build();
986 let archive = Archive::from_bytes(bytes).unwrap();
987 assert_eq!(archive.layout().offset_shift, 2);
988 assert_eq!(
989 archive.read_to_vec(archive.entry("a").unwrap()).unwrap(),
990 b"A"
991 );
992 }
993
994 #[test]
995 fn the_archive_comment_is_kept_and_does_not_hide_the_end_record() {
996 let comment = b"a comment containing PK\x05\x06 the signature bytes";
997 let bytes = ZipBuilder::new()
998 .with_comment(comment)
999 .stored("a", b"A")
1000 .build();
1001 let archive = Archive::from_bytes(bytes).unwrap();
1002 assert_eq!(archive.layout().comment, comment);
1003 assert_eq!(archive.layout().trailing_garbage, 0);
1004 assert_eq!(names(&archive), ["a"]);
1005 }
1006
1007 #[test]
1008 fn duplicate_names_keep_the_first_and_record_the_rest() {
1009 let bytes = ZipBuilder::new()
1010 .stored("a", b"first")
1011 .stored("a", b"second")
1012 .stored("b", b"")
1013 .build();
1014 let archive = Archive::from_bytes(bytes).unwrap();
1015 assert_eq!(archive.duplicates(), &[1]);
1016 assert_eq!(
1017 archive.read_to_vec(archive.entry("a").unwrap()).unwrap(),
1018 b"first"
1019 );
1020 }
1021
1022 #[test]
1023 fn not_a_zip_is_reported() {
1024 assert!(matches!(
1025 Archive::from_bytes(b"<?xml version=\"1.0\"?><x/>".to_vec()),
1026 Err(Error::NotZip)
1027 ));
1028 assert!(matches!(
1029 Archive::from_bytes(Vec::new()),
1030 Err(Error::NotZip)
1031 ));
1032 assert!(matches!(
1033 Archive::from_bytes(b"PK\x03\x04 and then nothing useful at all".to_vec()),
1034 Err(Error::NotZip)
1035 ));
1036 }
1037
1038 #[test]
1039 fn a_missing_central_directory_is_rebuilt_from_local_headers() {
1040 for builder in [
1041 ZipBuilder::new(),
1042 ZipBuilder::new().with_data_descriptors(),
1043 ZipBuilder::new().with_descriptor_signature(),
1044 ] {
1045 let bytes = builder
1046 .deflated("a.xml", b"<a>alpha</a>")
1047 .stored("b.bin", b"BB")
1048 .deflated("c.xml", &[b'c'; 4000])
1049 .build();
1050 let central = memmem::find(&bytes, &CENTRAL_HEADER_SIG).unwrap();
1051 let truncated = bytes[..central + 7].to_vec();
1052 let archive = Archive::from_bytes(truncated).unwrap();
1053 assert!(archive.layout().reconstructed);
1054 assert_eq!(names(&archive), ["a.xml", "b.bin", "c.xml"]);
1055 assert_eq!(
1056 archive
1057 .read_to_vec(archive.entry("a.xml").unwrap())
1058 .unwrap(),
1059 b"<a>alpha</a>"
1060 );
1061 assert_eq!(
1062 archive
1063 .read_to_vec(archive.entry("b.bin").unwrap())
1064 .unwrap(),
1065 b"BB"
1066 );
1067 assert_eq!(
1068 archive
1069 .read_to_vec(archive.entry("c.xml").unwrap())
1070 .unwrap(),
1071 vec![b'c'; 4000]
1072 );
1073 assert!(Archive::crc_matches(
1074 archive.entry("c.xml").unwrap(),
1075 &[b'c'; 4000]
1076 ));
1077 }
1078 }
1079
1080 #[test]
1081 fn a_file_cut_inside_its_last_entry_still_yields_the_earlier_ones() {
1082 let bytes = ZipBuilder::new()
1083 .deflated("a.xml", b"<a/>")
1084 .deflated("b.xml", &[b'b'; 3000])
1085 .build();
1086 let second = memmem::find_iter(&bytes, &LOCAL_HEADER_SIG).nth(1).unwrap();
1087 let archive = Archive::from_bytes(bytes[..second + 40].to_vec()).unwrap();
1088 assert!(archive.layout().reconstructed);
1089 assert_eq!(
1090 archive
1091 .read_to_vec(archive.entry("a.xml").unwrap())
1092 .unwrap(),
1093 b"<a/>"
1094 );
1095 assert!(archive
1096 .read_to_vec(archive.entry("b.xml").unwrap())
1097 .is_err());
1098 }
1099
1100 #[test]
1101 fn unsupported_method_and_encryption_are_refused_per_entry() {
1102 let bytes = ZipBuilder::new()
1103 .with_method_code(12)
1104 .stored("a", b"A")
1105 .build();
1106 let archive = Archive::from_bytes(bytes).unwrap();
1107 assert!(matches!(
1108 archive.read_to_vec(archive.entry("a").unwrap()),
1109 Err(Error::Unsupported(_))
1110 ));
1111 let mut bytes = ZipBuilder::new().stored("e", b"E").build();
1112 let central = memmem::find(&bytes, &CENTRAL_HEADER_SIG).unwrap();
1113 bytes[central + 8] |= FLAG_ENCRYPTED as u8;
1114 let archive = Archive::from_bytes(bytes).unwrap();
1115 assert!(archive.entry("e").unwrap().is_encrypted());
1116 assert!(matches!(
1117 archive.read_to_vec(archive.entry("e").unwrap()),
1118 Err(Error::Unsupported(_))
1119 ));
1120 }
1121
1122 #[test]
1123 fn a_truncated_central_directory_yields_the_readable_entries() {
1124 let bytes = ZipBuilder::new()
1125 .stored("a", b"A")
1126 .stored("b", b"B")
1127 .stored("c", b"C")
1128 .build();
1129 let central = memmem::find(&bytes, &CENTRAL_HEADER_SIG).unwrap();
1130 let end = memmem::rfind(&bytes, &END_RECORD_SIG).unwrap();
1131 let third = memmem::find_iter(&bytes[central..end], &CENTRAL_HEADER_SIG)
1132 .nth(2)
1133 .unwrap()
1134 + central;
1135 let mut damaged = bytes[..third].to_vec();
1136 damaged.extend_from_slice(&bytes[end..]);
1137 let archive = Archive::from_bytes(damaged).unwrap();
1138 assert_eq!(names(&archive), ["a", "b"]);
1139 assert!(archive.layout().truncated_central);
1140 assert_eq!(
1141 archive.read_to_vec(archive.entry("b").unwrap()).unwrap(),
1142 b"B"
1143 );
1144 }
1145
1146 #[test]
1147 fn local_header_reports_the_recorded_fields() {
1148 let bytes = ZipBuilder::new()
1149 .with_utf8_flag()
1150 .deflated("ppt/slides/slide1.xml", b"<p:sld/>")
1151 .build();
1152 let archive = Archive::from_bytes(bytes).unwrap();
1153 let entry = archive.entry("ppt/slides/slide1.xml").unwrap();
1154 assert!(entry.names_are_utf8());
1155 let header = archive.local_header(entry).unwrap();
1156 assert_eq!(header.raw_name, b"ppt/slides/slide1.xml");
1157 assert_eq!(header.method, METHOD_DEFLATE);
1158 assert_eq!(header.crc32, entry.crc32);
1159 assert_eq!(header.data_offset, 30 + 21);
1160 }
1161
1162 #[test]
1163 fn a_file_source_reads_the_same_bytes() {
1164 let bytes = ZipBuilder::new().deflated("a", b"from a file").build();
1165 let path = std::env::temp_dir().join(format!("pptxboss-zip-{}.zip", std::process::id()));
1166 std::fs::write(&path, &bytes).unwrap();
1167 let archive = Archive::open_path(&path).unwrap();
1168 assert_eq!(
1169 archive.read_to_vec(archive.entry("a").unwrap()).unwrap(),
1170 b"from a file"
1171 );
1172 std::fs::remove_file(&path).unwrap();
1173 }
1174
1175 #[test]
1176 fn corrupt_deflate_data_is_an_inflate_error() {
1177 let mut bytes = ZipBuilder::new().deflated("a", &[b'z'; 3000]).build();
1178 let data_start = 30 + 1;
1179 for byte in &mut bytes[data_start + 2..data_start + 12] {
1180 *byte = 0xff;
1181 }
1182 let archive = Archive::from_bytes(bytes).unwrap();
1183 assert!(matches!(
1184 archive.read_to_vec(archive.entry("a").unwrap()),
1185 Err(Error::Inflate(_))
1186 ));
1187 }
1188
1189 #[test]
1190 fn missing_compressed_size_falls_back_to_the_next_header() {
1191 let mut bytes = ZipBuilder::new()
1192 .with_data_descriptors()
1193 .deflated("a", b"alpha alpha alpha")
1194 .stored("b", b"beta")
1195 .build();
1196 let central = memmem::find(&bytes, &CENTRAL_HEADER_SIG).unwrap();
1197 for byte in &mut bytes[central + 20..central + 24] {
1198 *byte = 0;
1199 }
1200 let archive = Archive::from_bytes(bytes).unwrap();
1201 let a = archive.entry("a").unwrap();
1202 assert_eq!(a.compressed_size, 0);
1203 assert_eq!(archive.read_to_vec(a).unwrap(), b"alpha alpha alpha");
1204 }
1205}