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::{PeekSource, Source};
pub const PROBE_LEN: usize = 4096;
pub const MAX_CHAIN_DEPTH: usize = 4;
pub fn probe(mut src: Box<dyn Source>) -> Result<(Vec<u8>, Box<dyn Source>)> {
if src.caps().seekable {
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> {
let mut candidates = reg.match_magic(prefix);
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 = candidates.first().copied().or_else(|| {
path.and_then(|p| extensions(p).iter().rev().find_map(|e| reg.by_extension(e)))
});
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));
};
Ok(match kind {
FormatKind::Container => Chain::Container { container: outer },
FormatKind::Codec => Chain::Codec {
codec: outer,
inner: Box::new(inner_from_path(reg, path, outer)),
},
})
}
fn wrap_layers(layers: &[FormatId], innermost: Chain) -> Chain {
layers
.iter()
.rev()
.fold(innermost, |inner, codec| Chain::Codec {
codec: *codec,
inner: Box::new(inner),
})
}
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 = reg.require_codec(*codec)?.decoder(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 = reg.require_codec(current_codec)?.decoder(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;
}
}
#[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(&self, mut src: Box<dyn Source>, _o: &DecodeOpts) -> Result<Box<dyn Source>> {
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(&self, _src: Box<dyn Source>, _o: &DecodeOpts) -> Result<Box<dyn Source>> {
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_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"
);
}
}