use std::borrow::Cow;
use std::io::{Read, Seek};
use crate::error::AnamnesisError;
use crate::parse::utils::PREALLOC_SOFT_CAP;
use crate::ParseLimits;
const EOCD_SIG: u32 = 0x0605_4b50;
const CDFH_SIG: u32 = 0x0201_4b50;
const LFH_SIG: u32 = 0x0403_4b50;
const ZIP64_EOCD_SIG: u32 = 0x0606_4b50;
const ZIP64_LOCATOR_SIG: u32 = 0x0706_4b50;
const EOCD_FIXED_LEN: u64 = 22;
const MAX_COMMENT_LEN: u64 = 0xFFFF;
const EOCD_SCAN_MAX: u64 = EOCD_FIXED_LEN + MAX_COMMENT_LEN;
const CDFH_FIXED_LEN: usize = 46;
const LFH_FIXED_LEN: usize = 30;
const ZIP64_LOCATOR_LEN: u64 = 20;
const ZIP64_EOCD_FIXED_LEN: usize = 56;
const ZIP64_EXTRA_ID: u16 = 0x0001;
const METHOD_STORED: u16 = 0;
const METHOD_DEFLATE: u16 = 8;
const U32_SENTINEL: u32 = 0xFFFF_FFFF;
const U16_SENTINEL: u16 = 0xFFFF;
const ZIP_MAX_ENTRIES: u64 = 1 << 20;
const ZIP_MAX_NAME_LEN: usize = 4096;
pub(crate) trait ZipSource {
fn total_len(&self) -> u64;
fn read_at(&mut self, offset: u64, buf: &mut [u8]) -> crate::Result<()>;
fn as_slice(&self) -> Option<&[u8]> {
None
}
}
pub(crate) struct SliceSource<'a> {
data: &'a [u8],
}
impl<'a> SliceSource<'a> {
#[must_use]
pub(crate) const fn new(data: &'a [u8]) -> Self {
Self { data }
}
}
impl ZipSource for SliceSource<'_> {
fn total_len(&self) -> u64 {
#[allow(clippy::as_conversions)]
let len = self.data.len() as u64;
len
}
fn read_at(&mut self, offset: u64, buf: &mut [u8]) -> crate::Result<()> {
let start = usize::try_from(offset).map_err(|_| AnamnesisError::Parse {
reason: "ZIP read offset overflows usize".into(),
})?;
let end = start
.checked_add(buf.len())
.ok_or_else(|| AnamnesisError::Parse {
reason: "ZIP read range overflow".into(),
})?;
let src = self
.data
.get(start..end)
.ok_or_else(|| AnamnesisError::Parse {
reason: "ZIP read past end of mapped file".into(),
})?;
buf.copy_from_slice(src);
Ok(())
}
fn as_slice(&self) -> Option<&[u8]> {
Some(self.data)
}
}
pub(crate) struct ReaderSource<R: Read + Seek> {
reader: R,
len: u64,
}
impl<R: Read + Seek> ReaderSource<R> {
pub(crate) fn new(mut reader: R) -> crate::Result<Self> {
let len = reader
.seek(std::io::SeekFrom::End(0))
.map_err(AnamnesisError::Io)?;
Ok(Self { reader, len })
}
pub(crate) fn entry_data_reader(
&mut self,
entry: &ZipEntry,
) -> crate::Result<BoundedReader<'_, R>> {
let start = data_start(self, entry)?;
self.reader
.seek(std::io::SeekFrom::Start(start))
.map_err(AnamnesisError::Io)?;
Ok(BoundedReader {
inner: &mut self.reader,
remaining: entry.compressed_size,
})
}
}
impl<R: Read + Seek> ZipSource for ReaderSource<R> {
fn total_len(&self) -> u64 {
self.len
}
fn read_at(&mut self, offset: u64, buf: &mut [u8]) -> crate::Result<()> {
self.reader
.seek(std::io::SeekFrom::Start(offset))
.map_err(AnamnesisError::Io)?;
self.reader.read_exact(buf).map_err(AnamnesisError::Io)?;
Ok(())
}
}
pub(crate) struct BoundedReader<'a, R> {
inner: &'a mut R,
remaining: u64,
}
impl<R: Read> Read for BoundedReader<'_, R> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
if self.remaining == 0 {
return Ok(0);
}
#[allow(clippy::as_conversions, clippy::cast_possible_truncation)]
let want = (buf.len() as u64).min(self.remaining) as usize;
#[allow(clippy::indexing_slicing)]
let n = self.inner.read(&mut buf[..want])?;
#[allow(clippy::as_conversions)]
let read_u64 = n as u64;
self.remaining = self.remaining.saturating_sub(read_u64);
Ok(n)
}
}
struct ByteCursor<'a> {
buf: &'a [u8],
pos: usize,
}
impl<'a> ByteCursor<'a> {
const fn new(buf: &'a [u8]) -> Self {
Self { buf, pos: 0 }
}
fn remaining(&self) -> usize {
self.buf.len().saturating_sub(self.pos)
}
fn take(&mut self, n: usize) -> crate::Result<&'a [u8]> {
let end = self
.pos
.checked_add(n)
.ok_or_else(|| AnamnesisError::Parse {
reason: "ZIP record offset overflow".into(),
})?;
let slice = self
.buf
.get(self.pos..end)
.ok_or_else(|| AnamnesisError::Parse {
reason: "ZIP record truncated".into(),
})?;
self.pos = end;
Ok(slice)
}
fn u16(&mut self) -> crate::Result<u16> {
let arr: [u8; 2] = self
.take(2)?
.try_into()
.map_err(|_| AnamnesisError::Parse {
reason: "internal: ZIP u16 slice-to-array conversion failed".into(),
})?;
Ok(u16::from_le_bytes(arr))
}
fn u32(&mut self) -> crate::Result<u32> {
let arr: [u8; 4] = self
.take(4)?
.try_into()
.map_err(|_| AnamnesisError::Parse {
reason: "internal: ZIP u32 slice-to-array conversion failed".into(),
})?;
Ok(u32::from_le_bytes(arr))
}
fn u64(&mut self) -> crate::Result<u64> {
let arr: [u8; 8] = self
.take(8)?
.try_into()
.map_err(|_| AnamnesisError::Parse {
reason: "internal: ZIP u64 slice-to-array conversion failed".into(),
})?;
Ok(u64::from_le_bytes(arr))
}
fn skip(&mut self, n: usize) -> crate::Result<()> {
self.take(n)?;
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Compression {
Stored,
Deflate,
Unsupported(u16),
}
impl Compression {
const fn from_tag(tag: u16) -> Self {
match tag {
METHOD_STORED => Self::Stored,
METHOD_DEFLATE => Self::Deflate,
other => Self::Unsupported(other),
}
}
#[must_use]
pub(crate) const fn is_stored(self) -> bool {
matches!(self, Self::Stored)
}
}
#[derive(Debug, Clone)]
pub(crate) struct ZipEntry {
pub(crate) name: String,
pub(crate) method: Compression,
pub(crate) compressed_size: u64,
#[allow(dead_code)]
pub(crate) uncompressed_size: u64,
pub(crate) local_header_offset: u64,
}
struct CentralDirInfo {
entries: u64,
offset: u64,
size: u64,
}
pub(crate) fn read_central_directory<S: ZipSource>(
src: &mut S,
limits: &ParseLimits,
) -> crate::Result<Vec<ZipEntry>> {
let total_len = src.total_len();
let info = find_central_dir(src, total_len)?;
let cd_end = info
.offset
.checked_add(info.size)
.ok_or_else(|| AnamnesisError::Parse {
reason: "ZIP central directory range overflow".into(),
})?;
if cd_end > total_len {
return Err(AnamnesisError::Parse {
reason: format!(
"ZIP central directory range [{}..{cd_end}] exceeds file size {total_len}",
info.offset
),
});
}
if info.entries > ZIP_MAX_ENTRIES {
return Err(AnamnesisError::Parse {
reason: format!(
"ZIP central directory declares {} entries, exceeding the {ZIP_MAX_ENTRIES} cap",
info.entries
),
});
}
limits.check_item_count(info.entries, "ZIP central directory entry count")?;
let cd_size = usize::try_from(info.size).map_err(|_| AnamnesisError::Parse {
reason: "ZIP central directory size overflows usize".into(),
})?;
let cd_bytes = read_cd_bytes(src, info.offset, cd_size, limits)?;
let cap = usize::try_from(info.entries)
.unwrap_or(usize::MAX)
.min(PREALLOC_SOFT_CAP);
let mut entries = Vec::with_capacity(cap);
parse_cd_entries(&cd_bytes, info.entries, &mut entries)?;
Ok(entries)
}
fn read_cd_bytes<'a, S: ZipSource>(
src: &'a mut S,
offset: u64,
cd_size: usize,
limits: &ParseLimits,
) -> crate::Result<Cow<'a, [u8]>> {
if src.as_slice().is_some() {
let start = usize::try_from(offset).map_err(|_| AnamnesisError::Parse {
reason: "ZIP central directory offset overflows usize".into(),
})?;
let end = start
.checked_add(cd_size)
.ok_or_else(|| AnamnesisError::Parse {
reason: "ZIP central directory range overflow".into(),
})?;
let file = src.as_slice().ok_or_else(|| AnamnesisError::Parse {
reason: "internal: ZIP in-memory source slice unavailable".into(),
})?;
let slice = file.get(start..end).ok_or_else(|| AnamnesisError::Parse {
reason: "ZIP central directory range exceeds file size".into(),
})?;
return Ok(Cow::Borrowed(slice));
}
#[allow(clippy::as_conversions)]
let cd_size_u64 = cd_size as u64;
limits.check_alloc(cd_size_u64, "ZIP central directory")?;
let mut buf = vec![0u8; cd_size];
src.read_at(offset, &mut buf)?;
Ok(Cow::Owned(buf))
}
fn find_central_dir<S: ZipSource>(src: &mut S, total_len: u64) -> crate::Result<CentralDirInfo> {
if total_len < EOCD_FIXED_LEN {
return Err(AnamnesisError::Parse {
reason: "file too small to be a ZIP archive".into(),
});
}
let scan_len = total_len.min(EOCD_SCAN_MAX);
let scan_start = total_len
.checked_sub(scan_len)
.ok_or_else(|| AnamnesisError::Parse {
reason: "internal: ZIP EOCD scan window underflow".into(),
})?;
let scan_len_usize = usize::try_from(scan_len).map_err(|_| AnamnesisError::Parse {
reason: "ZIP EOCD scan window overflows usize".into(),
})?;
let mut tail = vec![0u8; scan_len_usize];
src.read_at(scan_start, &mut tail)?;
let eocd_pos = find_eocd_in_tail(&tail)?;
let eocd = tail.get(eocd_pos..).ok_or_else(|| AnamnesisError::Parse {
reason: "internal: ZIP EOCD position out of range".into(),
})?;
let mut c = ByteCursor::new(eocd);
c.skip(4)?; c.skip(6)?; let entries16 = c.u16()?;
let size32 = c.u32()?;
let offset32 = c.u32()?;
if entries16 == U16_SENTINEL || size32 == U32_SENTINEL || offset32 == U32_SENTINEL {
#[allow(clippy::as_conversions)]
let eocd_file_offset =
scan_start
.checked_add(eocd_pos as u64)
.ok_or_else(|| AnamnesisError::Parse {
reason: "ZIP EOCD file offset overflow".into(),
})?;
return read_zip64_eocd(src, eocd_file_offset);
}
Ok(CentralDirInfo {
entries: u64::from(entries16),
offset: u64::from(offset32),
size: u64::from(size32),
})
}
fn find_eocd_in_tail(tail: &[u8]) -> crate::Result<usize> {
let tail_len = tail.len();
let max_start = tail_len.checked_sub(usize::try_from(EOCD_FIXED_LEN).unwrap_or(usize::MAX));
let max_start = max_start.ok_or_else(|| AnamnesisError::Parse {
reason: "file too small to contain a ZIP end-of-central-directory record".into(),
})?;
for start in (0..=max_start).rev() {
let Some(sig_end) = start.checked_add(4) else {
continue;
};
let Some(sig) = tail.get(start..sig_end) else {
continue;
};
let Ok(arr) = <[u8; 4]>::try_from(sig) else {
continue;
};
if u32::from_le_bytes(arr) != EOCD_SIG {
continue;
}
let Some(comment_start) = start.checked_add(20) else {
continue;
};
let Some(comment_end) = start.checked_add(22) else {
continue;
};
let Some(comment) = tail.get(comment_start..comment_end) else {
continue;
};
let Ok(carr) = <[u8; 2]>::try_from(comment) else {
continue;
};
let comment_len = usize::from(u16::from_le_bytes(carr));
if comment_end.checked_add(comment_len) == Some(tail_len) {
return Ok(start);
}
}
Err(AnamnesisError::Parse {
reason: "ZIP end-of-central-directory record not found".into(),
})
}
fn read_zip64_eocd<S: ZipSource>(
src: &mut S,
eocd_file_offset: u64,
) -> crate::Result<CentralDirInfo> {
let locator_offset = eocd_file_offset
.checked_sub(ZIP64_LOCATOR_LEN)
.ok_or_else(|| AnamnesisError::Parse {
reason: "ZIP64 EOCD locator missing (no room before EOCD)".into(),
})?;
let mut loc = [0u8; 20];
src.read_at(locator_offset, &mut loc)?;
let mut lc = ByteCursor::new(&loc);
if lc.u32()? != ZIP64_LOCATOR_SIG {
return Err(AnamnesisError::Parse {
reason: "ZIP64 EOCD locator signature not found".into(),
});
}
lc.skip(4)?; let record_offset = lc.u64()?;
let mut rec = [0u8; ZIP64_EOCD_FIXED_LEN];
src.read_at(record_offset, &mut rec)?;
let mut rc = ByteCursor::new(&rec);
if rc.u32()? != ZIP64_EOCD_SIG {
return Err(AnamnesisError::Parse {
reason: "ZIP64 EOCD record signature not found".into(),
});
}
rc.skip(20)?;
let _entries_this_disk = rc.u64()?;
let entries = rc.u64()?;
let size = rc.u64()?;
let offset = rc.u64()?;
Ok(CentralDirInfo {
entries,
offset,
size,
})
}
fn parse_cd_entries(cd: &[u8], declared: u64, out: &mut Vec<ZipEntry>) -> crate::Result<()> {
let mut cur = ByteCursor::new(cd);
let mut count = 0u64;
while count < declared {
if cur.remaining() < CDFH_FIXED_LEN {
break; }
if cur.u32()? != CDFH_SIG {
break; }
cur.skip(4)?; let _flags = cur.u16()?;
let method = cur.u16()?;
cur.skip(4)?; cur.skip(4)?; let comp32 = cur.u32()?;
let uncomp32 = cur.u32()?;
let name_len = usize::from(cur.u16()?);
let extra_len = usize::from(cur.u16()?);
let comment_len = usize::from(cur.u16()?);
cur.skip(2)?; cur.skip(2)?; cur.skip(4)?; let offset32 = cur.u32()?;
if name_len > ZIP_MAX_NAME_LEN {
return Err(AnamnesisError::Parse {
reason: format!(
"ZIP entry name length {name_len} exceeds the {ZIP_MAX_NAME_LEN}-byte cap"
),
});
}
let name_bytes = cur.take(name_len)?;
let extra = cur.take(extra_len)?;
cur.skip(comment_len)?;
let (compressed_size, uncompressed_size, local_header_offset) =
apply_zip64_extra(extra, comp32, uncomp32, offset32)?;
let name = String::from_utf8_lossy(name_bytes).into_owned();
out.push(ZipEntry {
name,
method: Compression::from_tag(method),
compressed_size,
uncompressed_size,
local_header_offset,
});
count += 1;
}
Ok(())
}
fn apply_zip64_extra(
extra: &[u8],
comp32: u32,
uncomp32: u32,
offset32: u32,
) -> crate::Result<(u64, u64, u64)> {
let mut compressed = u64::from(comp32);
let mut uncompressed = u64::from(uncomp32);
let mut offset = u64::from(offset32);
let needs_zip64 =
comp32 == U32_SENTINEL || uncomp32 == U32_SENTINEL || offset32 == U32_SENTINEL;
if !needs_zip64 {
return Ok((compressed, uncompressed, offset));
}
let mut cur = ByteCursor::new(extra);
while cur.remaining() >= 4 {
let id = cur.u16()?;
let field_len = usize::from(cur.u16()?);
let data = cur.take(field_len)?;
if id == ZIP64_EXTRA_ID {
let mut d = ByteCursor::new(data);
if uncomp32 == U32_SENTINEL {
uncompressed = d.u64()?;
}
if comp32 == U32_SENTINEL {
compressed = d.u64()?;
}
if offset32 == U32_SENTINEL {
offset = d.u64()?;
}
return Ok((compressed, uncompressed, offset));
}
}
Err(AnamnesisError::Parse {
reason: "ZIP entry declares ZIP64 sizes but has no ZIP64 extra field".into(),
})
}
pub(crate) fn data_start<S: ZipSource>(src: &mut S, entry: &ZipEntry) -> crate::Result<u64> {
let mut hdr = [0u8; LFH_FIXED_LEN];
src.read_at(entry.local_header_offset, &mut hdr)?;
let mut c = ByteCursor::new(&hdr);
if c.u32()? != LFH_SIG {
return Err(AnamnesisError::Parse {
reason: format!(
"ZIP local file header signature not found at offset {}",
entry.local_header_offset
),
});
}
c.skip(22)?; let name_len = u64::from(c.u16()?);
let extra_len = u64::from(c.u16()?);
#[allow(clippy::as_conversions)]
let fixed = LFH_FIXED_LEN as u64;
let start = entry
.local_header_offset
.checked_add(fixed)
.and_then(|v| v.checked_add(name_len))
.and_then(|v| v.checked_add(extra_len))
.ok_or_else(|| AnamnesisError::Parse {
reason: "ZIP local header data offset overflow".into(),
})?;
let end = start
.checked_add(entry.compressed_size)
.ok_or_else(|| AnamnesisError::Parse {
reason: "ZIP entry data range overflow".into(),
})?;
if end > src.total_len() {
return Err(AnamnesisError::Parse {
reason: format!(
"ZIP entry `{}` data range [{start}..{end}] exceeds file size {}",
entry.name,
src.total_len()
),
});
}
Ok(start)
}
#[must_use]
pub(crate) fn strip_archive_prefix(name: &str) -> Cow<'_, str> {
match name.find('/') {
Some(pos) => Cow::Borrowed(name.get(pos + 1..).unwrap_or(name)),
None => Cow::Borrowed(name),
}
}
#[cfg(test)]
#[allow(
clippy::panic,
clippy::indexing_slicing,
clippy::unwrap_used,
clippy::expect_used,
clippy::as_conversions,
clippy::cast_possible_truncation
)]
mod tests {
use std::io::{Read, Write};
use super::*;
fn build_zip(entries: &[(&str, ::zip::CompressionMethod, &[u8])]) -> Vec<u8> {
let mut buf = Vec::new();
{
let cursor = std::io::Cursor::new(&mut buf);
let mut writer = ::zip::ZipWriter::new(cursor);
for (name, method, data) in entries {
let opts = ::zip::write::SimpleFileOptions::default().compression_method(*method);
writer.start_file(*name, opts).unwrap();
writer.write_all(data).unwrap();
}
writer.finish().unwrap();
}
buf
}
fn build_zip64(entries: &[(&str, &[u8])]) -> Vec<u8> {
let mut buf = Vec::new();
{
let cursor = std::io::Cursor::new(&mut buf);
let mut writer = ::zip::ZipWriter::new(cursor);
for (name, data) in entries {
let opts = ::zip::write::SimpleFileOptions::default()
.compression_method(::zip::CompressionMethod::Stored)
.large_file(true);
writer.start_file(*name, opts).unwrap();
writer.write_all(data).unwrap();
}
writer.finish().unwrap();
}
buf
}
fn assert_matches_zip_crate(archive: &[u8]) {
let mut src = SliceSource::new(archive);
let entries = read_central_directory(&mut src, &ParseLimits::unbounded())
.expect("vendored reader failed");
let cursor = std::io::Cursor::new(archive);
let mut zip = ::zip::ZipArchive::new(cursor).expect("zip crate failed");
assert_eq!(
entries.len(),
zip.len(),
"entry count mismatch: vendored {} vs zip {}",
entries.len(),
zip.len()
);
for entry in &entries {
let mut zfile = zip.by_name(&entry.name).expect("zip crate by_name failed");
assert_eq!(
entry.uncompressed_size,
zfile.size(),
"uncompressed size mismatch for `{}`",
entry.name
);
assert_eq!(
entry.compressed_size,
zfile.compressed_size(),
"compressed size mismatch for `{}`",
entry.name
);
let our_start = data_start(&mut src, entry).expect("vendored data_start failed");
assert_eq!(
our_start,
zfile.data_start(),
"data_start mismatch for `{}`",
entry.name
);
if entry.method.is_stored() {
let start = our_start as usize;
let end = start + entry.compressed_size as usize;
let mut expected = Vec::new();
zfile.read_to_end(&mut expected).unwrap();
assert_eq!(&archive[start..end], &expected[..], "data mismatch");
}
}
}
#[test]
fn single_stored_entry_matches_zip() {
let archive = build_zip(&[(
"data.pkl",
::zip::CompressionMethod::Stored,
b"hello pickle",
)]);
assert_matches_zip_crate(&archive);
}
#[test]
fn multi_entry_pth_layout_matches_zip() {
let archive = build_zip(&[
(
"archive/data.pkl",
::zip::CompressionMethod::Stored,
b"PKL-STREAM",
),
(
"archive/byteorder",
::zip::CompressionMethod::Stored,
b"little",
),
(
"archive/data/0",
::zip::CompressionMethod::Stored,
&[1u8; 64],
),
(
"archive/data/1",
::zip::CompressionMethod::Stored,
&[2u8; 128],
),
]);
assert_matches_zip_crate(&archive);
}
#[test]
fn deflate_entry_metadata_matches_zip() {
let payload = vec![0xABu8; 4096];
let archive = build_zip(&[("weight.npy", ::zip::CompressionMethod::Deflated, &payload)]);
assert_matches_zip_crate(&archive);
let mut src = SliceSource::new(&archive);
let entries = read_central_directory(&mut src, &ParseLimits::unbounded()).unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].method, Compression::Deflate);
assert!(entries[0].compressed_size < entries[0].uncompressed_size);
}
#[test]
fn zip64_entry_matches_zip() {
let archive = build_zip64(&[
("archive/data.pkl", b"zip64 stream"),
("archive/data/0", &[7u8; 256]),
]);
assert_matches_zip_crate(&archive);
}
#[test]
fn empty_archive_has_no_entries() {
let archive = build_zip(&[]);
let mut src = SliceSource::new(&archive);
let entries = read_central_directory(&mut src, &ParseLimits::unbounded()).unwrap();
assert!(entries.is_empty());
assert_matches_zip_crate(&archive);
}
#[test]
fn prefix_stripping() {
assert_eq!(strip_archive_prefix("archive/data.pkl"), "data.pkl");
assert_eq!(strip_archive_prefix("my_model/data/0"), "data/0");
assert_eq!(strip_archive_prefix("byteorder"), "byteorder");
assert_eq!(strip_archive_prefix("a/b/c"), "b/c");
}
#[test]
fn too_small_is_rejected() {
let mut src = SliceSource::new(b"PK");
assert!(read_central_directory(&mut src, &ParseLimits::unbounded()).is_err());
}
#[test]
fn not_a_zip_is_rejected() {
let junk = vec![0u8; 256];
let mut src = SliceSource::new(&junk);
assert!(read_central_directory(&mut src, &ParseLimits::unbounded()).is_err());
}
#[test]
fn truncated_central_directory_is_rejected() {
let mut archive = build_zip(&[("data.pkl", ::zip::CompressionMethod::Stored, b"abc")]);
archive.truncate(archive.len() - 1);
let mut src = SliceSource::new(&archive);
let _ = read_central_directory(&mut src, &ParseLimits::unbounded()); }
#[test]
fn differential_random_archives() {
let mut state: u64 = 0x9E37_79B9_7F4A_7C15;
let mut next = |bound: u64| -> u64 {
state = state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
(state >> 33) % bound.max(1)
};
for _ in 0..256 {
let n_entries = next(8) + 1;
let mut spec: Vec<(String, ::zip::CompressionMethod, Vec<u8>)> = Vec::new();
for i in 0..n_entries {
let prefix = if next(2) == 0 { "archive/" } else { "" };
let name = format!("{prefix}entry_{i}");
let len = usize::try_from(next(2048)).unwrap();
let data: Vec<u8> = if next(2) == 0 {
vec![0xAB; len]
} else {
(0..len)
.map(|k| u8::try_from((k as u64 + next(251)) % 256).unwrap())
.collect()
};
let method = if next(2) == 0 {
::zip::CompressionMethod::Stored
} else {
::zip::CompressionMethod::Deflated
};
spec.push((name, method, data));
}
let refs: Vec<(&str, ::zip::CompressionMethod, &[u8])> = spec
.iter()
.map(|(n, m, d)| (n.as_str(), *m, d.as_slice()))
.collect();
let archive = build_zip(&refs);
assert_matches_zip_crate(&archive);
}
}
#[test]
fn item_count_limit_rejects_container() {
let archive = build_zip(&[
("a.npy", ::zip::CompressionMethod::Stored, b"x"),
("b.npy", ::zip::CompressionMethod::Stored, b"y"),
("c.npy", ::zip::CompressionMethod::Stored, b"z"),
]);
let mut src = SliceSource::new(&archive);
let limits = ParseLimits::default().with_max_item_count(2);
let err = read_central_directory(&mut src, &limits).unwrap_err();
assert!(
matches!(err, AnamnesisError::Parse { ref reason } if reason.contains("entry count")),
"expected item-count rejection, got: {err}"
);
let ok = read_central_directory(&mut src, &ParseLimits::unbounded()).unwrap();
assert_eq!(ok.len(), 3);
}
#[test]
fn mmap_borrow_path_skips_cd_single_alloc_charge() {
let archive = build_zip(&[
(
"archive/data.pkl",
::zip::CompressionMethod::Stored,
b"data",
),
(
"archive/data/0",
::zip::CompressionMethod::Stored,
&[0u8; 32],
),
]);
let mut src = SliceSource::new(&archive);
let limits = ParseLimits::default().with_max_single_alloc(8);
let entries = read_central_directory(&mut src, &limits)
.expect("mmap borrow path must not be charged for the directory read");
assert_eq!(entries.len(), 2);
}
#[test]
fn cd_single_alloc_limit_rejects_reader_path() {
let archive = build_zip(&[
(
"archive/data.pkl",
::zip::CompressionMethod::Stored,
b"data",
),
(
"archive/data/0",
::zip::CompressionMethod::Stored,
&[0u8; 32],
),
]);
let mut src = ReaderSource::new(std::io::Cursor::new(&archive)).unwrap();
let limits = ParseLimits::default().with_max_single_alloc(8);
let err = read_central_directory(&mut src, &limits).unwrap_err();
assert!(
matches!(err, AnamnesisError::Parse { ref reason }
if reason.contains("central directory") || reason.contains("max_single_alloc")),
"expected central-directory single-alloc rejection, got: {err}"
);
}
#[test]
fn compression_tag_allowlist() {
assert_eq!(Compression::from_tag(0), Compression::Stored);
assert_eq!(Compression::from_tag(8), Compression::Deflate);
assert_eq!(Compression::from_tag(99), Compression::Unsupported(99));
assert!(Compression::Stored.is_stored());
assert!(!Compression::Deflate.is_stored());
}
}