use std::io::{self, Read, Write};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use crate::archive::{ArchiveRead, Container, CreateOpts, EntryMeta, OpenOpts, PlainSink};
use crate::error::Result;
use crate::fidelity::Rung;
use crate::format::FormatMeta;
use crate::source::{ReaderSource, SeekRead, Source, SourceCaps};
const MODE_TYPE_MASK: u32 = 0o170_000;
const MODE_PERMISSION_MASK: u32 = 0o7777;
const S_IFREG: u32 = 0o100_000;
#[derive(Clone)]
struct CaptureWriter {
buf: Arc<Mutex<Vec<u8>>>,
written: Arc<AtomicU64>,
fail_after: Option<u64>,
}
impl CaptureWriter {
fn new() -> Self {
Self {
buf: Arc::default(),
written: Arc::new(AtomicU64::new(0)),
fail_after: None,
}
}
fn failing_after(budget: u64) -> Self {
Self {
buf: Arc::default(),
written: Arc::new(AtomicU64::new(0)),
fail_after: Some(budget),
}
}
fn contents(&self) -> Vec<u8> {
self.buf
.lock()
.expect("CaptureWriter mutex poisoned")
.clone()
}
}
impl Write for CaptureWriter {
fn write(&mut self, data: &[u8]) -> io::Result<usize> {
if let Some(budget) = self.fail_after {
let already = self.written.load(Ordering::Relaxed);
if already >= budget {
return Err(io::Error::other("conformance: CaptureWriter failed"));
}
let allowed = ((budget - already).min(data.len() as u64)) as usize;
self.buf
.lock()
.expect("CaptureWriter mutex poisoned")
.extend_from_slice(&data[..allowed]);
self.written.fetch_add(allowed as u64, Ordering::Relaxed);
return Ok(allowed);
}
self.buf
.lock()
.expect("CaptureWriter mutex poisoned")
.extend_from_slice(data);
self.written.fetch_add(data.len() as u64, Ordering::Relaxed);
Ok(data.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
fn build(container: &dyn Container, entries: &[(&str, &[u8])]) -> Vec<u8> {
let id = container.id();
let cap = CaptureWriter::new();
let mut w = container
.create(
PlainSink::new(Box::new(cap.clone())),
&CreateOpts::default(),
)
.unwrap_or_else(|e| panic!("conformance[{id}] create: {e}"));
for (name, data) in entries {
let meta = EntryMeta::file(*name);
w.add(&meta, &mut io::Cursor::new(*data))
.unwrap_or_else(|e| panic!("conformance[{id}] add({name}): {e}"));
}
w.finish()
.unwrap_or_else(|e| panic!("conformance[{id}] finish: {e}"))
.finish()
.unwrap_or_else(|e| panic!("conformance[{id}] finish (sink): {e}"));
cap.contents()
}
fn read_all(container: &dyn Container, bytes: &[u8]) -> Result<Vec<(String, Vec<u8>)>> {
let src: Box<dyn Source> = Box::new(ReaderSource::new(io::Cursor::new(bytes.to_vec())));
let resolved = crate::resolve(
src,
container.id(),
container.caps(),
&crate::StreamPolicy::default(),
)?;
let mut ar = container.open(resolved, &OpenOpts::default())?;
let mut out = Vec::new();
while let Some(mut entry) = ar.next_entry()? {
let name = entry.meta().name.clone();
let mut data = Vec::new();
entry.reader().read_to_end(&mut data)?;
out.push((name, data));
}
Ok(out)
}
pub fn open_forward_only(container: &dyn Container, bytes: &[u8]) -> Box<dyn ArchiveRead> {
let id = container.id();
let src: Box<dyn Source> = Box::new(ReaderSource::new(io::Cursor::new(bytes.to_vec())));
let resolved = crate::resolve(src, id, container.caps(), &crate::StreamPolicy::ForwardOnly)
.unwrap_or_else(|e| panic!("conformance[{id}] resolve forward-only: {e}"));
container
.open(resolved, &OpenOpts::default())
.unwrap_or_else(|e| panic!("conformance[{id}] open forward-only: {e}"))
}
fn read_all_forward_only(container: &dyn Container, bytes: &[u8]) -> Vec<(String, Vec<u8>)> {
let id = container.id();
let mut ar = open_forward_only(container, bytes);
let mut out = Vec::new();
while let Some(mut entry) = ar
.next_entry()
.unwrap_or_else(|e| panic!("conformance[{id}] forward-only next_entry: {e}"))
{
let name = entry.meta().name.clone();
let mut data = Vec::new();
entry
.reader()
.read_to_end(&mut data)
.unwrap_or_else(|e| panic!("conformance[{id}] forward-only entry read: {e}"));
out.push((name, data));
}
out
}
struct CountingSource {
inner: io::Cursor<Vec<u8>>,
count: Arc<AtomicU64>,
}
impl CountingSource {
fn new(bytes: &[u8]) -> Self {
Self {
inner: io::Cursor::new(bytes.to_vec()),
count: Arc::new(AtomicU64::new(0)),
}
}
fn counter(&self) -> Arc<AtomicU64> {
Arc::clone(&self.count)
}
}
impl Read for CountingSource {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let n = self.inner.read(buf)?;
self.count.fetch_add(n as u64, Ordering::Relaxed);
Ok(n)
}
}
impl Source for CountingSource {
fn caps(&self) -> SourceCaps {
SourceCaps {
seekable: false,
len: None,
}
}
fn as_seek(&mut self) -> Option<&mut dyn SeekRead> {
None
}
}
fn first_entry_source_bytes(container: &dyn Container, source: CountingSource) -> usize {
let id = container.id();
let counter = source.counter();
let src: Box<dyn Source> = Box::new(source);
let resolved = crate::resolve(src, id, container.caps(), &crate::StreamPolicy::ForwardOnly)
.unwrap_or_else(|e| panic!("conformance[{id}] resolve forward-only: {e}"));
let mut ar = container
.open(resolved, &OpenOpts::default())
.unwrap_or_else(|e| panic!("conformance[{id}] open forward-only: {e}"));
ar.next_entry()
.unwrap_or_else(|e| panic!("conformance[{id}] forward-only next_entry: {e}"))
.unwrap_or_else(|| panic!("conformance[{id}] forward-only: expected at least one entry"));
counter.load(Ordering::Relaxed) as usize
}
fn classify_container_error(e: crate::error::Error) -> io::Error {
match e {
crate::error::Error::Io(io_err) => io_err,
crate::error::Error::Corrupt(msg) => io::Error::new(io::ErrorKind::InvalidData, msg),
other => io::Error::other(other.to_string()),
}
}
fn read_all_expecting_error(container: &dyn Container, bytes: &[u8]) -> Option<io::Error> {
read_all(container, bytes)
.err()
.map(classify_container_error)
}
struct FailingSource;
impl Read for FailingSource {
fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> {
Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"conformance: FailingSource",
))
}
}
impl Source for FailingSource {
fn caps(&self) -> SourceCaps {
SourceCaps {
seekable: false,
len: None,
}
}
fn as_seek(&mut self) -> Option<&mut dyn SeekRead> {
None
}
}
fn read_all_over_failing_source(container: &dyn Container) -> Option<io::Error> {
let id = container.id();
let src: Box<dyn Source> = Box::new(FailingSource);
let resolved = crate::resolve(src, id, container.caps(), &crate::StreamPolicy::default())
.unwrap_or_else(|e| panic!("conformance[{id}] resolve over a failing source: {e}"));
let mut ar = container
.open(resolved, &OpenOpts::default())
.unwrap_or_else(|e| panic!("conformance[{id}] open over a failing source: {e}"));
loop {
match ar.next_entry() {
Ok(Some(mut entry)) => {
let mut data = Vec::new();
if let Err(e) = entry.reader().read_to_end(&mut data) {
return Some(e);
}
}
Ok(None) => return None,
Err(e) => return Some(classify_container_error(e)),
}
}
}
fn build_with_meta(container: &dyn Container, entries: &[(EntryMeta, &[u8])]) -> Vec<u8> {
let id = container.id();
let cap = CaptureWriter::new();
let mut w = container
.create(
PlainSink::new(Box::new(cap.clone())),
&CreateOpts::default(),
)
.unwrap_or_else(|e| panic!("conformance[{id}] create: {e}"));
for (meta, data) in entries {
w.add(meta, &mut io::Cursor::new(*data))
.unwrap_or_else(|e| panic!("conformance[{id}] add({}): {e}", meta.name));
}
w.finish()
.unwrap_or_else(|e| panic!("conformance[{id}] finish: {e}"))
.finish()
.unwrap_or_else(|e| panic!("conformance[{id}] finish (sink): {e}"));
cap.contents()
}
fn read_all_meta(container: &dyn Container, bytes: &[u8]) -> Vec<EntryMeta> {
let id = container.id();
let src: Box<dyn Source> = Box::new(ReaderSource::new(io::Cursor::new(bytes.to_vec())));
let resolved = crate::resolve(
src,
container.id(),
container.caps(),
&crate::StreamPolicy::default(),
)
.unwrap_or_else(|e| panic!("conformance[{id}] resolve: {e}"));
let mut ar = container
.open(resolved, &OpenOpts::default())
.unwrap_or_else(|e| panic!("conformance[{id}] open: {e}"));
let mut out = Vec::new();
while let Some(mut entry) = ar
.next_entry()
.unwrap_or_else(|e| panic!("conformance[{id}] next_entry: {e}"))
{
let meta = entry.meta().clone();
let mut data = Vec::new();
entry
.reader()
.read_to_end(&mut data)
.unwrap_or_else(|e| panic!("conformance[{id}] entry read: {e}"));
out.push(meta);
}
out
}
fn read_all_meta_seekable(container: &dyn Container, bytes: &[u8]) -> Vec<EntryMeta> {
let id = container.id();
let forward: Box<dyn Source> = Box::new(ReaderSource::new(io::Cursor::new(bytes.to_vec())));
let seekable = crate::source::SpillSource::materialize(
forward,
&crate::source::SpillPolicy::Memory {
cap: 64 * 1024 * 1024,
},
)
.unwrap_or_else(|e| panic!("conformance[{id}] materialize: {e}"));
let resolved = crate::resolve(
Box::new(seekable),
container.id(),
container.caps(),
&crate::StreamPolicy::default(),
)
.unwrap_or_else(|e| panic!("conformance[{id}] resolve (seekable): {e}"));
let mut ar = container
.open(resolved, &OpenOpts::default())
.unwrap_or_else(|e| panic!("conformance[{id}] open (seekable): {e}"));
let mut out = Vec::new();
while let Some(mut entry) = ar
.next_entry()
.unwrap_or_else(|e| panic!("conformance[{id}] next_entry (seekable): {e}"))
{
let meta = entry.meta().clone();
let mut data = Vec::new();
entry
.reader()
.read_to_end(&mut data)
.unwrap_or_else(|e| panic!("conformance[{id}] entry read (seekable): {e}"));
out.push(meta);
}
out
}
pub fn assert_container_conforms(container: &dyn Container, meta: &FormatMeta) {
let id = container.id();
let caps = container.caps();
assert_eq!(
id, meta.id,
"conformance[{id}] property 1: Container::id() disagrees with its registered FormatMeta"
);
if caps.write && caps.read {
let bytes = build(container, &[("a.txt", b"alpha")]);
if !meta.magics.is_empty() {
let hit = meta.magics.iter().any(|r| {
bytes.len() >= r.offset + r.bytes.len()
&& &bytes[r.offset..r.offset + r.bytes.len()] == r.bytes
});
assert!(
hit,
"conformance[{id}] property 2: no registered magic rule matches what create() \
produced"
);
}
for entries in [
&[][..],
&[("a.txt", &b"alpha"[..])][..],
&[("a.txt", &b"alpha"[..]), ("b/c.bin", &b"\x00\xff\x00"[..])][..],
] {
let bytes = build(container, entries);
let got = read_all(container, &bytes).unwrap_or_else(|e| {
panic!(
"conformance[{id}] property 3: failed to read back {} entries: {e} (empty \
archives are the usual culprit)",
entries.len()
)
});
assert_eq!(
got.len(),
entries.len(),
"conformance[{id}] property 3: round trip yielded {} entries, expected {} \
(empty archives are the usual culprit)",
got.len(),
entries.len()
);
for ((want_name, want_data), (got_name, got_data)) in entries.iter().zip(&got) {
assert_eq!(
got_name, want_name,
"conformance[{id}] property 3: entry name mismatch"
);
assert_eq!(
&got_data[..],
*want_data,
"conformance[{id}] property 3: entry DATA mismatch for {want_name}"
);
}
}
let mut w = container
.create(
PlainSink::new(Box::new(CaptureWriter::failing_after(0))),
&CreateOpts::default(),
)
.unwrap_or_else(|e| panic!("conformance[{id}] create: {e}"));
let meta_e = EntryMeta::file("a.txt");
let added = w.add(&meta_e, &mut io::Cursor::new(b"alpha".as_slice()));
let finished = w.finish().and_then(|sink| sink.finish());
assert!(
added.is_err() || finished.is_err(),
"conformance[{id}] property 4: a destination that fails every write produced \
neither an add() nor a finish() error"
);
}
if caps.read {
let bytes = build(
container,
&[("a.txt", &b"alpha"[..]), ("b.txt", &b"beta"[..])],
);
if caps.forward_parse {
let fwd = read_all_forward_only(container, &bytes);
assert_eq!(
fwd.len(),
2,
"conformance[{id}] property 5: forward parse over a non-seekable source \
yielded {} entries, expected 2",
fwd.len()
);
}
{
let mut ar = open_forward_only(container, &bytes);
match ar.by_index(0) {
Err(crate::error::Error::NotSeekable { .. }) => {}
Ok(_) => panic!(
"conformance[{id}] property 6: by_index() returned an entry from a \
forward-only source, claiming random access it does not have"
),
Err(e) => panic!(
"conformance[{id}] property 6: by_index() on a forward-only source must \
be Err(NotSeekable), got {e:?}"
),
}
}
if caps.trailing_index {
let ar = open_forward_only(container, &bytes);
let rung = ar.fidelity().rung;
assert_ne!(
rung,
Rung::Exact,
"conformance[{id}] property 7: a trailing-index container read forward \
reported Rung::Exact, but its authoritative index is at the END of the \
stream and was never read"
);
}
{
let big = build(container, &[("big.bin", &vec![b'x'; 4 * 1024 * 1024][..])]);
let counting = CountingSource::new(&big);
let consumed = first_entry_source_bytes(container, counting);
assert!(
consumed < big.len(),
"conformance[{id}] property 8: reaching the first entry consumed \
{consumed} of {} archive bytes — the whole archive was buffered",
big.len()
);
}
for cut in [1usize, bytes.len() / 3, bytes.len() / 2, bytes.len() - 1] {
if cut == 0 || cut >= bytes.len() {
continue;
}
let err = read_all_expecting_error(container, &bytes[..cut]);
let Some(e) = err else {
panic!(
"conformance[{id}] property 9: an archive truncated at {cut} of {} bytes \
was accepted silently",
bytes.len()
)
};
assert_eq!(
e.kind(),
io::ErrorKind::InvalidData,
"conformance[{id}] property 9: truncation at {cut} reported as {:?}, must be \
InvalidData so the CLI exits 5 rather than reporting a full disk",
e.kind()
);
}
if caps.write {
const PAYLOAD: usize = 64 * 1024;
let name = "payload-truncation.bin";
let framing = build(container, &[(name, &[][..])]).len();
let payload = crate::conformance::incompressible(PAYLOAD);
let bytes = build(container, &[(name, &payload)]);
assert!(
framing * 4 < bytes.len(),
"conformance[{id}] property 9 (payload): this container's per-entry framing \
is {framing} bytes of a {}-byte archive, so a midpoint cut can no longer be \
proven to land inside the entry payload — raise PAYLOAD until it can",
bytes.len()
);
let cut = bytes.len() / 2;
let Some(e) = read_all_expecting_error(container, &bytes[..cut]) else {
panic!(
"conformance[{id}] property 9 (payload): an archive truncated at {cut} \
of {} bytes — INSIDE the entry's payload, which starts at or before \
{framing} and runs to at or after {} — was accepted silently. The \
entry's own header promises {PAYLOAD} bytes the stream does not deliver",
bytes.len(),
bytes.len() - framing
)
};
assert_eq!(
e.kind(),
io::ErrorKind::InvalidData,
"conformance[{id}] property 9 (payload): truncation inside the entry payload \
reported as {:?}, must be InvalidData so the CLI exits 5 rather than \
reporting a full disk",
e.kind()
);
}
{
let e = read_all_over_failing_source(container)
.expect("a source that fails every read must produce an error");
assert_eq!(
e.kind(),
io::ErrorKind::PermissionDenied,
"conformance[{id}] property 10: a source error surfaced as {:?} rather than \
passing through — a disk failure must not be reported as corruption",
e.kind()
);
}
if caps.write {
let mut meta_in = EntryMeta::file("m.txt");
meta_in.mode = Some(0o640);
let bytes = build_with_meta(container, &[(meta_in.clone(), &b"m"[..])]);
let got = read_all_meta(container, &bytes);
assert_eq!(
got[0].name, meta_in.name,
"conformance[{id}] property 11: entry name not preserved"
);
if let Some(got_mode) = got[0].mode {
let want = meta_in.mode.expect("the fixture sets a mode");
assert_eq!(
got_mode & MODE_PERMISSION_MASK,
want & MODE_PERMISSION_MASK,
"conformance[{id}] property 11: mode reported but altered — wrote \
{want:#o}, read back {got_mode:#o}, and the permission bits \
differ. Folding in the file-type bits a format needs is allowed; \
changing what the entry may be DONE with is not"
);
let type_bits = got_mode & MODE_TYPE_MASK;
assert!(
type_bits == 0 || type_bits == S_IFREG,
"conformance[{id}] property 11: mode reported but altered — wrote \
{want:#o} for a plain FILE, read back {got_mode:#o}, whose type \
bits are {type_bits:#o}. A container may add S_IFREG \
({S_IFREG:#o}) where its format records the kind in the mode \
field, and may leave the field as given; it may not relabel the \
entry as some other kind"
);
}
}
if caps.write {
let hostile = ["../../etc/passwd", "/abs/path", "a/../../b"];
for name in hostile {
let bytes = build(container, &[(name, &b"x"[..])]);
let got = read_all(container, &bytes).unwrap_or_else(|e| {
panic!("conformance[{id}] property 12: failed to read back {name:?}: {e}")
});
assert_eq!(
got[0].0, name,
"conformance[{id}] property 12: entry name {name:?} came back as {:?}. \
Containers must report names EXACTLY as stored — sanitising here \
destroys the evidence the ops-layer refusal depends on",
got[0].0
);
}
}
if caps.write {
if caps.stores_dirs {
let mut meta_in = EntryMeta::file("d");
meta_in.kind = crate::archive::EntryKind::Dir;
meta_in.mode = Some(0o755);
let bytes = build_with_meta(container, &[(meta_in, &[][..])]);
let got = read_all_meta_seekable(container, &bytes);
assert_eq!(
got.len(),
1,
"conformance[{id}] property 13 (directories): a directory entry was \
written but {} entries read back",
got.len()
);
assert_eq!(
got[0].kind,
crate::archive::EntryKind::Dir,
"conformance[{id}] property 13 (directories): caps claim stores_dirs, \
but a directory entry read back as {:?}. A directory written as a \
regular file makes every entry beneath it unextractable — its parent \
is a file",
got[0].kind
);
}
if caps.stores_symlinks {
let mut meta_in = EntryMeta::file("l");
meta_in.kind = crate::archive::EntryKind::Symlink {
target: "a.txt".into(),
};
let bytes = build_with_meta(container, &[(meta_in, &[][..])]);
let got = read_all_meta_seekable(container, &bytes);
assert_eq!(
got.len(),
1,
"conformance[{id}] property 13 (symlinks): a symlink entry was written \
but {} entries read back",
got.len()
);
match &got[0].kind {
crate::archive::EntryKind::Symlink { target } => assert_eq!(
target, "a.txt",
"conformance[{id}] property 13 (symlinks): the link came back \
pointing at {target:?}, so the restored link would point \
somewhere else"
),
other => panic!(
"conformance[{id}] property 13 (symlinks): caps claim \
stores_symlinks, but a symlink entry read back as {other:?}. \
Stored as a regular file, the target text becomes the file's \
contents"
),
}
}
}
}
}
#[cfg(test)]
fn assert_panics_naming(container: &dyn Container, meta: &FormatMeta, expected: &str) {
let id = container.id();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
assert_container_conforms(container, meta);
}));
match result {
Ok(()) => panic!(
"conformance[{id}]: expected assert_container_conforms to panic naming {expected:?}, \
but it passed"
),
Err(payload) => {
let message = payload
.downcast_ref::<String>()
.map(String::as_str)
.or_else(|| payload.downcast_ref::<&str>().copied())
.unwrap_or("<non-string panic payload>");
assert!(
message.contains(expected),
"conformance[{id}]: panicked, but the message did not mention {expected:?}: \
{message}"
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testing::{FramedMockContainer, framed_container_meta};
#[test]
fn a_well_behaved_container_satisfies_properties_one_to_thirteen() {
assert_container_conforms(&FramedMockContainer, &framed_container_meta());
}
}
#[cfg(test)]
mod broken_containers {
use super::*;
use crate::archive::{ArchiveRead, ArchiveWrite, Entry, Sink};
use crate::fidelity::FidelityReport;
use crate::format::{ContainerCaps, FormatId};
use crate::ladder::Resolved;
use crate::testing::{FramedMockContainer, framed_container_meta};
use std::io::Read;
struct WrongId;
impl Container for WrongId {
fn id(&self) -> FormatId {
FormatId::new("wrong-id")
}
fn caps(&self) -> ContainerCaps {
FramedMockContainer.caps()
}
fn open(&self, resolved: Resolved, o: &OpenOpts) -> Result<Box<dyn ArchiveRead>> {
FramedMockContainer.open(resolved, o)
}
fn create(&self, dst: Box<dyn Sink>, o: &CreateOpts) -> Result<Box<dyn ArchiveWrite>> {
FramedMockContainer.create(dst, o)
}
}
#[test]
fn property_one_catches_an_id_that_disagrees_with_registration() {
assert_panics_naming(&WrongId, &framed_container_meta(), "property 1");
}
struct EmptyBroken;
struct EmptyBrokenWrite {
inner: Box<dyn ArchiveWrite>,
count: usize,
}
impl ArchiveWrite for EmptyBrokenWrite {
fn add(&mut self, meta: &EntryMeta, data: &mut dyn Read) -> Result<()> {
self.count += 1;
self.inner.add(meta, data)
}
fn finish(self: Box<Self>) -> Result<Box<dyn Sink>> {
if self.count == 0 {
return Ok(PlainSink::new(Box::new(std::io::sink())));
}
self.inner.finish()
}
}
impl Container for EmptyBroken {
fn id(&self) -> FormatId {
FramedMockContainer.id()
}
fn caps(&self) -> ContainerCaps {
FramedMockContainer.caps()
}
fn open(&self, resolved: Resolved, o: &OpenOpts) -> Result<Box<dyn ArchiveRead>> {
FramedMockContainer.open(resolved, o)
}
fn create(&self, dst: Box<dyn Sink>, o: &CreateOpts) -> Result<Box<dyn ArchiveWrite>> {
Ok(Box::new(EmptyBrokenWrite {
inner: FramedMockContainer.create(dst, o)?,
count: 0,
}))
}
}
#[test]
fn property_three_catches_a_container_that_breaks_on_zero_entries() {
assert_panics_naming(&EmptyBroken, &framed_container_meta(), "property 3");
}
struct SwallowsError;
struct SwallowsErrorWrite {
inner: Box<dyn ArchiveWrite>,
}
impl ArchiveWrite for SwallowsErrorWrite {
fn add(&mut self, meta: &EntryMeta, data: &mut dyn Read) -> Result<()> {
let _ = self.inner.add(meta, data);
Ok(())
}
fn finish(self: Box<Self>) -> Result<Box<dyn Sink>> {
let _ = self.inner.finish();
Ok(PlainSink::new(Box::new(std::io::sink())))
}
}
impl Container for SwallowsError {
fn id(&self) -> FormatId {
FramedMockContainer.id()
}
fn caps(&self) -> ContainerCaps {
FramedMockContainer.caps()
}
fn open(&self, resolved: Resolved, o: &OpenOpts) -> Result<Box<dyn ArchiveRead>> {
FramedMockContainer.open(resolved, o)
}
fn create(&self, dst: Box<dyn Sink>, o: &CreateOpts) -> Result<Box<dyn ArchiveWrite>> {
Ok(Box::new(SwallowsErrorWrite {
inner: FramedMockContainer.create(dst, o)?,
}))
}
}
#[test]
fn property_four_catches_finish_swallowing_a_write_error() {
assert_panics_naming(&SwallowsError, &framed_container_meta(), "property 4");
}
struct FakesSeek;
struct FakesSeekRead {
inner: Box<dyn ArchiveRead>,
}
impl ArchiveRead for FakesSeekRead {
fn next_entry(&mut self) -> Result<Option<Entry<'_>>> {
self.inner.next_entry()
}
fn by_index(&mut self, _index: usize) -> Result<Entry<'_>> {
self.inner
.next_entry()?
.ok_or_else(|| crate::error::Error::EntryNotFound("0".into()))
}
fn fidelity(&self) -> &FidelityReport {
self.inner.fidelity()
}
}
impl Container for FakesSeek {
fn id(&self) -> FormatId {
FramedMockContainer.id()
}
fn caps(&self) -> ContainerCaps {
FramedMockContainer.caps()
}
fn open(&self, resolved: Resolved, o: &OpenOpts) -> Result<Box<dyn ArchiveRead>> {
Ok(Box::new(FakesSeekRead {
inner: FramedMockContainer.open(resolved, o)?,
}))
}
fn create(&self, dst: Box<dyn Sink>, o: &CreateOpts) -> Result<Box<dyn ArchiveWrite>> {
FramedMockContainer.create(dst, o)
}
}
#[test]
fn property_six_catches_by_index_faking_random_access() {
assert_panics_naming(&FakesSeek, &framed_container_meta(), "property 6");
}
struct ClaimsExact;
struct ClaimsExactRead {
inner: Box<dyn ArchiveRead>,
report: FidelityReport,
}
impl ArchiveRead for ClaimsExactRead {
fn next_entry(&mut self) -> Result<Option<Entry<'_>>> {
self.inner.next_entry()
}
fn by_index(&mut self, index: usize) -> Result<Entry<'_>> {
self.inner.by_index(index)
}
fn fidelity(&self) -> &FidelityReport {
&self.report
}
}
impl Container for ClaimsExact {
fn id(&self) -> FormatId {
FramedMockContainer.id()
}
fn caps(&self) -> ContainerCaps {
ContainerCaps {
trailing_index: true,
..FramedMockContainer.caps()
}
}
fn open(&self, resolved: Resolved, o: &OpenOpts) -> Result<Box<dyn ArchiveRead>> {
let inner = FramedMockContainer.open(resolved, o)?;
Ok(Box::new(ClaimsExactRead {
inner,
report: FidelityReport::exact(),
}))
}
fn create(&self, dst: Box<dyn Sink>, o: &CreateOpts) -> Result<Box<dyn ArchiveWrite>> {
FramedMockContainer.create(dst, o)
}
}
#[test]
fn property_seven_catches_a_trailing_index_container_claiming_exact() {
assert_panics_naming(&ClaimsExact, &framed_container_meta(), "property 7");
}
struct ReadsToEnd;
impl Container for ReadsToEnd {
fn id(&self) -> FormatId {
FramedMockContainer.id()
}
fn caps(&self) -> ContainerCaps {
FramedMockContainer.caps()
}
fn open(&self, resolved: Resolved, o: &OpenOpts) -> Result<Box<dyn ArchiveRead>> {
let Resolved {
mut source,
rung,
report,
} = resolved;
let mut buf = Vec::new();
source.read_to_end(&mut buf)?;
let refilled: Box<dyn crate::source::Source> =
Box::new(crate::source::ReaderSource::new(std::io::Cursor::new(buf)));
FramedMockContainer.open(
Resolved {
source: refilled,
rung,
report,
},
o,
)
}
fn create(&self, dst: Box<dyn Sink>, o: &CreateOpts) -> Result<Box<dyn ArchiveWrite>> {
FramedMockContainer.create(dst, o)
}
}
#[test]
fn property_eight_catches_a_container_that_buffers_the_whole_archive() {
assert_panics_naming(&ReadsToEnd, &framed_container_meta(), "property 8");
}
struct AcceptsTruncation;
struct AcceptsTruncationRead {
inner: Box<dyn ArchiveRead>,
}
impl ArchiveRead for AcceptsTruncationRead {
fn next_entry(&mut self) -> Result<Option<Entry<'_>>> {
Ok(self.inner.next_entry().unwrap_or(None))
}
fn by_index(&mut self, index: usize) -> Result<Entry<'_>> {
self.inner.by_index(index)
}
fn fidelity(&self) -> &FidelityReport {
self.inner.fidelity()
}
}
impl Container for AcceptsTruncation {
fn id(&self) -> FormatId {
FramedMockContainer.id()
}
fn caps(&self) -> ContainerCaps {
FramedMockContainer.caps()
}
fn open(&self, resolved: Resolved, o: &OpenOpts) -> Result<Box<dyn ArchiveRead>> {
Ok(Box::new(AcceptsTruncationRead {
inner: FramedMockContainer.open(resolved, o)?,
}))
}
fn create(&self, dst: Box<dyn Sink>, o: &CreateOpts) -> Result<Box<dyn ArchiveWrite>> {
FramedMockContainer.create(dst, o)
}
}
#[test]
fn property_nine_catches_silent_acceptance_of_a_truncated_archive() {
assert_panics_naming(&AcceptsTruncation, &framed_container_meta(), "property 9");
}
struct SwallowsPayloadTruncation;
struct SwallowsPayloadTruncationRead {
inner: Box<dyn ArchiveRead>,
done: Arc<std::sync::atomic::AtomicBool>,
}
struct PaddingReader<'a> {
entry: Entry<'a>,
remaining: u64,
done: Arc<std::sync::atomic::AtomicBool>,
}
impl Read for PaddingReader<'_> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let n = self.entry.reader().read(buf)?;
if n == 0 && self.remaining > 0 {
self.done.store(true, Ordering::Relaxed);
}
self.remaining = self.remaining.saturating_sub(n as u64);
Ok(n)
}
}
impl ArchiveRead for SwallowsPayloadTruncationRead {
fn next_entry(&mut self) -> Result<Option<Entry<'_>>> {
if self.done.load(Ordering::Relaxed) {
return Ok(None);
}
let Some(entry) = self.inner.next_entry()? else {
return Ok(None);
};
let meta = entry.meta().clone();
let remaining = meta.size.unwrap_or(0);
let reader = PaddingReader {
entry,
remaining,
done: Arc::clone(&self.done),
};
Ok(Some(Entry::new(meta, Box::new(reader))))
}
fn by_index(&mut self, index: usize) -> Result<Entry<'_>> {
self.inner.by_index(index)
}
fn fidelity(&self) -> &FidelityReport {
self.inner.fidelity()
}
}
impl Container for SwallowsPayloadTruncation {
fn id(&self) -> FormatId {
FramedMockContainer.id()
}
fn caps(&self) -> ContainerCaps {
FramedMockContainer.caps()
}
fn open(&self, resolved: Resolved, o: &OpenOpts) -> Result<Box<dyn ArchiveRead>> {
Ok(Box::new(SwallowsPayloadTruncationRead {
inner: FramedMockContainer.open(resolved, o)?,
done: Arc::new(std::sync::atomic::AtomicBool::new(false)),
}))
}
fn create(&self, dst: Box<dyn Sink>, o: &CreateOpts) -> Result<Box<dyn ArchiveWrite>> {
FramedMockContainer.create(dst, o)
}
}
#[test]
fn property_nine_catches_silent_acceptance_of_a_cut_inside_an_entry_payload() {
assert_panics_naming(
&SwallowsPayloadTruncation,
&framed_container_meta(),
"property 9 (payload)",
);
}
struct MislabelsSourceErrors;
struct MislabelsSourceErrorsRead {
inner: Box<dyn ArchiveRead>,
}
impl ArchiveRead for MislabelsSourceErrorsRead {
fn next_entry(&mut self) -> Result<Option<Entry<'_>>> {
self.inner
.next_entry()
.map_err(|e| crate::error::Error::Corrupt(e.to_string()))
}
fn by_index(&mut self, index: usize) -> Result<Entry<'_>> {
self.inner.by_index(index)
}
fn fidelity(&self) -> &FidelityReport {
self.inner.fidelity()
}
}
impl Container for MislabelsSourceErrors {
fn id(&self) -> FormatId {
FramedMockContainer.id()
}
fn caps(&self) -> ContainerCaps {
FramedMockContainer.caps()
}
fn open(&self, resolved: Resolved, o: &OpenOpts) -> Result<Box<dyn ArchiveRead>> {
Ok(Box::new(MislabelsSourceErrorsRead {
inner: FramedMockContainer.open(resolved, o)?,
}))
}
fn create(&self, dst: Box<dyn Sink>, o: &CreateOpts) -> Result<Box<dyn ArchiveWrite>> {
FramedMockContainer.create(dst, o)
}
}
#[test]
fn property_ten_catches_a_source_error_reported_as_corruption() {
assert_panics_naming(
&MislabelsSourceErrors,
&framed_container_meta(),
"property 10",
);
}
struct Sanitises;
struct SanitisesWrite {
inner: Box<dyn ArchiveWrite>,
}
impl ArchiveWrite for SanitisesWrite {
fn add(&mut self, meta: &EntryMeta, data: &mut dyn Read) -> Result<()> {
let mut sanitised = meta.clone();
sanitised.name = sanitised
.name
.replace("..", "")
.trim_start_matches('/')
.to_string();
self.inner.add(&sanitised, data)
}
fn finish(self: Box<Self>) -> Result<Box<dyn Sink>> {
self.inner.finish()
}
}
impl Container for Sanitises {
fn id(&self) -> FormatId {
FramedMockContainer.id()
}
fn caps(&self) -> ContainerCaps {
FramedMockContainer.caps()
}
fn open(&self, resolved: Resolved, o: &OpenOpts) -> Result<Box<dyn ArchiveRead>> {
FramedMockContainer.open(resolved, o)
}
fn create(&self, dst: Box<dyn Sink>, o: &CreateOpts) -> Result<Box<dyn ArchiveWrite>> {
Ok(Box::new(SanitisesWrite {
inner: FramedMockContainer.create(dst, o)?,
}))
}
}
#[test]
fn property_twelve_catches_a_container_that_sanitises_hostile_names() {
assert_panics_naming(&Sanitises, &framed_container_meta(), "property 12");
}
struct ClaimsDirs;
impl Container for ClaimsDirs {
fn id(&self) -> FormatId {
FramedMockContainer.id()
}
fn caps(&self) -> ContainerCaps {
ContainerCaps {
stores_dirs: true,
..FramedMockContainer.caps()
}
}
fn open(&self, resolved: Resolved, o: &OpenOpts) -> Result<Box<dyn ArchiveRead>> {
FramedMockContainer.open(resolved, o)
}
fn create(&self, dst: Box<dyn Sink>, o: &CreateOpts) -> Result<Box<dyn ArchiveWrite>> {
FramedMockContainer.create(dst, o)
}
}
#[test]
fn property_thirteen_catches_a_false_stores_dirs_claim() {
assert_panics_naming(
&ClaimsDirs,
&framed_container_meta(),
"property 13 (directories)",
);
}
struct ClaimsSymlinks;
impl Container for ClaimsSymlinks {
fn id(&self) -> FormatId {
FramedMockContainer.id()
}
fn caps(&self) -> ContainerCaps {
ContainerCaps {
stores_symlinks: true,
..FramedMockContainer.caps()
}
}
fn open(&self, resolved: Resolved, o: &OpenOpts) -> Result<Box<dyn ArchiveRead>> {
FramedMockContainer.open(resolved, o)
}
fn create(&self, dst: Box<dyn Sink>, o: &CreateOpts) -> Result<Box<dyn ArchiveWrite>> {
FramedMockContainer.create(dst, o)
}
}
#[test]
fn property_thirteen_catches_a_false_stores_symlinks_claim() {
assert_panics_naming(
&ClaimsSymlinks,
&framed_container_meta(),
"property 13 (symlinks)",
);
}
struct ModeOverlayRead {
inner: Box<dyn ArchiveRead>,
overlay: fn(u32) -> u32,
}
impl ArchiveRead for ModeOverlayRead {
fn next_entry(&mut self) -> Result<Option<Entry<'_>>> {
let overlay = self.overlay;
let Some(entry) = self.inner.next_entry()? else {
return Ok(None);
};
let mut meta = entry.meta().clone();
meta.mode = Some(overlay(meta.mode.unwrap_or(0o640)));
Ok(Some(Entry::new(meta, entry.into_reader())))
}
fn by_index(&mut self, index: usize) -> Result<Entry<'_>> {
self.inner.by_index(index)
}
fn fidelity(&self) -> &FidelityReport {
self.inner.fidelity()
}
}
struct ModeOverlay(fn(u32) -> u32);
impl Container for ModeOverlay {
fn id(&self) -> FormatId {
FramedMockContainer.id()
}
fn caps(&self) -> ContainerCaps {
FramedMockContainer.caps()
}
fn open(&self, resolved: Resolved, o: &OpenOpts) -> Result<Box<dyn ArchiveRead>> {
Ok(Box::new(ModeOverlayRead {
inner: FramedMockContainer.open(resolved, o)?,
overlay: self.0,
}))
}
fn create(&self, dst: Box<dyn Sink>, o: &CreateOpts) -> Result<Box<dyn ArchiveWrite>> {
FramedMockContainer.create(dst, o)
}
}
#[test]
fn property_eleven_catches_a_container_that_alters_the_permission_bits() {
assert_panics_naming(
&ModeOverlay(|mode| (mode & !0o007) | 0o004),
&framed_container_meta(),
"property 11",
);
}
#[test]
fn property_eleven_catches_a_container_that_relabels_the_entrys_kind() {
assert_panics_naming(
&ModeOverlay(|mode| (mode & !0o170_000) | 0o040_000),
&framed_container_meta(),
"property 11",
);
}
}