1use std::path::Path;
19
20use crate::archive::DecodeOpts;
21use crate::error::{Error, Result};
22use crate::format::{FormatId, FormatKind};
23use crate::registry::Registry;
24use crate::source::{DecodeSideSource, PeekSource, Source};
25
26pub const PROBE_LEN: usize = 4096;
28
29pub const MAX_CHAIN_DEPTH: usize = 4;
36
37pub const TAIL_LEN: usize = 512;
40
41#[non_exhaustive]
49#[derive(Clone, Copy, PartialEq, Eq, Debug)]
50pub enum Foreign {
51 AppleDiskImage,
53}
54
55impl std::fmt::Display for Foreign {
56 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57 match self {
58 Foreign::AppleDiskImage => f.write_str("an Apple disk image (UDIF)"),
59 }
60 }
61}
62
63pub fn foreign_from_tail(tail: &[u8]) -> Option<Foreign> {
72 (tail.len() == TAIL_LEN
73 && tail[0..4] == *b"koly"
74 && tail[4..8] == [0, 0, 0, 4]
75 && tail[8..12] == [0, 0, 2, 0])
76 .then_some(Foreign::AppleDiskImage)
77}
78
79pub fn read_tail(src: &mut dyn Source) -> Result<Option<Vec<u8>>> {
92 if !src.caps().seekable {
93 return Ok(None);
94 }
95 let Some(seek) = src.as_seek() else {
98 return Ok(None);
99 };
100 use std::io::SeekFrom;
101 let len = seek.seek(SeekFrom::End(0))?;
102 if len < TAIL_LEN as u64 {
103 seek.seek(SeekFrom::Start(0))?;
104 return Ok(None);
105 }
106 seek.seek(SeekFrom::Start(len - TAIL_LEN as u64))?;
107 let mut tail = vec![0u8; TAIL_LEN];
108 let mut filled = 0;
109 while filled < TAIL_LEN {
110 match seek.read(&mut tail[filled..])? {
111 0 => break,
112 n => filled += n,
113 }
114 }
115 seek.seek(SeekFrom::Start(0))?;
116 Ok((filled == TAIL_LEN).then_some(tail))
117}
118
119pub fn probe(mut src: Box<dyn Source>) -> Result<(Vec<u8>, Box<dyn Source>)> {
136 if src.caps().seekable {
137 if let Some(foreign) = read_tail(src.as_mut())?
138 .as_deref()
139 .and_then(foreign_from_tail)
140 {
141 return Err(Error::ForeignFormat(foreign));
142 }
143 let mut prefix = vec![0u8; PROBE_LEN];
144 let Some(seek) = src.as_seek() else {
154 return Err(Error::NotSeekable {
155 format: FormatId::new("<probe>"),
156 });
157 };
158 let mut filled = 0;
159 while filled < PROBE_LEN {
160 match std::io::Read::read(seek, &mut prefix[filled..])? {
161 0 => break,
162 n => filled += n,
163 }
164 }
165 prefix.truncate(filled);
166 std::io::Seek::seek(seek, std::io::SeekFrom::Start(0))?;
167 return Ok((prefix, src));
168 }
169
170 let peek = PeekSource::fill(src, PROBE_LEN)?;
171 let prefix = peek.prefix().to_vec();
172 Ok((prefix, Box::new(peek)))
173}
174
175#[non_exhaustive]
177#[derive(Clone, PartialEq, Eq, Debug)]
178pub enum Chain {
179 Codec { codec: FormatId, inner: Box<Chain> },
181 Container { container: FormatId },
183 Raw,
185}
186
187impl Chain {
188 pub fn container(&self) -> Option<FormatId> {
203 match self {
204 Chain::Container { container } => Some(*container),
205 Chain::Codec { inner, .. } => inner.container(),
206 _ => None,
207 }
208 }
209
210 pub fn outermost_codec(&self) -> Option<FormatId> {
220 match self {
221 Chain::Codec { codec, .. } => Some(*codec),
222 _ => None,
223 }
224 }
225
226 pub fn describe(&self) -> String {
228 match self {
229 Chain::Raw => "raw".to_string(),
230 Chain::Container { container } => container.to_string(),
231 Chain::Codec { codec, inner } => match inner.as_ref() {
232 Chain::Raw => codec.to_string(),
233 other => format!("{} over {}", other.describe(), codec),
234 },
235 }
236 }
237}
238
239fn hex_prefix(bytes: &[u8]) -> String {
240 if bytes.is_empty() {
241 return "empty input".to_string();
242 }
243 bytes
244 .iter()
245 .take(8)
246 .map(|b| format!("{b:02x}"))
247 .collect::<Vec<_>>()
248 .join(" ")
249}
250
251fn extensions(path: &Path) -> Vec<String> {
254 let name = path
255 .file_name()
256 .and_then(|s| s.to_str())
257 .unwrap_or_default();
258 name.split('.')
259 .skip(1)
260 .map(|s| s.to_ascii_lowercase())
261 .collect()
262}
263
264fn inner_from_path(reg: &Registry, path: Option<&Path>, outer: FormatId) -> Chain {
267 let Some(path) = path else {
268 return Chain::Raw;
269 };
270 let exts = extensions(path);
271
272 if let Some(last) = exts.last()
279 && compound_alias(reg, last) == Some(outer)
280 && let Some(id) = reg.by_extension("tar")
281 && reg.container(id).is_some()
282 {
283 return Chain::Container { container: id };
284 }
285
286 if exts.len() >= 2
288 && let Some(id) = reg.by_extension(&exts[exts.len() - 2])
289 && reg.container(id).is_some()
290 {
291 return Chain::Container { container: id };
292 }
293 Chain::Raw
294}
295
296fn compound_alias(reg: &Registry, ext: &str) -> Option<FormatId> {
326 let stripped = ext.strip_prefix(['t', 'T'])?;
327 if stripped.is_empty() {
328 return None;
329 }
330 let stripped = stripped.to_ascii_lowercase();
331 let spelling = COMPOUND_SPELLINGS
332 .iter()
333 .find(|(alias, _)| *alias == stripped)
334 .map_or(stripped.as_str(), |(_, real)| *real);
335 let id = reg.by_extension(spelling)?;
336 reg.container(id).is_none().then_some(id)
339}
340
341const COMPOUND_SPELLINGS: &[(&str, &str)] = &[("bz", "bz2")];
362
363pub fn chain_for_new_path(reg: &Registry, path: &Path) -> Chain {
374 let Some(ext) = path.extension().and_then(|e| e.to_str()) else {
375 return Chain::Raw;
376 };
377 let Some(outer) = reg.by_extension(ext).or_else(|| compound_alias(reg, ext)) else {
378 return Chain::Raw;
379 };
380 if reg.container(outer).is_some() {
382 return Chain::Container { container: outer };
383 }
384 let inner = inner_from_path(reg, Some(path), outer);
387 Chain::Codec {
388 codec: outer,
389 inner: Box::new(inner),
390 }
391}
392
393pub fn resolve_chain(reg: &Registry, path: Option<&Path>, prefix: &[u8]) -> Result<Chain> {
405 resolve_chain_checked(reg, path, prefix).map(|(chain, _)| chain)
406}
407
408#[non_exhaustive]
410#[derive(Clone, Copy, PartialEq, Eq, Debug)]
411pub enum Evidence {
412 Confirmed,
416 Unconfirmed,
420 ByName,
423}
424
425pub fn resolve_chain_checked(
445 reg: &Registry,
446 path: Option<&Path>,
447 prefix: &[u8],
448) -> Result<(Chain, Evidence)> {
449 resolve_checked(reg, path, prefix, prefix.len() < PROBE_LEN)
450}
451
452fn resolve_checked(
456 reg: &Registry,
457 path: Option<&Path>,
458 prefix: &[u8],
459 whole: bool,
460) -> Result<(Chain, Evidence)> {
461 let hits = reg.match_magic(prefix);
462 let confirmed: Vec<FormatId> = hits
465 .iter()
466 .copied()
467 .filter(|id| reg.confirm(*id, prefix, whole) != Some(false))
468 .collect();
469 let (mut candidates, mut evidence) = if confirmed.is_empty() {
470 (hits, Evidence::Unconfirmed)
471 } else {
472 (confirmed, Evidence::Confirmed)
473 };
474
475 if candidates.len() > 1 {
476 let by_ext =
482 path.and_then(|p| extensions(p).iter().rev().find_map(|e| reg.by_extension(e)));
483 match by_ext.filter(|id| candidates.contains(id)) {
484 Some(id) => candidates = vec![id],
485 None => {
486 let top = reg.priority_of(candidates[0]);
487 let tied: Vec<FormatId> = candidates
488 .iter()
489 .copied()
490 .filter(|id| reg.priority_of(*id) == top)
491 .collect();
492 if tied.len() == 1 {
493 candidates = tied;
494 } else {
495 let names: Vec<&str> = tied.iter().map(|id| id.as_str()).collect();
496 return Err(Error::AmbiguousFormat {
497 candidates: names.join(", "),
498 });
499 }
500 }
501 }
502 }
503
504 let outer = match candidates.first() {
505 Some(id) => Some(*id),
506 None => {
507 let by_name =
508 path.and_then(|p| extensions(p).iter().rev().find_map(|e| reg.by_extension(e)));
509 if let Some(id) = by_name {
510 evidence = match reg.confirm(id, prefix, whole) {
511 Some(true) => Evidence::Confirmed,
512 Some(false) => Evidence::Unconfirmed,
513 None => Evidence::ByName,
514 };
515 }
516 by_name
517 }
518 };
519
520 let Some(outer) = outer else {
521 return Err(Error::UnknownFormat {
522 seen: hex_prefix(prefix),
523 });
524 };
525
526 let kind = if reg.container(outer).is_some() {
527 FormatKind::Container
528 } else if reg.codec(outer).is_some() {
529 FormatKind::Codec
530 } else {
531 return Err(Error::FormatNotEnabled(outer));
532 };
533
534 let chain = match kind {
535 FormatKind::Container => Chain::Container { container: outer },
536 FormatKind::Codec => Chain::Codec {
537 codec: outer,
538 inner: Box::new(inner_from_path(reg, path, outer)),
539 },
540 };
541 Ok((chain, evidence))
542}
543
544fn wrap_layers(layers: &[FormatId], innermost: Chain) -> Chain {
547 layers
548 .iter()
549 .rev()
550 .fold(innermost, |inner, codec| Chain::Codec {
551 codec: *codec,
552 inner: Box::new(inner),
553 })
554}
555
556fn build_decoder(
583 reg: &Registry,
584 codec: FormatId,
585 src: Box<dyn Source>,
586 opts: &DecodeOpts,
587) -> Result<Box<dyn Source>> {
588 let decoded = reg
589 .require_codec(codec)?
590 .decoder(src, opts)
591 .map_err(|e| match e {
592 Error::Io(io_err) => Error::from_decode_io(io_err),
593 other => other,
594 })?;
595 Ok(DecodeSideSource::wrap(decoded))
596}
597
598fn decode_through_chain(
599 reg: &Registry,
600 chain: &Chain,
601 src: Box<dyn Source>,
602 opts: &DecodeOpts,
603) -> Result<Box<dyn Source>> {
604 match chain {
605 Chain::Codec { codec, inner } => {
606 let decoded = build_decoder(reg, *codec, src, opts)?;
607 decode_through_chain(reg, inner, decoded, opts)
608 }
609 Chain::Container { .. } | Chain::Raw => Ok(src),
610 }
611}
612
613pub fn resolve_chain_deep(
641 reg: &Registry,
642 path: Option<&Path>,
643 src: Box<dyn Source>,
644) -> Result<(Chain, Box<dyn Source>)> {
645 resolve_chain_deep_with(reg, path, src, &DecodeOpts::default())
646}
647
648pub fn resolve_chain_deep_with(
665 reg: &Registry,
666 path: Option<&Path>,
667 src: Box<dyn Source>,
668 opts: &DecodeOpts,
669) -> Result<(Chain, Box<dyn Source>)> {
670 let (prefix, src) = probe(src)?;
671 let chain = resolve_chain(reg, path, &prefix)?;
672
673 if !matches!(&chain, Chain::Codec { inner, .. } if matches!(inner.as_ref(), Chain::Raw)) {
678 let decoded = decode_through_chain(reg, &chain, src, opts)?;
679 return Ok((chain, decoded));
680 }
681
682 let Chain::Codec { codec, .. } = chain else {
686 unreachable!("guarded above: chain is Chain::Codec with a Raw inner")
687 };
688 let mut layers: Vec<FormatId> = vec![codec];
689 let mut current_codec = codec;
690 let mut src = src;
691 let mut depth = 1usize;
692
693 loop {
694 if depth >= MAX_CHAIN_DEPTH {
695 return Err(Error::ChainTooDeep {
696 depth: MAX_CHAIN_DEPTH,
697 });
698 }
699
700 let decoded = build_decoder(reg, current_codec, src, opts)?;
701 let (next_prefix, next_src) = probe(decoded).map_err(|e| match e {
734 Error::Io(io_err) => Error::from_decode_io(io_err),
735 other => other,
736 })?;
737 depth += 1;
738
739 let next_chain = match resolve_chain(reg, None, &next_prefix) {
740 Ok(chain) => chain,
741 Err(Error::UnknownFormat { .. }) => {
742 return Ok((wrap_layers(&layers, Chain::Raw), next_src));
743 }
744 Err(e) => return Err(e),
745 };
746
747 let Chain::Codec { codec: nested, .. } = next_chain else {
748 return Ok((wrap_layers(&layers, next_chain), next_src));
749 };
750
751 layers.push(nested);
752 current_codec = nested;
753 src = next_src;
754 }
755}
756
757pub fn identify_strict(
810 reg: &Registry,
811 path: Option<&Path>,
812 prefix: &[u8],
813 tail: Option<&[u8]>,
814 opts: &DecodeOpts,
815) -> Option<Chain> {
816 if tail.and_then(foreign_from_tail).is_some() {
817 return None;
818 }
819 let (chain, evidence) = resolve_chain_checked(reg, path, prefix).ok()?;
820 if evidence == Evidence::Unconfirmed {
821 return None;
822 }
823 match chain {
824 Chain::Container { .. } => Some(chain),
825 Chain::Codec { codec, inner } => {
826 let layer = StrictInput {
827 bytes: prefix,
828 cut: prefix.len() == PROBE_LEN,
829 unseen_inner: *inner,
830 };
831 strict_codec_layer(reg, codec, layer, opts, 1)
832 }
833 _ => None,
836 }
837}
838
839pub fn first_unconfirmed_layer(
855 reg: &Registry,
856 chain: &Chain,
857 prefix: &[u8],
858 opts: &DecodeOpts,
859) -> Option<FormatId> {
860 let mut layer = chain;
861 let mut owned: Vec<u8>;
862 let mut bytes = prefix;
863 let mut whole = prefix.len() < PROBE_LEN;
866 for _ in 0..MAX_CHAIN_DEPTH {
867 match layer {
868 Chain::Container { container } => {
869 return (reg.confirm(*container, bytes, whole) == Some(false))
870 .then_some(*container);
871 }
872 Chain::Codec { codec, inner } => {
873 if reg.confirm(*codec, bytes, whole) == Some(false) {
874 return Some(*codec);
875 }
876 if matches!(**inner, Chain::Raw) {
877 return None;
878 }
879 let decoded = decode_prefix(reg, *codec, bytes, opts)?;
880 if decoded.bytes.is_empty() {
881 return None;
882 }
883 whole = decoded.is_whole();
884 owned = decoded.bytes;
885 bytes = &owned;
886 layer = inner;
887 }
888 _ => return None,
889 }
890 }
891 None
892}
893
894struct StrictInput<'a> {
896 bytes: &'a [u8],
897 cut: bool,
899 unseen_inner: Chain,
903}
904
905fn strict_codec_layer(
909 reg: &Registry,
910 codec: FormatId,
911 input: StrictInput<'_>,
912 opts: &DecodeOpts,
913 depth: usize,
914) -> Option<Chain> {
915 let decoded = decode_prefix(reg, codec, input.bytes, opts)?;
916 let whole = decoded.is_whole();
917 let inner = if decoded.bytes.is_empty() {
918 match (input.cut, decoded.failed) {
919 (true, _) => input.unseen_inner,
922 (false, true) => return None,
924 (false, false) => Chain::Raw,
926 }
927 } else {
928 match resolve_checked(reg, None, &decoded.bytes, whole) {
929 Ok((inner @ Chain::Container { .. }, Evidence::Confirmed)) => inner,
930 Ok((Chain::Codec { codec: nested, .. }, Evidence::Confirmed))
931 if depth + 1 < MAX_CHAIN_DEPTH =>
932 {
933 let nested_input = StrictInput {
934 bytes: &decoded.bytes,
935 cut: !whole,
936 unseen_inner: Chain::Raw,
937 };
938 strict_codec_layer(reg, nested, nested_input, opts, depth + 1).unwrap_or(Chain::Raw)
942 }
943 _ if reg.match_magic(&decoded.bytes).is_empty() => match input.unseen_inner {
947 Chain::Container { container }
948 if reg.confirm(container, &decoded.bytes, whole) != Some(false) =>
949 {
950 Chain::Container { container }
951 }
952 _ => Chain::Raw,
953 },
954 _ => Chain::Raw,
955 }
956 };
957 Some(Chain::Codec {
958 codec,
959 inner: Box::new(inner),
960 })
961}
962
963struct DecodedPrefix {
965 bytes: Vec<u8>,
967 failed: bool,
969}
970
971impl DecodedPrefix {
972 fn is_whole(&self) -> bool {
975 !self.failed && self.bytes.len() < PROBE_LEN
976 }
977}
978
979const MAX_INTERRUPTED_RETRIES: usize = 64;
984
985fn decode_prefix(
989 reg: &Registry,
990 codec: FormatId,
991 bytes: &[u8],
992 opts: &DecodeOpts,
993) -> Option<DecodedPrefix> {
994 let src = Box::new(crate::source::ReaderSource::new(std::io::Cursor::new(
997 bytes.to_vec(),
998 )));
999 let mut decoder = build_decoder(reg, codec, src, opts).ok()?;
1000 let mut out = vec![0u8; PROBE_LEN];
1001 let mut filled = 0;
1002 let mut failed = false;
1003 let mut interrupted = 0;
1004 while filled < PROBE_LEN {
1005 match std::io::Read::read(&mut decoder, &mut out[filled..]) {
1006 Ok(0) => break,
1007 Ok(n) => {
1008 filled += n;
1009 interrupted = 0;
1010 }
1011 Err(e)
1012 if e.kind() == std::io::ErrorKind::Interrupted
1013 && interrupted < MAX_INTERRUPTED_RETRIES =>
1014 {
1015 interrupted += 1;
1016 }
1017 Err(_) => {
1018 failed = true;
1019 break;
1020 }
1021 }
1022 }
1023 out.truncate(filled);
1024 Some(DecodedPrefix { bytes: out, failed })
1025}
1026
1027#[cfg(test)]
1028mod tests {
1029 use super::*;
1030 use crate::archive::{Codec, DecodeOpts, EncodeOpts, Sink};
1031 use crate::format::{CodecCaps, FormatId, FormatMeta, MagicRule};
1032 use crate::registry::Registry;
1033 use crate::source::{ReaderSource, Source, StreamOnly};
1034 use crate::testing::{
1035 MOCK_CODEC, MOCK_CONTAINER, MockCodec, MockContainer, SharedBuf, mock_archive_bytes,
1036 };
1037 use std::io;
1038 use std::io::{Read, Write};
1039 use std::path::Path;
1040 use std::sync::Arc;
1041
1042 const GZIP: FormatId = FormatId::new("gzip");
1043 const TAR: FormatId = FormatId::new("tar");
1044
1045 const GZIP_MAGIC: &[MagicRule] = &[MagicRule {
1046 offset: 0,
1047 bytes: &[0x1f, 0x8b],
1048 format: GZIP,
1049 }];
1050 const TAR_MAGIC: &[MagicRule] = &[MagicRule {
1051 offset: 257,
1052 bytes: b"ustar",
1053 format: TAR,
1054 }];
1055
1056 fn registry() -> Registry {
1057 let mut r = Registry::new();
1058 r.register_codec(
1059 Arc::new(MockCodec),
1060 FormatMeta::codec(GZIP, &["gz", "tgz"], GZIP_MAGIC),
1061 );
1062 r.register_container(
1063 Arc::new(MockContainer),
1064 FormatMeta::container(TAR, &["tar"], TAR_MAGIC),
1065 );
1066 r
1067 }
1068
1069 #[test]
1073 fn an_output_path_resolves_to_the_chain_its_name_promises() {
1074 let reg = registry();
1075 let c = |p: &str| chain_for_new_path(®, Path::new(p)).describe();
1076 assert_eq!(c("x.tar"), "tar");
1077 assert_eq!(c("x.gz"), "gzip");
1078 assert_eq!(c("x.tar.gz"), "tar over gzip");
1079 assert_eq!(
1080 c("x.tgz"),
1081 "tar over gzip",
1082 "the compound alias must resolve too"
1083 );
1084 assert_eq!(c("x.TAR.GZ"), "tar over gzip", "extensions are caseless");
1085 assert_eq!(c("x.txt"), "raw");
1086 assert_eq!(c("x"), "raw");
1087 }
1088
1089 #[test]
1097 fn tbz_means_tar_over_bzip2_without_making_plain_bz_writable() {
1098 const BZIP2: FormatId = FormatId::new("bzip2");
1099 const BZIP2_MAGIC: &[MagicRule] = &[MagicRule {
1100 offset: 0,
1101 bytes: b"BZh",
1102 format: BZIP2,
1103 }];
1104 let mut reg = Registry::new();
1105 reg.register_codec(
1107 Arc::new(MockCodec),
1108 FormatMeta::codec(BZIP2, &["bz2"], BZIP2_MAGIC),
1109 );
1110 reg.register_container(
1111 Arc::new(MockContainer),
1112 FormatMeta::container(TAR, &["tar"], TAR_MAGIC),
1113 );
1114
1115 let c = |p: &str| chain_for_new_path(®, Path::new(p)).describe();
1116 assert_eq!(
1117 c("f.tbz"),
1118 "tar over bzip2",
1119 "GNU tar and libarchive both read .tbz as tar.bz2"
1120 );
1121 assert_eq!(c("f.TBZ"), "tar over bzip2", "and extensions are caseless");
1122 assert_eq!(
1123 c("f.tbz2"),
1124 "tar over bzip2",
1125 "the spelling that always worked must keep working"
1126 );
1127 assert_eq!(
1128 c("f.tar.bz2"),
1129 "tar over bzip2",
1130 "and so must the long form"
1131 );
1132
1133 assert_eq!(
1138 c("f.bz"),
1139 "raw",
1140 "`bz` must resolve to nothing outside the t-prefixed compound form"
1141 );
1142
1143 let chain = resolve_chain(®, Some(Path::new("f.tbz")), b"BZh\0").unwrap();
1147 assert_eq!(
1148 chain.describe(),
1149 "tar over bzip2",
1150 "reading a .tbz must agree with writing one"
1151 );
1152 }
1153
1154 #[test]
1157 fn the_outermost_codec_is_the_layer_a_writer_creates_first() {
1158 let reg = registry();
1159 let pair = |p: &str| {
1160 let c = chain_for_new_path(®, Path::new(p));
1161 (c.container(), c.outermost_codec())
1162 };
1163 assert_eq!(pair("x.tar"), (Some(TAR), None));
1164 assert_eq!(pair("x.gz"), (None, Some(GZIP)));
1165 assert_eq!(pair("x.tar.gz"), (Some(TAR), Some(GZIP)));
1166 assert_eq!(pair("x.tgz"), (Some(TAR), Some(GZIP)));
1167 assert_eq!(pair("x.txt"), (None, None));
1168 }
1169
1170 #[test]
1171 fn probe_does_not_consume_the_stream() {
1172 let src: Box<dyn Source> = Box::new(ReaderSource::new(std::io::Cursor::new(
1173 b"HEADERbody".to_vec(),
1174 )));
1175 let (prefix, mut rest) = probe(src).unwrap();
1176 assert_eq!(&prefix[..6], b"HEADER");
1177
1178 let mut all = Vec::new();
1179 rest.read_to_end(&mut all).unwrap();
1180 assert_eq!(all, b"HEADERbody", "the consumer must still see byte zero");
1181 }
1182
1183 #[test]
1184 fn probe_reads_at_most_the_window() {
1185 let big = vec![b'x'; PROBE_LEN * 3];
1186 let src: Box<dyn Source> = Box::new(ReaderSource::new(std::io::Cursor::new(big)));
1187 let (prefix, _) = probe(src).unwrap();
1188 assert_eq!(prefix.len(), PROBE_LEN);
1189 }
1190
1191 #[test]
1192 fn magic_identifies_a_bare_codec_stream() {
1193 let r = registry();
1194 let chain = resolve_chain(&r, Some(Path::new("blob.gz")), &[0x1f, 0x8b, 0x08]).unwrap();
1195 assert_eq!(
1196 chain,
1197 Chain::Codec {
1198 codec: GZIP,
1199 inner: Box::new(Chain::Raw)
1200 }
1201 );
1202 assert_eq!(chain.describe(), "gzip");
1203 }
1204
1205 #[test]
1206 fn a_double_extension_nests_the_container_inside_the_codec() {
1207 let r = registry();
1208 let chain = resolve_chain(&r, Some(Path::new("out.tar.gz")), &[0x1f, 0x8b, 0x08]).unwrap();
1209 assert_eq!(
1210 chain,
1211 Chain::Codec {
1212 codec: GZIP,
1213 inner: Box::new(Chain::Container { container: TAR })
1214 }
1215 );
1216 assert_eq!(chain.describe(), "tar over gzip");
1217 }
1218
1219 #[test]
1220 fn a_codec_on_stdin_resolves_to_raw_inner() {
1221 let r = registry();
1224 let chain = resolve_chain(&r, None, &[0x1f, 0x8b, 0x08]).unwrap();
1225 assert_eq!(
1226 chain,
1227 Chain::Codec {
1228 codec: GZIP,
1229 inner: Box::new(Chain::Raw)
1230 }
1231 );
1232 }
1233
1234 #[test]
1235 fn container_magic_resolves_directly() {
1236 let r = registry();
1237 let mut prefix = vec![0u8; 512];
1238 prefix[257..262].copy_from_slice(b"ustar");
1239 let chain = resolve_chain(&r, Some(Path::new("x.tar")), &prefix).unwrap();
1240 assert_eq!(chain, Chain::Container { container: TAR });
1241 assert_eq!(chain.describe(), "tar");
1242 }
1243
1244 #[test]
1245 fn extension_is_the_fallback_when_magic_says_nothing() {
1246 let r = registry();
1247 let chain = resolve_chain(&r, Some(Path::new("mystery.tar")), b"no magic here").unwrap();
1248 assert_eq!(chain, Chain::Container { container: TAR });
1249 }
1250
1251 #[test]
1257 fn chain_container_sees_through_codec_layers() {
1258 let tar = FormatId::new("tar");
1259 assert_eq!(Chain::Container { container: tar }.container(), Some(tar));
1260 assert_eq!(
1261 Chain::Codec {
1262 codec: GZIP,
1263 inner: Box::new(Chain::Container { container: tar }),
1264 }
1265 .container(),
1266 Some(tar),
1267 "tar over gzip still has a container nobody has opened"
1268 );
1269 assert_eq!(
1270 Chain::Codec {
1271 codec: GZIP,
1272 inner: Box::new(Chain::Raw),
1273 }
1274 .container(),
1275 None,
1276 "a bare codec stream has no container, so an empty warning list is a real finding"
1277 );
1278 assert_eq!(Chain::Raw.container(), None);
1279 }
1280
1281 #[test]
1282 fn tgz_single_extension_still_nests_tar_inside_gzip() {
1283 let r = registry();
1284 let chain = resolve_chain(&r, Some(Path::new("a.tgz")), &[0x1f, 0x8b]).unwrap();
1285 assert_eq!(
1286 chain,
1287 Chain::Codec {
1288 codec: GZIP,
1289 inner: Box::new(Chain::Container { container: TAR })
1290 }
1291 );
1292 }
1293
1294 #[test]
1295 fn a_compound_extension_resolves_without_a_registered_compound_alias() {
1296 let mut r = Registry::new();
1301 r.register_codec(
1302 Arc::new(MockCodec),
1303 FormatMeta::codec(GZIP, &["gz"], GZIP_MAGIC), );
1305 r.register_container(
1306 Arc::new(MockContainer),
1307 FormatMeta::container(TAR, &["tar"], TAR_MAGIC),
1308 );
1309
1310 let chain = resolve_chain(&r, Some(Path::new("a.tgz")), &[0x1f, 0x8b]).unwrap();
1311 assert_eq!(
1312 chain,
1313 Chain::Codec {
1314 codec: GZIP,
1315 inner: Box::new(Chain::Container { container: TAR })
1316 }
1317 );
1318 }
1319
1320 #[test]
1321 fn probe_does_not_consume_a_seekable_stream_either() {
1322 let mut f = tempfile::NamedTempFile::new().unwrap();
1325 std::io::Write::write_all(&mut f, b"HEADERbody").unwrap();
1326 let (_, path) = f.keep().unwrap();
1327 let src: Box<dyn Source> = Box::new(crate::source::FileSource::open(&path).unwrap());
1328
1329 let (prefix, mut rest) = probe(src).unwrap();
1330 assert_eq!(&prefix[..6], b"HEADER");
1331
1332 let mut all = Vec::new();
1333 rest.read_to_end(&mut all).unwrap();
1334 assert_eq!(
1335 all, b"HEADERbody",
1336 "a seekable source must be rewound to byte zero"
1337 );
1338 }
1339
1340 #[test]
1341 fn probe_bounds_the_window_on_a_seekable_stream_too() {
1342 let big = vec![b'x'; PROBE_LEN * 3];
1343 let mut f = tempfile::NamedTempFile::new().unwrap();
1344 std::io::Write::write_all(&mut f, &big).unwrap();
1345 let (_, path) = f.keep().unwrap();
1346 let src: Box<dyn Source> = Box::new(crate::source::FileSource::open(&path).unwrap());
1347
1348 let (prefix, _) = probe(src).unwrap();
1349 assert_eq!(prefix.len(), PROBE_LEN);
1350 }
1351
1352 #[test]
1353 fn unknown_input_names_the_bytes_it_actually_saw() {
1354 let r = registry();
1356 let err = resolve_chain(&r, None, &[0xde, 0xad, 0xbe, 0xef]).unwrap_err();
1357 let msg = err.to_string();
1358 assert!(msg.contains("de ad be ef"), "message was: {msg}");
1359 assert!(matches!(err, crate::Error::UnknownFormat { .. }));
1360 }
1361
1362 #[test]
1363 fn unknown_empty_input_still_produces_a_usable_message() {
1364 let r = registry();
1365 let err = resolve_chain(&r, None, b"").unwrap_err();
1366 assert!(err.to_string().contains("empty"));
1367 }
1368
1369 #[test]
1370 fn equally_ranked_candidates_with_no_extension_are_an_error_not_a_guess() {
1371 const A: &[MagicRule] = &[MagicRule {
1374 offset: 0,
1375 bytes: b"PK",
1376 format: FormatId::new("alpha"),
1377 }];
1378 const B: &[MagicRule] = &[MagicRule {
1379 offset: 0,
1380 bytes: b"PK",
1381 format: FormatId::new("beta"),
1382 }];
1383 let mut r = Registry::new();
1384 r.register_container(
1385 Arc::new(MockContainer),
1386 FormatMeta::container(FormatId::new("alpha"), &["alpha"], A),
1387 );
1388 r.register_container(
1389 Arc::new(MockContainer),
1390 FormatMeta::container(FormatId::new("beta"), &["beta"], B),
1391 );
1392
1393 let err = resolve_chain(&r, None, b"PKrest").unwrap_err();
1394 assert!(matches!(err, crate::Error::AmbiguousFormat { .. }));
1395 let msg = err.to_string();
1396 assert!(
1397 msg.contains("alpha") && msg.contains("beta"),
1398 "must name the candidates: {msg}"
1399 );
1400 assert_eq!(err.exit_code(), 2);
1408 }
1409
1410 #[test]
1411 fn a_higher_priority_candidate_resolves_without_an_extension() {
1412 const A: &[MagicRule] = &[MagicRule {
1413 offset: 0,
1414 bytes: b"PK",
1415 format: FormatId::new("alpha"),
1416 }];
1417 const B: &[MagicRule] = &[MagicRule {
1418 offset: 0,
1419 bytes: b"PK",
1420 format: FormatId::new("beta"),
1421 }];
1422 let mut r = Registry::new();
1423 r.register_container(
1424 Arc::new(MockContainer),
1425 FormatMeta::container(FormatId::new("alpha"), &["alpha"], A).with_priority(-5),
1426 );
1427 r.register_container(
1428 Arc::new(MockContainer),
1429 FormatMeta::container(FormatId::new("beta"), &["beta"], B),
1430 );
1431
1432 let chain = resolve_chain(&r, None, b"PKrest").unwrap();
1433 assert_eq!(
1434 chain,
1435 Chain::Container {
1436 container: FormatId::new("beta")
1437 }
1438 );
1439 }
1440
1441 #[test]
1442 fn an_extension_still_disambiguates_equal_candidates() {
1443 const A: &[MagicRule] = &[MagicRule {
1444 offset: 0,
1445 bytes: b"PK",
1446 format: FormatId::new("alpha"),
1447 }];
1448 const B: &[MagicRule] = &[MagicRule {
1449 offset: 0,
1450 bytes: b"PK",
1451 format: FormatId::new("beta"),
1452 }];
1453 let mut r = Registry::new();
1454 r.register_container(
1455 Arc::new(MockContainer),
1456 FormatMeta::container(FormatId::new("alpha"), &["alpha"], A),
1457 );
1458 r.register_container(
1459 Arc::new(MockContainer),
1460 FormatMeta::container(FormatId::new("beta"), &["beta"], B),
1461 );
1462
1463 let chain = resolve_chain(&r, Some(Path::new("x.beta")), b"PKrest").unwrap();
1464 assert_eq!(
1465 chain,
1466 Chain::Container {
1467 container: FormatId::new("beta")
1468 }
1469 );
1470 }
1471
1472 #[test]
1473 fn an_extension_outside_the_tied_set_does_not_break_the_tie() {
1474 const A: &[MagicRule] = &[MagicRule {
1480 offset: 0,
1481 bytes: b"PK",
1482 format: FormatId::new("alpha"),
1483 }];
1484 const B: &[MagicRule] = &[MagicRule {
1485 offset: 0,
1486 bytes: b"PK",
1487 format: FormatId::new("beta"),
1488 }];
1489 const C: &[MagicRule] = &[MagicRule {
1490 offset: 0,
1491 bytes: b"GZ",
1492 format: FormatId::new("gamma"),
1493 }];
1494 let mut r = Registry::new();
1495 r.register_container(
1496 Arc::new(MockContainer),
1497 FormatMeta::container(FormatId::new("alpha"), &["alpha"], A),
1498 );
1499 r.register_container(
1500 Arc::new(MockContainer),
1501 FormatMeta::container(FormatId::new("beta"), &["beta"], B),
1502 );
1503 r.register_container(
1504 Arc::new(MockContainer),
1505 FormatMeta::container(FormatId::new("gamma"), &["gamma"], C),
1506 );
1507
1508 let err = resolve_chain(&r, Some(Path::new("x.gamma")), b"PKrest").unwrap_err();
1509 assert!(matches!(err, crate::Error::AmbiguousFormat { .. }));
1510 let msg = err.to_string();
1511 assert!(
1512 msg.contains("alpha") && msg.contains("beta"),
1513 "must name the tied candidates: {msg}"
1514 );
1515 assert!(
1516 !msg.contains("gamma"),
1517 "must not name the irrelevant extension: {msg}"
1518 );
1519 }
1520
1521 #[test]
1522 fn an_extension_reaches_a_lower_priority_format_that_shares_the_magic() {
1523 const ZIP_MAGIC: &[MagicRule] = &[MagicRule {
1527 offset: 0,
1528 bytes: b"PK",
1529 format: FormatId::new("zip"),
1530 }];
1531 const APK_MAGIC: &[MagicRule] = &[MagicRule {
1532 offset: 0,
1533 bytes: b"PK",
1534 format: FormatId::new("apk"),
1535 }];
1536 let mut r = Registry::new();
1537 r.register_container(
1538 Arc::new(MockContainer),
1539 FormatMeta::container(FormatId::new("zip"), &["zip"], ZIP_MAGIC),
1540 );
1541 r.register_container(
1542 Arc::new(MockContainer),
1543 FormatMeta::container(FormatId::new("apk"), &["apk"], APK_MAGIC).with_priority(-5),
1544 );
1545
1546 let chain = resolve_chain(&r, None, b"PKrest").unwrap();
1548 assert_eq!(
1549 chain,
1550 Chain::Container {
1551 container: FormatId::new("zip")
1552 }
1553 );
1554
1555 let chain = resolve_chain(&r, Some(Path::new("archive.apk")), b"PKrest").unwrap();
1558 assert_eq!(
1559 chain,
1560 Chain::Container {
1561 container: FormatId::new("apk")
1562 }
1563 );
1564 }
1565
1566 struct LiesAboutSeekability(std::io::Cursor<Vec<u8>>);
1572
1573 impl Read for LiesAboutSeekability {
1574 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
1575 std::io::Read::read(&mut self.0, buf)
1576 }
1577 }
1578
1579 impl Source for LiesAboutSeekability {
1580 fn caps(&self) -> crate::source::SourceCaps {
1581 crate::source::SourceCaps {
1582 seekable: true,
1583 len: Some(self.0.get_ref().len() as u64),
1584 }
1585 }
1586
1587 fn as_seek(&mut self) -> Option<&mut dyn crate::source::SeekRead> {
1588 None
1589 }
1590 }
1591
1592 #[test]
1593 fn a_source_that_claims_seekable_but_has_no_seek_view_errors_instead_of_panicking() {
1594 let src: Box<dyn Source> = Box::new(LiesAboutSeekability(std::io::Cursor::new(
1595 b"payload".to_vec(),
1596 )));
1597 match probe(src) {
1601 Err(err) => assert!(
1602 matches!(err, crate::Error::NotSeekable { .. }),
1603 "must be NotSeekable, not a panic: {err:?}"
1604 ),
1605 Ok(_) => panic!("expected NotSeekable, got Ok"),
1606 }
1607 }
1608
1609 const NESTED_MAGIC: &[u8] = b"MOCK";
1620
1621 struct PrefixedMockCodec;
1622
1623 impl Codec for PrefixedMockCodec {
1624 fn id(&self) -> FormatId {
1625 MOCK_CODEC
1626 }
1627
1628 fn caps(&self) -> CodecCaps {
1629 CodecCaps {
1630 encode: true,
1631 decode: true,
1632 ..Default::default()
1633 }
1634 }
1635
1636 fn decoder(&self, mut src: Box<dyn Source>, _o: &DecodeOpts) -> Result<Box<dyn Source>> {
1637 let mut discard = vec![0u8; NESTED_MAGIC.len()];
1638 src.read_exact(&mut discard)?;
1639 Ok(Box::new(StreamOnly::new(src)))
1640 }
1641
1642 fn encoder(
1643 &self,
1644 mut dst: Box<dyn Write + Send>,
1645 _o: &EncodeOpts,
1646 ) -> Result<Box<dyn Sink>> {
1647 dst.write_all(NESTED_MAGIC)?;
1648 Ok(Box::new(PassthroughSink(dst)))
1649 }
1650 }
1651
1652 struct PassthroughSink(Box<dyn Write + Send>);
1653
1654 impl Write for PassthroughSink {
1655 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
1656 self.0.write(buf)
1657 }
1658 fn flush(&mut self) -> std::io::Result<()> {
1659 self.0.flush()
1660 }
1661 }
1662
1663 impl Sink for PassthroughSink {
1664 fn finish(mut self: Box<Self>) -> Result<()> {
1665 self.0.flush()?;
1666 Ok(())
1667 }
1668 }
1669
1670 fn mock_registry_with_codec_and_container() -> Registry {
1671 const CODEC_MAGIC: &[MagicRule] = &[MagicRule {
1672 offset: 0,
1673 bytes: NESTED_MAGIC,
1674 format: MOCK_CODEC,
1675 }];
1676 const CONTAINER_MAGIC: &[MagicRule] = &[MagicRule {
1677 offset: 0,
1678 bytes: b"ME",
1679 format: MOCK_CONTAINER,
1680 }];
1681 let mut r = Registry::new();
1682 r.register_codec(
1683 Arc::new(PrefixedMockCodec),
1684 FormatMeta::codec(MOCK_CODEC, &["mock"], CODEC_MAGIC),
1685 );
1686 r.register_container(
1687 Arc::new(MockContainer),
1688 FormatMeta::container(MOCK_CONTAINER, &["mar"], CONTAINER_MAGIC),
1689 );
1690 r
1691 }
1692
1693 fn mock_codec_encode(payload: &[u8]) -> Vec<u8> {
1697 let sink = SharedBuf::new();
1698 let mut s = PrefixedMockCodec
1699 .encoder(Box::new(sink.clone()), &EncodeOpts::default())
1700 .unwrap();
1701 s.write_all(payload).unwrap();
1702 s.finish().unwrap();
1703 sink.contents()
1704 }
1705
1706 #[test]
1707 fn a_container_inside_a_codec_resolves_from_the_stream_with_no_path() {
1708 let reg = mock_registry_with_codec_and_container();
1709 let inner = mock_archive_bytes(&[("a.txt", b"alpha")]);
1710 let outer = mock_codec_encode(&inner);
1711
1712 let (chain, _src) = resolve_chain_deep(
1713 ®,
1714 None,
1715 Box::new(ReaderSource::new(io::Cursor::new(outer))),
1716 )
1717 .expect("resolve");
1718
1719 assert_eq!(
1722 chain,
1723 Chain::Codec {
1724 codec: MOCK_CODEC,
1725 inner: Box::new(Chain::Container {
1726 container: MOCK_CONTAINER
1727 }),
1728 },
1729 "a container nested inside a codec must be found by peeking the decoded stream"
1730 );
1731 }
1732
1733 #[test]
1734 fn nesting_beyond_the_depth_bound_is_a_typed_error_naming_the_depth() {
1735 let reg = mock_registry_with_codec_and_container();
1736 let mut bytes = mock_archive_bytes(&[("a.txt", b"alpha")]);
1737 for _ in 0..MAX_CHAIN_DEPTH + 2 {
1738 bytes = mock_codec_encode(&bytes);
1739 }
1740
1741 match resolve_chain_deep(
1747 ®,
1748 None,
1749 Box::new(ReaderSource::new(io::Cursor::new(bytes))),
1750 ) {
1751 Err(Error::ChainTooDeep { depth }) => assert_eq!(depth, MAX_CHAIN_DEPTH),
1752 Err(other) => panic!("expected ChainTooDeep, got {other:?}"),
1753 Ok(_) => panic!("nesting past the bound must be refused"),
1754 }
1755 }
1756
1757 #[test]
1758 fn nesting_beyond_the_depth_bound_is_refused_via_the_path_route_too() {
1759 let reg = mock_registry_with_codec_and_container();
1767 let mut bytes = mock_archive_bytes(&[("a.txt", b"alpha")]);
1768 for _ in 0..MAX_CHAIN_DEPTH + 2 {
1769 bytes = mock_codec_encode(&bytes);
1770 }
1771
1772 match resolve_chain_deep(
1773 ®,
1774 Some(Path::new("nested.mock")),
1775 Box::new(ReaderSource::new(io::Cursor::new(bytes))),
1776 ) {
1777 Err(Error::ChainTooDeep { depth }) => assert_eq!(depth, MAX_CHAIN_DEPTH),
1778 Err(other) => panic!("expected ChainTooDeep, got {other:?}"),
1779 Ok(_) => panic!("nesting past the bound must be refused, path or no path"),
1780 }
1781 }
1782
1783 struct AlwaysInvalidData;
1795
1796 impl Read for AlwaysInvalidData {
1797 fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
1798 Err(std::io::Error::new(
1799 std::io::ErrorKind::InvalidData,
1800 "decoded bytes are malformed",
1801 ))
1802 }
1803 }
1804
1805 impl Source for AlwaysInvalidData {
1806 fn caps(&self) -> crate::source::SourceCaps {
1807 crate::source::SourceCaps {
1808 seekable: false,
1809 len: None,
1810 }
1811 }
1812
1813 fn as_seek(&mut self) -> Option<&mut dyn crate::source::SeekRead> {
1814 None
1815 }
1816 }
1817
1818 struct FailingDecodeCodec;
1829
1830 impl Codec for FailingDecodeCodec {
1831 fn id(&self) -> FormatId {
1832 FormatId::new("always-fails-to-decode")
1833 }
1834
1835 fn caps(&self) -> CodecCaps {
1836 CodecCaps {
1837 encode: false,
1838 decode: true,
1839 ..Default::default()
1840 }
1841 }
1842
1843 fn decoder(&self, _src: Box<dyn Source>, _o: &DecodeOpts) -> Result<Box<dyn Source>> {
1844 Ok(Box::new(AlwaysInvalidData))
1845 }
1846
1847 fn encoder(&self, _dst: Box<dyn Write + Send>, _o: &EncodeOpts) -> Result<Box<dyn Sink>> {
1848 unimplemented!("this test never encodes")
1849 }
1850 }
1851
1852 #[test]
1862 fn a_decoder_side_io_error_while_re_probing_becomes_corrupt() {
1863 let mut reg = Registry::new();
1864 const FAILING: FormatId = FormatId::new("always-fails-to-decode");
1865 const FAILING_MAGIC: &[MagicRule] = &[MagicRule {
1866 offset: 0,
1867 bytes: &[0xab, 0xcd],
1868 format: FAILING,
1869 }];
1870 reg.register_codec(
1871 Arc::new(FailingDecodeCodec),
1872 FormatMeta::codec(FAILING, &["fail"], FAILING_MAGIC),
1873 );
1874
1875 let bytes = vec![0xab, 0xcd, 0x00, 0x00];
1878 match resolve_chain_deep(
1879 ®,
1880 None,
1881 Box::new(ReaderSource::new(io::Cursor::new(bytes))),
1882 ) {
1883 Err(Error::Corrupt(msg)) => assert!(
1884 msg.contains("malformed"),
1885 "must carry the decoder's own message via Error::from_decode_io: {msg}"
1886 ),
1887 Err(other) => panic!("expected Corrupt (a decoder-side read failed), got {other:?}"),
1888 Ok(_) => panic!("a decoder that always fails to read cannot resolve successfully"),
1889 }
1890 }
1891
1892 #[test]
1906 fn a_decoder_that_refuses_at_construction_is_corrupt_not_an_io_failure() {
1907 struct RefusesToBuild;
1908
1909 impl Codec for RefusesToBuild {
1910 fn id(&self) -> FormatId {
1911 FormatId::new("refuses-to-build")
1912 }
1913
1914 fn caps(&self) -> CodecCaps {
1915 CodecCaps {
1916 encode: false,
1917 decode: true,
1918 ..Default::default()
1919 }
1920 }
1921
1922 fn decoder(&self, _src: Box<dyn Source>, _o: &DecodeOpts) -> Result<Box<dyn Source>> {
1923 Err(Error::Io(io::Error::new(
1924 io::ErrorKind::InvalidData,
1925 "the header is malformed and I will not build a decoder for it",
1926 )))
1927 }
1928
1929 fn encoder(
1930 &self,
1931 _dst: Box<dyn Write + Send>,
1932 _o: &EncodeOpts,
1933 ) -> Result<Box<dyn Sink>> {
1934 unimplemented!("this test never encodes")
1935 }
1936 }
1937
1938 const REFUSES: FormatId = FormatId::new("refuses-to-build");
1939 const REFUSES_MAGIC: &[MagicRule] = &[MagicRule {
1940 offset: 0,
1941 bytes: &[0xab, 0xcd],
1942 format: REFUSES,
1943 }];
1944
1945 let mut reg = Registry::new();
1946 reg.register_codec(
1947 Arc::new(RefusesToBuild),
1948 FormatMeta::codec(REFUSES, &["refuse"], REFUSES_MAGIC),
1949 );
1950
1951 for path in [None, Some(Path::new("thing.refuse"))] {
1956 let bytes = vec![0xab, 0xcd, 0x00, 0x00];
1957 match resolve_chain_deep(
1958 ®,
1959 path,
1960 Box::new(ReaderSource::new(io::Cursor::new(bytes))),
1961 ) {
1962 Err(Error::Corrupt(msg)) => assert!(
1963 msg.contains("malformed"),
1964 "must carry the codec's own message: {msg}"
1965 ),
1966 Err(other) => panic!("expected Corrupt for path {path:?}, got {other:?}"),
1967 Ok(_) => panic!("a codec that refuses to build cannot resolve successfully"),
1968 }
1969 }
1970 }
1971
1972 #[test]
1981 fn a_raw_source_io_error_of_the_identical_kind_stays_io_not_corrupt() {
1982 let reg = mock_registry_with_codec_and_container();
1983 match resolve_chain_deep(®, None, Box::new(AlwaysInvalidData)) {
1984 Err(Error::Io(io_err)) => assert_eq!(
1985 io_err.kind(),
1986 std::io::ErrorKind::InvalidData,
1987 "sanity: this must be the same error kind the test above reclassifies, so the \
1988 two tests differ only in WHERE the read happened"
1989 ),
1990 Err(other) => panic!(
1991 "a raw-source failure must stay Io, not become {other:?} — reclassifying it \
1992 would be a lie in the opposite direction from Task 4b's bug"
1993 ),
1994 Ok(_) => panic!("a raw source that always errors cannot resolve successfully"),
1995 }
1996 }
1997
1998 #[test]
1999 fn the_returned_source_is_decoded_through_every_codec_layer_on_both_routes() {
2000 let reg = mock_registry_with_codec_and_container();
2016 let inner = mock_archive_bytes(&[("a.txt", b"alpha")]);
2017 let outer = mock_codec_encode(&inner);
2018
2019 let (chain_with_path, mut src_with_path) = resolve_chain_deep(
2020 ®,
2021 Some(Path::new("thing.mar.mock")),
2022 Box::new(ReaderSource::new(io::Cursor::new(outer.clone()))),
2023 )
2024 .expect("resolve with path");
2025 let (chain_no_path, mut src_no_path) = resolve_chain_deep(
2026 ®,
2027 None,
2028 Box::new(ReaderSource::new(io::Cursor::new(outer))),
2029 )
2030 .expect("resolve without path");
2031
2032 let expected_chain = Chain::Codec {
2033 codec: MOCK_CODEC,
2034 inner: Box::new(Chain::Container {
2035 container: MOCK_CONTAINER,
2036 }),
2037 };
2038 assert_eq!(chain_with_path, expected_chain);
2039 assert_eq!(chain_no_path, expected_chain);
2040
2041 let mut bytes_with_path = Vec::new();
2042 src_with_path.read_to_end(&mut bytes_with_path).unwrap();
2043 let mut bytes_no_path = Vec::new();
2044 src_no_path.read_to_end(&mut bytes_no_path).unwrap();
2045
2046 assert_eq!(
2047 bytes_with_path, inner,
2048 "the path route must decode through the codec layer, not return raw bytes"
2049 );
2050 assert_eq!(
2051 bytes_with_path, bytes_no_path,
2052 "both routes must leave the source in the identical, already-decoded state"
2053 );
2054 }
2055}
2056
2057#[cfg(test)]
2058mod truthful_identification_tests {
2059 use super::*;
2062 use crate::archive::{Codec, DecodeOpts, EncodeOpts, Sink};
2063 use crate::format::{CodecCaps, ConfirmFn, FormatId, FormatMeta, MagicRule};
2064 use crate::registry::Registry;
2065 use crate::source::{ReaderSource, SeekRead, Source, SourceCaps, StreamOnly};
2066 use crate::testing::{
2067 MOCK_CODEC, MOCK_CONTAINER, MockCodec, MockContainer, mock_archive_bytes,
2068 };
2069 use std::io::{self, Read, Write};
2070 use std::path::Path;
2071 use std::sync::Arc;
2072
2073 fn yes(_: &[u8], _: bool) -> bool {
2074 true
2075 }
2076
2077 fn no(_: &[u8], _: bool) -> bool {
2078 false
2079 }
2080
2081 fn koly(version: u32, size: u32) -> Vec<u8> {
2084 let mut t = vec![0u8; TAIL_LEN];
2085 t[0..4].copy_from_slice(b"koly");
2086 t[4..8].copy_from_slice(&version.to_be_bytes());
2087 t[8..12].copy_from_slice(&size.to_be_bytes());
2088 t
2089 }
2090
2091 struct SeekCursor(io::Cursor<Vec<u8>>);
2093
2094 impl Read for SeekCursor {
2095 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
2096 self.0.read(buf)
2097 }
2098 }
2099
2100 impl Source for SeekCursor {
2101 fn caps(&self) -> SourceCaps {
2102 SourceCaps {
2103 seekable: true,
2104 len: Some(self.0.get_ref().len() as u64),
2105 }
2106 }
2107
2108 fn as_seek(&mut self) -> Option<&mut dyn SeekRead> {
2109 Some(&mut self.0)
2110 }
2111 }
2112
2113 fn zlib_ish_image() -> Vec<u8> {
2114 let mut bytes = vec![0x78, 0x9c];
2115 bytes.extend(std::iter::repeat_n(0xa5, 1000));
2116 bytes.extend(koly(4, 512));
2117 bytes
2118 }
2119
2120 #[test]
2121 fn foreign_tail_needs_signature_version_and_size() {
2122 assert_eq!(
2123 foreign_from_tail(&koly(4, 512)),
2124 Some(Foreign::AppleDiskImage)
2125 );
2126 assert_eq!(foreign_from_tail(&koly(3, 512)), None, "version 3");
2127 assert_eq!(foreign_from_tail(&koly(4, 256)), None, "header size 256");
2128 assert_eq!(foreign_from_tail(&koly(4, 512)[..511]), None, "511 bytes");
2129 let mut kolx = koly(4, 512);
2130 kolx[3] = b'x';
2131 assert_eq!(foreign_from_tail(&kolx), None, "kolx");
2132 assert_eq!(
2133 Foreign::AppleDiskImage.to_string(),
2134 "an Apple disk image (UDIF)"
2135 );
2136 }
2137
2138 #[test]
2139 fn probe_refuses_a_seekable_source_with_a_koly_tail() {
2140 let src: Box<dyn Source> = Box::new(SeekCursor(io::Cursor::new(zlib_ish_image())));
2141 match probe(src) {
2142 Err(e) => {
2143 assert!(
2144 matches!(e, Error::ForeignFormat(Foreign::AppleDiskImage)),
2145 "{e:?}"
2146 );
2147 assert_eq!(e.exit_code(), 3);
2148 assert_eq!(
2149 e.to_string(),
2150 "this is an Apple disk image (UDIF), which stuffr cannot open"
2151 );
2152 }
2153 Ok(_) => panic!("a koly tail must be refused"),
2154 }
2155 }
2156
2157 #[test]
2158 fn probe_does_not_read_the_tail_of_a_pipe() {
2159 let src: Box<dyn Source> = Box::new(ReaderSource::new(io::Cursor::new(zlib_ish_image())));
2160 let Ok((prefix, _)) = probe(src) else {
2161 panic!("a pipe has no tail to read, so it must probe as before");
2162 };
2163 assert_eq!(&prefix[..2], &[0x78, 0x9c]);
2164 }
2165
2166 #[test]
2167 fn read_tail_leaves_a_seekable_source_at_zero() {
2168 let mut bytes = b"HEAD".to_vec();
2169 bytes.extend(vec![7u8; 600]);
2170 let mut src = SeekCursor(io::Cursor::new(bytes.clone()));
2171 let tail = read_tail(&mut src)
2172 .unwrap()
2173 .expect("600+ bytes have a tail");
2174 assert_eq!(tail.len(), TAIL_LEN);
2175 assert_eq!(tail, bytes[bytes.len() - TAIL_LEN..]);
2176 let mut all = Vec::new();
2177 src.read_to_end(&mut all).unwrap();
2178 assert_eq!(all, bytes, "the position must be back at byte zero");
2179
2180 let mut short = SeekCursor(io::Cursor::new(b"tiny".to_vec()));
2182 assert_eq!(read_tail(&mut short).unwrap(), None);
2183 let mut all = Vec::new();
2184 short.read_to_end(&mut all).unwrap();
2185 assert_eq!(all, b"tiny");
2186
2187 let mut pipe = ReaderSource::new(io::Cursor::new(bytes));
2189 assert_eq!(read_tail(&mut pipe).unwrap(), None);
2190 }
2191
2192 const ALPHA: FormatId = FormatId::new("alpha");
2193 const BETA: FormatId = FormatId::new("beta");
2194 const SHARED_MAGIC: &[MagicRule] = &[
2195 MagicRule {
2196 offset: 0,
2197 bytes: b"SH",
2198 format: ALPHA,
2199 },
2200 MagicRule {
2201 offset: 0,
2202 bytes: b"SH",
2203 format: BETA,
2204 },
2205 ];
2206
2207 #[test]
2208 fn confirmed_candidate_beats_an_unconfirmed_one() {
2209 for (alpha_ok, winner) in [(true, ALPHA), (false, BETA)] {
2213 let mut r = Registry::new();
2214 let (a, b): (ConfirmFn, ConfirmFn) = if alpha_ok { (yes, no) } else { (no, yes) };
2215 r.register_codec(
2216 Arc::new(MockCodec),
2217 FormatMeta::codec(ALPHA, &[], SHARED_MAGIC).with_confirm(a),
2218 );
2219 r.register_codec(
2220 Arc::new(MockCodec),
2221 FormatMeta::codec(BETA, &[], SHARED_MAGIC).with_confirm(b),
2222 );
2223 let (chain, ev) = resolve_chain_checked(&r, None, b"SH..").expect("no ambiguity");
2224 assert_eq!(chain.outermost_codec(), Some(winner));
2225 assert_eq!(ev, Evidence::Confirmed);
2226 }
2227 }
2228
2229 #[test]
2230 fn a_format_with_no_confirm_function_counts_as_confirmed() {
2231 let mut r = Registry::new();
2232 r.register_codec(
2233 Arc::new(MockCodec),
2234 FormatMeta::codec(ALPHA, &[], SHARED_MAGIC).with_confirm(no),
2235 );
2236 r.register_codec(
2237 Arc::new(MockCodec),
2238 FormatMeta::codec(BETA, &[], SHARED_MAGIC),
2239 );
2240 let (chain, ev) = resolve_chain_checked(&r, None, b"SH..").unwrap();
2241 assert_eq!(chain.outermost_codec(), Some(BETA));
2242 assert_eq!(ev, Evidence::Confirmed);
2243 }
2244
2245 #[test]
2246 fn unconfirmed_magic_still_resolves_leniently() {
2247 let mut r = Registry::new();
2248 r.register_codec(
2249 Arc::new(MockCodec),
2250 FormatMeta::codec(ALPHA, &["al"], &SHARED_MAGIC[..1]).with_confirm(no),
2251 );
2252 let chain = resolve_chain(&r, None, b"SH..").expect("lenient resolution keeps it");
2253 assert_eq!(chain.outermost_codec(), Some(ALPHA));
2254 let (_, ev) = resolve_chain_checked(&r, None, b"SH..").unwrap();
2255 assert_eq!(ev, Evidence::Unconfirmed);
2256 }
2257
2258 #[test]
2259 fn extension_without_confirm_is_by_name() {
2260 let mut r = Registry::new();
2261 r.register_codec(Arc::new(MockCodec), FormatMeta::codec(ALPHA, &["al"], &[]));
2262 let (chain, ev) = resolve_chain_checked(&r, Some(Path::new("x.al")), b"whatever").unwrap();
2263 assert_eq!(chain.outermost_codec(), Some(ALPHA));
2264 assert_eq!(ev, Evidence::ByName);
2265 }
2266
2267 #[test]
2268 fn extension_with_failing_confirm_is_unconfirmed() {
2269 let mut r = Registry::new();
2270 r.register_codec(
2271 Arc::new(MockCodec),
2272 FormatMeta::codec(ALPHA, &["al"], &[]).with_confirm(no),
2273 );
2274 let (_, ev) = resolve_chain_checked(&r, Some(Path::new("x.al")), b"whatever").unwrap();
2275 assert_eq!(ev, Evidence::Unconfirmed);
2276
2277 let mut r = Registry::new();
2278 r.register_codec(
2279 Arc::new(MockCodec),
2280 FormatMeta::codec(ALPHA, &["al"], &[]).with_confirm(yes),
2281 );
2282 let (_, ev) = resolve_chain_checked(&r, Some(Path::new("x.al")), b"whatever").unwrap();
2283 assert_eq!(ev, Evidence::Confirmed);
2284 }
2285
2286 #[test]
2287 fn errors_are_unchanged() {
2288 let r = Registry::new();
2289 assert!(matches!(
2290 resolve_chain_checked(&r, None, b"??"),
2291 Err(Error::UnknownFormat { .. })
2292 ));
2293 let mut r = Registry::new();
2294 r.register_codec(
2295 Arc::new(MockCodec),
2296 FormatMeta::codec(ALPHA, &[], SHARED_MAGIC).with_confirm(no),
2297 );
2298 r.register_codec(
2299 Arc::new(MockCodec),
2300 FormatMeta::codec(BETA, &[], SHARED_MAGIC).with_confirm(no),
2301 );
2302 assert!(matches!(
2303 resolve_chain_checked(&r, None, b"SH"),
2304 Err(Error::AmbiguousFormat { .. })
2305 ));
2306 }
2307
2308 const LAYER_MAGIC: &[u8] = b"MOCK";
2311
2312 struct LayerCodec;
2316
2317 impl Codec for LayerCodec {
2318 fn id(&self) -> FormatId {
2319 MOCK_CODEC
2320 }
2321
2322 fn caps(&self) -> CodecCaps {
2323 CodecCaps::round_trip()
2324 }
2325
2326 fn decoder(&self, mut src: Box<dyn Source>, _o: &DecodeOpts) -> Result<Box<dyn Source>> {
2327 let mut discard = [0u8; 4];
2328 src.read_exact(&mut discard)?;
2329 Ok(Box::new(StreamOnly::new(src)))
2330 }
2331
2332 fn encoder(&self, _dst: Box<dyn Write + Send>, _o: &EncodeOpts) -> Result<Box<dyn Sink>> {
2333 unreachable!("identification never encodes")
2334 }
2335 }
2336
2337 fn mock_archive_confirms(p: &[u8], _whole: bool) -> bool {
2340 p.len() >= 4 && {
2341 let n = u16::from_le_bytes([p[2], p[3]]) as usize;
2342 n > 0 && 4 + n <= p.len()
2343 }
2344 }
2345
2346 fn strict_registry() -> Registry {
2347 const CODEC_MAGIC: &[MagicRule] = &[MagicRule {
2348 offset: 0,
2349 bytes: LAYER_MAGIC,
2350 format: MOCK_CODEC,
2351 }];
2352 const CONTAINER_MAGIC: &[MagicRule] = &[MagicRule {
2353 offset: 0,
2354 bytes: b"ME",
2355 format: MOCK_CONTAINER,
2356 }];
2357 let mut r = Registry::new();
2358 r.register_codec(
2359 Arc::new(LayerCodec),
2360 FormatMeta::codec(MOCK_CODEC, &["mock"], CODEC_MAGIC)
2361 .with_confirm(|p, _| p.starts_with(LAYER_MAGIC)),
2362 );
2363 r.register_container(
2364 Arc::new(MockContainer),
2365 FormatMeta::container(MOCK_CONTAINER, &["mar"], CONTAINER_MAGIC)
2366 .with_confirm(mock_archive_confirms),
2367 );
2368 r
2369 }
2370
2371 fn layered(layers: usize, inner: &[u8]) -> Vec<u8> {
2372 let mut out = LAYER_MAGIC.repeat(layers);
2373 out.extend_from_slice(inner);
2374 out
2375 }
2376
2377 fn strict(
2378 r: &Registry,
2379 path: Option<&str>,
2380 prefix: &[u8],
2381 tail: Option<&[u8]>,
2382 ) -> Option<Chain> {
2383 identify_strict(r, path.map(Path::new), prefix, tail, &DecodeOpts::default())
2384 }
2385
2386 #[test]
2387 fn identify_strict_rejects_unconfirmed_and_foreign() {
2388 let r = strict_registry();
2389 assert_eq!(
2390 strict(&r, None, b"ME\xff\xffabc", None),
2391 None,
2392 "a magic hit whose confirmation fails is not openable"
2393 );
2394 let archive = mock_archive_bytes(&[("a.txt", b"alpha")]);
2395 assert_eq!(
2396 strict(&r, None, &archive, None),
2397 Some(Chain::Container {
2398 container: MOCK_CONTAINER
2399 })
2400 );
2401 assert_eq!(
2402 strict(&r, None, &archive, Some(&koly(4, 512))),
2403 None,
2404 "a koly tail outranks a confirmed prefix"
2405 );
2406 assert_eq!(strict(&r, None, b"nothing", None), None, "unknown");
2407 }
2408
2409 #[test]
2410 fn identify_strict_decodes_the_inner_layer_from_the_prefix() {
2411 let r = strict_registry();
2412 let archive = mock_archive_bytes(&[("a.txt", b"alpha")]);
2413 assert_eq!(
2414 strict(&r, None, &layered(1, &archive), None),
2415 Some(Chain::Codec {
2416 codec: MOCK_CODEC,
2417 inner: Box::new(Chain::Container {
2418 container: MOCK_CONTAINER
2419 }),
2420 })
2421 );
2422 assert_eq!(
2423 strict(&r, Some("x.mar.mock"), &layered(1, b"ME\xff\xffabc"), None),
2424 Some(Chain::Codec {
2425 codec: MOCK_CODEC,
2426 inner: Box::new(Chain::Raw),
2427 }),
2428 "an inner layer failing confirmation is Raw, whatever the path says"
2429 );
2430 }
2431
2432 #[test]
2433 fn identify_strict_bounds_the_nesting() {
2434 let r = strict_registry();
2435 let archive = mock_archive_bytes(&[("a.txt", b"alpha")]);
2436 let depth = |c: &Chain| {
2437 let mut n = 0;
2438 let mut c = c;
2439 while let Chain::Codec { inner, .. } = c {
2440 n += 1;
2441 c = inner;
2442 }
2443 n
2444 };
2445 let three = strict(&r, None, &layered(3, &archive), None).unwrap();
2446 assert_eq!(depth(&three), 3);
2447 assert_eq!(three.container(), Some(MOCK_CONTAINER));
2448 let many = strict(&r, None, &layered(10, &archive), None).unwrap();
2449 assert!(depth(&many) < MAX_CHAIN_DEPTH, "{many:?}");
2450 assert_eq!(many.container(), None, "stopped with Raw at the bound");
2451 }
2452
2453 struct Scripted {
2455 yields: &'static [u8],
2456 then_error: bool,
2457 }
2458
2459 struct ScriptedSource {
2460 rest: &'static [u8],
2461 then_error: bool,
2462 }
2463
2464 impl Read for ScriptedSource {
2465 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
2466 if self.rest.is_empty() {
2467 return if self.then_error {
2468 Err(io::Error::new(io::ErrorKind::InvalidData, "bad stream"))
2469 } else {
2470 Ok(0)
2471 };
2472 }
2473 let n = buf.len().min(self.rest.len());
2474 buf[..n].copy_from_slice(&self.rest[..n]);
2475 self.rest = &self.rest[n..];
2476 Ok(n)
2477 }
2478 }
2479
2480 impl Source for ScriptedSource {
2481 fn caps(&self) -> SourceCaps {
2482 SourceCaps::default()
2483 }
2484
2485 fn as_seek(&mut self) -> Option<&mut dyn SeekRead> {
2486 None
2487 }
2488 }
2489
2490 impl Codec for Scripted {
2491 fn id(&self) -> FormatId {
2492 MOCK_CODEC
2493 }
2494
2495 fn caps(&self) -> CodecCaps {
2496 CodecCaps::decode_only()
2497 }
2498
2499 fn decoder(&self, _src: Box<dyn Source>, _o: &DecodeOpts) -> Result<Box<dyn Source>> {
2500 Ok(Box::new(ScriptedSource {
2501 rest: self.yields,
2502 then_error: self.then_error,
2503 }))
2504 }
2505
2506 fn encoder(&self, _dst: Box<dyn Write + Send>, _o: &EncodeOpts) -> Result<Box<dyn Sink>> {
2507 unreachable!("identification never encodes")
2508 }
2509 }
2510
2511 fn scripted_registry(yields: &'static [u8], then_error: bool) -> Registry {
2512 let mut r = strict_registry();
2513 r.register_codec(
2514 Arc::new(Scripted { yields, then_error }),
2515 FormatMeta::codec(
2516 MOCK_CODEC,
2517 &["mock"],
2518 &[MagicRule {
2519 offset: 0,
2520 bytes: LAYER_MAGIC,
2521 format: MOCK_CODEC,
2522 }],
2523 )
2524 .with_confirm(yes),
2525 );
2526 r
2527 }
2528
2529 #[test]
2530 fn identify_strict_drops_a_codec_that_decodes_nothing() {
2531 let raw = Some(Chain::Codec {
2532 codec: MOCK_CODEC,
2533 inner: Box::new(Chain::Raw),
2534 });
2535 assert_eq!(
2536 strict(&scripted_registry(b"", true), None, b"MOCKjunk", None),
2537 None,
2538 "an error before the first byte: nothing here decodes"
2539 );
2540 assert_eq!(
2541 strict(&scripted_registry(b"", false), None, b"MOCKjunk", None),
2542 raw,
2543 "a clean, empty stream is still a stream"
2544 );
2545 assert_eq!(
2546 strict(
2547 &scripted_registry(b"plain text", true),
2548 None,
2549 b"MOCKjunk",
2550 None
2551 ),
2552 raw,
2553 "what arrived before the error is kept"
2554 );
2555 }
2556
2557 struct NeedsMore;
2562
2563 const NEEDS: usize = PROBE_LEN * 2;
2564 const SLOW: FormatId = FormatId::new("slow");
2565 const TAR: FormatId = FormatId::new("tar");
2566
2567 impl Codec for NeedsMore {
2568 fn id(&self) -> FormatId {
2569 SLOW
2570 }
2571
2572 fn caps(&self) -> CodecCaps {
2573 CodecCaps::decode_only()
2574 }
2575
2576 fn decoder(&self, mut src: Box<dyn Source>, _o: &DecodeOpts) -> Result<Box<dyn Source>> {
2577 let mut all = Vec::new();
2578 src.read_to_end(&mut all)?;
2579 Ok(Box::new(ScriptedSource {
2580 rest: if all.len() >= NEEDS { b"payload" } else { b"" },
2581 then_error: all.len() < NEEDS,
2582 }))
2583 }
2584
2585 fn encoder(&self, _dst: Box<dyn Write + Send>, _o: &EncodeOpts) -> Result<Box<dyn Sink>> {
2586 unreachable!("identification never encodes")
2587 }
2588 }
2589
2590 fn needs_more_registry() -> Registry {
2591 let mut r = strict_registry();
2592 r.register_codec(
2593 Arc::new(NeedsMore),
2594 FormatMeta::codec(
2595 SLOW,
2596 &["slow"],
2597 &[MagicRule {
2598 offset: 0,
2599 bytes: b"SLOW",
2600 format: SLOW,
2601 }],
2602 )
2603 .with_confirm(yes),
2604 );
2605 r.register_container(
2606 Arc::new(MockContainer),
2607 FormatMeta::container(TAR, &["tar"], &[]),
2608 );
2609 r
2610 }
2611
2612 fn slow_prefix(len: usize) -> Vec<u8> {
2613 let mut p = b"SLOW".to_vec();
2614 p.resize(len, 0x5a);
2615 p
2616 }
2617
2618 #[test]
2619 fn a_cut_off_prefix_that_decodes_nothing_keeps_the_codec_over_the_paths_inner() {
2620 let r = needs_more_registry();
2621 assert_eq!(
2622 strict(&r, Some("big.tar.slow"), &slow_prefix(PROBE_LEN), None),
2623 Some(Chain::Codec {
2624 codec: SLOW,
2625 inner: Box::new(Chain::Container { container: TAR }),
2626 }),
2627 "the window ran out, not the file: as 0.21.0 listed it"
2628 );
2629 assert_eq!(
2630 strict(&r, Some("big.slow"), &slow_prefix(PROBE_LEN), None),
2631 Some(Chain::Codec {
2632 codec: SLOW,
2633 inner: Box::new(Chain::Raw),
2634 }),
2635 );
2636 }
2637
2638 #[test]
2639 fn a_whole_file_that_decodes_nothing_before_an_error_is_not_openable() {
2640 let r = needs_more_registry();
2641 assert_eq!(
2642 strict(&r, Some("small.tar.slow"), &slow_prefix(100), None),
2643 None,
2644 "the prefix is the whole file, and it decodes to nothing but an error"
2645 );
2646 }
2647
2648 #[test]
2649 fn an_extension_naming_a_refused_hit_falls_to_the_confirmed_ones() {
2650 const ZIP: FormatId = FormatId::new("zip");
2651 const JAR: FormatId = FormatId::new("jar");
2652 const APK: FormatId = FormatId::new("apk");
2653 const PK: &[MagicRule] = &[
2654 MagicRule {
2655 offset: 0,
2656 bytes: b"PK",
2657 format: ZIP,
2658 },
2659 MagicRule {
2660 offset: 0,
2661 bytes: b"PK",
2662 format: JAR,
2663 },
2664 MagicRule {
2665 offset: 0,
2666 bytes: b"PK",
2667 format: APK,
2668 },
2669 ];
2670 let mut r = Registry::new();
2671 r.register_container(
2672 Arc::new(MockContainer),
2673 FormatMeta::container(ZIP, &["zip"], PK)
2674 .with_priority(1)
2675 .with_confirm(yes),
2676 );
2677 r.register_container(
2678 Arc::new(MockContainer),
2679 FormatMeta::container(JAR, &["jar"], PK).with_confirm(yes),
2680 );
2681 r.register_container(
2682 Arc::new(MockContainer),
2683 FormatMeta::container(APK, &["apk"], PK).with_confirm(no),
2684 );
2685 let (chain, ev) = resolve_chain_checked(&r, Some(Path::new("x.apk")), b"PK..").unwrap();
2686 assert_eq!(chain, Chain::Container { container: ZIP });
2687 assert_eq!(ev, Evidence::Confirmed);
2688 }
2689 fn eight_when_whole(p: &[u8], whole: bool) -> bool {
2693 !whole || p.len() >= 8
2694 }
2695
2696 #[test]
2697 fn a_probe_prefix_short_of_the_window_is_confirmed_as_the_whole_stream() {
2698 let mut r = Registry::new();
2699 r.register_codec(
2700 Arc::new(MockCodec),
2701 FormatMeta::codec(ALPHA, &[], &SHARED_MAGIC[..1]).with_confirm(eight_when_whole),
2702 );
2703 let ev = |prefix: &[u8]| resolve_chain_checked(&r, None, prefix).unwrap().1;
2704 assert_eq!(ev(b"SH"), Evidence::Unconfirmed, "a two-byte file is whole");
2705 assert_eq!(ev(b"SH345678"), Evidence::Confirmed);
2706 let mut window = vec![0u8; PROBE_LEN];
2708 window[..2].copy_from_slice(b"SH");
2709 assert_eq!(ev(&window), Evidence::Confirmed);
2710 }
2711
2712 const PLAIN: FormatId = FormatId::new("plain");
2713 const WHOLE: FormatId = FormatId::new("wholecheck");
2714
2715 fn layered_registry(yields: &'static [u8], then_error: bool) -> Registry {
2718 let mut r = scripted_registry(yields, then_error);
2719 r.register_container(
2720 Arc::new(MockContainer),
2721 FormatMeta::container(PLAIN, &["pln"], &[]).with_confirm(|p, _| p.starts_with(b"PL")),
2722 );
2723 r.register_container(
2724 Arc::new(MockContainer),
2725 FormatMeta::container(
2726 WHOLE,
2727 &["wh"],
2728 &[MagicRule {
2729 offset: 0,
2730 bytes: b"WH",
2731 format: WHOLE,
2732 }],
2733 )
2734 .with_confirm(eight_when_whole),
2735 );
2736 r
2737 }
2738
2739 fn over(inner: Chain) -> Option<Chain> {
2740 Some(Chain::Codec {
2741 codec: MOCK_CODEC,
2742 inner: Box::new(inner),
2743 })
2744 }
2745
2746 #[test]
2747 fn a_decoded_layer_is_whole_only_when_it_ended_cleanly_inside_the_window() {
2748 let r = layered_registry(b"WH", false);
2750 assert_eq!(strict(&r, None, b"MOCKjunk", None), over(Chain::Raw));
2751 let chain = over(Chain::Container { container: WHOLE }).unwrap();
2752 assert_eq!(
2753 first_unconfirmed_layer(&r, &chain, b"MOCKjunk", &DecodeOpts::default()),
2754 Some(WHOLE)
2755 );
2756 let r = layered_registry(b"WH", true);
2758 assert_eq!(
2759 strict(&r, None, b"MOCKjunk", None),
2760 over(Chain::Container { container: WHOLE })
2761 );
2762 assert_eq!(
2763 first_unconfirmed_layer(&r, &chain, b"MOCKjunk", &DecodeOpts::default()),
2764 None
2765 );
2766 }
2767
2768 #[test]
2769 fn decoded_bytes_with_no_magic_take_the_paths_container_when_it_confirms() {
2770 let plain = Chain::Container { container: PLAIN };
2771 let r = layered_registry(b"PLAIN DATA", false);
2772 assert_eq!(
2773 strict(&r, Some("x.pln.mock"), b"MOCKjunk", None),
2774 over(plain.clone()),
2775 "a magicless inner layer the name gives and the bytes confirm"
2776 );
2777 assert_eq!(
2778 strict(&r, None, b"MOCKjunk", None),
2779 over(Chain::Raw),
2780 "no name"
2781 );
2782 assert_eq!(
2783 strict(&r, Some("x.mock"), b"MOCKjunk", None),
2784 over(Chain::Raw),
2785 "a name with nothing inside"
2786 );
2787 let r = layered_registry(b"just text", false);
2788 assert_eq!(
2789 strict(&r, Some("x.pln.mock"), b"MOCKjunk", None),
2790 over(Chain::Raw),
2791 "the name alone is not enough when the check refuses the bytes"
2792 );
2793 let (chain, _) =
2795 resolve_chain_checked(&r, Some(Path::new("x.pln.mock")), b"MOCKjunk").unwrap();
2796 assert_eq!(chain, over(plain).unwrap());
2797 assert_eq!(
2798 first_unconfirmed_layer(&r, &chain, b"MOCKjunk", &DecodeOpts::default()),
2799 Some(PLAIN)
2800 );
2801 }
2802}