1use std::io::{self, Read, Seek, SeekFrom, Write};
23use std::path::Path;
24use std::sync::Arc;
25
26use crate::archive::Archive;
27use crate::error::{ArchiveError, Result};
28use crate::plan::{detect, Access, AccessPlan, Codec};
29use crate::resolve::Limits;
30
31pub trait ReadSeek: Read + Seek {}
34impl<T: Read + Seek> ReadSeek for T {}
35
36#[derive(Debug)]
42pub struct SubRange {
43 data: Arc<Vec<u8>>,
44 start: u64,
45 len: u64,
46 pos: u64,
47}
48
49impl SubRange {
50 #[cfg(test)]
59 fn new(data: Vec<u8>, offset: u64, len: u64, cap: u64) -> Result<Self> {
60 Self::new_shared(Arc::new(data), offset, len, cap)
61 }
62
63 pub(crate) fn new_shared(data: Arc<Vec<u8>>, offset: u64, len: u64, cap: u64) -> Result<Self> {
69 if len > cap {
70 return Err(ArchiveError::TooLarge { cap });
71 }
72 offset
73 .checked_add(len)
74 .filter(|&e| e <= data.len() as u64)
75 .ok_or(ArchiveError::Read {
76 format: "in-place",
77 detail: format!(
78 "member window [{offset}, {offset}+{len}) exceeds the {}-byte source",
79 data.len()
80 ),
81 })?;
82 Ok(SubRange {
83 data,
84 start: offset,
85 len,
86 pos: 0,
87 })
88 }
89
90 #[must_use]
92 pub fn offset(&self) -> u64 {
93 self.start
94 }
95
96 #[must_use]
98 pub fn len(&self) -> u64 {
99 self.len
100 }
101
102 #[must_use]
104 pub fn is_empty(&self) -> bool {
105 self.len == 0
106 }
107}
108
109impl Read for SubRange {
110 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
111 let remaining = self.len - self.pos;
112 if remaining == 0 || buf.is_empty() {
113 return Ok(0);
114 }
115 let n = remaining.min(buf.len() as u64) as usize;
116 let start = self.start.saturating_add(self.pos) as usize;
117 let Some(src) = self.data.get(start..start + n) else {
120 return Ok(0); };
122 buf[..n].copy_from_slice(src);
123 self.pos += n as u64;
124 Ok(n)
125 }
126}
127
128impl Seek for SubRange {
129 fn seek(&mut self, from: SeekFrom) -> io::Result<u64> {
130 let target = match from {
131 SeekFrom::Start(o) => o,
132 SeekFrom::End(o) => offset_from(self.len, o)?,
133 SeekFrom::Current(o) => offset_from(self.pos, o)?,
134 };
135 self.pos = target.min(self.len);
137 Ok(self.pos)
138 }
139}
140
141#[derive(Debug)]
144pub struct TempBacked {
145 inner: tempfile::NamedTempFile,
146 len: u64,
147}
148
149impl TempBacked {
150 #[must_use]
152 pub fn len(&self) -> u64 {
153 self.len
154 }
155
156 #[must_use]
158 pub fn is_empty(&self) -> bool {
159 self.len == 0
160 }
161
162 #[must_use]
164 pub fn path(&self) -> &Path {
165 self.inner.path()
166 }
167}
168
169impl Read for TempBacked {
170 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
171 self.inner.as_file_mut().read(buf)
172 }
173}
174
175impl Seek for TempBacked {
176 fn seek(&mut self, from: SeekFrom) -> io::Result<u64> {
177 self.inner.as_file_mut().seek(from)
178 }
179}
180
181pub struct ZranBacked {
188 entry: zip_core::StoredZipEntry,
189 len: u64,
190 pos: u64,
191 archive: tempfile::NamedTempFile,
195}
196
197impl ZranBacked {
198 #[must_use]
200 pub fn len(&self) -> u64 {
201 self.len
202 }
203
204 #[must_use]
206 pub fn is_empty(&self) -> bool {
207 self.len == 0
208 }
209}
210
211impl std::fmt::Debug for ZranBacked {
212 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
213 f.debug_struct("ZranBacked")
214 .field("len", &self.len)
215 .field("pos", &self.pos)
216 .field("archive", &self.archive.path())
217 .finish_non_exhaustive()
218 }
219}
220
221impl Read for ZranBacked {
222 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
223 let n = self.entry.read_at(buf, self.pos)?;
224 self.pos = self.pos.saturating_add(n as u64);
225 Ok(n)
226 }
227}
228
229impl Seek for ZranBacked {
230 fn seek(&mut self, from: SeekFrom) -> io::Result<u64> {
231 let target = match from {
232 SeekFrom::Start(o) => o,
233 SeekFrom::End(o) => offset_from(self.len, o)?,
234 SeekFrom::Current(o) => offset_from(self.pos, o)?,
235 };
236 self.pos = target.min(self.len);
238 Ok(self.pos)
239 }
240}
241
242#[derive(Debug)]
246#[non_exhaustive]
247pub enum PeeledSource {
248 InPlace(SubRange),
250 Zran(ZranBacked),
253 Temp(TempBacked),
255}
256
257impl PeeledSource {
258 #[must_use]
261 pub fn len(&self) -> u64 {
262 match self {
263 PeeledSource::InPlace(s) => s.len(),
264 PeeledSource::Zran(z) => z.len(),
265 PeeledSource::Temp(t) => t.len(),
266 }
267 }
268
269 #[must_use]
271 pub fn is_empty(&self) -> bool {
272 self.len() == 0
273 }
274}
275
276impl Read for PeeledSource {
277 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
278 match self {
279 PeeledSource::InPlace(s) => s.read(buf),
280 PeeledSource::Zran(z) => z.read(buf),
281 PeeledSource::Temp(t) => t.read(buf),
282 }
283 }
284}
285
286impl Seek for PeeledSource {
287 fn seek(&mut self, from: SeekFrom) -> io::Result<u64> {
288 match self {
289 PeeledSource::InPlace(s) => s.seek(from),
290 PeeledSource::Zran(z) => z.seek(from),
291 PeeledSource::Temp(t) => t.seek(from),
292 }
293 }
294}
295
296#[derive(Debug)]
299#[non_exhaustive]
300pub enum PeelSource {
301 NotPacked,
304 Inner(PeeledSource),
307}
308
309pub fn peel_archive_seekable(
323 data: Vec<u8>,
324 _name: Option<&str>,
325 limits: &Limits,
326) -> Result<PeelSource> {
327 let cap = limits.max_total_inflated;
328 let plan = detect(&data)?;
329 let data = Arc::new(data);
332 match plan {
333 AccessPlan::Direct | AccessPlan::Collection { .. } | AccessPlan::SegmentSet { .. } => {
335 Ok(PeelSource::NotPacked)
336 }
337
338 AccessPlan::Wrapper { codec, .. } => {
340 let temp = spill_wrapper_to_temp(&data, codec, cap)?;
341 Ok(PeelSource::Inner(PeeledSource::Temp(temp)))
342 }
343
344 AccessPlan::Member {
346 format,
347 index,
348 name,
349 access,
350 } => Ok(PeelSource::Inner(execute_member_access(
351 format, &data, index, &name, &access, limits,
352 )?)),
353 }
354}
355
356pub(crate) fn execute_member_access(
367 format: crate::Format,
368 data: &Arc<Vec<u8>>,
369 index: usize,
370 name: &str,
371 access: &Access,
372 limits: &Limits,
373) -> Result<PeeledSource> {
374 let cap = limits.max_total_inflated;
375 match *access {
376 Access::InPlace { offset, len } => {
378 let sub = SubRange::new_shared(Arc::clone(data), offset, len, cap)?;
379 Ok(PeeledSource::InPlace(sub))
380 }
381 Access::Zran => peel_zran(format, data, index, name, limits),
384 Access::SpillToTemp => {
387 let temp = spill_member_to_temp(format, data, index, cap)?;
388 Ok(PeeledSource::Temp(temp))
389 }
390 }
391}
392
393fn spill_wrapper_to_temp(data: &[u8], codec: Codec, cap: u64) -> Result<TempBacked> {
395 let reader: Box<dyn Read> = match codec {
396 Codec::Gzip => Box::new(flate2::read::GzDecoder::new(data)),
397 Codec::Bzip2 => Box::new(bzip2_rs::DecoderReader::new(data)),
398 };
399 spill(|out| copy_capped(reader, out, cap, codec_name(codec)))
400}
401
402fn spill_member_to_temp(
404 format: crate::Format,
405 data: &[u8],
406 index: usize,
407 cap: u64,
408) -> Result<TempBacked> {
409 let Some(mut archive) = Archive::open_with_format(format, data)? else {
412 return Err(ArchiveError::Open {
414 format: "archive",
415 detail: "member plan produced for a non-archive format".to_string(),
416 });
417 };
418 spill(|out| archive.stream_member(index, out, cap))
419}
420
421const ZRAN_CHECKPOINT_INTERVAL: u64 = 8 * 1024 * 1024;
424
425const ZRAN_CHECKPOINT_BYTES: u64 = 128 * 1024;
427
428fn peel_zran(
433 format: crate::Format,
434 data: &[u8],
435 index: usize,
436 name: &str,
437 limits: &Limits,
438) -> Result<PeeledSource> {
439 if format != crate::Format::Zip {
442 return spill_member_to_temp(format, data, index, limits.max_total_inflated)
444 .map(PeeledSource::Temp);
445 }
446
447 let size = member_uncompressed_size(format, data, index)?;
452 if estimate_zran_index_bytes(size) > limits.max_index_bytes as u64 {
453 return spill_member_to_temp(format, data, index, limits.max_total_inflated)
454 .map(PeeledSource::Temp);
455 }
456
457 let archive = write_archive_temp(data)?;
461 let entry = zip_core::open_entry(archive.path(), name).map_err(|e| ArchiveError::Read {
462 format: "zip-zran",
463 detail: e.to_string(),
464 })?;
465 let len = entry.len();
466 Ok(PeeledSource::Zran(ZranBacked {
467 entry,
468 len,
469 pos: 0,
470 archive,
471 }))
472}
473
474fn member_uncompressed_size(format: crate::Format, data: &[u8], index: usize) -> Result<u64> {
477 let Some(archive) = Archive::open_with_format(format, data)? else {
478 return Err(ArchiveError::Open {
480 format: "archive",
481 detail: "member plan produced for a non-archive format".to_string(),
482 });
483 };
484 let count = archive.entries().len();
485 archive
486 .entries()
487 .get(index)
488 .map(|e| e.size)
489 .ok_or(ArchiveError::IndexOutOfRange { index, count })
490}
491
492fn estimate_zran_index_bytes(uncompressed: u64) -> u64 {
497 let checkpoints = uncompressed.div_ceil(ZRAN_CHECKPOINT_INTERVAL).max(1);
498 checkpoints.saturating_mul(ZRAN_CHECKPOINT_BYTES)
499}
500
501fn write_archive_temp(data: &[u8]) -> Result<tempfile::NamedTempFile> {
504 let mut tmp = tempfile::NamedTempFile::new().map_err(|e| ArchiveError::Read {
505 format: "zip-zran",
506 detail: e.to_string(),
507 })?;
508 tmp.write_all(data).map_err(|e| ArchiveError::Read {
509 format: "zip-zran",
510 detail: e.to_string(),
511 })?;
512 tmp.as_file_mut().flush().map_err(|e| ArchiveError::Read {
513 format: "zip-zran",
514 detail: e.to_string(),
515 })?;
516 Ok(tmp)
517}
518
519fn spill(write_into: impl FnOnce(&mut dyn Write) -> Result<u64>) -> Result<TempBacked> {
523 let mut tmp = tempfile::NamedTempFile::new().map_err(|e| ArchiveError::Read {
524 format: "temp-spill",
525 detail: e.to_string(),
526 })?;
527 let written = write_into(tmp.as_file_mut())?;
528 let file = tmp.as_file_mut();
529 file.flush().map_err(|e| ArchiveError::Read {
530 format: "temp-spill",
531 detail: e.to_string(),
532 })?;
533 file.seek(SeekFrom::Start(0))
534 .map_err(|e| ArchiveError::Read {
535 format: "temp-spill",
536 detail: e.to_string(),
537 })?;
538 Ok(TempBacked {
539 inner: tmp,
540 len: written,
541 })
542}
543
544fn copy_capped(
547 reader: impl Read,
548 out: &mut dyn Write,
549 cap: u64,
550 format: &'static str,
551) -> Result<u64> {
552 let mut limited = reader.take(cap + 1);
553 let n = io::copy(&mut limited, out).map_err(|e| ArchiveError::Decode {
554 format,
555 detail: e.to_string(),
556 })?;
557 if n > cap {
558 return Err(ArchiveError::TooLarge { cap });
559 }
560 Ok(n)
561}
562
563fn codec_name(codec: Codec) -> &'static str {
565 match codec {
566 Codec::Gzip => "gzip",
567 Codec::Bzip2 => "bzip2",
568 }
569}
570
571pub(crate) fn offset_from(base: u64, off: i64) -> io::Result<u64> {
573 let r = if off >= 0 {
574 base.checked_add(off as u64)
575 } else {
576 base.checked_sub(off.unsigned_abs())
577 };
578 r.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "seek position out of range"))
579}
580
581#[cfg(test)]
582mod tests {
583 use super::*;
584 use crate::plan::AccessPlan;
585 use flate2::write::GzEncoder;
586 use flate2::Compression;
587
588 const FX: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../tests/data/fixtures/");
589
590 fn load(name: &str) -> Vec<u8> {
591 std::fs::read(format!("{FX}{name}")).unwrap()
592 }
593
594 fn gzip(data: &[u8]) -> Vec<u8> {
595 let mut e = GzEncoder::new(Vec::new(), Compression::default());
596 e.write_all(data).unwrap();
597 e.finish().unwrap()
598 }
599
600 fn pattern(n: usize) -> Vec<u8> {
603 (0..n).map(|i| (i % 251) as u8).collect()
604 }
605
606 fn deflate_zip(name: &str, payload: &[u8], level: Compression) -> Vec<u8> {
611 use flate2::write::DeflateEncoder;
612 let mut enc = DeflateEncoder::new(Vec::new(), level);
613 enc.write_all(payload).unwrap();
614 let comp = enc.finish().unwrap();
615 let mut crc = flate2::Crc::new();
616 crc.update(payload);
617 let crc = crc.sum();
618 let nb = name.as_bytes();
619 let (csz, usz, nlen) = (comp.len() as u32, payload.len() as u32, nb.len() as u16);
620
621 let mut z = Vec::new();
622 z.extend_from_slice(&0x0403_4b50u32.to_le_bytes());
624 z.extend_from_slice(&20u16.to_le_bytes()); z.extend_from_slice(&0u16.to_le_bytes()); z.extend_from_slice(&8u16.to_le_bytes()); z.extend_from_slice(&0u16.to_le_bytes()); z.extend_from_slice(&0u16.to_le_bytes()); z.extend_from_slice(&crc.to_le_bytes());
630 z.extend_from_slice(&csz.to_le_bytes());
631 z.extend_from_slice(&usz.to_le_bytes());
632 z.extend_from_slice(&nlen.to_le_bytes());
633 z.extend_from_slice(&0u16.to_le_bytes()); z.extend_from_slice(nb);
635 z.extend_from_slice(&comp);
636 let cd_offset = z.len() as u32;
637 z.extend_from_slice(&0x0201_4b50u32.to_le_bytes());
639 z.extend_from_slice(&20u16.to_le_bytes()); z.extend_from_slice(&20u16.to_le_bytes()); z.extend_from_slice(&0u16.to_le_bytes()); z.extend_from_slice(&8u16.to_le_bytes()); z.extend_from_slice(&0u16.to_le_bytes()); z.extend_from_slice(&0u16.to_le_bytes()); z.extend_from_slice(&crc.to_le_bytes());
646 z.extend_from_slice(&csz.to_le_bytes());
647 z.extend_from_slice(&usz.to_le_bytes());
648 z.extend_from_slice(&nlen.to_le_bytes());
649 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(&0u32.to_le_bytes()); z.extend_from_slice(&0u32.to_le_bytes()); z.extend_from_slice(nb);
656 let cd_size = z.len() as u32 - cd_offset;
657 z.extend_from_slice(&0x0605_4b50u32.to_le_bytes());
659 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());
664 z.extend_from_slice(&cd_offset.to_le_bytes());
665 z.extend_from_slice(&0u16.to_le_bytes()); z
667 }
668
669 #[test]
676 fn deflate_member_zran_random_access() {
677 let payload = pattern(200_000);
678 let data = deflate_zip("big.dd", &payload, Compression::none());
679 assert!(
680 matches!(
681 detect(&data).unwrap(),
682 AccessPlan::Member {
683 access: Access::Zran,
684 ..
685 }
686 ),
687 "a Deflate member routes to Zran"
688 );
689 let PeelSource::Inner(PeeledSource::Zran(mut z)) =
690 peel_archive_seekable(data, Some("big.dd.zip"), &Limits::default()).unwrap()
691 else {
692 panic!("expected a Zran-backed peel");
693 };
694 assert_eq!(z.len(), payload.len() as u64);
695 assert!(!z.is_empty());
696
697 z.seek(SeekFrom::Start(0)).unwrap();
699 let mut head = vec![0u8; 4096];
700 z.read_exact(&mut head).unwrap();
701 assert_eq!(head, payload[..4096]);
702
703 let mid = 65_535 - 10;
705 z.seek(SeekFrom::Start(mid as u64)).unwrap();
706 let mut span = vec![0u8; 5000];
707 z.read_exact(&mut span).unwrap();
708 assert_eq!(span, payload[mid..mid + 5000]);
709
710 z.seek(SeekFrom::End(-100)).unwrap();
712 let mut tail = vec![0u8; 100];
713 z.read_exact(&mut tail).unwrap();
714 assert_eq!(tail, payload[payload.len() - 100..]);
715
716 z.seek(SeekFrom::Start(1234)).unwrap();
718 let mut back = vec![0u8; 2000];
719 z.read_exact(&mut back).unwrap();
720 assert_eq!(back, payload[1234..1234 + 2000]);
721
722 assert_eq!(z.seek(SeekFrom::Current(6)).unwrap(), 1234 + 2000 + 6);
724 let mut cur = vec![0u8; 50];
725 z.read_exact(&mut cur).unwrap();
726 assert_eq!(cur, payload[1234 + 2000 + 6..1234 + 2000 + 6 + 50]);
727
728 assert_eq!(
730 z.seek(SeekFrom::Start(u64::MAX)).unwrap(),
731 payload.len() as u64
732 );
733 let _ = format!("{z:?}"); z.seek(SeekFrom::Start(0)).unwrap();
737 let mut all = Vec::new();
738 z.read_to_end(&mut all).unwrap();
739 assert_eq!(all, payload);
740 }
741
742 #[test]
745 fn genuinely_compressed_deflate_member_zran_reads_correctly() {
746 let payload = b"the quick brown fox jumps over the lazy dog\n".repeat(4000);
747 let data = deflate_zip("log.txt", &payload, Compression::best());
748 let PeelSource::Inner(PeeledSource::Zran(mut z)) =
749 peel_archive_seekable(data, Some("log.txt.zip"), &Limits::default()).unwrap()
750 else {
751 panic!("expected a Zran-backed peel");
752 };
753 assert_eq!(z.len(), payload.len() as u64);
754 let off = payload.len() / 2;
755 z.seek(SeekFrom::Start(off as u64)).unwrap();
756 let mut got = vec![0u8; 3000];
757 z.read_exact(&mut got).unwrap();
758 assert_eq!(got, payload[off..off + 3000]);
759 }
760
761 #[test]
765 fn zran_path_does_not_spill_decompressed_member() {
766 let payload = pattern(120_000);
767 let data = deflate_zip("img.dd", &payload, Compression::none());
768 match peel_archive_seekable(data, Some("img.dd.zip"), &Limits::default()).unwrap() {
769 PeelSource::Inner(PeeledSource::Zran(_)) => {}
770 other => panic!("expected Zran (no decompressed-member spill), got {other:?}"),
771 }
772 }
773
774 #[test]
778 fn zran_index_cap_falls_back_to_spill() {
779 let payload = pattern(120_000);
780 let expected = payload.clone();
781 let data = deflate_zip("img.dd", &payload, Compression::none());
782 let limits = Limits {
783 max_index_bytes: 1,
784 ..Limits::default()
785 };
786 match peel_archive_seekable(data, Some("img.dd.zip"), &limits).unwrap() {
787 PeelSource::Inner(PeeledSource::Temp(mut t)) => {
788 let mut got = Vec::new();
789 t.read_to_end(&mut got).unwrap();
790 assert_eq!(
791 got, expected,
792 "spill fallback still yields the member bytes"
793 );
794 }
795 other => panic!("expected a Temp spill fallback, got {other:?}"),
796 }
797 }
798
799 fn member_oracle(data: &[u8], name: &str, member: &str) -> Vec<u8> {
801 let mut a = Archive::open(data, Some(name)).unwrap().unwrap();
802 let idx = a
803 .entries()
804 .iter()
805 .position(|e| e.name == member && !e.is_dir)
806 .unwrap();
807 a.read(idx).unwrap()
808 }
809
810 #[test]
814 fn stored_member_is_inplace_zero_copy() {
815 let data = load("stored_one.zip");
816 let AccessPlan::Member {
817 access: Access::InPlace { offset, len },
818 ..
819 } = detect(&data).unwrap()
820 else {
821 panic!("expected an InPlace Member plan");
822 };
823 let expected = member_oracle(&data, "stored_one.zip", "disk.dd");
824
825 match peel_archive_seekable(data.clone(), Some("stored_one.zip"), &Limits::default())
826 .unwrap()
827 {
828 PeelSource::Inner(PeeledSource::InPlace(mut sub)) => {
829 assert_eq!(sub.offset(), offset, "window offset matches the plan");
830 assert_eq!(sub.len(), len, "window len matches the plan");
831 let mut got = Vec::new();
832 sub.read_to_end(&mut got).unwrap();
833 assert_eq!(got, expected, "reads the exact member bytes");
834 let s = offset as usize;
837 let e = (offset + len) as usize;
838 assert_eq!(got.as_slice(), &data[s..e]);
839 sub.seek(SeekFrom::Start(10)).unwrap();
841 let mut three = [0u8; 3];
842 sub.read_exact(&mut three).unwrap();
843 assert_eq!(&three[..], &data[s + 10..s + 13]);
844 }
845 other => panic!("expected InPlace, got {other:?}"),
846 }
847 }
848
849 #[test]
855 fn deflate_member_reads_via_zran() {
856 let data = load("deflate_one.zip");
857 let expected = member_oracle(&data, "deflate_one.zip", "big.dd");
858 match peel_archive_seekable(data, Some("deflate_one.zip"), &Limits::default()).unwrap() {
859 PeelSource::Inner(PeeledSource::Zran(mut z)) => {
860 assert_eq!(z.len(), expected.len() as u64);
861 let mut got = Vec::new();
862 z.read_to_end(&mut got).unwrap();
863 assert_eq!(got, expected, "zran reads the exact decompressed member");
864 }
865 other => panic!("expected Zran, got {other:?}"),
866 }
867 }
868
869 #[test]
872 fn bare_gzip_wrapper_spills_full_stream_to_temp() {
873 let inner = b"raw disk sector bytes, not an archive at all ".repeat(50);
874 let gz = gzip(&inner);
875 match peel_archive_seekable(gz, Some("disk.dd.gz"), &Limits::default()).unwrap() {
876 PeelSource::Inner(PeeledSource::Temp(mut t)) => {
877 assert_eq!(t.len(), inner.len() as u64);
878 assert!(!t.is_empty());
879 let mut got = Vec::new();
880 t.read_to_end(&mut got).unwrap();
881 assert_eq!(got, inner);
882 t.seek(SeekFrom::Start(5)).unwrap();
884 let mut chunk = [0u8; 4];
885 t.read_exact(&mut chunk).unwrap();
886 assert_eq!(&chunk[..], &inner[5..9]);
887 }
888 other => panic!("expected Temp, got {other:?}"),
889 }
890 }
891
892 #[test]
894 fn over_cap_spill_fails_loud() {
895 let gz = gzip(&vec![0xAB_u8; 10_000]);
896 let limits = Limits {
897 max_total_inflated: 500,
898 ..Limits::default()
899 };
900 match peel_archive_seekable(gz, Some("x.gz"), &limits) {
901 Err(ArchiveError::TooLarge { cap }) => assert_eq!(cap, 500),
902 other => panic!("expected TooLarge, got {other:?}"),
903 }
904 }
905
906 #[test]
908 fn over_cap_inplace_fails_loud() {
909 let data = load("stored_one.zip"); let limits = Limits {
911 max_total_inflated: 100,
912 ..Limits::default()
913 };
914 match peel_archive_seekable(data, Some("stored_one.zip"), &limits) {
915 Err(ArchiveError::TooLarge { cap }) => assert_eq!(cap, 100),
916 other => panic!("expected TooLarge, got {other:?}"),
917 }
918 }
919
920 #[test]
922 fn raw_bytes_are_not_packed() {
923 match peel_archive_seekable(
924 b"\x00\x01\x02 not packed".to_vec(),
925 Some("x.raw"),
926 &Limits::default(),
927 )
928 .unwrap()
929 {
930 PeelSource::NotPacked => {}
931 other => panic!("expected NotPacked, got {other:?}"),
932 }
933 }
934
935 #[test]
937 fn multi_member_zip_is_not_packed() {
938 let data = load("payload.zip");
939 assert!(matches!(
940 peel_archive_seekable(data, Some("payload.zip"), &Limits::default()).unwrap(),
941 PeelSource::NotPacked
942 ));
943 }
944
945 #[test]
947 fn segment_set_is_not_packed() {
948 let data = load("seg_ewf.zip");
949 assert!(matches!(
950 peel_archive_seekable(data, Some("seg_ewf.zip"), &Limits::default()).unwrap(),
951 PeelSource::NotPacked
952 ));
953 }
954
955 #[test]
958 fn targz_single_member_spills_to_temp() {
959 let mut b = tar::Builder::new(Vec::new());
960 let payload = b"RAW-IMAGE-BYTES-in-a-tar".repeat(10);
961 let mut h = tar::Header::new_gnu();
962 h.set_size(payload.len() as u64);
963 h.set_mode(0o644);
964 h.set_cksum();
965 b.append_data(&mut h, "disk.img", payload.as_slice())
966 .unwrap();
967 let tar = b.into_inner().unwrap();
968 let gz = gzip(&tar);
969 match peel_archive_seekable(gz, Some("disk.tgz"), &Limits::default()).unwrap() {
970 PeelSource::Inner(PeeledSource::Temp(mut t)) => {
971 let mut got = Vec::new();
972 t.read_to_end(&mut got).unwrap();
973 assert_eq!(got, payload);
974 }
975 other => panic!("expected Temp, got {other:?}"),
976 }
977 }
978
979 #[test]
983 fn bare_bzip2_wrapper_spills_to_temp() {
984 let expected = "archive-detour bzip2 test payload — the quick brown fox\n"
985 .repeat(30)
986 .into_bytes();
987 match peel_archive_seekable(load("payload.bz2"), Some("payload.bz2"), &Limits::default())
988 .unwrap()
989 {
990 PeelSource::Inner(PeeledSource::Temp(mut t)) => {
991 let mut got = Vec::new();
992 t.read_to_end(&mut got).unwrap();
993 assert_eq!(got, expected);
994 }
995 other => panic!("expected Temp, got {other:?}"),
996 }
997 }
998
999 #[test]
1003 fn subrange_seek_end_and_current() {
1004 let data: Vec<u8> = (0u8..50).collect();
1005 let mut sub = SubRange::new(data, 10, 20, u64::MAX).unwrap();
1007 assert_eq!(sub.offset(), 10);
1008 assert!(!sub.is_empty());
1009
1010 assert_eq!(sub.seek(SeekFrom::End(-4)).unwrap(), 16);
1012 let mut four = [0u8; 4];
1013 sub.read_exact(&mut four).unwrap();
1014 assert_eq!(four, [26, 27, 28, 29]);
1015
1016 assert_eq!(sub.seek(SeekFrom::Current(-4)).unwrap(), 16);
1018 sub.seek(SeekFrom::Start(0)).unwrap();
1019 assert_eq!(sub.seek(SeekFrom::Current(5)).unwrap(), 5);
1020
1021 assert_eq!(sub.seek(SeekFrom::Start(999)).unwrap(), 20);
1023
1024 assert!(sub.seek(SeekFrom::Current(-999)).is_err());
1026 }
1027
1028 #[test]
1031 fn subrange_rejects_overrun_and_reports_empty() {
1032 let overrun = SubRange::new(vec![0u8; 10], 5, 20, u64::MAX);
1033 assert!(matches!(overrun, Err(ArchiveError::Read { .. })));
1034
1035 let empty = SubRange::new(vec![0u8; 10], 4, 0, u64::MAX).unwrap();
1036 assert!(empty.is_empty());
1037 assert_eq!(empty.len(), 0);
1038 }
1039
1040 #[test]
1044 fn peeled_source_reads_and_seeks_as_trait_object() {
1045 let data = load("stored_one.zip");
1046 let expected = member_oracle(&data, "stored_one.zip", "disk.dd");
1047 let PeelSource::Inner(source) =
1048 peel_archive_seekable(data, Some("stored_one.zip"), &Limits::default()).unwrap()
1049 else {
1050 panic!("expected an Inner source");
1051 };
1052 let mut handle: Box<dyn ReadSeek> = Box::new(source);
1054 handle.seek(SeekFrom::Start(0)).unwrap();
1055 let mut got = Vec::new();
1056 handle.read_to_end(&mut got).unwrap();
1057 assert_eq!(got, expected);
1058 }
1059}