use std::path::Path;
use crate::archive::DecodeOpts;
use crate::error::{Error, Result};
use crate::format::{FormatId, FormatKind};
use crate::registry::Registry;
use crate::source::{DecodeSideSource, PeekSource, Source};
pub const PROBE_LEN: usize = 4096;
pub const MAX_CHAIN_DEPTH: usize = 4;
pub const TAIL_LEN: usize = 512;
#[non_exhaustive]
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Foreign {
AppleDiskImage,
}
impl std::fmt::Display for Foreign {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Foreign::AppleDiskImage => f.write_str("an Apple disk image (UDIF)"),
}
}
}
pub fn foreign_from_tail(tail: &[u8]) -> Option<Foreign> {
(tail.len() == TAIL_LEN
&& tail[0..4] == *b"koly"
&& tail[4..8] == [0, 0, 0, 4]
&& tail[8..12] == [0, 0, 2, 0])
.then_some(Foreign::AppleDiskImage)
}
pub fn read_tail(src: &mut dyn Source) -> Result<Option<Vec<u8>>> {
if !src.caps().seekable {
return Ok(None);
}
let Some(seek) = src.as_seek() else {
return Ok(None);
};
use std::io::SeekFrom;
let len = seek.seek(SeekFrom::End(0))?;
if len < TAIL_LEN as u64 {
seek.seek(SeekFrom::Start(0))?;
return Ok(None);
}
seek.seek(SeekFrom::Start(len - TAIL_LEN as u64))?;
let mut tail = vec![0u8; TAIL_LEN];
let mut filled = 0;
while filled < TAIL_LEN {
match seek.read(&mut tail[filled..])? {
0 => break,
n => filled += n,
}
}
seek.seek(SeekFrom::Start(0))?;
Ok((filled == TAIL_LEN).then_some(tail))
}
pub fn probe(mut src: Box<dyn Source>) -> Result<(Vec<u8>, Box<dyn Source>)> {
if src.caps().seekable {
if let Some(foreign) = read_tail(src.as_mut())?
.as_deref()
.and_then(foreign_from_tail)
{
return Err(Error::ForeignFormat(foreign));
}
let mut prefix = vec![0u8; PROBE_LEN];
let Some(seek) = src.as_seek() else {
return Err(Error::NotSeekable {
format: FormatId::new("<probe>"),
});
};
let mut filled = 0;
while filled < PROBE_LEN {
match std::io::Read::read(seek, &mut prefix[filled..])? {
0 => break,
n => filled += n,
}
}
prefix.truncate(filled);
std::io::Seek::seek(seek, std::io::SeekFrom::Start(0))?;
return Ok((prefix, src));
}
let peek = PeekSource::fill(src, PROBE_LEN)?;
let prefix = peek.prefix().to_vec();
Ok((prefix, Box::new(peek)))
}
#[non_exhaustive]
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum Chain {
Codec { codec: FormatId, inner: Box<Chain> },
Container { container: FormatId },
Raw,
}
impl Chain {
pub fn container(&self) -> Option<FormatId> {
match self {
Chain::Container { container } => Some(*container),
Chain::Codec { inner, .. } => inner.container(),
_ => None,
}
}
pub fn outermost_codec(&self) -> Option<FormatId> {
match self {
Chain::Codec { codec, .. } => Some(*codec),
_ => None,
}
}
pub fn describe(&self) -> String {
match self {
Chain::Raw => "raw".to_string(),
Chain::Container { container } => container.to_string(),
Chain::Codec { codec, inner } => match inner.as_ref() {
Chain::Raw => codec.to_string(),
other => format!("{} over {}", other.describe(), codec),
},
}
}
}
fn hex_prefix(bytes: &[u8]) -> String {
if bytes.is_empty() {
return "empty input".to_string();
}
bytes
.iter()
.take(8)
.map(|b| format!("{b:02x}"))
.collect::<Vec<_>>()
.join(" ")
}
fn extensions(path: &Path) -> Vec<String> {
let name = path
.file_name()
.and_then(|s| s.to_str())
.unwrap_or_default();
name.split('.')
.skip(1)
.map(|s| s.to_ascii_lowercase())
.collect()
}
fn inner_from_path(reg: &Registry, path: Option<&Path>, outer: FormatId) -> Chain {
let Some(path) = path else {
return Chain::Raw;
};
let exts = extensions(path);
if let Some(last) = exts.last()
&& compound_alias(reg, last) == Some(outer)
&& let Some(id) = reg.by_extension("tar")
&& reg.container(id).is_some()
{
return Chain::Container { container: id };
}
if exts.len() >= 2
&& let Some(id) = reg.by_extension(&exts[exts.len() - 2])
&& reg.container(id).is_some()
{
return Chain::Container { container: id };
}
Chain::Raw
}
fn compound_alias(reg: &Registry, ext: &str) -> Option<FormatId> {
let stripped = ext.strip_prefix(['t', 'T'])?;
if stripped.is_empty() {
return None;
}
let stripped = stripped.to_ascii_lowercase();
let spelling = COMPOUND_SPELLINGS
.iter()
.find(|(alias, _)| *alias == stripped)
.map_or(stripped.as_str(), |(_, real)| *real);
let id = reg.by_extension(spelling)?;
reg.container(id).is_none().then_some(id)
}
const COMPOUND_SPELLINGS: &[(&str, &str)] = &[("bz", "bz2")];
pub fn chain_for_new_path(reg: &Registry, path: &Path) -> Chain {
let Some(ext) = path.extension().and_then(|e| e.to_str()) else {
return Chain::Raw;
};
let Some(outer) = reg.by_extension(ext).or_else(|| compound_alias(reg, ext)) else {
return Chain::Raw;
};
if reg.container(outer).is_some() {
return Chain::Container { container: outer };
}
let inner = inner_from_path(reg, Some(path), outer);
Chain::Codec {
codec: outer,
inner: Box::new(inner),
}
}
pub fn resolve_chain(reg: &Registry, path: Option<&Path>, prefix: &[u8]) -> Result<Chain> {
resolve_chain_checked(reg, path, prefix).map(|(chain, _)| chain)
}
#[non_exhaustive]
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Evidence {
Confirmed,
Unconfirmed,
ByName,
}
pub fn resolve_chain_checked(
reg: &Registry,
path: Option<&Path>,
prefix: &[u8],
) -> Result<(Chain, Evidence)> {
resolve_checked(reg, path, prefix, prefix.len() < PROBE_LEN)
}
fn resolve_checked(
reg: &Registry,
path: Option<&Path>,
prefix: &[u8],
whole: bool,
) -> Result<(Chain, Evidence)> {
let hits = reg.match_magic(prefix);
let confirmed: Vec<FormatId> = hits
.iter()
.copied()
.filter(|id| reg.confirm(*id, prefix, whole) != Some(false))
.collect();
let (mut candidates, mut evidence) = if confirmed.is_empty() {
(hits, Evidence::Unconfirmed)
} else {
(confirmed, Evidence::Confirmed)
};
if candidates.len() > 1 {
let by_ext =
path.and_then(|p| extensions(p).iter().rev().find_map(|e| reg.by_extension(e)));
match by_ext.filter(|id| candidates.contains(id)) {
Some(id) => candidates = vec![id],
None => {
let top = reg.priority_of(candidates[0]);
let tied: Vec<FormatId> = candidates
.iter()
.copied()
.filter(|id| reg.priority_of(*id) == top)
.collect();
if tied.len() == 1 {
candidates = tied;
} else {
let names: Vec<&str> = tied.iter().map(|id| id.as_str()).collect();
return Err(Error::AmbiguousFormat {
candidates: names.join(", "),
});
}
}
}
}
let outer = match candidates.first() {
Some(id) => Some(*id),
None => {
let by_name =
path.and_then(|p| extensions(p).iter().rev().find_map(|e| reg.by_extension(e)));
if let Some(id) = by_name {
evidence = match reg.confirm(id, prefix, whole) {
Some(true) => Evidence::Confirmed,
Some(false) => Evidence::Unconfirmed,
None => Evidence::ByName,
};
}
by_name
}
};
let Some(outer) = outer else {
return Err(Error::UnknownFormat {
seen: hex_prefix(prefix),
});
};
let kind = if reg.container(outer).is_some() {
FormatKind::Container
} else if reg.codec(outer).is_some() {
FormatKind::Codec
} else {
return Err(Error::FormatNotEnabled(outer));
};
let chain = match kind {
FormatKind::Container => Chain::Container { container: outer },
FormatKind::Codec => Chain::Codec {
codec: outer,
inner: Box::new(inner_from_path(reg, path, outer)),
},
};
Ok((chain, evidence))
}
fn wrap_layers(layers: &[FormatId], innermost: Chain) -> Chain {
layers
.iter()
.rev()
.fold(innermost, |inner, codec| Chain::Codec {
codec: *codec,
inner: Box::new(inner),
})
}
fn build_decoder(
reg: &Registry,
codec: FormatId,
src: Box<dyn Source>,
opts: &DecodeOpts,
) -> Result<Box<dyn Source>> {
let decoded = reg
.require_codec(codec)?
.decoder(src, opts)
.map_err(|e| match e {
Error::Io(io_err) => Error::from_decode_io(io_err),
other => other,
})?;
Ok(DecodeSideSource::wrap(decoded))
}
fn decode_through_chain(
reg: &Registry,
chain: &Chain,
src: Box<dyn Source>,
opts: &DecodeOpts,
) -> Result<Box<dyn Source>> {
match chain {
Chain::Codec { codec, inner } => {
let decoded = build_decoder(reg, *codec, src, opts)?;
decode_through_chain(reg, inner, decoded, opts)
}
Chain::Container { .. } | Chain::Raw => Ok(src),
}
}
pub fn resolve_chain_deep(
reg: &Registry,
path: Option<&Path>,
src: Box<dyn Source>,
) -> Result<(Chain, Box<dyn Source>)> {
resolve_chain_deep_with(reg, path, src, &DecodeOpts::default())
}
pub fn resolve_chain_deep_with(
reg: &Registry,
path: Option<&Path>,
src: Box<dyn Source>,
opts: &DecodeOpts,
) -> Result<(Chain, Box<dyn Source>)> {
let (prefix, src) = probe(src)?;
let chain = resolve_chain(reg, path, &prefix)?;
if !matches!(&chain, Chain::Codec { inner, .. } if matches!(inner.as_ref(), Chain::Raw)) {
let decoded = decode_through_chain(reg, &chain, src, opts)?;
return Ok((chain, decoded));
}
let Chain::Codec { codec, .. } = chain else {
unreachable!("guarded above: chain is Chain::Codec with a Raw inner")
};
let mut layers: Vec<FormatId> = vec![codec];
let mut current_codec = codec;
let mut src = src;
let mut depth = 1usize;
loop {
if depth >= MAX_CHAIN_DEPTH {
return Err(Error::ChainTooDeep {
depth: MAX_CHAIN_DEPTH,
});
}
let decoded = build_decoder(reg, current_codec, src, opts)?;
let (next_prefix, next_src) = probe(decoded).map_err(|e| match e {
Error::Io(io_err) => Error::from_decode_io(io_err),
other => other,
})?;
depth += 1;
let next_chain = match resolve_chain(reg, None, &next_prefix) {
Ok(chain) => chain,
Err(Error::UnknownFormat { .. }) => {
return Ok((wrap_layers(&layers, Chain::Raw), next_src));
}
Err(e) => return Err(e),
};
let Chain::Codec { codec: nested, .. } = next_chain else {
return Ok((wrap_layers(&layers, next_chain), next_src));
};
layers.push(nested);
current_codec = nested;
src = next_src;
}
}
pub fn identify_strict(
reg: &Registry,
path: Option<&Path>,
prefix: &[u8],
tail: Option<&[u8]>,
opts: &DecodeOpts,
) -> Option<Chain> {
if tail.and_then(foreign_from_tail).is_some() {
return None;
}
let (chain, evidence) = resolve_chain_checked(reg, path, prefix).ok()?;
if evidence == Evidence::Unconfirmed {
return None;
}
match chain {
Chain::Container { .. } => Some(chain),
Chain::Codec { codec, inner } => {
let layer = StrictInput {
bytes: prefix,
cut: prefix.len() == PROBE_LEN,
unseen_inner: *inner,
};
strict_codec_layer(reg, codec, layer, opts, 1)
}
_ => None,
}
}
pub fn first_unconfirmed_layer(
reg: &Registry,
chain: &Chain,
prefix: &[u8],
opts: &DecodeOpts,
) -> Option<FormatId> {
let mut layer = chain;
let mut owned: Vec<u8>;
let mut bytes = prefix;
let mut whole = prefix.len() < PROBE_LEN;
for _ in 0..MAX_CHAIN_DEPTH {
match layer {
Chain::Container { container } => {
return (reg.confirm(*container, bytes, whole) == Some(false))
.then_some(*container);
}
Chain::Codec { codec, inner } => {
if reg.confirm(*codec, bytes, whole) == Some(false) {
return Some(*codec);
}
if matches!(**inner, Chain::Raw) {
return None;
}
let decoded = decode_prefix(reg, *codec, bytes, opts)?;
if decoded.bytes.is_empty() {
return None;
}
whole = decoded.is_whole();
owned = decoded.bytes;
bytes = &owned;
layer = inner;
}
_ => return None,
}
}
None
}
struct StrictInput<'a> {
bytes: &'a [u8],
cut: bool,
unseen_inner: Chain,
}
fn strict_codec_layer(
reg: &Registry,
codec: FormatId,
input: StrictInput<'_>,
opts: &DecodeOpts,
depth: usize,
) -> Option<Chain> {
let decoded = decode_prefix(reg, codec, input.bytes, opts)?;
let whole = decoded.is_whole();
let inner = if decoded.bytes.is_empty() {
match (input.cut, decoded.failed) {
(true, _) => input.unseen_inner,
(false, true) => return None,
(false, false) => Chain::Raw,
}
} else {
match resolve_checked(reg, None, &decoded.bytes, whole) {
Ok((inner @ Chain::Container { .. }, Evidence::Confirmed)) => inner,
Ok((Chain::Codec { codec: nested, .. }, Evidence::Confirmed))
if depth + 1 < MAX_CHAIN_DEPTH =>
{
let nested_input = StrictInput {
bytes: &decoded.bytes,
cut: !whole,
unseen_inner: Chain::Raw,
};
strict_codec_layer(reg, nested, nested_input, opts, depth + 1).unwrap_or(Chain::Raw)
}
_ if reg.match_magic(&decoded.bytes).is_empty() => match input.unseen_inner {
Chain::Container { container }
if reg.confirm(container, &decoded.bytes, whole) != Some(false) =>
{
Chain::Container { container }
}
_ => Chain::Raw,
},
_ => Chain::Raw,
}
};
Some(Chain::Codec {
codec,
inner: Box::new(inner),
})
}
struct DecodedPrefix {
bytes: Vec<u8>,
failed: bool,
}
impl DecodedPrefix {
fn is_whole(&self) -> bool {
!self.failed && self.bytes.len() < PROBE_LEN
}
}
const MAX_INTERRUPTED_RETRIES: usize = 64;
fn decode_prefix(
reg: &Registry,
codec: FormatId,
bytes: &[u8],
opts: &DecodeOpts,
) -> Option<DecodedPrefix> {
let src = Box::new(crate::source::ReaderSource::new(std::io::Cursor::new(
bytes.to_vec(),
)));
let mut decoder = build_decoder(reg, codec, src, opts).ok()?;
let mut out = vec![0u8; PROBE_LEN];
let mut filled = 0;
let mut failed = false;
let mut interrupted = 0;
while filled < PROBE_LEN {
match std::io::Read::read(&mut decoder, &mut out[filled..]) {
Ok(0) => break,
Ok(n) => {
filled += n;
interrupted = 0;
}
Err(e)
if e.kind() == std::io::ErrorKind::Interrupted
&& interrupted < MAX_INTERRUPTED_RETRIES =>
{
interrupted += 1;
}
Err(_) => {
failed = true;
break;
}
}
}
out.truncate(filled);
Some(DecodedPrefix { bytes: out, failed })
}
#[cfg(test)]
mod tests {
use super::*;
use crate::archive::{Codec, DecodeOpts, EncodeOpts, Sink};
use crate::format::{CodecCaps, FormatId, FormatMeta, MagicRule};
use crate::registry::Registry;
use crate::source::{ReaderSource, Source, StreamOnly};
use crate::testing::{
MOCK_CODEC, MOCK_CONTAINER, MockCodec, MockContainer, SharedBuf, mock_archive_bytes,
};
use std::io;
use std::io::{Read, Write};
use std::path::Path;
use std::sync::Arc;
const GZIP: FormatId = FormatId::new("gzip");
const TAR: FormatId = FormatId::new("tar");
const GZIP_MAGIC: &[MagicRule] = &[MagicRule {
offset: 0,
bytes: &[0x1f, 0x8b],
format: GZIP,
}];
const TAR_MAGIC: &[MagicRule] = &[MagicRule {
offset: 257,
bytes: b"ustar",
format: TAR,
}];
fn registry() -> Registry {
let mut r = Registry::new();
r.register_codec(
Arc::new(MockCodec),
FormatMeta::codec(GZIP, &["gz", "tgz"], GZIP_MAGIC),
);
r.register_container(
Arc::new(MockContainer),
FormatMeta::container(TAR, &["tar"], TAR_MAGIC),
);
r
}
#[test]
fn an_output_path_resolves_to_the_chain_its_name_promises() {
let reg = registry();
let c = |p: &str| chain_for_new_path(®, Path::new(p)).describe();
assert_eq!(c("x.tar"), "tar");
assert_eq!(c("x.gz"), "gzip");
assert_eq!(c("x.tar.gz"), "tar over gzip");
assert_eq!(
c("x.tgz"),
"tar over gzip",
"the compound alias must resolve too"
);
assert_eq!(c("x.TAR.GZ"), "tar over gzip", "extensions are caseless");
assert_eq!(c("x.txt"), "raw");
assert_eq!(c("x"), "raw");
}
#[test]
fn tbz_means_tar_over_bzip2_without_making_plain_bz_writable() {
const BZIP2: FormatId = FormatId::new("bzip2");
const BZIP2_MAGIC: &[MagicRule] = &[MagicRule {
offset: 0,
bytes: b"BZh",
format: BZIP2,
}];
let mut reg = Registry::new();
reg.register_codec(
Arc::new(MockCodec),
FormatMeta::codec(BZIP2, &["bz2"], BZIP2_MAGIC),
);
reg.register_container(
Arc::new(MockContainer),
FormatMeta::container(TAR, &["tar"], TAR_MAGIC),
);
let c = |p: &str| chain_for_new_path(®, Path::new(p)).describe();
assert_eq!(
c("f.tbz"),
"tar over bzip2",
"GNU tar and libarchive both read .tbz as tar.bz2"
);
assert_eq!(c("f.TBZ"), "tar over bzip2", "and extensions are caseless");
assert_eq!(
c("f.tbz2"),
"tar over bzip2",
"the spelling that always worked must keep working"
);
assert_eq!(
c("f.tar.bz2"),
"tar over bzip2",
"and so must the long form"
);
assert_eq!(
c("f.bz"),
"raw",
"`bz` must resolve to nothing outside the t-prefixed compound form"
);
let chain = resolve_chain(®, Some(Path::new("f.tbz")), b"BZh\0").unwrap();
assert_eq!(
chain.describe(),
"tar over bzip2",
"reading a .tbz must agree with writing one"
);
}
#[test]
fn the_outermost_codec_is_the_layer_a_writer_creates_first() {
let reg = registry();
let pair = |p: &str| {
let c = chain_for_new_path(®, Path::new(p));
(c.container(), c.outermost_codec())
};
assert_eq!(pair("x.tar"), (Some(TAR), None));
assert_eq!(pair("x.gz"), (None, Some(GZIP)));
assert_eq!(pair("x.tar.gz"), (Some(TAR), Some(GZIP)));
assert_eq!(pair("x.tgz"), (Some(TAR), Some(GZIP)));
assert_eq!(pair("x.txt"), (None, None));
}
#[test]
fn probe_does_not_consume_the_stream() {
let src: Box<dyn Source> = Box::new(ReaderSource::new(std::io::Cursor::new(
b"HEADERbody".to_vec(),
)));
let (prefix, mut rest) = probe(src).unwrap();
assert_eq!(&prefix[..6], b"HEADER");
let mut all = Vec::new();
rest.read_to_end(&mut all).unwrap();
assert_eq!(all, b"HEADERbody", "the consumer must still see byte zero");
}
#[test]
fn probe_reads_at_most_the_window() {
let big = vec![b'x'; PROBE_LEN * 3];
let src: Box<dyn Source> = Box::new(ReaderSource::new(std::io::Cursor::new(big)));
let (prefix, _) = probe(src).unwrap();
assert_eq!(prefix.len(), PROBE_LEN);
}
#[test]
fn magic_identifies_a_bare_codec_stream() {
let r = registry();
let chain = resolve_chain(&r, Some(Path::new("blob.gz")), &[0x1f, 0x8b, 0x08]).unwrap();
assert_eq!(
chain,
Chain::Codec {
codec: GZIP,
inner: Box::new(Chain::Raw)
}
);
assert_eq!(chain.describe(), "gzip");
}
#[test]
fn a_double_extension_nests_the_container_inside_the_codec() {
let r = registry();
let chain = resolve_chain(&r, Some(Path::new("out.tar.gz")), &[0x1f, 0x8b, 0x08]).unwrap();
assert_eq!(
chain,
Chain::Codec {
codec: GZIP,
inner: Box::new(Chain::Container { container: TAR })
}
);
assert_eq!(chain.describe(), "tar over gzip");
}
#[test]
fn a_codec_on_stdin_resolves_to_raw_inner() {
let r = registry();
let chain = resolve_chain(&r, None, &[0x1f, 0x8b, 0x08]).unwrap();
assert_eq!(
chain,
Chain::Codec {
codec: GZIP,
inner: Box::new(Chain::Raw)
}
);
}
#[test]
fn container_magic_resolves_directly() {
let r = registry();
let mut prefix = vec![0u8; 512];
prefix[257..262].copy_from_slice(b"ustar");
let chain = resolve_chain(&r, Some(Path::new("x.tar")), &prefix).unwrap();
assert_eq!(chain, Chain::Container { container: TAR });
assert_eq!(chain.describe(), "tar");
}
#[test]
fn extension_is_the_fallback_when_magic_says_nothing() {
let r = registry();
let chain = resolve_chain(&r, Some(Path::new("mystery.tar")), b"no magic here").unwrap();
assert_eq!(chain, Chain::Container { container: TAR });
}
#[test]
fn chain_container_sees_through_codec_layers() {
let tar = FormatId::new("tar");
assert_eq!(Chain::Container { container: tar }.container(), Some(tar));
assert_eq!(
Chain::Codec {
codec: GZIP,
inner: Box::new(Chain::Container { container: tar }),
}
.container(),
Some(tar),
"tar over gzip still has a container nobody has opened"
);
assert_eq!(
Chain::Codec {
codec: GZIP,
inner: Box::new(Chain::Raw),
}
.container(),
None,
"a bare codec stream has no container, so an empty warning list is a real finding"
);
assert_eq!(Chain::Raw.container(), None);
}
#[test]
fn tgz_single_extension_still_nests_tar_inside_gzip() {
let r = registry();
let chain = resolve_chain(&r, Some(Path::new("a.tgz")), &[0x1f, 0x8b]).unwrap();
assert_eq!(
chain,
Chain::Codec {
codec: GZIP,
inner: Box::new(Chain::Container { container: TAR })
}
);
}
#[test]
fn a_compound_extension_resolves_without_a_registered_compound_alias() {
let mut r = Registry::new();
r.register_codec(
Arc::new(MockCodec),
FormatMeta::codec(GZIP, &["gz"], GZIP_MAGIC), );
r.register_container(
Arc::new(MockContainer),
FormatMeta::container(TAR, &["tar"], TAR_MAGIC),
);
let chain = resolve_chain(&r, Some(Path::new("a.tgz")), &[0x1f, 0x8b]).unwrap();
assert_eq!(
chain,
Chain::Codec {
codec: GZIP,
inner: Box::new(Chain::Container { container: TAR })
}
);
}
#[test]
fn probe_does_not_consume_a_seekable_stream_either() {
let mut f = tempfile::NamedTempFile::new().unwrap();
std::io::Write::write_all(&mut f, b"HEADERbody").unwrap();
let (_, path) = f.keep().unwrap();
let src: Box<dyn Source> = Box::new(crate::source::FileSource::open(&path).unwrap());
let (prefix, mut rest) = probe(src).unwrap();
assert_eq!(&prefix[..6], b"HEADER");
let mut all = Vec::new();
rest.read_to_end(&mut all).unwrap();
assert_eq!(
all, b"HEADERbody",
"a seekable source must be rewound to byte zero"
);
}
#[test]
fn probe_bounds_the_window_on_a_seekable_stream_too() {
let big = vec![b'x'; PROBE_LEN * 3];
let mut f = tempfile::NamedTempFile::new().unwrap();
std::io::Write::write_all(&mut f, &big).unwrap();
let (_, path) = f.keep().unwrap();
let src: Box<dyn Source> = Box::new(crate::source::FileSource::open(&path).unwrap());
let (prefix, _) = probe(src).unwrap();
assert_eq!(prefix.len(), PROBE_LEN);
}
#[test]
fn unknown_input_names_the_bytes_it_actually_saw() {
let r = registry();
let err = resolve_chain(&r, None, &[0xde, 0xad, 0xbe, 0xef]).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("de ad be ef"), "message was: {msg}");
assert!(matches!(err, crate::Error::UnknownFormat { .. }));
}
#[test]
fn unknown_empty_input_still_produces_a_usable_message() {
let r = registry();
let err = resolve_chain(&r, None, b"").unwrap_err();
assert!(err.to_string().contains("empty"));
}
#[test]
fn equally_ranked_candidates_with_no_extension_are_an_error_not_a_guess() {
const A: &[MagicRule] = &[MagicRule {
offset: 0,
bytes: b"PK",
format: FormatId::new("alpha"),
}];
const B: &[MagicRule] = &[MagicRule {
offset: 0,
bytes: b"PK",
format: FormatId::new("beta"),
}];
let mut r = Registry::new();
r.register_container(
Arc::new(MockContainer),
FormatMeta::container(FormatId::new("alpha"), &["alpha"], A),
);
r.register_container(
Arc::new(MockContainer),
FormatMeta::container(FormatId::new("beta"), &["beta"], B),
);
let err = resolve_chain(&r, None, b"PKrest").unwrap_err();
assert!(matches!(err, crate::Error::AmbiguousFormat { .. }));
let msg = err.to_string();
assert!(
msg.contains("alpha") && msg.contains("beta"),
"must name the candidates: {msg}"
);
assert_eq!(err.exit_code(), 2);
}
#[test]
fn a_higher_priority_candidate_resolves_without_an_extension() {
const A: &[MagicRule] = &[MagicRule {
offset: 0,
bytes: b"PK",
format: FormatId::new("alpha"),
}];
const B: &[MagicRule] = &[MagicRule {
offset: 0,
bytes: b"PK",
format: FormatId::new("beta"),
}];
let mut r = Registry::new();
r.register_container(
Arc::new(MockContainer),
FormatMeta::container(FormatId::new("alpha"), &["alpha"], A).with_priority(-5),
);
r.register_container(
Arc::new(MockContainer),
FormatMeta::container(FormatId::new("beta"), &["beta"], B),
);
let chain = resolve_chain(&r, None, b"PKrest").unwrap();
assert_eq!(
chain,
Chain::Container {
container: FormatId::new("beta")
}
);
}
#[test]
fn an_extension_still_disambiguates_equal_candidates() {
const A: &[MagicRule] = &[MagicRule {
offset: 0,
bytes: b"PK",
format: FormatId::new("alpha"),
}];
const B: &[MagicRule] = &[MagicRule {
offset: 0,
bytes: b"PK",
format: FormatId::new("beta"),
}];
let mut r = Registry::new();
r.register_container(
Arc::new(MockContainer),
FormatMeta::container(FormatId::new("alpha"), &["alpha"], A),
);
r.register_container(
Arc::new(MockContainer),
FormatMeta::container(FormatId::new("beta"), &["beta"], B),
);
let chain = resolve_chain(&r, Some(Path::new("x.beta")), b"PKrest").unwrap();
assert_eq!(
chain,
Chain::Container {
container: FormatId::new("beta")
}
);
}
#[test]
fn an_extension_outside_the_tied_set_does_not_break_the_tie() {
const A: &[MagicRule] = &[MagicRule {
offset: 0,
bytes: b"PK",
format: FormatId::new("alpha"),
}];
const B: &[MagicRule] = &[MagicRule {
offset: 0,
bytes: b"PK",
format: FormatId::new("beta"),
}];
const C: &[MagicRule] = &[MagicRule {
offset: 0,
bytes: b"GZ",
format: FormatId::new("gamma"),
}];
let mut r = Registry::new();
r.register_container(
Arc::new(MockContainer),
FormatMeta::container(FormatId::new("alpha"), &["alpha"], A),
);
r.register_container(
Arc::new(MockContainer),
FormatMeta::container(FormatId::new("beta"), &["beta"], B),
);
r.register_container(
Arc::new(MockContainer),
FormatMeta::container(FormatId::new("gamma"), &["gamma"], C),
);
let err = resolve_chain(&r, Some(Path::new("x.gamma")), b"PKrest").unwrap_err();
assert!(matches!(err, crate::Error::AmbiguousFormat { .. }));
let msg = err.to_string();
assert!(
msg.contains("alpha") && msg.contains("beta"),
"must name the tied candidates: {msg}"
);
assert!(
!msg.contains("gamma"),
"must not name the irrelevant extension: {msg}"
);
}
#[test]
fn an_extension_reaches_a_lower_priority_format_that_shares_the_magic() {
const ZIP_MAGIC: &[MagicRule] = &[MagicRule {
offset: 0,
bytes: b"PK",
format: FormatId::new("zip"),
}];
const APK_MAGIC: &[MagicRule] = &[MagicRule {
offset: 0,
bytes: b"PK",
format: FormatId::new("apk"),
}];
let mut r = Registry::new();
r.register_container(
Arc::new(MockContainer),
FormatMeta::container(FormatId::new("zip"), &["zip"], ZIP_MAGIC),
);
r.register_container(
Arc::new(MockContainer),
FormatMeta::container(FormatId::new("apk"), &["apk"], APK_MAGIC).with_priority(-5),
);
let chain = resolve_chain(&r, None, b"PKrest").unwrap();
assert_eq!(
chain,
Chain::Container {
container: FormatId::new("zip")
}
);
let chain = resolve_chain(&r, Some(Path::new("archive.apk")), b"PKrest").unwrap();
assert_eq!(
chain,
Chain::Container {
container: FormatId::new("apk")
}
);
}
struct LiesAboutSeekability(std::io::Cursor<Vec<u8>>);
impl Read for LiesAboutSeekability {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
std::io::Read::read(&mut self.0, buf)
}
}
impl Source for LiesAboutSeekability {
fn caps(&self) -> crate::source::SourceCaps {
crate::source::SourceCaps {
seekable: true,
len: Some(self.0.get_ref().len() as u64),
}
}
fn as_seek(&mut self) -> Option<&mut dyn crate::source::SeekRead> {
None
}
}
#[test]
fn a_source_that_claims_seekable_but_has_no_seek_view_errors_instead_of_panicking() {
let src: Box<dyn Source> = Box::new(LiesAboutSeekability(std::io::Cursor::new(
b"payload".to_vec(),
)));
match probe(src) {
Err(err) => assert!(
matches!(err, crate::Error::NotSeekable { .. }),
"must be NotSeekable, not a panic: {err:?}"
),
Ok(_) => panic!("expected NotSeekable, got Ok"),
}
}
const NESTED_MAGIC: &[u8] = b"MOCK";
struct PrefixedMockCodec;
impl Codec for PrefixedMockCodec {
fn id(&self) -> FormatId {
MOCK_CODEC
}
fn caps(&self) -> CodecCaps {
CodecCaps {
encode: true,
decode: true,
..Default::default()
}
}
fn decoder<'a>(
&self,
mut src: Box<dyn Source + 'a>,
_o: &DecodeOpts,
) -> Result<Box<dyn Source + 'a>> {
let mut discard = vec![0u8; NESTED_MAGIC.len()];
src.read_exact(&mut discard)?;
Ok(Box::new(StreamOnly::new(src)))
}
fn encoder(
&self,
mut dst: Box<dyn Write + Send>,
_o: &EncodeOpts,
) -> Result<Box<dyn Sink>> {
dst.write_all(NESTED_MAGIC)?;
Ok(Box::new(PassthroughSink(dst)))
}
}
struct PassthroughSink(Box<dyn Write + Send>);
impl Write for PassthroughSink {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0.write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
self.0.flush()
}
}
impl Sink for PassthroughSink {
fn finish(mut self: Box<Self>) -> Result<()> {
self.0.flush()?;
Ok(())
}
}
fn mock_registry_with_codec_and_container() -> Registry {
const CODEC_MAGIC: &[MagicRule] = &[MagicRule {
offset: 0,
bytes: NESTED_MAGIC,
format: MOCK_CODEC,
}];
const CONTAINER_MAGIC: &[MagicRule] = &[MagicRule {
offset: 0,
bytes: b"ME",
format: MOCK_CONTAINER,
}];
let mut r = Registry::new();
r.register_codec(
Arc::new(PrefixedMockCodec),
FormatMeta::codec(MOCK_CODEC, &["mock"], CODEC_MAGIC),
);
r.register_container(
Arc::new(MockContainer),
FormatMeta::container(MOCK_CONTAINER, &["mar"], CONTAINER_MAGIC),
);
r
}
fn mock_codec_encode(payload: &[u8]) -> Vec<u8> {
let sink = SharedBuf::new();
let mut s = PrefixedMockCodec
.encoder(Box::new(sink.clone()), &EncodeOpts::default())
.unwrap();
s.write_all(payload).unwrap();
s.finish().unwrap();
sink.contents()
}
#[test]
fn a_container_inside_a_codec_resolves_from_the_stream_with_no_path() {
let reg = mock_registry_with_codec_and_container();
let inner = mock_archive_bytes(&[("a.txt", b"alpha")]);
let outer = mock_codec_encode(&inner);
let (chain, _src) = resolve_chain_deep(
®,
None,
Box::new(ReaderSource::new(io::Cursor::new(outer))),
)
.expect("resolve");
assert_eq!(
chain,
Chain::Codec {
codec: MOCK_CODEC,
inner: Box::new(Chain::Container {
container: MOCK_CONTAINER
}),
},
"a container nested inside a codec must be found by peeking the decoded stream"
);
}
#[test]
fn nesting_beyond_the_depth_bound_is_a_typed_error_naming_the_depth() {
let reg = mock_registry_with_codec_and_container();
let mut bytes = mock_archive_bytes(&[("a.txt", b"alpha")]);
for _ in 0..MAX_CHAIN_DEPTH + 2 {
bytes = mock_codec_encode(&bytes);
}
match resolve_chain_deep(
®,
None,
Box::new(ReaderSource::new(io::Cursor::new(bytes))),
) {
Err(Error::ChainTooDeep { depth }) => assert_eq!(depth, MAX_CHAIN_DEPTH),
Err(other) => panic!("expected ChainTooDeep, got {other:?}"),
Ok(_) => panic!("nesting past the bound must be refused"),
}
}
#[test]
fn nesting_beyond_the_depth_bound_is_refused_via_the_path_route_too() {
let reg = mock_registry_with_codec_and_container();
let mut bytes = mock_archive_bytes(&[("a.txt", b"alpha")]);
for _ in 0..MAX_CHAIN_DEPTH + 2 {
bytes = mock_codec_encode(&bytes);
}
match resolve_chain_deep(
®,
Some(Path::new("nested.mock")),
Box::new(ReaderSource::new(io::Cursor::new(bytes))),
) {
Err(Error::ChainTooDeep { depth }) => assert_eq!(depth, MAX_CHAIN_DEPTH),
Err(other) => panic!("expected ChainTooDeep, got {other:?}"),
Ok(_) => panic!("nesting past the bound must be refused, path or no path"),
}
}
struct AlwaysInvalidData;
impl Read for AlwaysInvalidData {
fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"decoded bytes are malformed",
))
}
}
impl Source for AlwaysInvalidData {
fn caps(&self) -> crate::source::SourceCaps {
crate::source::SourceCaps {
seekable: false,
len: None,
}
}
fn as_seek(&mut self) -> Option<&mut dyn crate::source::SeekRead> {
None
}
}
struct FailingDecodeCodec;
impl Codec for FailingDecodeCodec {
fn id(&self) -> FormatId {
FormatId::new("always-fails-to-decode")
}
fn caps(&self) -> CodecCaps {
CodecCaps {
encode: false,
decode: true,
..Default::default()
}
}
fn decoder<'a>(
&self,
_src: Box<dyn Source + 'a>,
_o: &DecodeOpts,
) -> Result<Box<dyn Source + 'a>> {
Ok(Box::new(AlwaysInvalidData))
}
fn encoder(&self, _dst: Box<dyn Write + Send>, _o: &EncodeOpts) -> Result<Box<dyn Sink>> {
unimplemented!("this test never encodes")
}
}
#[test]
fn a_decoder_side_io_error_while_re_probing_becomes_corrupt() {
let mut reg = Registry::new();
const FAILING: FormatId = FormatId::new("always-fails-to-decode");
const FAILING_MAGIC: &[MagicRule] = &[MagicRule {
offset: 0,
bytes: &[0xab, 0xcd],
format: FAILING,
}];
reg.register_codec(
Arc::new(FailingDecodeCodec),
FormatMeta::codec(FAILING, &["fail"], FAILING_MAGIC),
);
let bytes = vec![0xab, 0xcd, 0x00, 0x00];
match resolve_chain_deep(
®,
None,
Box::new(ReaderSource::new(io::Cursor::new(bytes))),
) {
Err(Error::Corrupt(msg)) => assert!(
msg.contains("malformed"),
"must carry the decoder's own message via Error::from_decode_io: {msg}"
),
Err(other) => panic!("expected Corrupt (a decoder-side read failed), got {other:?}"),
Ok(_) => panic!("a decoder that always fails to read cannot resolve successfully"),
}
}
#[test]
fn a_decoder_that_refuses_at_construction_is_corrupt_not_an_io_failure() {
struct RefusesToBuild;
impl Codec for RefusesToBuild {
fn id(&self) -> FormatId {
FormatId::new("refuses-to-build")
}
fn caps(&self) -> CodecCaps {
CodecCaps {
encode: false,
decode: true,
..Default::default()
}
}
fn decoder<'a>(
&self,
_src: Box<dyn Source + 'a>,
_o: &DecodeOpts,
) -> Result<Box<dyn Source + 'a>> {
Err(Error::Io(io::Error::new(
io::ErrorKind::InvalidData,
"the header is malformed and I will not build a decoder for it",
)))
}
fn encoder(
&self,
_dst: Box<dyn Write + Send>,
_o: &EncodeOpts,
) -> Result<Box<dyn Sink>> {
unimplemented!("this test never encodes")
}
}
const REFUSES: FormatId = FormatId::new("refuses-to-build");
const REFUSES_MAGIC: &[MagicRule] = &[MagicRule {
offset: 0,
bytes: &[0xab, 0xcd],
format: REFUSES,
}];
let mut reg = Registry::new();
reg.register_codec(
Arc::new(RefusesToBuild),
FormatMeta::codec(REFUSES, &["refuse"], REFUSES_MAGIC),
);
for path in [None, Some(Path::new("thing.refuse"))] {
let bytes = vec![0xab, 0xcd, 0x00, 0x00];
match resolve_chain_deep(
®,
path,
Box::new(ReaderSource::new(io::Cursor::new(bytes))),
) {
Err(Error::Corrupt(msg)) => assert!(
msg.contains("malformed"),
"must carry the codec's own message: {msg}"
),
Err(other) => panic!("expected Corrupt for path {path:?}, got {other:?}"),
Ok(_) => panic!("a codec that refuses to build cannot resolve successfully"),
}
}
}
#[test]
fn a_raw_source_io_error_of_the_identical_kind_stays_io_not_corrupt() {
let reg = mock_registry_with_codec_and_container();
match resolve_chain_deep(®, None, Box::new(AlwaysInvalidData)) {
Err(Error::Io(io_err)) => assert_eq!(
io_err.kind(),
std::io::ErrorKind::InvalidData,
"sanity: this must be the same error kind the test above reclassifies, so the \
two tests differ only in WHERE the read happened"
),
Err(other) => panic!(
"a raw-source failure must stay Io, not become {other:?} — reclassifying it \
would be a lie in the opposite direction from Task 4b's bug"
),
Ok(_) => panic!("a raw source that always errors cannot resolve successfully"),
}
}
#[test]
fn the_returned_source_is_decoded_through_every_codec_layer_on_both_routes() {
let reg = mock_registry_with_codec_and_container();
let inner = mock_archive_bytes(&[("a.txt", b"alpha")]);
let outer = mock_codec_encode(&inner);
let (chain_with_path, mut src_with_path) = resolve_chain_deep(
®,
Some(Path::new("thing.mar.mock")),
Box::new(ReaderSource::new(io::Cursor::new(outer.clone()))),
)
.expect("resolve with path");
let (chain_no_path, mut src_no_path) = resolve_chain_deep(
®,
None,
Box::new(ReaderSource::new(io::Cursor::new(outer))),
)
.expect("resolve without path");
let expected_chain = Chain::Codec {
codec: MOCK_CODEC,
inner: Box::new(Chain::Container {
container: MOCK_CONTAINER,
}),
};
assert_eq!(chain_with_path, expected_chain);
assert_eq!(chain_no_path, expected_chain);
let mut bytes_with_path = Vec::new();
src_with_path.read_to_end(&mut bytes_with_path).unwrap();
let mut bytes_no_path = Vec::new();
src_no_path.read_to_end(&mut bytes_no_path).unwrap();
assert_eq!(
bytes_with_path, inner,
"the path route must decode through the codec layer, not return raw bytes"
);
assert_eq!(
bytes_with_path, bytes_no_path,
"both routes must leave the source in the identical, already-decoded state"
);
}
}
#[cfg(test)]
mod truthful_identification_tests {
use super::*;
use crate::archive::{Codec, DecodeOpts, EncodeOpts, Sink};
use crate::format::{CodecCaps, ConfirmFn, FormatId, FormatMeta, MagicRule};
use crate::registry::Registry;
use crate::source::{ReaderSource, SeekRead, Source, SourceCaps, StreamOnly};
use crate::testing::{
MOCK_CODEC, MOCK_CONTAINER, MockCodec, MockContainer, mock_archive_bytes,
};
use std::io::{self, Read, Write};
use std::path::Path;
use std::sync::Arc;
fn yes(_: &[u8], _: bool) -> bool {
true
}
fn no(_: &[u8], _: bool) -> bool {
false
}
fn koly(version: u32, size: u32) -> Vec<u8> {
let mut t = vec![0u8; TAIL_LEN];
t[0..4].copy_from_slice(b"koly");
t[4..8].copy_from_slice(&version.to_be_bytes());
t[8..12].copy_from_slice(&size.to_be_bytes());
t
}
struct SeekCursor(io::Cursor<Vec<u8>>);
impl Read for SeekCursor {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.0.read(buf)
}
}
impl Source for SeekCursor {
fn caps(&self) -> SourceCaps {
SourceCaps {
seekable: true,
len: Some(self.0.get_ref().len() as u64),
}
}
fn as_seek(&mut self) -> Option<&mut dyn SeekRead> {
Some(&mut self.0)
}
}
fn zlib_ish_image() -> Vec<u8> {
let mut bytes = vec![0x78, 0x9c];
bytes.extend(std::iter::repeat_n(0xa5, 1000));
bytes.extend(koly(4, 512));
bytes
}
#[test]
fn foreign_tail_needs_signature_version_and_size() {
assert_eq!(
foreign_from_tail(&koly(4, 512)),
Some(Foreign::AppleDiskImage)
);
assert_eq!(foreign_from_tail(&koly(3, 512)), None, "version 3");
assert_eq!(foreign_from_tail(&koly(4, 256)), None, "header size 256");
assert_eq!(foreign_from_tail(&koly(4, 512)[..511]), None, "511 bytes");
let mut kolx = koly(4, 512);
kolx[3] = b'x';
assert_eq!(foreign_from_tail(&kolx), None, "kolx");
assert_eq!(
Foreign::AppleDiskImage.to_string(),
"an Apple disk image (UDIF)"
);
}
#[test]
fn probe_refuses_a_seekable_source_with_a_koly_tail() {
let src: Box<dyn Source> = Box::new(SeekCursor(io::Cursor::new(zlib_ish_image())));
match probe(src) {
Err(e) => {
assert!(
matches!(e, Error::ForeignFormat(Foreign::AppleDiskImage)),
"{e:?}"
);
assert_eq!(e.exit_code(), 3);
assert_eq!(
e.to_string(),
"this is an Apple disk image (UDIF), which stuffr cannot open"
);
}
Ok(_) => panic!("a koly tail must be refused"),
}
}
#[test]
fn probe_does_not_read_the_tail_of_a_pipe() {
let src: Box<dyn Source> = Box::new(ReaderSource::new(io::Cursor::new(zlib_ish_image())));
let Ok((prefix, _)) = probe(src) else {
panic!("a pipe has no tail to read, so it must probe as before");
};
assert_eq!(&prefix[..2], &[0x78, 0x9c]);
}
#[test]
fn read_tail_leaves_a_seekable_source_at_zero() {
let mut bytes = b"HEAD".to_vec();
bytes.extend(vec![7u8; 600]);
let mut src = SeekCursor(io::Cursor::new(bytes.clone()));
let tail = read_tail(&mut src)
.unwrap()
.expect("600+ bytes have a tail");
assert_eq!(tail.len(), TAIL_LEN);
assert_eq!(tail, bytes[bytes.len() - TAIL_LEN..]);
let mut all = Vec::new();
src.read_to_end(&mut all).unwrap();
assert_eq!(all, bytes, "the position must be back at byte zero");
let mut short = SeekCursor(io::Cursor::new(b"tiny".to_vec()));
assert_eq!(read_tail(&mut short).unwrap(), None);
let mut all = Vec::new();
short.read_to_end(&mut all).unwrap();
assert_eq!(all, b"tiny");
let mut pipe = ReaderSource::new(io::Cursor::new(bytes));
assert_eq!(read_tail(&mut pipe).unwrap(), None);
}
const ALPHA: FormatId = FormatId::new("alpha");
const BETA: FormatId = FormatId::new("beta");
const SHARED_MAGIC: &[MagicRule] = &[
MagicRule {
offset: 0,
bytes: b"SH",
format: ALPHA,
},
MagicRule {
offset: 0,
bytes: b"SH",
format: BETA,
},
];
#[test]
fn confirmed_candidate_beats_an_unconfirmed_one() {
for (alpha_ok, winner) in [(true, ALPHA), (false, BETA)] {
let mut r = Registry::new();
let (a, b): (ConfirmFn, ConfirmFn) = if alpha_ok { (yes, no) } else { (no, yes) };
r.register_codec(
Arc::new(MockCodec),
FormatMeta::codec(ALPHA, &[], SHARED_MAGIC).with_confirm(a),
);
r.register_codec(
Arc::new(MockCodec),
FormatMeta::codec(BETA, &[], SHARED_MAGIC).with_confirm(b),
);
let (chain, ev) = resolve_chain_checked(&r, None, b"SH..").expect("no ambiguity");
assert_eq!(chain.outermost_codec(), Some(winner));
assert_eq!(ev, Evidence::Confirmed);
}
}
#[test]
fn a_format_with_no_confirm_function_counts_as_confirmed() {
let mut r = Registry::new();
r.register_codec(
Arc::new(MockCodec),
FormatMeta::codec(ALPHA, &[], SHARED_MAGIC).with_confirm(no),
);
r.register_codec(
Arc::new(MockCodec),
FormatMeta::codec(BETA, &[], SHARED_MAGIC),
);
let (chain, ev) = resolve_chain_checked(&r, None, b"SH..").unwrap();
assert_eq!(chain.outermost_codec(), Some(BETA));
assert_eq!(ev, Evidence::Confirmed);
}
#[test]
fn unconfirmed_magic_still_resolves_leniently() {
let mut r = Registry::new();
r.register_codec(
Arc::new(MockCodec),
FormatMeta::codec(ALPHA, &["al"], &SHARED_MAGIC[..1]).with_confirm(no),
);
let chain = resolve_chain(&r, None, b"SH..").expect("lenient resolution keeps it");
assert_eq!(chain.outermost_codec(), Some(ALPHA));
let (_, ev) = resolve_chain_checked(&r, None, b"SH..").unwrap();
assert_eq!(ev, Evidence::Unconfirmed);
}
#[test]
fn extension_without_confirm_is_by_name() {
let mut r = Registry::new();
r.register_codec(Arc::new(MockCodec), FormatMeta::codec(ALPHA, &["al"], &[]));
let (chain, ev) = resolve_chain_checked(&r, Some(Path::new("x.al")), b"whatever").unwrap();
assert_eq!(chain.outermost_codec(), Some(ALPHA));
assert_eq!(ev, Evidence::ByName);
}
#[test]
fn extension_with_failing_confirm_is_unconfirmed() {
let mut r = Registry::new();
r.register_codec(
Arc::new(MockCodec),
FormatMeta::codec(ALPHA, &["al"], &[]).with_confirm(no),
);
let (_, ev) = resolve_chain_checked(&r, Some(Path::new("x.al")), b"whatever").unwrap();
assert_eq!(ev, Evidence::Unconfirmed);
let mut r = Registry::new();
r.register_codec(
Arc::new(MockCodec),
FormatMeta::codec(ALPHA, &["al"], &[]).with_confirm(yes),
);
let (_, ev) = resolve_chain_checked(&r, Some(Path::new("x.al")), b"whatever").unwrap();
assert_eq!(ev, Evidence::Confirmed);
}
#[test]
fn errors_are_unchanged() {
let r = Registry::new();
assert!(matches!(
resolve_chain_checked(&r, None, b"??"),
Err(Error::UnknownFormat { .. })
));
let mut r = Registry::new();
r.register_codec(
Arc::new(MockCodec),
FormatMeta::codec(ALPHA, &[], SHARED_MAGIC).with_confirm(no),
);
r.register_codec(
Arc::new(MockCodec),
FormatMeta::codec(BETA, &[], SHARED_MAGIC).with_confirm(no),
);
assert!(matches!(
resolve_chain_checked(&r, None, b"SH"),
Err(Error::AmbiguousFormat { .. })
));
}
const LAYER_MAGIC: &[u8] = b"MOCK";
struct LayerCodec;
impl Codec for LayerCodec {
fn id(&self) -> FormatId {
MOCK_CODEC
}
fn caps(&self) -> CodecCaps {
CodecCaps::round_trip()
}
fn decoder<'a>(
&self,
mut src: Box<dyn Source + 'a>,
_o: &DecodeOpts,
) -> Result<Box<dyn Source + 'a>> {
let mut discard = [0u8; 4];
src.read_exact(&mut discard)?;
Ok(Box::new(StreamOnly::new(src)))
}
fn encoder(&self, _dst: Box<dyn Write + Send>, _o: &EncodeOpts) -> Result<Box<dyn Sink>> {
unreachable!("identification never encodes")
}
}
fn mock_archive_confirms(p: &[u8], _whole: bool) -> bool {
p.len() >= 4 && {
let n = u16::from_le_bytes([p[2], p[3]]) as usize;
n > 0 && 4 + n <= p.len()
}
}
fn strict_registry() -> Registry {
const CODEC_MAGIC: &[MagicRule] = &[MagicRule {
offset: 0,
bytes: LAYER_MAGIC,
format: MOCK_CODEC,
}];
const CONTAINER_MAGIC: &[MagicRule] = &[MagicRule {
offset: 0,
bytes: b"ME",
format: MOCK_CONTAINER,
}];
let mut r = Registry::new();
r.register_codec(
Arc::new(LayerCodec),
FormatMeta::codec(MOCK_CODEC, &["mock"], CODEC_MAGIC)
.with_confirm(|p, _| p.starts_with(LAYER_MAGIC)),
);
r.register_container(
Arc::new(MockContainer),
FormatMeta::container(MOCK_CONTAINER, &["mar"], CONTAINER_MAGIC)
.with_confirm(mock_archive_confirms),
);
r
}
fn layered(layers: usize, inner: &[u8]) -> Vec<u8> {
let mut out = LAYER_MAGIC.repeat(layers);
out.extend_from_slice(inner);
out
}
fn strict(
r: &Registry,
path: Option<&str>,
prefix: &[u8],
tail: Option<&[u8]>,
) -> Option<Chain> {
identify_strict(r, path.map(Path::new), prefix, tail, &DecodeOpts::default())
}
#[test]
fn identify_strict_rejects_unconfirmed_and_foreign() {
let r = strict_registry();
assert_eq!(
strict(&r, None, b"ME\xff\xffabc", None),
None,
"a magic hit whose confirmation fails is not openable"
);
let archive = mock_archive_bytes(&[("a.txt", b"alpha")]);
assert_eq!(
strict(&r, None, &archive, None),
Some(Chain::Container {
container: MOCK_CONTAINER
})
);
assert_eq!(
strict(&r, None, &archive, Some(&koly(4, 512))),
None,
"a koly tail outranks a confirmed prefix"
);
assert_eq!(strict(&r, None, b"nothing", None), None, "unknown");
}
#[test]
fn identify_strict_decodes_the_inner_layer_from_the_prefix() {
let r = strict_registry();
let archive = mock_archive_bytes(&[("a.txt", b"alpha")]);
assert_eq!(
strict(&r, None, &layered(1, &archive), None),
Some(Chain::Codec {
codec: MOCK_CODEC,
inner: Box::new(Chain::Container {
container: MOCK_CONTAINER
}),
})
);
assert_eq!(
strict(&r, Some("x.mar.mock"), &layered(1, b"ME\xff\xffabc"), None),
Some(Chain::Codec {
codec: MOCK_CODEC,
inner: Box::new(Chain::Raw),
}),
"an inner layer failing confirmation is Raw, whatever the path says"
);
}
#[test]
fn identify_strict_bounds_the_nesting() {
let r = strict_registry();
let archive = mock_archive_bytes(&[("a.txt", b"alpha")]);
let depth = |c: &Chain| {
let mut n = 0;
let mut c = c;
while let Chain::Codec { inner, .. } = c {
n += 1;
c = inner;
}
n
};
let three = strict(&r, None, &layered(3, &archive), None).unwrap();
assert_eq!(depth(&three), 3);
assert_eq!(three.container(), Some(MOCK_CONTAINER));
let many = strict(&r, None, &layered(10, &archive), None).unwrap();
assert!(depth(&many) < MAX_CHAIN_DEPTH, "{many:?}");
assert_eq!(many.container(), None, "stopped with Raw at the bound");
}
struct Scripted {
yields: &'static [u8],
then_error: bool,
}
struct ScriptedSource {
rest: &'static [u8],
then_error: bool,
}
impl Read for ScriptedSource {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if self.rest.is_empty() {
return if self.then_error {
Err(io::Error::new(io::ErrorKind::InvalidData, "bad stream"))
} else {
Ok(0)
};
}
let n = buf.len().min(self.rest.len());
buf[..n].copy_from_slice(&self.rest[..n]);
self.rest = &self.rest[n..];
Ok(n)
}
}
impl Source for ScriptedSource {
fn caps(&self) -> SourceCaps {
SourceCaps::default()
}
fn as_seek(&mut self) -> Option<&mut dyn SeekRead> {
None
}
}
impl Codec for Scripted {
fn id(&self) -> FormatId {
MOCK_CODEC
}
fn caps(&self) -> CodecCaps {
CodecCaps::decode_only()
}
fn decoder<'a>(
&self,
_src: Box<dyn Source + 'a>,
_o: &DecodeOpts,
) -> Result<Box<dyn Source + 'a>> {
Ok(Box::new(ScriptedSource {
rest: self.yields,
then_error: self.then_error,
}))
}
fn encoder(&self, _dst: Box<dyn Write + Send>, _o: &EncodeOpts) -> Result<Box<dyn Sink>> {
unreachable!("identification never encodes")
}
}
fn scripted_registry(yields: &'static [u8], then_error: bool) -> Registry {
let mut r = strict_registry();
r.register_codec(
Arc::new(Scripted { yields, then_error }),
FormatMeta::codec(
MOCK_CODEC,
&["mock"],
&[MagicRule {
offset: 0,
bytes: LAYER_MAGIC,
format: MOCK_CODEC,
}],
)
.with_confirm(yes),
);
r
}
#[test]
fn identify_strict_drops_a_codec_that_decodes_nothing() {
let raw = Some(Chain::Codec {
codec: MOCK_CODEC,
inner: Box::new(Chain::Raw),
});
assert_eq!(
strict(&scripted_registry(b"", true), None, b"MOCKjunk", None),
None,
"an error before the first byte: nothing here decodes"
);
assert_eq!(
strict(&scripted_registry(b"", false), None, b"MOCKjunk", None),
raw,
"a clean, empty stream is still a stream"
);
assert_eq!(
strict(
&scripted_registry(b"plain text", true),
None,
b"MOCKjunk",
None
),
raw,
"what arrived before the error is kept"
);
}
struct NeedsMore;
const NEEDS: usize = PROBE_LEN * 2;
const SLOW: FormatId = FormatId::new("slow");
const TAR: FormatId = FormatId::new("tar");
impl Codec for NeedsMore {
fn id(&self) -> FormatId {
SLOW
}
fn caps(&self) -> CodecCaps {
CodecCaps::decode_only()
}
fn decoder<'a>(
&self,
mut src: Box<dyn Source + 'a>,
_o: &DecodeOpts,
) -> Result<Box<dyn Source + 'a>> {
let mut all = Vec::new();
src.read_to_end(&mut all)?;
Ok(Box::new(ScriptedSource {
rest: if all.len() >= NEEDS { b"payload" } else { b"" },
then_error: all.len() < NEEDS,
}))
}
fn encoder(&self, _dst: Box<dyn Write + Send>, _o: &EncodeOpts) -> Result<Box<dyn Sink>> {
unreachable!("identification never encodes")
}
}
fn needs_more_registry() -> Registry {
let mut r = strict_registry();
r.register_codec(
Arc::new(NeedsMore),
FormatMeta::codec(
SLOW,
&["slow"],
&[MagicRule {
offset: 0,
bytes: b"SLOW",
format: SLOW,
}],
)
.with_confirm(yes),
);
r.register_container(
Arc::new(MockContainer),
FormatMeta::container(TAR, &["tar"], &[]),
);
r
}
fn slow_prefix(len: usize) -> Vec<u8> {
let mut p = b"SLOW".to_vec();
p.resize(len, 0x5a);
p
}
#[test]
fn a_cut_off_prefix_that_decodes_nothing_keeps_the_codec_over_the_paths_inner() {
let r = needs_more_registry();
assert_eq!(
strict(&r, Some("big.tar.slow"), &slow_prefix(PROBE_LEN), None),
Some(Chain::Codec {
codec: SLOW,
inner: Box::new(Chain::Container { container: TAR }),
}),
"the window ran out, not the file: as 0.21.0 listed it"
);
assert_eq!(
strict(&r, Some("big.slow"), &slow_prefix(PROBE_LEN), None),
Some(Chain::Codec {
codec: SLOW,
inner: Box::new(Chain::Raw),
}),
);
}
#[test]
fn a_whole_file_that_decodes_nothing_before_an_error_is_not_openable() {
let r = needs_more_registry();
assert_eq!(
strict(&r, Some("small.tar.slow"), &slow_prefix(100), None),
None,
"the prefix is the whole file, and it decodes to nothing but an error"
);
}
#[test]
fn an_extension_naming_a_refused_hit_falls_to_the_confirmed_ones() {
const ZIP: FormatId = FormatId::new("zip");
const JAR: FormatId = FormatId::new("jar");
const APK: FormatId = FormatId::new("apk");
const PK: &[MagicRule] = &[
MagicRule {
offset: 0,
bytes: b"PK",
format: ZIP,
},
MagicRule {
offset: 0,
bytes: b"PK",
format: JAR,
},
MagicRule {
offset: 0,
bytes: b"PK",
format: APK,
},
];
let mut r = Registry::new();
r.register_container(
Arc::new(MockContainer),
FormatMeta::container(ZIP, &["zip"], PK)
.with_priority(1)
.with_confirm(yes),
);
r.register_container(
Arc::new(MockContainer),
FormatMeta::container(JAR, &["jar"], PK).with_confirm(yes),
);
r.register_container(
Arc::new(MockContainer),
FormatMeta::container(APK, &["apk"], PK).with_confirm(no),
);
let (chain, ev) = resolve_chain_checked(&r, Some(Path::new("x.apk")), b"PK..").unwrap();
assert_eq!(chain, Chain::Container { container: ZIP });
assert_eq!(ev, Evidence::Confirmed);
}
fn eight_when_whole(p: &[u8], whole: bool) -> bool {
!whole || p.len() >= 8
}
#[test]
fn a_probe_prefix_short_of_the_window_is_confirmed_as_the_whole_stream() {
let mut r = Registry::new();
r.register_codec(
Arc::new(MockCodec),
FormatMeta::codec(ALPHA, &[], &SHARED_MAGIC[..1]).with_confirm(eight_when_whole),
);
let ev = |prefix: &[u8]| resolve_chain_checked(&r, None, prefix).unwrap().1;
assert_eq!(ev(b"SH"), Evidence::Unconfirmed, "a two-byte file is whole");
assert_eq!(ev(b"SH345678"), Evidence::Confirmed);
let mut window = vec![0u8; PROBE_LEN];
window[..2].copy_from_slice(b"SH");
assert_eq!(ev(&window), Evidence::Confirmed);
}
const PLAIN: FormatId = FormatId::new("plain");
const WHOLE: FormatId = FormatId::new("wholecheck");
fn layered_registry(yields: &'static [u8], then_error: bool) -> Registry {
let mut r = scripted_registry(yields, then_error);
r.register_container(
Arc::new(MockContainer),
FormatMeta::container(PLAIN, &["pln"], &[]).with_confirm(|p, _| p.starts_with(b"PL")),
);
r.register_container(
Arc::new(MockContainer),
FormatMeta::container(
WHOLE,
&["wh"],
&[MagicRule {
offset: 0,
bytes: b"WH",
format: WHOLE,
}],
)
.with_confirm(eight_when_whole),
);
r
}
fn over(inner: Chain) -> Option<Chain> {
Some(Chain::Codec {
codec: MOCK_CODEC,
inner: Box::new(inner),
})
}
#[test]
fn a_decoded_layer_is_whole_only_when_it_ended_cleanly_inside_the_window() {
let r = layered_registry(b"WH", false);
assert_eq!(strict(&r, None, b"MOCKjunk", None), over(Chain::Raw));
let chain = over(Chain::Container { container: WHOLE }).unwrap();
assert_eq!(
first_unconfirmed_layer(&r, &chain, b"MOCKjunk", &DecodeOpts::default()),
Some(WHOLE)
);
let r = layered_registry(b"WH", true);
assert_eq!(
strict(&r, None, b"MOCKjunk", None),
over(Chain::Container { container: WHOLE })
);
assert_eq!(
first_unconfirmed_layer(&r, &chain, b"MOCKjunk", &DecodeOpts::default()),
None
);
}
#[test]
fn decoded_bytes_with_no_magic_take_the_paths_container_when_it_confirms() {
let plain = Chain::Container { container: PLAIN };
let r = layered_registry(b"PLAIN DATA", false);
assert_eq!(
strict(&r, Some("x.pln.mock"), b"MOCKjunk", None),
over(plain.clone()),
"a magicless inner layer the name gives and the bytes confirm"
);
assert_eq!(
strict(&r, None, b"MOCKjunk", None),
over(Chain::Raw),
"no name"
);
assert_eq!(
strict(&r, Some("x.mock"), b"MOCKjunk", None),
over(Chain::Raw),
"a name with nothing inside"
);
let r = layered_registry(b"just text", false);
assert_eq!(
strict(&r, Some("x.pln.mock"), b"MOCKjunk", None),
over(Chain::Raw),
"the name alone is not enough when the check refuses the bytes"
);
let (chain, _) =
resolve_chain_checked(&r, Some(Path::new("x.pln.mock")), b"MOCKjunk").unwrap();
assert_eq!(chain, over(plain).unwrap());
assert_eq!(
first_unconfirmed_layer(&r, &chain, b"MOCKjunk", &DecodeOpts::default()),
Some(PLAIN)
);
}
}