1use crate::detect::{sniff, Format};
13use crate::error::{ArchiveError, Result};
14use crate::peel::MAX_INFLATED;
15use crate::plan::Access;
16
17#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct ArchiveEntry {
20 pub name: String,
22 pub size: u64,
24 pub is_dir: bool,
26}
27
28pub struct Archive {
30 format: Format,
31 entries: Vec<ArchiveEntry>,
32 backend: Backend,
33}
34
35#[allow(clippy::large_enum_variant)]
40enum Backend {
41 Tar { compressed: Vec<u8>, outer: Format },
45 Zip {
47 archive: zip_core::ZipArchive<std::io::Cursor<Vec<u8>>>,
48 },
49 SevenZip {
51 reader: sevenz_rust2::ArchiveReader<std::io::Cursor<Vec<u8>>>,
52 },
53}
54
55impl Archive {
56 pub fn open(data: &[u8], name: Option<&str>) -> Result<Option<Archive>> {
68 Self::open_with_format(sniff(name, data), data)
69 }
70
71 pub(crate) fn open_with_format(format: Format, data: &[u8]) -> Result<Option<Archive>> {
81 match format {
82 Format::Tar | Format::TarGz | Format::TarBz2 => {
83 Self::open_tar(format, data.to_vec()).map(Some)
84 }
85 Format::Zip => Self::open_zip(data).map(Some),
86 Format::SevenZip => Self::open_7z(data).map(Some),
87 _ => Ok(None),
88 }
89 }
90
91 pub fn member_access(&mut self, index: usize) -> Result<Access> {
101 let count = self.entries.len();
102 if index >= count {
103 return Err(ArchiveError::IndexOutOfRange { index, count });
104 }
105 match &mut self.backend {
106 Backend::Zip { archive } => {
107 let f = archive.by_index(index).map_err(|e| ArchiveError::Read {
108 format: "zip",
109 detail: e.to_string(),
112 })?;
113 Ok(match f.compression() {
114 zip_core::CompressionMethod::Stored => Access::InPlace {
115 offset: f.data_start(),
116 len: f.compressed_size(),
117 },
118 zip_core::CompressionMethod::Deflated
119 | zip_core::CompressionMethod::Deflate64 => Access::Zran,
120 _ => Access::SpillToTemp,
121 })
122 }
123 Backend::Tar { .. } | Backend::SevenZip { .. } => Ok(Access::SpillToTemp),
126 }
127 }
128
129 #[must_use]
131 pub fn format(&self) -> Format {
132 self.format
133 }
134
135 #[must_use]
137 pub fn entries(&self) -> &[ArchiveEntry] {
138 &self.entries
139 }
140
141 pub fn read(&mut self, index: usize) -> Result<Vec<u8>> {
148 let count = self.entries.len();
149 if index >= count {
150 return Err(ArchiveError::IndexOutOfRange { index, count });
151 }
152 let (name, declared_size) = {
155 let e = &self.entries[index];
156 (e.name.clone(), e.size)
157 };
158 match &mut self.backend {
159 Backend::Tar { compressed, outer } => {
160 extract_tar_member_streaming(compressed, *outer, index, MAX_INFLATED)
161 }
162 Backend::Zip { archive } => read_zip_member(archive, index),
163 Backend::SevenZip { reader } => read_7z_member(reader, &name, declared_size),
164 }
165 }
166
167 pub fn stream_member(
180 &mut self,
181 index: usize,
182 out: &mut dyn std::io::Write,
183 cap: u64,
184 ) -> Result<u64> {
185 let count = self.entries.len();
186 if index >= count {
187 return Err(ArchiveError::IndexOutOfRange { index, count });
188 }
189 let (name, declared_size) = {
190 let e = &self.entries[index];
191 (e.name.clone(), e.size)
192 };
193 match &mut self.backend {
194 Backend::Tar { compressed, outer } => {
195 stream_tar_member(compressed, *outer, index, out, cap)
196 }
197 Backend::Zip { archive } => stream_zip_member(archive, index, out, cap),
198 Backend::SevenZip { reader } => {
199 let bytes = read_7z_member(reader, &name, declared_size)?;
200 if bytes.len() as u64 > cap {
201 return Err(ArchiveError::TooLarge { cap });
202 }
203 out.write_all(&bytes).map_err(|e| ArchiveError::Read {
204 format: "7z",
205 detail: e.to_string(),
206 })?;
207 Ok(bytes.len() as u64)
208 }
209 }
210 }
211
212 fn open_tar(outer: Format, compressed: Vec<u8>) -> Result<Archive> {
217 let entries = {
218 let mut ar = tar::Archive::new(tar_stream(&compressed, outer));
219 let iter = ar.entries().map_err(|e| ArchiveError::Open {
220 format: "tar",
221 detail: e.to_string(),
222 })?;
223 let mut entries = Vec::new();
224 for entry in iter {
225 let entry = entry.map_err(|e| ArchiveError::Open {
226 format: "tar",
227 detail: e.to_string(),
228 })?;
229 let name = entry.path().map_or_else(
230 |_| String::from_utf8_lossy(&entry.path_bytes()).into_owned(),
231 |p| p.to_string_lossy().into_owned(),
232 );
233 let header = entry.header();
234 let size = header.size().map_err(|e| ArchiveError::Open {
235 format: "tar",
236 detail: e.to_string(),
237 })?;
238 entries.push(ArchiveEntry {
239 name,
240 size,
241 is_dir: header.entry_type().is_dir(),
242 });
243 }
244 entries
245 };
246 Ok(Archive {
247 format: outer,
248 entries,
249 backend: Backend::Tar { compressed, outer },
250 })
251 }
252
253 fn open_zip(data: &[u8]) -> Result<Archive> {
255 let mut archive =
256 zip_core::ZipArchive::new(std::io::Cursor::new(data.to_vec())).map_err(|e| {
257 ArchiveError::Open {
258 format: "zip",
259 detail: e.to_string(),
260 }
261 })?;
262 let count = archive.len();
263 let mut entries = Vec::with_capacity(count);
264 for i in 0..count {
265 let f = archive.by_index(i).map_err(|e| ArchiveError::Open {
266 format: "zip",
267 detail: e.to_string(),
268 })?;
269 entries.push(ArchiveEntry {
270 name: f.name().to_string(),
271 size: f.size(),
272 is_dir: f.is_dir(),
273 });
274 }
275 Ok(Archive {
276 format: Format::Zip,
277 entries,
278 backend: Backend::Zip { archive },
279 })
280 }
281
282 fn open_7z(data: &[u8]) -> Result<Archive> {
284 let reader = sevenz_rust2::ArchiveReader::new(
285 std::io::Cursor::new(data.to_vec()),
286 sevenz_rust2::Password::empty(),
287 )
288 .map_err(|e| ArchiveError::Open {
289 format: "7z",
290 detail: e.to_string(),
291 })?;
292 let entries = reader
293 .archive()
294 .files
295 .iter()
296 .map(|f| ArchiveEntry {
297 name: f.name.clone(),
298 size: f.size,
299 is_dir: f.is_directory,
300 })
301 .collect();
302 Ok(Archive {
303 format: Format::SevenZip,
304 entries,
305 backend: Backend::SevenZip { reader },
306 })
307 }
308}
309
310fn tar_stream(compressed: &[u8], outer: Format) -> Box<dyn std::io::Read + '_> {
314 let cursor = std::io::Cursor::new(compressed);
315 match outer {
316 Format::TarGz => Box::new(flate2::read::GzDecoder::new(cursor)),
317 Format::TarBz2 => Box::new(bzip2_rs::DecoderReader::new(cursor)),
318 _ => Box::new(cursor),
321 }
322}
323
324fn extract_tar_member_streaming(
330 compressed: &[u8],
331 outer: Format,
332 index: usize,
333 cap: u64,
334) -> Result<Vec<u8>> {
335 use std::io::Read;
336 let mut ar = tar::Archive::new(tar_stream(compressed, outer));
337 let mut iter = ar.entries().map_err(|e| ArchiveError::Read {
338 format: "tar",
339 detail: e.to_string(),
340 })?;
341 let entry = iter
342 .nth(index)
343 .ok_or(ArchiveError::IndexOutOfRange {
344 index,
345 count: index,
346 })?
347 .map_err(|e| ArchiveError::Read {
348 format: "tar",
349 detail: e.to_string(),
350 })?;
351 let mut out = Vec::new();
352 let mut limited = entry.take(cap + 1);
353 limited
354 .read_to_end(&mut out)
355 .map_err(|e| ArchiveError::Read {
356 format: "tar",
357 detail: e.to_string(),
358 })?;
359 if out.len() as u64 > cap {
360 return Err(ArchiveError::TooLarge { cap });
361 }
362 Ok(out)
363}
364
365fn read_zip_member(
368 archive: &mut zip_core::ZipArchive<std::io::Cursor<Vec<u8>>>,
369 index: usize,
370) -> Result<Vec<u8>> {
371 use std::io::Read;
372 let zf = archive.by_index(index).map_err(|e| ArchiveError::Read {
373 format: "zip",
374 detail: e.to_string(),
375 })?;
376 let mut out = Vec::new();
377 let mut limited = zf.take(MAX_INFLATED + 1);
378 limited
379 .read_to_end(&mut out)
380 .map_err(|e| ArchiveError::Read {
381 format: "zip",
382 detail: e.to_string(),
383 })?;
384 if out.len() as u64 > MAX_INFLATED {
385 return Err(ArchiveError::TooLarge { cap: MAX_INFLATED });
386 }
387 Ok(out)
388}
389
390fn read_7z_member(
395 reader: &mut sevenz_rust2::ArchiveReader<std::io::Cursor<Vec<u8>>>,
396 name: &str,
397 declared_size: u64,
398) -> Result<Vec<u8>> {
399 if declared_size > MAX_INFLATED {
400 return Err(ArchiveError::TooLarge { cap: MAX_INFLATED });
401 }
402 let out = reader.read_file(name).map_err(|e| ArchiveError::Read {
403 format: "7z",
404 detail: e.to_string(),
405 })?;
406 if out.len() as u64 > MAX_INFLATED {
407 return Err(ArchiveError::TooLarge { cap: MAX_INFLATED });
408 }
409 Ok(out)
410}
411
412fn copy_capped(
416 reader: impl std::io::Read,
417 out: &mut dyn std::io::Write,
418 cap: u64,
419 format: &'static str,
420) -> Result<u64> {
421 let mut limited = reader.take(cap + 1);
422 let n = std::io::copy(&mut limited, out).map_err(|e| ArchiveError::Read {
423 format,
424 detail: e.to_string(),
425 })?;
426 if n > cap {
427 return Err(ArchiveError::TooLarge { cap });
428 }
429 Ok(n)
430}
431
432fn stream_tar_member(
436 compressed: &[u8],
437 outer: Format,
438 index: usize,
439 out: &mut dyn std::io::Write,
440 cap: u64,
441) -> Result<u64> {
442 let mut ar = tar::Archive::new(tar_stream(compressed, outer));
443 let mut iter = ar.entries().map_err(|e| ArchiveError::Read {
444 format: "tar",
445 detail: e.to_string(),
446 })?;
447 let entry = iter
448 .nth(index)
449 .ok_or(ArchiveError::IndexOutOfRange {
450 index,
451 count: index,
452 })?
453 .map_err(|e| ArchiveError::Read {
454 format: "tar",
455 detail: e.to_string(),
456 })?;
457 copy_capped(entry, out, cap, "tar")
458}
459
460fn stream_zip_member(
463 archive: &mut zip_core::ZipArchive<std::io::Cursor<Vec<u8>>>,
464 index: usize,
465 out: &mut dyn std::io::Write,
466 cap: u64,
467) -> Result<u64> {
468 let zf = archive.by_index(index).map_err(|e| ArchiveError::Read {
469 format: "zip",
470 detail: e.to_string(),
471 })?;
472 copy_capped(zf, out, cap, "zip")
473}
474
475#[cfg(test)]
476mod tests {
477 use super::*;
478 use std::io::Write;
479
480 fn build_tar(members: &[(&str, Vec<u8>)]) -> Vec<u8> {
482 let mut b = tar::Builder::new(Vec::new());
483 for (name, data) in members {
484 let mut h = tar::Header::new_gnu();
485 h.set_size(data.len() as u64);
486 h.set_mode(0o644);
487 h.set_cksum();
488 b.append_data(&mut h, name, data.as_slice()).unwrap();
489 }
490 b.into_inner().unwrap()
491 }
492
493 fn gzip(data: &[u8]) -> Vec<u8> {
494 let mut e = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
495 e.write_all(data).unwrap();
496 e.finish().unwrap()
497 }
498
499 #[test]
506 fn streaming_targz_extracts_each_member_under_whole_tar_cap() {
507 let a = vec![0xAA_u8; 1000];
508 let b = vec![0xBB_u8; 1000];
509 let tar = build_tar(&[("a.bin", a.clone()), ("b.bin", b.clone())]);
510 assert!(
511 tar.len() as u64 > 1200,
512 "combined tar ({}) must exceed the per-member cap",
513 tar.len()
514 );
515 let targz = gzip(&tar);
516 assert_eq!(
517 extract_tar_member_streaming(&targz, Format::TarGz, 0, 1200).unwrap(),
518 a
519 );
520 assert_eq!(
521 extract_tar_member_streaming(&targz, Format::TarGz, 1, 1200).unwrap(),
522 b
523 );
524 }
525
526 #[test]
527 fn streaming_plain_tar_extracts_member() {
528 let a = vec![0x11_u8; 800];
529 let b = vec![0x22_u8; 800];
530 let tar = build_tar(&[("a.bin", a.clone()), ("b.bin", b.clone())]);
531 assert_eq!(
532 extract_tar_member_streaming(&tar, Format::Tar, 0, MAX_INFLATED).unwrap(),
533 a
534 );
535 assert_eq!(
536 extract_tar_member_streaming(&tar, Format::Tar, 1, MAX_INFLATED).unwrap(),
537 b
538 );
539 }
540
541 #[test]
542 fn streaming_member_over_cap_fails_loud() {
543 let targz = gzip(&build_tar(&[("a.bin", vec![0xAA_u8; 1000])]));
544 match extract_tar_member_streaming(&targz, Format::TarGz, 0, 500) {
545 Err(ArchiveError::TooLarge { cap }) => assert_eq!(cap, 500),
546 other => panic!("expected TooLarge, got {other:?}"),
547 }
548 }
549
550 #[test]
551 fn streaming_bad_index_fails_loud() {
552 let tar = build_tar(&[("only.bin", vec![0x33_u8; 10])]);
553 assert!(matches!(
554 extract_tar_member_streaming(&tar, Format::Tar, 99, MAX_INFLATED),
555 Err(ArchiveError::IndexOutOfRange { .. })
556 ));
557 }
558
559 const PAYLOAD_TBZ2: &[u8] = include_bytes!("../../tests/data/fixtures/payload.tbz2");
563
564 #[test]
565 fn member_access_out_of_range_fails_loud() {
566 let mut a = Archive::open(PAYLOAD_TBZ2, Some("payload.tbz2"))
567 .unwrap()
568 .unwrap();
569 assert!(matches!(
570 a.member_access(9999),
571 Err(ArchiveError::IndexOutOfRange { .. })
572 ));
573 }
574
575 #[test]
576 fn streaming_tbz2_reads_member_via_same_path() {
577 let mut a = Archive::open(PAYLOAD_TBZ2, Some("payload.tbz2"))
578 .unwrap()
579 .unwrap();
580 assert_eq!(a.format(), Format::TarBz2);
581 let ia = a
582 .entries()
583 .iter()
584 .position(|e| e.name == "a.txt" && !e.is_dir)
585 .unwrap();
586 assert_eq!(a.read(ia).unwrap(), b"alpha member contents\n");
587 }
588
589 const FX: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../tests/data/fixtures/");
590
591 fn load(name: &str) -> Vec<u8> {
592 std::fs::read(format!("{FX}{name}")).unwrap()
593 }
594
595 fn member_index(a: &Archive, name: &str) -> usize {
596 a.entries()
597 .iter()
598 .position(|e| e.name == name && !e.is_dir)
599 .unwrap_or_else(|| panic!("member {name} not found"))
600 }
601
602 #[test]
605 fn stream_member_tar_writes_bytes_and_count() {
606 let payload = vec![0xCD_u8; 700];
607 let tar = build_tar(&[("only.bin", payload.clone())]);
608 let mut a = Archive::open(&tar, Some("x.tar")).unwrap().unwrap();
609 let mut sink = Vec::new();
610 let n = a.stream_member(0, &mut sink, MAX_INFLATED).unwrap();
611 assert_eq!(n, payload.len() as u64);
612 assert_eq!(sink, payload);
613 }
614
615 #[test]
618 fn stream_member_tar_over_cap_fails_loud() {
619 let tar = build_tar(&[("big.bin", vec![0x7E_u8; 1000])]);
620 let mut a = Archive::open(&tar, Some("x.tar")).unwrap().unwrap();
621 let mut sink = Vec::new();
622 match a.stream_member(0, &mut sink, 100) {
623 Err(ArchiveError::TooLarge { cap }) => assert_eq!(cap, 100),
624 other => panic!("expected TooLarge, got {other:?}"),
625 }
626 }
627
628 #[test]
630 fn stream_member_out_of_range_fails_loud() {
631 let tar = build_tar(&[("only.bin", vec![0x33_u8; 10])]);
632 let mut a = Archive::open(&tar, Some("x.tar")).unwrap().unwrap();
633 let mut sink = Vec::new();
634 assert!(matches!(
635 a.stream_member(99, &mut sink, MAX_INFLATED),
636 Err(ArchiveError::IndexOutOfRange { .. })
637 ));
638 }
639
640 #[test]
643 fn stream_member_7z_writes_bytes() {
644 let data = load("payload.7z");
645 let mut a = Archive::open(&data, Some("payload.7z")).unwrap().unwrap();
646 let ia = member_index(&a, "a.txt");
647 let mut sink = Vec::new();
648 let n = a.stream_member(ia, &mut sink, MAX_INFLATED).unwrap();
649 assert_eq!(sink, b"alpha member contents\n");
650 assert_eq!(n, sink.len() as u64);
651 }
652
653 #[test]
656 fn stream_member_7z_over_cap_fails_loud() {
657 let data = load("payload.7z");
658 let mut a = Archive::open(&data, Some("payload.7z")).unwrap().unwrap();
659 let ia = member_index(&a, "a.txt");
660 let mut sink = Vec::new();
661 match a.stream_member(ia, &mut sink, 3) {
662 Err(ArchiveError::TooLarge { cap }) => assert_eq!(cap, 3),
663 other => panic!("expected TooLarge, got {other:?}"),
664 }
665 }
666
667 #[test]
669 fn stream_member_zip_writes_bytes() {
670 let data = load("payload.zip");
671 let mut a = Archive::open(&data, Some("payload.zip")).unwrap().unwrap();
672 let ia = member_index(&a, "a.txt");
673 let mut sink = Vec::new();
674 let n = a.stream_member(ia, &mut sink, MAX_INFLATED).unwrap();
675 assert_eq!(sink, b"alpha member contents\n");
676 assert_eq!(n, sink.len() as u64);
677 }
678
679 #[test]
682 fn corrupt_zip_open_fails_loud() {
683 let mut bytes = b"PK\x03\x04".to_vec();
684 bytes.extend_from_slice(&[0u8; 64]);
685 match Archive::open(&bytes, Some("x.zip")) {
686 Err(ArchiveError::Open { format, .. }) => assert_eq!(format, "zip"),
687 Err(e) => panic!("expected a zip Open error, got a different error: {e:?}"),
688 Ok(_) => panic!("expected a loud zip Open error, got Ok"),
689 }
690 }
691
692 #[test]
695 fn corrupt_7z_open_fails_loud() {
696 let mut bytes = vec![0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C];
697 bytes.extend_from_slice(&[0u8; 64]);
698 match Archive::open(&bytes, Some("x.7z")) {
699 Err(ArchiveError::Open { format, .. }) => assert_eq!(format, "7z"),
700 Err(e) => panic!("expected a 7z Open error, got a different error: {e:?}"),
701 Ok(_) => panic!("expected a loud 7z Open error, got Ok"),
702 }
703 }
704
705 #[test]
708 fn member_access_classifies_zip_stored_and_deflated() {
709 let mut stored = Archive::open(&load("stored_one.zip"), Some("stored_one.zip"))
710 .unwrap()
711 .unwrap();
712 assert!(matches!(
713 stored.member_access(0).unwrap(),
714 Access::InPlace { .. }
715 ));
716 let mut deflated = Archive::open(&load("deflate_one.zip"), Some("deflate_one.zip"))
717 .unwrap()
718 .unwrap();
719 assert!(matches!(deflated.member_access(0).unwrap(), Access::Zran));
720 }
721
722 fn stored_zip_bad_crc(name: &str, payload: &[u8]) -> Vec<u8> {
725 let nb = name.as_bytes();
726 let mut crc = flate2::Crc::new();
727 crc.update(payload);
728 let bad_crc = crc.sum() ^ 0xFFFF_FFFF; let (sz, nlen) = (payload.len() as u32, nb.len() as u16);
730 let mut z = Vec::new();
731 z.extend_from_slice(&0x0403_4b50u32.to_le_bytes());
733 z.extend_from_slice(&20u16.to_le_bytes()); z.extend_from_slice(&0u16.to_le_bytes()); z.extend_from_slice(&0u16.to_le_bytes()); z.extend_from_slice(&0u16.to_le_bytes()); z.extend_from_slice(&0u16.to_le_bytes()); z.extend_from_slice(&bad_crc.to_le_bytes());
739 z.extend_from_slice(&sz.to_le_bytes()); z.extend_from_slice(&sz.to_le_bytes()); z.extend_from_slice(&nlen.to_le_bytes());
742 z.extend_from_slice(&0u16.to_le_bytes()); z.extend_from_slice(nb);
744 z.extend_from_slice(payload);
745 let cd_offset = z.len() as u32;
746 let mut central = Vec::new();
748 central.extend_from_slice(&0x0201_4b50u32.to_le_bytes());
749 central.extend_from_slice(&20u16.to_le_bytes()); central.extend_from_slice(&20u16.to_le_bytes()); central.extend_from_slice(&0u16.to_le_bytes()); central.extend_from_slice(&0u16.to_le_bytes()); central.extend_from_slice(&0u16.to_le_bytes()); central.extend_from_slice(&0u16.to_le_bytes()); central.extend_from_slice(&bad_crc.to_le_bytes());
756 central.extend_from_slice(&sz.to_le_bytes());
757 central.extend_from_slice(&sz.to_le_bytes());
758 central.extend_from_slice(&nlen.to_le_bytes());
759 central.extend_from_slice(&0u16.to_le_bytes()); central.extend_from_slice(&0u16.to_le_bytes()); central.extend_from_slice(&0u16.to_le_bytes()); central.extend_from_slice(&0u16.to_le_bytes()); central.extend_from_slice(&0u32.to_le_bytes()); central.extend_from_slice(&0u32.to_le_bytes()); central.extend_from_slice(nb);
766 let cd_size = central.len() as u32;
767 z.extend_from_slice(¢ral);
768 z.extend_from_slice(&0x0605_4b50u32.to_le_bytes());
770 z.extend_from_slice(&0u16.to_le_bytes()); z.extend_from_slice(&0u16.to_le_bytes()); z.extend_from_slice(&1u16.to_le_bytes()); z.extend_from_slice(&1u16.to_le_bytes()); z.extend_from_slice(&cd_size.to_le_bytes());
775 z.extend_from_slice(&cd_offset.to_le_bytes());
776 z.extend_from_slice(&0u16.to_le_bytes()); z
778 }
779
780 #[test]
784 fn zip_member_bad_crc_fails_loud_on_read_and_stream() {
785 let data = stored_zip_bad_crc("blob.bin", b"corruption-detected-by-crc");
786 let mut a = Archive::open(&data, Some("x.zip")).unwrap().unwrap();
787 assert_eq!(a.entries().len(), 1);
788 match a.read(0) {
789 Err(ArchiveError::Read { format, .. }) => assert_eq!(format, "zip"),
790 other => panic!("expected a loud zip Read error on read, got {other:?}"),
791 }
792 let mut sink = Vec::new();
793 match a.stream_member(0, &mut sink, MAX_INFLATED) {
794 Err(ArchiveError::Read { format, .. }) => assert_eq!(format, "zip"),
795 other => panic!("expected a loud zip Read error on stream, got {other:?}"),
796 }
797 }
798}