use std::io::{self, Read, Seek, SeekFrom, Write};
use std::path::Path;
use std::sync::Arc;
use crate::archive::Archive;
use crate::error::{ArchiveError, Result};
use crate::plan::{detect, Access, AccessPlan, Codec};
use crate::resolve::Limits;
pub trait ReadSeek: Read + Seek {}
impl<T: Read + Seek> ReadSeek for T {}
#[derive(Debug)]
pub struct SubRange {
data: Arc<Vec<u8>>,
start: u64,
len: u64,
pos: u64,
}
impl SubRange {
#[cfg(test)]
fn new(data: Vec<u8>, offset: u64, len: u64, cap: u64) -> Result<Self> {
Self::new_shared(Arc::new(data), offset, len, cap)
}
pub(crate) fn new_shared(data: Arc<Vec<u8>>, offset: u64, len: u64, cap: u64) -> Result<Self> {
if len > cap {
return Err(ArchiveError::TooLarge { cap });
}
offset
.checked_add(len)
.filter(|&e| e <= data.len() as u64)
.ok_or(ArchiveError::Read {
format: "in-place",
detail: format!(
"member window [{offset}, {offset}+{len}) exceeds the {}-byte source",
data.len()
),
})?;
Ok(SubRange {
data,
start: offset,
len,
pos: 0,
})
}
#[must_use]
pub fn offset(&self) -> u64 {
self.start
}
#[must_use]
pub fn len(&self) -> u64 {
self.len
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len == 0
}
}
impl Read for SubRange {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let remaining = self.len - self.pos;
if remaining == 0 || buf.is_empty() {
return Ok(0);
}
let n = remaining.min(buf.len() as u64) as usize;
let start = self.start.saturating_add(self.pos) as usize;
let Some(src) = self.data.get(start..start + n) else {
return Ok(0); };
buf[..n].copy_from_slice(src);
self.pos += n as u64;
Ok(n)
}
}
impl Seek for SubRange {
fn seek(&mut self, from: SeekFrom) -> io::Result<u64> {
let target = match from {
SeekFrom::Start(o) => o,
SeekFrom::End(o) => offset_from(self.len, o)?,
SeekFrom::Current(o) => offset_from(self.pos, o)?,
};
self.pos = target.min(self.len);
Ok(self.pos)
}
}
#[derive(Debug)]
pub struct TempBacked {
inner: tempfile::NamedTempFile,
len: u64,
}
impl TempBacked {
#[must_use]
pub fn len(&self) -> u64 {
self.len
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len == 0
}
#[must_use]
pub fn path(&self) -> &Path {
self.inner.path()
}
}
impl Read for TempBacked {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.inner.as_file_mut().read(buf)
}
}
impl Seek for TempBacked {
fn seek(&mut self, from: SeekFrom) -> io::Result<u64> {
self.inner.as_file_mut().seek(from)
}
}
pub struct ZranBacked {
entry: zip_core::StoredZipEntry,
len: u64,
pos: u64,
archive: tempfile::NamedTempFile,
}
impl ZranBacked {
#[must_use]
pub fn len(&self) -> u64 {
self.len
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len == 0
}
}
impl std::fmt::Debug for ZranBacked {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ZranBacked")
.field("len", &self.len)
.field("pos", &self.pos)
.field("archive", &self.archive.path())
.finish_non_exhaustive()
}
}
impl Read for ZranBacked {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let n = self.entry.read_at(buf, self.pos)?;
self.pos = self.pos.saturating_add(n as u64);
Ok(n)
}
}
impl Seek for ZranBacked {
fn seek(&mut self, from: SeekFrom) -> io::Result<u64> {
let target = match from {
SeekFrom::Start(o) => o,
SeekFrom::End(o) => offset_from(self.len, o)?,
SeekFrom::Current(o) => offset_from(self.pos, o)?,
};
self.pos = target.min(self.len);
Ok(self.pos)
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum PeeledSource {
InPlace(SubRange),
Zran(ZranBacked),
Temp(TempBacked),
}
impl PeeledSource {
#[must_use]
pub fn len(&self) -> u64 {
match self {
PeeledSource::InPlace(s) => s.len(),
PeeledSource::Zran(z) => z.len(),
PeeledSource::Temp(t) => t.len(),
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
impl Read for PeeledSource {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match self {
PeeledSource::InPlace(s) => s.read(buf),
PeeledSource::Zran(z) => z.read(buf),
PeeledSource::Temp(t) => t.read(buf),
}
}
}
impl Seek for PeeledSource {
fn seek(&mut self, from: SeekFrom) -> io::Result<u64> {
match self {
PeeledSource::InPlace(s) => s.seek(from),
PeeledSource::Zran(z) => z.seek(from),
PeeledSource::Temp(t) => t.seek(from),
}
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum PeelSource {
NotPacked,
Inner(PeeledSource),
}
pub fn peel_archive_seekable(
data: Vec<u8>,
_name: Option<&str>,
limits: &Limits,
) -> Result<PeelSource> {
let cap = limits.max_total_inflated;
let plan = detect(&data)?;
let data = Arc::new(data);
match plan {
AccessPlan::Direct | AccessPlan::Collection { .. } | AccessPlan::SegmentSet { .. } => {
Ok(PeelSource::NotPacked)
}
AccessPlan::Wrapper { codec, .. } => {
let temp = spill_wrapper_to_temp(&data, codec, cap)?;
Ok(PeelSource::Inner(PeeledSource::Temp(temp)))
}
AccessPlan::Member {
format,
index,
name,
access,
} => Ok(PeelSource::Inner(execute_member_access(
format, &data, index, &name, &access, limits,
)?)),
}
}
pub(crate) fn execute_member_access(
format: crate::Format,
data: &Arc<Vec<u8>>,
index: usize,
name: &str,
access: &Access,
limits: &Limits,
) -> Result<PeeledSource> {
let cap = limits.max_total_inflated;
match *access {
Access::InPlace { offset, len } => {
let sub = SubRange::new_shared(Arc::clone(data), offset, len, cap)?;
Ok(PeeledSource::InPlace(sub))
}
Access::Zran => peel_zran(format, data, index, name, limits),
Access::SpillToTemp => {
let temp = spill_member_to_temp(format, data, index, cap)?;
Ok(PeeledSource::Temp(temp))
}
}
}
fn spill_wrapper_to_temp(data: &[u8], codec: Codec, cap: u64) -> Result<TempBacked> {
let reader: Box<dyn Read> = match codec {
Codec::Gzip => Box::new(flate2::read::GzDecoder::new(data)),
Codec::Bzip2 => Box::new(bzip2_rs::DecoderReader::new(data)),
};
spill(|out| copy_capped(reader, out, cap, codec_name(codec)))
}
fn spill_member_to_temp(
format: crate::Format,
data: &[u8],
index: usize,
cap: u64,
) -> Result<TempBacked> {
let Some(mut archive) = Archive::open_with_format(format, data)? else {
return Err(ArchiveError::Open {
format: "archive",
detail: "member plan produced for a non-archive format".to_string(),
});
};
spill(|out| archive.stream_member(index, out, cap))
}
const ZRAN_CHECKPOINT_INTERVAL: u64 = 8 * 1024 * 1024;
const ZRAN_CHECKPOINT_BYTES: u64 = 128 * 1024;
fn peel_zran(
format: crate::Format,
data: &[u8],
index: usize,
name: &str,
limits: &Limits,
) -> Result<PeeledSource> {
if format != crate::Format::Zip {
return spill_member_to_temp(format, data, index, limits.max_total_inflated)
.map(PeeledSource::Temp);
}
let size = member_uncompressed_size(format, data, index)?;
if estimate_zran_index_bytes(size) > limits.max_index_bytes as u64 {
return spill_member_to_temp(format, data, index, limits.max_total_inflated)
.map(PeeledSource::Temp);
}
let archive = write_archive_temp(data)?;
let entry = zip_core::open_entry(archive.path(), name).map_err(|e| ArchiveError::Read {
format: "zip-zran",
detail: e.to_string(),
})?;
let len = entry.len();
Ok(PeeledSource::Zran(ZranBacked {
entry,
len,
pos: 0,
archive,
}))
}
fn member_uncompressed_size(format: crate::Format, data: &[u8], index: usize) -> Result<u64> {
let Some(archive) = Archive::open_with_format(format, data)? else {
return Err(ArchiveError::Open {
format: "archive",
detail: "member plan produced for a non-archive format".to_string(),
});
};
let count = archive.entries().len();
archive
.entries()
.get(index)
.map(|e| e.size)
.ok_or(ArchiveError::IndexOutOfRange { index, count })
}
fn estimate_zran_index_bytes(uncompressed: u64) -> u64 {
let checkpoints = uncompressed.div_ceil(ZRAN_CHECKPOINT_INTERVAL).max(1);
checkpoints.saturating_mul(ZRAN_CHECKPOINT_BYTES)
}
fn write_archive_temp(data: &[u8]) -> Result<tempfile::NamedTempFile> {
let mut tmp = tempfile::NamedTempFile::new().map_err(|e| ArchiveError::Read {
format: "zip-zran",
detail: e.to_string(),
})?;
tmp.write_all(data).map_err(|e| ArchiveError::Read {
format: "zip-zran",
detail: e.to_string(),
})?;
tmp.as_file_mut().flush().map_err(|e| ArchiveError::Read {
format: "zip-zran",
detail: e.to_string(),
})?;
Ok(tmp)
}
fn spill(write_into: impl FnOnce(&mut dyn Write) -> Result<u64>) -> Result<TempBacked> {
let mut tmp = tempfile::NamedTempFile::new().map_err(|e| ArchiveError::Read {
format: "temp-spill",
detail: e.to_string(),
})?;
let written = write_into(tmp.as_file_mut())?;
let file = tmp.as_file_mut();
file.flush().map_err(|e| ArchiveError::Read {
format: "temp-spill",
detail: e.to_string(),
})?;
file.seek(SeekFrom::Start(0))
.map_err(|e| ArchiveError::Read {
format: "temp-spill",
detail: e.to_string(),
})?;
Ok(TempBacked {
inner: tmp,
len: written,
})
}
fn copy_capped(
reader: impl Read,
out: &mut dyn Write,
cap: u64,
format: &'static str,
) -> Result<u64> {
let mut limited = reader.take(cap + 1);
let n = io::copy(&mut limited, out).map_err(|e| ArchiveError::Decode {
format,
detail: e.to_string(),
})?;
if n > cap {
return Err(ArchiveError::TooLarge { cap });
}
Ok(n)
}
fn codec_name(codec: Codec) -> &'static str {
match codec {
Codec::Gzip => "gzip",
Codec::Bzip2 => "bzip2",
}
}
pub(crate) fn offset_from(base: u64, off: i64) -> io::Result<u64> {
let r = if off >= 0 {
base.checked_add(off as u64)
} else {
base.checked_sub(off.unsigned_abs())
};
r.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "seek position out of range"))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::plan::AccessPlan;
use flate2::write::GzEncoder;
use flate2::Compression;
const FX: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../tests/data/fixtures/");
fn load(name: &str) -> Vec<u8> {
std::fs::read(format!("{FX}{name}")).unwrap()
}
fn gzip(data: &[u8]) -> Vec<u8> {
let mut e = GzEncoder::new(Vec::new(), Compression::default());
e.write_all(data).unwrap();
e.finish().unwrap()
}
fn pattern(n: usize) -> Vec<u8> {
(0..n).map(|i| (i % 251) as u8).collect()
}
fn deflate_zip(name: &str, payload: &[u8], level: Compression) -> Vec<u8> {
use flate2::write::DeflateEncoder;
let mut enc = DeflateEncoder::new(Vec::new(), level);
enc.write_all(payload).unwrap();
let comp = enc.finish().unwrap();
let mut crc = flate2::Crc::new();
crc.update(payload);
let crc = crc.sum();
let nb = name.as_bytes();
let (csz, usz, nlen) = (comp.len() as u32, payload.len() as u32, nb.len() as u16);
let mut z = Vec::new();
z.extend_from_slice(&0x0403_4b50u32.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());
z.extend_from_slice(&csz.to_le_bytes());
z.extend_from_slice(&usz.to_le_bytes());
z.extend_from_slice(&nlen.to_le_bytes());
z.extend_from_slice(&0u16.to_le_bytes()); z.extend_from_slice(nb);
z.extend_from_slice(&comp);
let cd_offset = z.len() as u32;
z.extend_from_slice(&0x0201_4b50u32.to_le_bytes());
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());
z.extend_from_slice(&csz.to_le_bytes());
z.extend_from_slice(&usz.to_le_bytes());
z.extend_from_slice(&nlen.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(&0u32.to_le_bytes()); z.extend_from_slice(&0u32.to_le_bytes()); z.extend_from_slice(nb);
let cd_size = z.len() as u32 - cd_offset;
z.extend_from_slice(&0x0605_4b50u32.to_le_bytes());
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());
z.extend_from_slice(&cd_offset.to_le_bytes());
z.extend_from_slice(&0u16.to_le_bytes()); z
}
#[test]
fn deflate_member_zran_random_access() {
let payload = pattern(200_000);
let data = deflate_zip("big.dd", &payload, Compression::none());
assert!(
matches!(
detect(&data).unwrap(),
AccessPlan::Member {
access: Access::Zran,
..
}
),
"a Deflate member routes to Zran"
);
let PeelSource::Inner(PeeledSource::Zran(mut z)) =
peel_archive_seekable(data, Some("big.dd.zip"), &Limits::default()).unwrap()
else {
panic!("expected a Zran-backed peel");
};
assert_eq!(z.len(), payload.len() as u64);
assert!(!z.is_empty());
z.seek(SeekFrom::Start(0)).unwrap();
let mut head = vec![0u8; 4096];
z.read_exact(&mut head).unwrap();
assert_eq!(head, payload[..4096]);
let mid = 65_535 - 10;
z.seek(SeekFrom::Start(mid as u64)).unwrap();
let mut span = vec![0u8; 5000];
z.read_exact(&mut span).unwrap();
assert_eq!(span, payload[mid..mid + 5000]);
z.seek(SeekFrom::End(-100)).unwrap();
let mut tail = vec![0u8; 100];
z.read_exact(&mut tail).unwrap();
assert_eq!(tail, payload[payload.len() - 100..]);
z.seek(SeekFrom::Start(1234)).unwrap();
let mut back = vec![0u8; 2000];
z.read_exact(&mut back).unwrap();
assert_eq!(back, payload[1234..1234 + 2000]);
assert_eq!(z.seek(SeekFrom::Current(6)).unwrap(), 1234 + 2000 + 6);
let mut cur = vec![0u8; 50];
z.read_exact(&mut cur).unwrap();
assert_eq!(cur, payload[1234 + 2000 + 6..1234 + 2000 + 6 + 50]);
assert_eq!(
z.seek(SeekFrom::Start(u64::MAX)).unwrap(),
payload.len() as u64
);
let _ = format!("{z:?}");
z.seek(SeekFrom::Start(0)).unwrap();
let mut all = Vec::new();
z.read_to_end(&mut all).unwrap();
assert_eq!(all, payload);
}
#[test]
fn genuinely_compressed_deflate_member_zran_reads_correctly() {
let payload = b"the quick brown fox jumps over the lazy dog\n".repeat(4000);
let data = deflate_zip("log.txt", &payload, Compression::best());
let PeelSource::Inner(PeeledSource::Zran(mut z)) =
peel_archive_seekable(data, Some("log.txt.zip"), &Limits::default()).unwrap()
else {
panic!("expected a Zran-backed peel");
};
assert_eq!(z.len(), payload.len() as u64);
let off = payload.len() / 2;
z.seek(SeekFrom::Start(off as u64)).unwrap();
let mut got = vec![0u8; 3000];
z.read_exact(&mut got).unwrap();
assert_eq!(got, payload[off..off + 3000]);
}
#[test]
fn zran_path_does_not_spill_decompressed_member() {
let payload = pattern(120_000);
let data = deflate_zip("img.dd", &payload, Compression::none());
match peel_archive_seekable(data, Some("img.dd.zip"), &Limits::default()).unwrap() {
PeelSource::Inner(PeeledSource::Zran(_)) => {}
other => panic!("expected Zran (no decompressed-member spill), got {other:?}"),
}
}
#[test]
fn zran_index_cap_falls_back_to_spill() {
let payload = pattern(120_000);
let expected = payload.clone();
let data = deflate_zip("img.dd", &payload, Compression::none());
let limits = Limits {
max_index_bytes: 1,
..Limits::default()
};
match peel_archive_seekable(data, Some("img.dd.zip"), &limits).unwrap() {
PeelSource::Inner(PeeledSource::Temp(mut t)) => {
let mut got = Vec::new();
t.read_to_end(&mut got).unwrap();
assert_eq!(
got, expected,
"spill fallback still yields the member bytes"
);
}
other => panic!("expected a Temp spill fallback, got {other:?}"),
}
}
fn member_oracle(data: &[u8], name: &str, member: &str) -> Vec<u8> {
let mut a = Archive::open(data, Some(name)).unwrap().unwrap();
let idx = a
.entries()
.iter()
.position(|e| e.name == member && !e.is_dir)
.unwrap();
a.read(idx).unwrap()
}
#[test]
fn stored_member_is_inplace_zero_copy() {
let data = load("stored_one.zip");
let AccessPlan::Member {
access: Access::InPlace { offset, len },
..
} = detect(&data).unwrap()
else {
panic!("expected an InPlace Member plan");
};
let expected = member_oracle(&data, "stored_one.zip", "disk.dd");
match peel_archive_seekable(data.clone(), Some("stored_one.zip"), &Limits::default())
.unwrap()
{
PeelSource::Inner(PeeledSource::InPlace(mut sub)) => {
assert_eq!(sub.offset(), offset, "window offset matches the plan");
assert_eq!(sub.len(), len, "window len matches the plan");
let mut got = Vec::new();
sub.read_to_end(&mut got).unwrap();
assert_eq!(got, expected, "reads the exact member bytes");
let s = offset as usize;
let e = (offset + len) as usize;
assert_eq!(got.as_slice(), &data[s..e]);
sub.seek(SeekFrom::Start(10)).unwrap();
let mut three = [0u8; 3];
sub.read_exact(&mut three).unwrap();
assert_eq!(&three[..], &data[s + 10..s + 13]);
}
other => panic!("expected InPlace, got {other:?}"),
}
}
#[test]
fn deflate_member_reads_via_zran() {
let data = load("deflate_one.zip");
let expected = member_oracle(&data, "deflate_one.zip", "big.dd");
match peel_archive_seekable(data, Some("deflate_one.zip"), &Limits::default()).unwrap() {
PeelSource::Inner(PeeledSource::Zran(mut z)) => {
assert_eq!(z.len(), expected.len() as u64);
let mut got = Vec::new();
z.read_to_end(&mut got).unwrap();
assert_eq!(got, expected, "zran reads the exact decompressed member");
}
other => panic!("expected Zran, got {other:?}"),
}
}
#[test]
fn bare_gzip_wrapper_spills_full_stream_to_temp() {
let inner = b"raw disk sector bytes, not an archive at all ".repeat(50);
let gz = gzip(&inner);
match peel_archive_seekable(gz, Some("disk.dd.gz"), &Limits::default()).unwrap() {
PeelSource::Inner(PeeledSource::Temp(mut t)) => {
assert_eq!(t.len(), inner.len() as u64);
assert!(!t.is_empty());
let mut got = Vec::new();
t.read_to_end(&mut got).unwrap();
assert_eq!(got, inner);
t.seek(SeekFrom::Start(5)).unwrap();
let mut chunk = [0u8; 4];
t.read_exact(&mut chunk).unwrap();
assert_eq!(&chunk[..], &inner[5..9]);
}
other => panic!("expected Temp, got {other:?}"),
}
}
#[test]
fn over_cap_spill_fails_loud() {
let gz = gzip(&vec![0xAB_u8; 10_000]);
let limits = Limits {
max_total_inflated: 500,
..Limits::default()
};
match peel_archive_seekable(gz, Some("x.gz"), &limits) {
Err(ArchiveError::TooLarge { cap }) => assert_eq!(cap, 500),
other => panic!("expected TooLarge, got {other:?}"),
}
}
#[test]
fn over_cap_inplace_fails_loud() {
let data = load("stored_one.zip"); let limits = Limits {
max_total_inflated: 100,
..Limits::default()
};
match peel_archive_seekable(data, Some("stored_one.zip"), &limits) {
Err(ArchiveError::TooLarge { cap }) => assert_eq!(cap, 100),
other => panic!("expected TooLarge, got {other:?}"),
}
}
#[test]
fn raw_bytes_are_not_packed() {
match peel_archive_seekable(
b"\x00\x01\x02 not packed".to_vec(),
Some("x.raw"),
&Limits::default(),
)
.unwrap()
{
PeelSource::NotPacked => {}
other => panic!("expected NotPacked, got {other:?}"),
}
}
#[test]
fn multi_member_zip_is_not_packed() {
let data = load("payload.zip");
assert!(matches!(
peel_archive_seekable(data, Some("payload.zip"), &Limits::default()).unwrap(),
PeelSource::NotPacked
));
}
#[test]
fn segment_set_is_not_packed() {
let data = load("seg_ewf.zip");
assert!(matches!(
peel_archive_seekable(data, Some("seg_ewf.zip"), &Limits::default()).unwrap(),
PeelSource::NotPacked
));
}
#[test]
fn targz_single_member_spills_to_temp() {
let mut b = tar::Builder::new(Vec::new());
let payload = b"RAW-IMAGE-BYTES-in-a-tar".repeat(10);
let mut h = tar::Header::new_gnu();
h.set_size(payload.len() as u64);
h.set_mode(0o644);
h.set_cksum();
b.append_data(&mut h, "disk.img", payload.as_slice())
.unwrap();
let tar = b.into_inner().unwrap();
let gz = gzip(&tar);
match peel_archive_seekable(gz, Some("disk.tgz"), &Limits::default()).unwrap() {
PeelSource::Inner(PeeledSource::Temp(mut t)) => {
let mut got = Vec::new();
t.read_to_end(&mut got).unwrap();
assert_eq!(got, payload);
}
other => panic!("expected Temp, got {other:?}"),
}
}
#[test]
fn bare_bzip2_wrapper_spills_to_temp() {
let expected = "archive-detour bzip2 test payload — the quick brown fox\n"
.repeat(30)
.into_bytes();
match peel_archive_seekable(load("payload.bz2"), Some("payload.bz2"), &Limits::default())
.unwrap()
{
PeelSource::Inner(PeeledSource::Temp(mut t)) => {
let mut got = Vec::new();
t.read_to_end(&mut got).unwrap();
assert_eq!(got, expected);
}
other => panic!("expected Temp, got {other:?}"),
}
}
#[test]
fn subrange_seek_end_and_current() {
let data: Vec<u8> = (0u8..50).collect();
let mut sub = SubRange::new(data, 10, 20, u64::MAX).unwrap();
assert_eq!(sub.offset(), 10);
assert!(!sub.is_empty());
assert_eq!(sub.seek(SeekFrom::End(-4)).unwrap(), 16);
let mut four = [0u8; 4];
sub.read_exact(&mut four).unwrap();
assert_eq!(four, [26, 27, 28, 29]);
assert_eq!(sub.seek(SeekFrom::Current(-4)).unwrap(), 16);
sub.seek(SeekFrom::Start(0)).unwrap();
assert_eq!(sub.seek(SeekFrom::Current(5)).unwrap(), 5);
assert_eq!(sub.seek(SeekFrom::Start(999)).unwrap(), 20);
assert!(sub.seek(SeekFrom::Current(-999)).is_err());
}
#[test]
fn subrange_rejects_overrun_and_reports_empty() {
let overrun = SubRange::new(vec![0u8; 10], 5, 20, u64::MAX);
assert!(matches!(overrun, Err(ArchiveError::Read { .. })));
let empty = SubRange::new(vec![0u8; 10], 4, 0, u64::MAX).unwrap();
assert!(empty.is_empty());
assert_eq!(empty.len(), 0);
}
#[test]
fn peeled_source_reads_and_seeks_as_trait_object() {
let data = load("stored_one.zip");
let expected = member_oracle(&data, "stored_one.zip", "disk.dd");
let PeelSource::Inner(source) =
peel_archive_seekable(data, Some("stored_one.zip"), &Limits::default()).unwrap()
else {
panic!("expected an Inner source");
};
let mut handle: Box<dyn ReadSeek> = Box::new(source);
handle.seek(SeekFrom::Start(0)).unwrap();
let mut got = Vec::new();
handle.read_to_end(&mut got).unwrap();
assert_eq!(got, expected);
}
}