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::{ContainerCaps, FormatId, FormatMeta};
use crate::honesty::check_error_is_classified;
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 crc16_arc_witness(data: &[u8]) -> u16 {
let mut crc: u16 = 0;
for &b in data {
crc ^= b as u16;
for _ in 0..8 {
crc = if crc & 1 != 0 {
(crc >> 1) ^ 0xA001
} else {
crc >> 1
};
}
}
crc
}
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)
}
fn reachable_forward_only(id: FormatId, caps: ContainerCaps) -> bool {
let src: Box<dyn Source> = Box::new(ReaderSource::new(io::Cursor::new(Vec::new())));
match crate::resolve(src, id, caps, &crate::StreamPolicy::ForwardOnly) {
Ok(resolved) => {
assert_eq!(
resolved.rung,
Rung::ForwardOnly,
"conformance[{id}]: StreamPolicy::ForwardOnly resolved to {:?}, not \
Rung::ForwardOnly — this harness reads that policy as \"rung 2 or nothing\", \
and every forward-only property's setup depends on it",
resolved.rung
);
true
}
Err(crate::error::Error::NotSeekable { .. }) => false,
Err(e) => panic!(
"conformance[{id}]: resolving under StreamPolicy::ForwardOnly failed with {e:?}, \
which is neither rung 2 nor the NotSeekable refusal this harness knows how to \
read"
),
}
}
fn why_not_forward_only(caps: ContainerCaps) -> String {
match (caps.forward_parse, caps.needs_seek) {
(false, true) => "this container declares needs_seek and not forward_parse, so the \
ladder never supplies it a forward-only source"
.to_string(),
(false, false) => "this container does not declare forward_parse, so the ladder's \
forward-only rung is not open to it"
.to_string(),
(true, true) => "this container declares needs_seek, which closes the ladder's \
forward-only rung even though it also claims forward_parse"
.to_string(),
(true, false) => "the ladder refused a forward-only source for a reason this \
explanation does not model — see crate::ladder::resolve"
.to_string(),
}
}
fn open_under_the_full_ladder(
container: &dyn Container,
bytes: &[u8],
) -> (Rung, 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::default())
.unwrap_or_else(|e| panic!("conformance[{id}] resolve (full ladder): {e}"));
let rung = resolved.rung;
let ar = container
.open(resolved, &OpenOpts::default())
.unwrap_or_else(|e| panic!("conformance[{id}] open (full ladder): {e}"));
(rung, ar)
}
struct PropertyLedger {
label: &'static str,
highest: u8,
ran: Vec<u8>,
skipped: Vec<u8>,
}
impl PropertyLedger {
fn write_capable() -> Self {
Self {
label: "property",
highest: 13,
ran: Vec::new(),
skipped: Vec::new(),
}
}
fn fixture_driven() -> Self {
Self {
label: "fixture property",
highest: 10,
ran: Vec::new(),
skipped: Vec::new(),
}
}
fn ran(&mut self, n: u8) {
self.ran.push(n);
}
fn skipped(&mut self, id: FormatId, n: u8, why: &str) {
eprintln!("conformance[{id}] {} {n}: skipped — {why}", self.label);
self.skipped.push(n);
}
fn record(&mut self, id: FormatId, n: u8, did: bool, why: &str) {
if did {
self.ran(n);
} else {
self.skipped(id, n, why);
}
}
fn assert_run_set(&self, id: FormatId, expected_skipped: &[u8]) {
let sorted = |v: &[u8]| {
let mut v = v.to_vec();
v.sort_unstable();
v.dedup();
v
};
let ran = sorted(&self.ran);
let skipped = sorted(&self.skipped);
let want = sorted(expected_skipped);
let both: Vec<u8> = ran
.iter()
.copied()
.filter(|n| skipped.contains(n))
.collect();
let label = self.label;
assert!(
both.is_empty(),
"conformance[{id}] {label} run-set: {both:?} recorded as BOTH run and skipped, \
so the ledger describes neither"
);
assert_eq!(
skipped, want,
"conformance[{id}] {label} run-set: this container skipped {skipped:?}, but its \
own test says it should skip exactly {want:?}. A property that stops running is \
invisible otherwise — the whole suite stays green while the check is gone. Either \
the harness stopped running something it used to, or this container's expected \
skip list needs updating with a reason a reviewer can read"
);
let mut seen = ran;
seen.extend_from_slice(&skipped);
seen.sort_unstable();
seen.dedup();
let all: Vec<u8> = (1u8..=self.highest).collect();
assert_eq!(
seen,
all,
"conformance[{id}] {label} run-set: {:?} are accounted for as neither run nor \
skipped. Every one of 1..={} must record itself, or a deleted block leaves no \
trace at all",
all.iter().filter(|n| !seen.contains(n)).collect::<Vec<_>>(),
self.highest
);
}
}
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 src: Box<dyn Source> = Box::new(FailingSource);
let resolved = match crate::resolve(
src,
container.id(),
container.caps(),
&crate::StreamPolicy::default(),
) {
Ok(r) => r,
Err(e) => return Some(classify_container_error(e)),
};
let mut ar = match container.open(resolved, &OpenOpts::default()) {
Ok(ar) => ar,
Err(e) => return Some(classify_container_error(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
}
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct ExpectedEntry {
pub name: &'static str,
pub content: &'static [u8],
pub stored_crc: Option<u16>,
}
impl ExpectedEntry {
pub const fn new(name: &'static str, content: &'static [u8]) -> Self {
Self {
name,
content,
stored_crc: None,
}
}
pub const fn with_crc(name: &'static str, content: &'static [u8], stored_crc: u16) -> Self {
Self {
name,
content,
stored_crc: Some(stored_crc),
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct ContainerFixture {
pub bytes: &'static [u8],
pub expected: &'static [ExpectedEntry],
pub provenance: &'static str,
}
impl ContainerFixture {
pub const fn new(
bytes: &'static [u8],
expected: &'static [ExpectedEntry],
provenance: &'static str,
) -> Self {
Self {
bytes,
expected,
provenance,
}
}
}
pub fn assert_container_conforms_with(
container: &dyn Container,
meta: &FormatMeta,
fixture: &ContainerFixture,
) {
assert_container_conforms_with_skipping(container, meta, fixture, &[]);
}
pub fn assert_container_conforms_with_skipping(
container: &dyn Container,
meta: &FormatMeta,
fixture: &ContainerFixture,
expected_skipped: &[u8],
) {
let id = container.id();
let caps = container.caps();
let provenance = fixture.provenance;
let mut ledger = PropertyLedger::fixture_driven();
assert_eq!(
id, meta.id,
"conformance[{id}] fixture property 1: Container::id() disagrees with its registered \
FormatMeta (fixture provenance: {provenance})"
);
ledger.ran(1);
let mut classified_an_error = false;
assert!(
caps.read,
"conformance[{id}] fixture property 2: assert_container_conforms_with requires caps.read \
(fixture provenance: {provenance})"
);
ledger.record(
id,
2,
!caps.write,
"this container declares write: true, so it has no read-only refusal to prove",
);
if !caps.write {
let result = container.create(
PlainSink::new(Box::new(CaptureWriter::new())),
&CreateOpts::default(),
);
match result {
Ok(_) => panic!(
"conformance[{id}] fixture property 2: caps.write is false but create() succeeded — \
a read-only container must refuse to write, not silently accept \
(fixture provenance: {provenance})"
),
Err(e) => {
if let Err(msg) = check_error_is_classified(&e) {
panic!(
"conformance[{id}] fixture property 8: create()'s read-only refusal is not \
classified: {msg} (fixture provenance: {provenance})"
);
}
classified_an_error = true;
}
}
}
if !meta.magics.is_empty() {
let hit = meta.magics.iter().any(|r| {
fixture.bytes.len() >= r.offset + r.bytes.len()
&& &fixture.bytes[r.offset..r.offset + r.bytes.len()] == r.bytes
});
assert!(
hit,
"conformance[{id}] fixture property 3: no registered magic rule matches the fixture's \
bytes (fixture provenance: {provenance})"
);
ledger.ran(3);
} else {
ledger.skipped(
id,
3,
"this format registers no magic rules to match against",
);
}
let got = read_all(container, fixture.bytes).unwrap_or_else(|e| {
panic!(
"conformance[{id}] fixture property 4: failed to read the fixture back: {e} \
(fixture provenance: {provenance})"
)
});
let mut witnessed = 0usize;
match caps.detects_corruption {
crate::format::CorruptionDetection::Always
| crate::format::CorruptionDetection::WhenPresent => {
for want in fixture.expected {
let Some(stored_crc) = want.stored_crc else {
eprintln!(
"conformance[{id}] fixture property 10: skipped for entry {:?} — this \
fixture records no archive-stored CRC to witness against (fixture \
provenance: {provenance})",
want.name
);
continue;
};
if let Some((_, got_bytes)) = got.iter().find(|(name, _)| name == want.name) {
let got_crc = crc16_arc_witness(got_bytes);
assert_eq!(
got_crc, stored_crc,
"conformance[{id}] fixture property 10: decoded bytes for entry {:?} \
differ from the CRC the archive stores — CRC-16/ARC {got_crc:#06x} \
decoded vs {stored_crc:#06x} recorded in the archive's own header, which \
is the archive's own encoder checking this decoder from outside the crate \
(fixture provenance: {provenance})",
want.name
);
witnessed += 1;
}
}
ledger.record(
id,
10,
witnessed > 0,
"no entry in this fixture's manifest records an archive-stored CRC",
);
}
crate::format::CorruptionDetection::Structural
| crate::format::CorruptionDetection::Never => {
ledger.skipped(
id,
10,
"this container declares neither Always nor WhenPresent corruption detection, \
so no per-entry CRC exists to witness against",
);
}
}
let got_names: Vec<&str> = got.iter().map(|(name, _)| name.as_str()).collect();
let want_names: Vec<&str> = fixture.expected.iter().map(|e| e.name).collect();
assert_eq!(
got_names, want_names,
"conformance[{id}] fixture property 4: entry names/order disagree with the fixture's \
manifest (fixture provenance: {provenance})"
);
ledger.ran(4);
let mut compared = 0usize;
for (got_entry, want_entry) in got.iter().zip(fixture.expected.iter()) {
assert_eq!(
&got_entry.1[..],
want_entry.content,
"conformance[{id}] fixture property 5: content mismatch for entry {:?} \
(fixture provenance: {provenance})",
want_entry.name
);
compared += 1;
}
ledger.record(
id,
5,
compared > 0,
"this fixture's manifest declares no entries, so there is no content to compare",
);
let cut = fixture.bytes.len() / 2;
if cut > 0 {
match read_all(container, &fixture.bytes[..cut]) {
Err(e) => {
if let Err(msg) = check_error_is_classified(&e) {
panic!(
"conformance[{id}] fixture property 8: truncation error is not classified: \
{msg} (fixture provenance: {provenance})"
);
}
classified_an_error = true;
}
Ok(entries) => {
assert!(
entries.len() < fixture.expected.len(),
"conformance[{id}] fixture property 6: a fixture truncated to {cut} of {} bytes \
was accepted silently, returning all {} expected entries \
(fixture provenance: {provenance})",
fixture.bytes.len(),
fixture.expected.len(),
);
}
}
ledger.ran(6);
} else {
ledger.skipped(
id,
6,
"the fixture is empty, so there is nothing to truncate",
);
}
if caps.detects_corruption == crate::format::CorruptionDetection::Never {
ledger.skipped(
id,
7,
"this container declares CorruptionDetection::Never, so no check exists to prove",
);
} else if fixture.bytes.is_empty() {
ledger.skipped(id, 7, "the fixture is empty, so there is no byte to flip");
} else {
let mut corrupted = fixture.bytes.to_vec();
let mid = corrupted.len() / 2;
corrupted[mid] ^= 0xFF;
match read_all(container, &corrupted) {
Err(e) => {
if let Err(msg) = check_error_is_classified(&e) {
panic!(
"conformance[{id}] fixture property 8: corruption error is not classified: \
{msg} (fixture provenance: {provenance})"
);
}
classified_an_error = true;
}
Ok(entries) => {
let unchanged = entries.len() == fixture.expected.len()
&& entries
.iter()
.zip(fixture.expected.iter())
.all(|(g, w)| g.0 == w.name && g.1 == w.content);
assert!(
!unchanged,
"conformance[{id}] fixture property 7: a corrupted fixture (middle byte flipped \
at offset {mid} of {}) read back byte-identical to the uncorrupted \
expectation — corruption went completely undetected \
(fixture provenance: {provenance})",
fixture.bytes.len(),
);
}
}
ledger.ran(7);
}
{
let e = read_all_over_failing_source(container).unwrap_or_else(|| {
panic!(
"conformance[{id}] fixture property 9: a source that fails every read produced no \
error at all — the failure went completely unnoticed \
(fixture provenance: {provenance})"
)
});
assert_eq!(
e.kind(),
io::ErrorKind::PermissionDenied,
"conformance[{id}] fixture property 9: a source error surfaced as {:?} rather than \
passing through — a disk failure must not be reported as corruption \
(fixture provenance: {provenance})",
e.kind()
);
ledger.ran(9);
}
ledger.record(
id,
8,
classified_an_error,
"no refusal path (read-only create, truncation, corruption) produced an error to classify",
);
ledger.assert_run_set(id, expected_skipped);
}
pub fn assert_container_conforms(container: &dyn Container, meta: &FormatMeta) {
assert_container_conforms_skipping(container, meta, &[]);
}
pub fn assert_container_conforms_skipping(
container: &dyn Container,
meta: &FormatMeta,
expected_skipped: &[u8],
) {
let id = container.id();
let caps = container.caps();
let mut ledger = PropertyLedger::write_capable();
assert_eq!(
id, meta.id,
"conformance[{id}] property 1: Container::id() disagrees with its registered FormatMeta"
);
ledger.ran(1);
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"
);
ledger.ran(2);
} else {
ledger.skipped(id, 2, "this format registers no magic rule to agree with");
}
let mut round_trips = 0usize;
for entries in [
&[][..],
&[("a.txt", &b"alpha"[..])][..],
&[("a.txt", &b"alpha"[..]), ("b/c.bin", &b"\x00\xff\x00"[..])][..],
] {
round_trips += 1;
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}"
);
}
}
ledger.record(
id,
3,
round_trips == 3,
"the round-trip loop ran no shape at all",
);
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"
);
ledger.ran(4);
} else {
for n in [2u8, 3, 4] {
ledger.skipped(
id,
n,
"this container does not declare both read and write, so there is no round \
trip to make",
);
}
}
if caps.read {
let bytes = build(
container,
&[("a.txt", &b"alpha"[..]), ("b.txt", &b"beta"[..])],
);
let forward_only = reachable_forward_only(id, caps);
if forward_only {
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()
);
} else {
let (rung, mut ar) = open_under_the_full_ladder(container, &bytes);
assert_eq!(
rung,
Rung::Spilled,
"conformance[{id}] property 5 (spilled): {}, so the full ladder must have \
SPOOLED this pipe; it reported {rung:?} instead",
why_not_forward_only(caps)
);
let reported = ar.fidelity().rung;
assert_eq!(
reported, rung,
"conformance[{id}] property 5 (spilled): the ladder reached {rung:?} but the \
container reports {reported:?}. A read that cost a temp file must not be \
presented as one that did not"
);
assert!(
reported.is_authoritative(),
"conformance[{id}] property 5 (spilled): a spooled read is authoritative — \
{reported:?} says otherwise, which would warn a user about a loss that did \
not happen"
);
let mut got = Vec::new();
while let Some(mut entry) = ar.next_entry().unwrap_or_else(|e| {
panic!("conformance[{id}] property 5 (spilled): next_entry: {e}")
}) {
got.push(entry.meta().name.clone());
let mut data = Vec::new();
entry.reader().read_to_end(&mut data).unwrap_or_else(|e| {
panic!("conformance[{id}] property 5 (spilled): entry read: {e}")
});
}
assert_eq!(
got,
vec!["a.txt".to_string(), "b.txt".to_string()],
"conformance[{id}] property 5 (spilled): a spooled pipe must still yield every \
entry, in order"
);
}
ledger.ran(5);
if forward_only {
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:?}"
),
}
} else {
let (_, mut ar) = open_under_the_full_ladder(container, &bytes);
match ar.by_index(0) {
Ok(entry) => assert_eq!(
entry.meta().name,
"a.txt",
"conformance[{id}] property 6 (spilled): by_index(0) returned the WRONG \
entry. A wrong entry is worse than a refusal — `cat --index 0` would \
print another file's bytes and exit 0"
),
Err(e) => {
if let Err(msg) = check_error_is_classified(&e) {
panic!(
"conformance[{id}] property 6 (spilled): by_index()'s refusal is \
not classified: {msg}"
);
}
assert!(
matches!(
e,
crate::error::Error::Unsupported(_)
| crate::error::Error::NotSeekable { .. }
),
"conformance[{id}] property 6 (spilled): by_index() must answer with \
the entry, `Unsupported` (this format has no index) or `NotSeekable` \
(this source cannot seek) — `entries.rs` routes those two to a \
counted walk and anything else straight to the caller. Got {e:?}"
);
}
}
}
ledger.ran(6);
if caps.trailing_index && forward_only {
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"
);
ledger.ran(7);
} else if !caps.trailing_index {
ledger.skipped(
id,
7,
"this container declares no trailing_index, so there is no end-of-stream \
index for a forward read to leave unconsulted",
);
} else {
ledger.skipped(
id,
7,
&format!(
"{}, so there is no forward read for the trailing index to go \
unconsulted by",
why_not_forward_only(caps)
),
);
}
if forward_only {
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()
);
ledger.ran(8);
} else {
ledger.skipped(
id,
8,
&format!(
"{}. The ladder SPOOLS such a source, so the whole archive is read \
before the container sees a byte of it and this measurement would \
describe the ladder, not the container",
why_not_forward_only(caps)
),
);
}
let mut framing_cuts = 0usize;
for cut in [1usize, bytes.len() / 3, bytes.len() / 2, bytes.len() - 1] {
if cut == 0 || cut >= bytes.len() {
continue;
}
framing_cuts += 1;
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()
);
}
let mut payload_cut_ran = false;
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()
);
payload_cut_ran = true;
}
let nine_ran = framing_cuts > 0 && (!caps.write || payload_cut_ran);
ledger.record(
id,
9,
nine_ran,
"the truncation sweep checked no cut at all, or checked framing and not payload — \
see PropertyLedger's own doc for why this is the property that must never stop \
running",
);
{
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()
);
ledger.ran(10);
}
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"
);
}
ledger.ran(11);
} else {
ledger.skipped(
id,
11,
"this container cannot write, so there is no entry of ours to read metadata \
back from",
);
}
if caps.write {
let hostile = ["../../etc/passwd", "/abs/path", "a/../../b"];
let mut names_checked = 0usize;
for name in hostile {
names_checked += 1;
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
);
}
ledger.record(
id,
12,
names_checked > 0,
"the hostile-name loop checked no name",
);
} else {
ledger.skipped(
id,
12,
"this container cannot write, so there is no hostile name of ours to store \
and read back",
);
}
if caps.write && (caps.stores_dirs || caps.stores_symlinks) {
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"
),
}
}
ledger.ran(13);
} else if !caps.write {
ledger.skipped(
id,
13,
"this container cannot write, so there is no directory or symlink entry of \
ours to read back",
);
} else {
ledger.skipped(
id,
13,
"this container claims neither stores_dirs nor stores_symlinks, so it is \
asked for neither",
);
}
} else {
for n in 5u8..=13 {
ledger.skipped(id, n, "this container does not declare read");
}
}
ledger.assert_run_set(id, expected_skipped);
}
#[cfg(test)]
struct SeekOnlyMockContainer;
#[cfg(test)]
impl Container for SeekOnlyMockContainer {
fn id(&self) -> FormatId {
crate::testing::FramedMockContainer.id()
}
fn caps(&self) -> ContainerCaps {
ContainerCaps {
needs_seek: true,
..ContainerCaps::read_write()
}
}
fn open(
&self,
resolved: crate::ladder::Resolved,
o: &OpenOpts,
) -> Result<Box<dyn ArchiveRead>> {
crate::testing::FramedMockContainer.open(resolved, o)
}
fn create(
&self,
dst: Box<dyn crate::archive::Sink>,
o: &CreateOpts,
) -> Result<Box<dyn crate::archive::ArchiveWrite>> {
crate::testing::FramedMockContainer.create(dst, o)
}
}
#[cfg(test)]
fn assert_panics_naming(container: &dyn Container, meta: &FormatMeta, expected: &str) {
assert_panics_naming_skipping(container, meta, &[7, 13], expected);
}
#[cfg(test)]
fn assert_panics_naming_skipping(
container: &dyn Container,
meta: &FormatMeta,
expected_skipped: &[u8],
expected: &str,
) {
let id = container.id();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
assert_container_conforms_skipping(container, meta, expected_skipped);
}));
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)]
fn assert_panics_naming_with(
container: &dyn Container,
meta: &FormatMeta,
fixture: &ContainerFixture,
expected: &str,
) {
assert_panics_naming_with_skipping(container, meta, fixture, &[], expected)
}
#[cfg(test)]
fn assert_panics_naming_with_skipping(
container: &dyn Container,
meta: &FormatMeta,
fixture: &ContainerFixture,
expected_skipped: &[u8],
expected: &str,
) {
let id = container.id();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
assert_container_conforms_with_skipping(container, meta, fixture, expected_skipped);
}));
match result {
Ok(()) => panic!(
"conformance[{id}]: expected assert_container_conforms_with 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_skipping(
&FramedMockContainer,
&framed_container_meta(),
&[7, 13],
);
}
#[test]
fn a_seek_only_container_runs_the_harness_to_completion() {
assert_container_conforms_skipping(
&SeekOnlyMockContainer,
&framed_container_meta(),
&[7, 8, 13],
);
}
#[test]
fn the_forward_only_gate_reads_both_halves_of_the_ladder_rung() {
let id = FramedMockContainer.id();
let fwd = ContainerCaps {
forward_parse: true,
..ContainerCaps::read_only()
};
assert!(reachable_forward_only(id, fwd));
assert!(!reachable_forward_only(id, ContainerCaps::read_only()));
assert!(!reachable_forward_only(
id,
ContainerCaps {
needs_seek: true,
..fwd
}
));
assert!(
why_not_forward_only(ContainerCaps {
needs_seek: true,
..fwd
})
.contains("even though it also claims forward_parse"),
"a container claiming both must not be told it lacks forward_parse"
);
assert!(why_not_forward_only(ContainerCaps::read_only()).contains("does not declare"));
}
#[test]
fn crc16_arc_witness_matches_the_published_check_value() {
assert_eq!(crc16_arc_witness(b"123456789"), 0xBB3D);
}
}
#[cfg(test)]
mod broken_containers {
use super::*;
use crate::archive::{ArchiveRead, ArchiveWrite, Entry, Sink};
use crate::error::Error;
use crate::fidelity::FidelityReport;
use crate::format::{ContainerCaps, CorruptionDetection, FormatId, MagicRule};
use crate::ladder::Resolved;
use crate::testing::{FramedMockContainer, framed_container_meta};
use std::io::Read;
const READ_ONLY_DOUBLE: FormatId = FormatId::new("read-only-double");
fn read_only_meta() -> FormatMeta {
FormatMeta {
id: READ_ONLY_DOUBLE,
kind: crate::format::FormatKind::Container,
magics: &[],
extensions: &[],
priority: 0,
}
}
fn read_only_double_caps() -> ContainerCaps {
ContainerCaps {
forward_parse: true,
detects_corruption: CorruptionDetection::Always,
..ContainerCaps::read_only()
}
}
fn read_only_meta_with_magics(magics: &'static [MagicRule]) -> FormatMeta {
FormatMeta {
magics,
..read_only_meta()
}
}
const FRAMED_FIXTURE_MAGIC: MagicRule = MagicRule {
offset: 0,
bytes: b"FE",
format: READ_ONLY_DOUBLE,
};
const ABSENT_MAGIC: MagicRule = MagicRule {
offset: 0,
bytes: b"NO",
format: READ_ONLY_DOUBLE,
};
struct ReadOnlyDouble;
impl Container for ReadOnlyDouble {
fn id(&self) -> FormatId {
READ_ONLY_DOUBLE
}
fn caps(&self) -> ContainerCaps {
ContainerCaps {
read: true,
write: false,
forward_parse: false,
needs_seek: true,
..Default::default()
}
}
fn open(&self, _resolved: Resolved, _o: &OpenOpts) -> Result<Box<dyn ArchiveRead>> {
Err(Error::Unsupported("read-only-double: no fixture".into()))
}
fn create(&self, _dst: Box<dyn Sink>, _o: &CreateOpts) -> Result<Box<dyn ArchiveWrite>> {
Err(Error::Unsupported("read-only-double cannot write".into()))
}
}
#[test]
#[should_panic(expected = "create")]
fn the_write_gated_entry_point_cannot_run_a_read_only_container() {
assert_container_conforms(&ReadOnlyDouble, &read_only_meta());
}
struct MockReadOnly {
fixture: ContainerFixture,
}
impl MockReadOnly {
fn new(fixture: &ContainerFixture) -> Self {
Self {
fixture: fixture.clone(),
}
}
fn fixture(&self) -> &ContainerFixture {
&self.fixture
}
}
impl Container for MockReadOnly {
fn id(&self) -> FormatId {
READ_ONLY_DOUBLE
}
fn caps(&self) -> ContainerCaps {
read_only_double_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>> {
Err(Error::Unsupported("mock-read-only cannot write".into()))
}
}
fn framed_fixture_bytes(entries: &[(&str, &[u8])]) -> &'static [u8] {
let mut out = Vec::new();
for (name, data) in entries {
out.extend_from_slice(b"FE");
out.extend_from_slice(&(name.len() as u32).to_le_bytes());
out.extend_from_slice(name.as_bytes());
out.extend_from_slice(&(data.len() as u64).to_le_bytes());
out.extend_from_slice(data);
}
out.extend_from_slice(b"FZ");
Box::leak(out.into_boxed_slice())
}
#[test]
fn the_fixture_entry_point_accepts_a_read_only_container() {
let fx = ContainerFixture {
bytes: framed_fixture_bytes(&[("a.txt", b"alpha")]),
expected: &[ExpectedEntry {
stored_crc: None,
name: "a.txt",
content: b"alpha",
}],
provenance: "hand-built in this test",
};
assert_container_conforms_with_skipping(
&MockReadOnly::new(&fx),
&read_only_meta(),
&fx,
&[3, 10],
);
}
#[test]
fn an_understated_fixture_skip_list_fails_the_run_set() {
let fx = ContainerFixture {
bytes: framed_fixture_bytes(&[("a.txt", b"alpha")]),
expected: &[ExpectedEntry {
stored_crc: None,
name: "a.txt",
content: b"alpha",
}],
provenance: "an_understated_fixture_skip_list_fails_the_run_set",
};
assert_panics_naming_with_skipping(
&MockReadOnly::new(&fx),
&read_only_meta(),
&fx,
&[],
"fixture property run-set",
);
}
#[test]
fn an_overstated_fixture_skip_list_fails_the_run_set() {
let fx = ContainerFixture {
bytes: framed_fixture_bytes(&[("a.txt", b"alpha")]),
expected: &[ExpectedEntry {
stored_crc: None,
name: "a.txt",
content: b"alpha",
}],
provenance: "an_overstated_fixture_skip_list_fails_the_run_set",
};
assert_panics_naming_with_skipping(
&MockReadOnly::new(&fx),
&read_only_meta(),
&fx,
&[3, 7, 10],
"fixture property run-set",
);
}
#[test]
fn mock_read_only_exposes_the_fixture_it_was_built_from() {
let fx = ContainerFixture {
bytes: framed_fixture_bytes(&[("a.txt", b"alpha")]),
expected: &[ExpectedEntry {
stored_crc: None,
name: "a.txt",
content: b"alpha",
}],
provenance: "mock_read_only_exposes_the_fixture_it_was_built_from",
};
let mock = MockReadOnly::new(&fx);
assert_eq!(mock.fixture().bytes, fx.bytes);
assert_eq!(mock.fixture().provenance, fx.provenance);
}
#[test]
fn the_fixture_entry_point_checks_magic_agreement_when_the_format_declares_one() {
let fx = ContainerFixture {
bytes: framed_fixture_bytes(&[("a.txt", b"alpha")]),
expected: &[ExpectedEntry {
stored_crc: None,
name: "a.txt",
content: b"alpha",
}],
provenance: "hand-built in this test",
};
let meta = read_only_meta_with_magics(&[FRAMED_FIXTURE_MAGIC]);
assert_container_conforms_with_skipping(&MockReadOnly::new(&fx), &meta, &fx, &[10]);
}
#[test]
fn property_three_catches_a_fixture_that_matches_no_registered_magic() {
let fx = ContainerFixture {
bytes: framed_fixture_bytes(&[("a.txt", b"alpha")]),
expected: &[ExpectedEntry {
stored_crc: None,
name: "a.txt",
content: b"alpha",
}],
provenance: "hand-built in this test",
};
let meta = read_only_meta_with_magics(&[ABSENT_MAGIC]);
assert_panics_naming_with(&MockReadOnly::new(&fx), &meta, &fx, "fixture property 3");
}
struct WrongIdReadOnly;
impl Container for WrongIdReadOnly {
fn id(&self) -> FormatId {
FormatId::new("wrong-id-read-only")
}
fn caps(&self) -> ContainerCaps {
read_only_double_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>> {
Err(Error::Unsupported("wrong-id-read-only cannot write".into()))
}
}
#[test]
fn fixture_property_one_catches_a_mismatched_id() {
let fx = ContainerFixture {
bytes: framed_fixture_bytes(&[("a.txt", b"alpha")]),
expected: &[ExpectedEntry {
stored_crc: None,
name: "a.txt",
content: b"alpha",
}],
provenance: "hand-built in this test",
};
assert_panics_naming_with(
&WrongIdReadOnly,
&read_only_meta(),
&fx,
"fixture property 1",
);
}
struct FalselyWritable;
impl Container for FalselyWritable {
fn id(&self) -> FormatId {
READ_ONLY_DOUBLE
}
fn caps(&self) -> ContainerCaps {
ContainerCaps {
read: true,
write: false,
forward_parse: true,
detects_corruption: CorruptionDetection::Always,
..Default::default()
}
}
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 fixture_property_two_catches_a_write_that_should_have_been_refused() {
let fx = ContainerFixture {
bytes: framed_fixture_bytes(&[("a.txt", b"alpha")]),
expected: &[ExpectedEntry {
stored_crc: None,
name: "a.txt",
content: b"alpha",
}],
provenance: "hand-built in this test",
};
assert_panics_naming_with(
&FalselyWritable,
&read_only_meta(),
&fx,
"fixture property 2",
);
}
struct MisclassifiesRefusal;
impl Container for MisclassifiesRefusal {
fn id(&self) -> FormatId {
READ_ONLY_DOUBLE
}
fn caps(&self) -> ContainerCaps {
read_only_double_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>> {
Err(Error::Io(io::Error::other(
"misclassifies-refusal: refuses to write",
)))
}
}
#[test]
fn fixture_property_eight_catches_an_unclassified_write_refusal() {
let fx = ContainerFixture {
bytes: framed_fixture_bytes(&[("a.txt", b"alpha")]),
expected: &[ExpectedEntry {
stored_crc: None,
name: "a.txt",
content: b"alpha",
}],
provenance: "hand-built in this test",
};
assert_panics_naming_with(
&MisclassifiesRefusal,
&read_only_meta(),
&fx,
"fixture property 8",
);
}
struct DropsAnEntry;
struct DropsAnEntryRead {
inner: Box<dyn ArchiveRead>,
pending: Option<(EntryMeta, Vec<u8>)>,
}
impl DropsAnEntryRead {
fn pull(inner: &mut dyn ArchiveRead) -> Result<Option<(EntryMeta, Vec<u8>)>> {
let Some(mut entry) = inner.next_entry()? else {
return Ok(None);
};
let meta = entry.meta().clone();
let mut data = Vec::new();
entry.reader().read_to_end(&mut data)?;
Ok(Some((meta, data)))
}
}
impl ArchiveRead for DropsAnEntryRead {
fn next_entry(&mut self) -> Result<Option<Entry<'_>>> {
if self.pending.is_none() {
self.pending = Self::pull(&mut *self.inner)?;
}
let Some(current) = self.pending.take() else {
return Ok(None);
};
self.pending = Self::pull(&mut *self.inner)?;
if self.pending.is_none() {
return Ok(None);
}
let (meta, data) = current;
Ok(Some(Entry::new(meta, Box::new(io::Cursor::new(data)))))
}
fn by_index(&mut self, index: usize) -> Result<Entry<'_>> {
self.inner.by_index(index)
}
fn fidelity(&self) -> &FidelityReport {
self.inner.fidelity()
}
}
impl Container for DropsAnEntry {
fn id(&self) -> FormatId {
READ_ONLY_DOUBLE
}
fn caps(&self) -> ContainerCaps {
read_only_double_caps()
}
fn open(&self, resolved: Resolved, o: &OpenOpts) -> Result<Box<dyn ArchiveRead>> {
Ok(Box::new(DropsAnEntryRead {
inner: FramedMockContainer.open(resolved, o)?,
pending: None,
}))
}
fn create(&self, _dst: Box<dyn Sink>, _o: &CreateOpts) -> Result<Box<dyn ArchiveWrite>> {
Err(Error::Unsupported("drops-an-entry cannot write".into()))
}
}
#[test]
fn fixture_property_four_catches_a_reader_that_drops_the_last_entry() {
let fx = ContainerFixture {
bytes: framed_fixture_bytes(&[("a.txt", b"alpha"), ("b.txt", b"beta")]),
expected: &[
ExpectedEntry {
stored_crc: None,
name: "a.txt",
content: b"alpha",
},
ExpectedEntry {
stored_crc: None,
name: "b.txt",
content: b"beta",
},
],
provenance: "hand-built in this test",
};
assert_panics_naming_with(&DropsAnEntry, &read_only_meta(), &fx, "fixture property 4");
}
struct RenamesAnEntry;
struct RenamesAnEntryRead {
inner: Box<dyn ArchiveRead>,
seen_first: bool,
}
impl ArchiveRead for RenamesAnEntryRead {
fn next_entry(&mut self) -> Result<Option<Entry<'_>>> {
let Some(entry) = self.inner.next_entry()? else {
return Ok(None);
};
if !self.seen_first {
self.seen_first = true;
let mut meta = entry.meta().clone();
meta.name.push_str("-renamed");
return Ok(Some(Entry::new(meta, entry.into_reader())));
}
Ok(Some(entry))
}
fn by_index(&mut self, index: usize) -> Result<Entry<'_>> {
self.inner.by_index(index)
}
fn fidelity(&self) -> &FidelityReport {
self.inner.fidelity()
}
}
impl Container for RenamesAnEntry {
fn id(&self) -> FormatId {
READ_ONLY_DOUBLE
}
fn caps(&self) -> ContainerCaps {
read_only_double_caps()
}
fn open(&self, resolved: Resolved, o: &OpenOpts) -> Result<Box<dyn ArchiveRead>> {
Ok(Box::new(RenamesAnEntryRead {
inner: FramedMockContainer.open(resolved, o)?,
seen_first: false,
}))
}
fn create(&self, _dst: Box<dyn Sink>, _o: &CreateOpts) -> Result<Box<dyn ArchiveWrite>> {
Err(Error::Unsupported("renames-an-entry cannot write".into()))
}
}
#[test]
fn fixture_property_four_catches_a_reader_that_renames_the_first_entry() {
let fx = ContainerFixture {
bytes: framed_fixture_bytes(&[("a.txt", b"alpha"), ("b.txt", b"beta")]),
expected: &[
ExpectedEntry {
stored_crc: None,
name: "a.txt",
content: b"alpha",
},
ExpectedEntry {
stored_crc: None,
name: "b.txt",
content: b"beta",
},
],
provenance: "hand-built in this test",
};
assert_panics_naming_with(
&RenamesAnEntry,
&read_only_meta(),
&fx,
"fixture property 4",
);
}
struct CorruptsContent;
struct CorruptsContentRead {
inner: Box<dyn ArchiveRead>,
}
impl ArchiveRead for CorruptsContentRead {
fn next_entry(&mut self) -> Result<Option<Entry<'_>>> {
let Some(mut entry) = self.inner.next_entry()? else {
return Ok(None);
};
let meta = entry.meta().clone();
let mut data = Vec::new();
entry.reader().read_to_end(&mut data)?;
match data.first_mut() {
Some(byte) => *byte ^= 0xFF,
None => data.push(0xFF),
}
Ok(Some(Entry::new(meta, Box::new(io::Cursor::new(data)))))
}
fn by_index(&mut self, index: usize) -> Result<Entry<'_>> {
self.inner.by_index(index)
}
fn fidelity(&self) -> &FidelityReport {
self.inner.fidelity()
}
}
impl Container for CorruptsContent {
fn id(&self) -> FormatId {
READ_ONLY_DOUBLE
}
fn caps(&self) -> ContainerCaps {
ContainerCaps {
detects_corruption: CorruptionDetection::Structural,
..read_only_double_caps()
}
}
fn open(&self, resolved: Resolved, o: &OpenOpts) -> Result<Box<dyn ArchiveRead>> {
Ok(Box::new(CorruptsContentRead {
inner: FramedMockContainer.open(resolved, o)?,
}))
}
fn create(&self, _dst: Box<dyn Sink>, _o: &CreateOpts) -> Result<Box<dyn ArchiveWrite>> {
Err(Error::Unsupported("corrupts-content cannot write".into()))
}
}
#[test]
fn fixture_property_five_catches_a_reader_that_serves_wrong_content_for_the_right_name() {
let fx = ContainerFixture {
bytes: framed_fixture_bytes(&[("a.txt", b"alpha")]),
expected: &[ExpectedEntry {
stored_crc: None,
name: "a.txt",
content: b"alpha",
}],
provenance: "hand-built in this test",
};
assert_panics_naming_with(
&CorruptsContent,
&read_only_meta(),
&fx,
"fixture property 5",
);
}
struct DecodesPastItsCrc {
declares: CorruptionDetection,
}
impl Container for DecodesPastItsCrc {
fn id(&self) -> FormatId {
READ_ONLY_DOUBLE
}
fn caps(&self) -> ContainerCaps {
ContainerCaps {
detects_corruption: self.declares,
..read_only_double_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>> {
Err(Error::Unsupported(
"decodes-past-its-crc cannot write".into(),
))
}
}
#[test]
fn fixture_property_ten_catches_content_that_disagrees_with_the_stored_crc() {
let content: &[u8] = b"alpha";
let real_crc = crc16_arc_witness(content);
let fx = ContainerFixture {
bytes: framed_fixture_bytes(&[("a.txt", content)]),
expected: Box::leak(Box::new([ExpectedEntry {
name: "a.txt",
content,
stored_crc: Some(real_crc ^ 1),
}])),
provenance: "hand-built in this test",
};
assert_panics_naming_with(
&DecodesPastItsCrc {
declares: CorruptionDetection::Always,
},
&read_only_meta(),
&fx,
"fixture property 10",
);
}
#[test]
fn a_container_declaring_structural_corruption_detection_skips_property_ten() {
let content: &[u8] = b"alpha";
let real_crc = crc16_arc_witness(content);
let fx = ContainerFixture {
bytes: framed_fixture_bytes(&[("a.txt", content)]),
expected: Box::leak(Box::new([ExpectedEntry {
name: "a.txt",
content,
stored_crc: Some(real_crc ^ 1),
}])),
provenance: "hand-built in this test",
};
assert_container_conforms_with_skipping(
&DecodesPastItsCrc {
declares: CorruptionDetection::Structural,
},
&read_only_meta(),
&fx,
&[3, 10],
);
}
struct IgnoresTruncation {
known: Vec<(String, Vec<u8>)>,
}
impl IgnoresTruncation {
fn from_fixture(fixture: &ContainerFixture) -> Self {
Self {
known: fixture
.expected
.iter()
.map(|e| (e.name.to_string(), e.content.to_vec()))
.collect(),
}
}
}
struct IgnoresTruncationRead {
known: Vec<(String, Vec<u8>)>,
pos: usize,
report: FidelityReport,
}
impl ArchiveRead for IgnoresTruncationRead {
fn next_entry(&mut self) -> Result<Option<Entry<'_>>> {
let Some((name, data)) = self.known.get(self.pos).cloned() else {
return Ok(None);
};
self.pos += 1;
Ok(Some(Entry::new(
EntryMeta::file(name),
Box::new(io::Cursor::new(data)),
)))
}
fn by_index(&mut self, index: usize) -> Result<Entry<'_>> {
match self.known.get(index).cloned() {
Some((name, data)) => Ok(Entry::new(
EntryMeta::file(name),
Box::new(io::Cursor::new(data)),
)),
None => Err(Error::EntryNotFound(index.to_string())),
}
}
fn fidelity(&self) -> &FidelityReport {
&self.report
}
}
impl Container for IgnoresTruncation {
fn id(&self) -> FormatId {
READ_ONLY_DOUBLE
}
fn caps(&self) -> ContainerCaps {
read_only_double_caps()
}
fn open(&self, _resolved: Resolved, _o: &OpenOpts) -> Result<Box<dyn ArchiveRead>> {
Ok(Box::new(IgnoresTruncationRead {
known: self.known.clone(),
pos: 0,
report: FidelityReport::exact(),
}))
}
fn create(&self, _dst: Box<dyn Sink>, _o: &CreateOpts) -> Result<Box<dyn ArchiveWrite>> {
Err(Error::Unsupported("ignores-truncation cannot write".into()))
}
}
#[test]
fn fixture_property_six_catches_a_reader_that_ignores_truncation() {
let fx = ContainerFixture {
bytes: framed_fixture_bytes(&[("a.txt", b"alpha"), ("b.txt", b"beta")]),
expected: &[
ExpectedEntry {
stored_crc: None,
name: "a.txt",
content: b"alpha",
},
ExpectedEntry {
stored_crc: None,
name: "b.txt",
content: b"beta",
},
],
provenance: "hand-built in this test",
};
assert_panics_naming_with(
&IgnoresTruncation::from_fixture(&fx),
&read_only_meta(),
&fx,
"fixture property 6",
);
}
struct RestoresKnownContent {
known: Vec<(String, Vec<u8>)>,
declares: CorruptionDetection,
}
impl RestoresKnownContent {
fn from_fixture(fixture: &ContainerFixture) -> Self {
Self {
known: fixture
.expected
.iter()
.map(|e| (e.name.to_string(), e.content.to_vec()))
.collect(),
declares: CorruptionDetection::Always,
}
}
fn undeclared(fixture: &ContainerFixture) -> Self {
Self {
declares: CorruptionDetection::Never,
..Self::from_fixture(fixture)
}
}
}
struct RestoresKnownContentRead {
inner: Box<dyn ArchiveRead>,
known: Vec<(String, Vec<u8>)>,
}
impl ArchiveRead for RestoresKnownContentRead {
fn next_entry(&mut self) -> Result<Option<Entry<'_>>> {
let Some(mut entry) = self.inner.next_entry()? else {
return Ok(None);
};
let meta = entry.meta().clone();
let mut real = Vec::new();
entry.reader().read_to_end(&mut real)?;
let content = self
.known
.iter()
.find(|(name, _)| *name == meta.name)
.map(|(_, data)| data.clone())
.unwrap_or(real);
Ok(Some(Entry::new(meta, Box::new(io::Cursor::new(content)))))
}
fn by_index(&mut self, index: usize) -> Result<Entry<'_>> {
self.inner.by_index(index)
}
fn fidelity(&self) -> &FidelityReport {
self.inner.fidelity()
}
}
impl Container for RestoresKnownContent {
fn id(&self) -> FormatId {
READ_ONLY_DOUBLE
}
fn caps(&self) -> ContainerCaps {
ContainerCaps {
detects_corruption: self.declares,
..read_only_double_caps()
}
}
fn open(&self, resolved: Resolved, o: &OpenOpts) -> Result<Box<dyn ArchiveRead>> {
Ok(Box::new(RestoresKnownContentRead {
inner: FramedMockContainer.open(resolved, o)?,
known: self.known.clone(),
}))
}
fn create(&self, _dst: Box<dyn Sink>, _o: &CreateOpts) -> Result<Box<dyn ArchiveWrite>> {
Err(Error::Unsupported(
"restores-known-content cannot write".into(),
))
}
}
#[test]
fn fixture_property_seven_catches_a_reader_that_repairs_corrupted_content() {
const PAYLOAD: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn";
assert_eq!(PAYLOAD.len(), 40);
let fx = ContainerFixture {
bytes: framed_fixture_bytes(&[("a", PAYLOAD)]),
expected: &[ExpectedEntry {
stored_crc: None,
name: "a",
content: PAYLOAD,
}],
provenance: "hand-built in this test",
};
assert_panics_naming_with(
&RestoresKnownContent::from_fixture(&fx),
&read_only_meta(),
&fx,
"fixture property 7",
);
}
#[test]
fn a_container_declaring_no_corruption_detection_skips_property_seven() {
const PAYLOAD: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn";
let fx = ContainerFixture {
bytes: framed_fixture_bytes(&[("a", PAYLOAD)]),
expected: &[ExpectedEntry {
stored_crc: None,
name: "a",
content: PAYLOAD,
}],
provenance: "hand-built in this test",
};
assert_container_conforms_with_skipping(
&RestoresKnownContent::undeclared(&fx),
&read_only_meta(),
&fx,
&[3, 7, 10],
);
}
struct SwallowsSourceError {
relabel: bool,
}
struct SwallowsSourceErrorRead {
inner: Box<dyn ArchiveRead>,
relabel: bool,
report: FidelityReport,
}
impl ArchiveRead for SwallowsSourceErrorRead {
fn next_entry(&mut self) -> Result<Option<Entry<'_>>> {
match self.inner.next_entry() {
Ok(entry) => Ok(entry),
Err(_) if self.relabel => Err(Error::Corrupt(
"swallows-source-error: archive is corrupt".into(),
)),
Err(_) => Ok(None),
}
}
fn by_index(&mut self, index: usize) -> Result<Entry<'_>> {
self.inner.by_index(index)
}
fn fidelity(&self) -> &FidelityReport {
&self.report
}
}
impl Container for SwallowsSourceError {
fn id(&self) -> FormatId {
READ_ONLY_DOUBLE
}
fn caps(&self) -> ContainerCaps {
read_only_double_caps()
}
fn open(&self, resolved: Resolved, o: &OpenOpts) -> Result<Box<dyn ArchiveRead>> {
let report = resolved.report.clone();
Ok(Box::new(SwallowsSourceErrorRead {
inner: FramedMockContainer.open(resolved, o)?,
relabel: self.relabel,
report,
}))
}
fn create(&self, _dst: Box<dyn Sink>, _o: &CreateOpts) -> Result<Box<dyn ArchiveWrite>> {
Err(Error::CapabilityUnavailable {
format: READ_ONLY_DOUBLE,
available: "read",
requested: "written",
})
}
}
#[test]
fn fixture_property_nine_catches_a_reader_that_swallows_a_source_error() {
let fx = ContainerFixture {
bytes: framed_fixture_bytes(&[("a.txt", b"alpha")]),
expected: &[ExpectedEntry {
stored_crc: None,
name: "a.txt",
content: b"alpha",
}],
provenance: "hand-built in this test",
};
assert_panics_naming_with(
&SwallowsSourceError { relabel: false },
&read_only_meta(),
&fx,
"fixture property 9",
);
}
#[test]
fn fixture_property_nine_catches_a_reader_that_relabels_a_source_error() {
let fx = ContainerFixture {
bytes: framed_fixture_bytes(&[("a.txt", b"alpha")]),
expected: &[ExpectedEntry {
stored_crc: None,
name: "a.txt",
content: b"alpha",
}],
provenance: "hand-built in this test",
};
assert_panics_naming_with(
&SwallowsSourceError { relabel: true },
&read_only_meta(),
&fx,
"fixture property 9",
);
}
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 SeekOnlyAcceptsTruncation;
impl Container for SeekOnlyAcceptsTruncation {
fn id(&self) -> FormatId {
FramedMockContainer.id()
}
fn caps(&self) -> ContainerCaps {
SeekOnlyMockContainer.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_still_catches_truncation_on_a_seek_only_container() {
assert_panics_naming_skipping(
&SeekOnlyAcceptsTruncation,
&framed_container_meta(),
&[7, 8, 13],
"property 9:",
);
}
struct SeekOnlyClaimsExact;
struct SeekOnlyClaimsExactRead {
inner: Box<dyn ArchiveRead>,
report: FidelityReport,
}
impl ArchiveRead for SeekOnlyClaimsExactRead {
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 SeekOnlyClaimsExact {
fn id(&self) -> FormatId {
FramedMockContainer.id()
}
fn caps(&self) -> ContainerCaps {
SeekOnlyMockContainer.caps()
}
fn open(&self, resolved: Resolved, o: &OpenOpts) -> Result<Box<dyn ArchiveRead>> {
Ok(Box::new(SeekOnlyClaimsExactRead {
inner: FramedMockContainer.open(resolved, o)?,
report: FidelityReport::exact(),
}))
}
fn create(&self, dst: Box<dyn Sink>, o: &CreateOpts) -> Result<Box<dyn ArchiveWrite>> {
FramedMockContainer.create(dst, o)
}
}
#[test]
fn property_five_catches_a_spooled_read_reported_as_exact() {
assert_panics_naming_skipping(
&SeekOnlyClaimsExact,
&framed_container_meta(),
&[7, 8, 13],
"property 5 (spilled)",
);
}
struct SeekOnlyMisroutesByIndex;
struct SeekOnlyMisroutesByIndexRead {
inner: Box<dyn ArchiveRead>,
}
impl ArchiveRead for SeekOnlyMisroutesByIndexRead {
fn next_entry(&mut self) -> Result<Option<Entry<'_>>> {
self.inner.next_entry()
}
fn by_index(&mut self, _index: usize) -> Result<Entry<'_>> {
Err(Error::Corrupt("seek-only double: no index".into()))
}
fn fidelity(&self) -> &FidelityReport {
self.inner.fidelity()
}
}
impl Container for SeekOnlyMisroutesByIndex {
fn id(&self) -> FormatId {
FramedMockContainer.id()
}
fn caps(&self) -> ContainerCaps {
SeekOnlyMockContainer.caps()
}
fn open(&self, resolved: Resolved, o: &OpenOpts) -> Result<Box<dyn ArchiveRead>> {
Ok(Box::new(SeekOnlyMisroutesByIndexRead {
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_a_by_index_refusal_the_fallback_cannot_route() {
assert_panics_naming_skipping(
&SeekOnlyMisroutesByIndex,
&framed_container_meta(),
&[7, 8, 13],
"property 6 (spilled)",
);
}
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",
);
}
}