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<'a>(
1637 &self,
1638 mut src: Box<dyn Source + 'a>,
1639 _o: &DecodeOpts,
1640 ) -> Result<Box<dyn Source + 'a>> {
1641 let mut discard = vec![0u8; NESTED_MAGIC.len()];
1642 src.read_exact(&mut discard)?;
1643 Ok(Box::new(StreamOnly::new(src)))
1644 }
1645
1646 fn encoder(
1647 &self,
1648 mut dst: Box<dyn Write + Send>,
1649 _o: &EncodeOpts,
1650 ) -> Result<Box<dyn Sink>> {
1651 dst.write_all(NESTED_MAGIC)?;
1652 Ok(Box::new(PassthroughSink(dst)))
1653 }
1654 }
1655
1656 struct PassthroughSink(Box<dyn Write + Send>);
1657
1658 impl Write for PassthroughSink {
1659 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
1660 self.0.write(buf)
1661 }
1662 fn flush(&mut self) -> std::io::Result<()> {
1663 self.0.flush()
1664 }
1665 }
1666
1667 impl Sink for PassthroughSink {
1668 fn finish(mut self: Box<Self>) -> Result<()> {
1669 self.0.flush()?;
1670 Ok(())
1671 }
1672 }
1673
1674 fn mock_registry_with_codec_and_container() -> Registry {
1675 const CODEC_MAGIC: &[MagicRule] = &[MagicRule {
1676 offset: 0,
1677 bytes: NESTED_MAGIC,
1678 format: MOCK_CODEC,
1679 }];
1680 const CONTAINER_MAGIC: &[MagicRule] = &[MagicRule {
1681 offset: 0,
1682 bytes: b"ME",
1683 format: MOCK_CONTAINER,
1684 }];
1685 let mut r = Registry::new();
1686 r.register_codec(
1687 Arc::new(PrefixedMockCodec),
1688 FormatMeta::codec(MOCK_CODEC, &["mock"], CODEC_MAGIC),
1689 );
1690 r.register_container(
1691 Arc::new(MockContainer),
1692 FormatMeta::container(MOCK_CONTAINER, &["mar"], CONTAINER_MAGIC),
1693 );
1694 r
1695 }
1696
1697 fn mock_codec_encode(payload: &[u8]) -> Vec<u8> {
1701 let sink = SharedBuf::new();
1702 let mut s = PrefixedMockCodec
1703 .encoder(Box::new(sink.clone()), &EncodeOpts::default())
1704 .unwrap();
1705 s.write_all(payload).unwrap();
1706 s.finish().unwrap();
1707 sink.contents()
1708 }
1709
1710 #[test]
1711 fn a_container_inside_a_codec_resolves_from_the_stream_with_no_path() {
1712 let reg = mock_registry_with_codec_and_container();
1713 let inner = mock_archive_bytes(&[("a.txt", b"alpha")]);
1714 let outer = mock_codec_encode(&inner);
1715
1716 let (chain, _src) = resolve_chain_deep(
1717 ®,
1718 None,
1719 Box::new(ReaderSource::new(io::Cursor::new(outer))),
1720 )
1721 .expect("resolve");
1722
1723 assert_eq!(
1726 chain,
1727 Chain::Codec {
1728 codec: MOCK_CODEC,
1729 inner: Box::new(Chain::Container {
1730 container: MOCK_CONTAINER
1731 }),
1732 },
1733 "a container nested inside a codec must be found by peeking the decoded stream"
1734 );
1735 }
1736
1737 #[test]
1738 fn nesting_beyond_the_depth_bound_is_a_typed_error_naming_the_depth() {
1739 let reg = mock_registry_with_codec_and_container();
1740 let mut bytes = mock_archive_bytes(&[("a.txt", b"alpha")]);
1741 for _ in 0..MAX_CHAIN_DEPTH + 2 {
1742 bytes = mock_codec_encode(&bytes);
1743 }
1744
1745 match resolve_chain_deep(
1751 ®,
1752 None,
1753 Box::new(ReaderSource::new(io::Cursor::new(bytes))),
1754 ) {
1755 Err(Error::ChainTooDeep { depth }) => assert_eq!(depth, MAX_CHAIN_DEPTH),
1756 Err(other) => panic!("expected ChainTooDeep, got {other:?}"),
1757 Ok(_) => panic!("nesting past the bound must be refused"),
1758 }
1759 }
1760
1761 #[test]
1762 fn nesting_beyond_the_depth_bound_is_refused_via_the_path_route_too() {
1763 let reg = mock_registry_with_codec_and_container();
1771 let mut bytes = mock_archive_bytes(&[("a.txt", b"alpha")]);
1772 for _ in 0..MAX_CHAIN_DEPTH + 2 {
1773 bytes = mock_codec_encode(&bytes);
1774 }
1775
1776 match resolve_chain_deep(
1777 ®,
1778 Some(Path::new("nested.mock")),
1779 Box::new(ReaderSource::new(io::Cursor::new(bytes))),
1780 ) {
1781 Err(Error::ChainTooDeep { depth }) => assert_eq!(depth, MAX_CHAIN_DEPTH),
1782 Err(other) => panic!("expected ChainTooDeep, got {other:?}"),
1783 Ok(_) => panic!("nesting past the bound must be refused, path or no path"),
1784 }
1785 }
1786
1787 struct AlwaysInvalidData;
1799
1800 impl Read for AlwaysInvalidData {
1801 fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
1802 Err(std::io::Error::new(
1803 std::io::ErrorKind::InvalidData,
1804 "decoded bytes are malformed",
1805 ))
1806 }
1807 }
1808
1809 impl Source for AlwaysInvalidData {
1810 fn caps(&self) -> crate::source::SourceCaps {
1811 crate::source::SourceCaps {
1812 seekable: false,
1813 len: None,
1814 }
1815 }
1816
1817 fn as_seek(&mut self) -> Option<&mut dyn crate::source::SeekRead> {
1818 None
1819 }
1820 }
1821
1822 struct FailingDecodeCodec;
1833
1834 impl Codec for FailingDecodeCodec {
1835 fn id(&self) -> FormatId {
1836 FormatId::new("always-fails-to-decode")
1837 }
1838
1839 fn caps(&self) -> CodecCaps {
1840 CodecCaps {
1841 encode: false,
1842 decode: true,
1843 ..Default::default()
1844 }
1845 }
1846
1847 fn decoder<'a>(
1848 &self,
1849 _src: Box<dyn Source + 'a>,
1850 _o: &DecodeOpts,
1851 ) -> Result<Box<dyn Source + 'a>> {
1852 Ok(Box::new(AlwaysInvalidData))
1853 }
1854
1855 fn encoder(&self, _dst: Box<dyn Write + Send>, _o: &EncodeOpts) -> Result<Box<dyn Sink>> {
1856 unimplemented!("this test never encodes")
1857 }
1858 }
1859
1860 #[test]
1870 fn a_decoder_side_io_error_while_re_probing_becomes_corrupt() {
1871 let mut reg = Registry::new();
1872 const FAILING: FormatId = FormatId::new("always-fails-to-decode");
1873 const FAILING_MAGIC: &[MagicRule] = &[MagicRule {
1874 offset: 0,
1875 bytes: &[0xab, 0xcd],
1876 format: FAILING,
1877 }];
1878 reg.register_codec(
1879 Arc::new(FailingDecodeCodec),
1880 FormatMeta::codec(FAILING, &["fail"], FAILING_MAGIC),
1881 );
1882
1883 let bytes = vec![0xab, 0xcd, 0x00, 0x00];
1886 match resolve_chain_deep(
1887 ®,
1888 None,
1889 Box::new(ReaderSource::new(io::Cursor::new(bytes))),
1890 ) {
1891 Err(Error::Corrupt(msg)) => assert!(
1892 msg.contains("malformed"),
1893 "must carry the decoder's own message via Error::from_decode_io: {msg}"
1894 ),
1895 Err(other) => panic!("expected Corrupt (a decoder-side read failed), got {other:?}"),
1896 Ok(_) => panic!("a decoder that always fails to read cannot resolve successfully"),
1897 }
1898 }
1899
1900 #[test]
1914 fn a_decoder_that_refuses_at_construction_is_corrupt_not_an_io_failure() {
1915 struct RefusesToBuild;
1916
1917 impl Codec for RefusesToBuild {
1918 fn id(&self) -> FormatId {
1919 FormatId::new("refuses-to-build")
1920 }
1921
1922 fn caps(&self) -> CodecCaps {
1923 CodecCaps {
1924 encode: false,
1925 decode: true,
1926 ..Default::default()
1927 }
1928 }
1929
1930 fn decoder<'a>(
1931 &self,
1932 _src: Box<dyn Source + 'a>,
1933 _o: &DecodeOpts,
1934 ) -> Result<Box<dyn Source + 'a>> {
1935 Err(Error::Io(io::Error::new(
1936 io::ErrorKind::InvalidData,
1937 "the header is malformed and I will not build a decoder for it",
1938 )))
1939 }
1940
1941 fn encoder(
1942 &self,
1943 _dst: Box<dyn Write + Send>,
1944 _o: &EncodeOpts,
1945 ) -> Result<Box<dyn Sink>> {
1946 unimplemented!("this test never encodes")
1947 }
1948 }
1949
1950 const REFUSES: FormatId = FormatId::new("refuses-to-build");
1951 const REFUSES_MAGIC: &[MagicRule] = &[MagicRule {
1952 offset: 0,
1953 bytes: &[0xab, 0xcd],
1954 format: REFUSES,
1955 }];
1956
1957 let mut reg = Registry::new();
1958 reg.register_codec(
1959 Arc::new(RefusesToBuild),
1960 FormatMeta::codec(REFUSES, &["refuse"], REFUSES_MAGIC),
1961 );
1962
1963 for path in [None, Some(Path::new("thing.refuse"))] {
1968 let bytes = vec![0xab, 0xcd, 0x00, 0x00];
1969 match resolve_chain_deep(
1970 ®,
1971 path,
1972 Box::new(ReaderSource::new(io::Cursor::new(bytes))),
1973 ) {
1974 Err(Error::Corrupt(msg)) => assert!(
1975 msg.contains("malformed"),
1976 "must carry the codec's own message: {msg}"
1977 ),
1978 Err(other) => panic!("expected Corrupt for path {path:?}, got {other:?}"),
1979 Ok(_) => panic!("a codec that refuses to build cannot resolve successfully"),
1980 }
1981 }
1982 }
1983
1984 #[test]
1993 fn a_raw_source_io_error_of_the_identical_kind_stays_io_not_corrupt() {
1994 let reg = mock_registry_with_codec_and_container();
1995 match resolve_chain_deep(®, None, Box::new(AlwaysInvalidData)) {
1996 Err(Error::Io(io_err)) => assert_eq!(
1997 io_err.kind(),
1998 std::io::ErrorKind::InvalidData,
1999 "sanity: this must be the same error kind the test above reclassifies, so the \
2000 two tests differ only in WHERE the read happened"
2001 ),
2002 Err(other) => panic!(
2003 "a raw-source failure must stay Io, not become {other:?} — reclassifying it \
2004 would be a lie in the opposite direction from Task 4b's bug"
2005 ),
2006 Ok(_) => panic!("a raw source that always errors cannot resolve successfully"),
2007 }
2008 }
2009
2010 #[test]
2011 fn the_returned_source_is_decoded_through_every_codec_layer_on_both_routes() {
2012 let reg = mock_registry_with_codec_and_container();
2028 let inner = mock_archive_bytes(&[("a.txt", b"alpha")]);
2029 let outer = mock_codec_encode(&inner);
2030
2031 let (chain_with_path, mut src_with_path) = resolve_chain_deep(
2032 ®,
2033 Some(Path::new("thing.mar.mock")),
2034 Box::new(ReaderSource::new(io::Cursor::new(outer.clone()))),
2035 )
2036 .expect("resolve with path");
2037 let (chain_no_path, mut src_no_path) = resolve_chain_deep(
2038 ®,
2039 None,
2040 Box::new(ReaderSource::new(io::Cursor::new(outer))),
2041 )
2042 .expect("resolve without path");
2043
2044 let expected_chain = Chain::Codec {
2045 codec: MOCK_CODEC,
2046 inner: Box::new(Chain::Container {
2047 container: MOCK_CONTAINER,
2048 }),
2049 };
2050 assert_eq!(chain_with_path, expected_chain);
2051 assert_eq!(chain_no_path, expected_chain);
2052
2053 let mut bytes_with_path = Vec::new();
2054 src_with_path.read_to_end(&mut bytes_with_path).unwrap();
2055 let mut bytes_no_path = Vec::new();
2056 src_no_path.read_to_end(&mut bytes_no_path).unwrap();
2057
2058 assert_eq!(
2059 bytes_with_path, inner,
2060 "the path route must decode through the codec layer, not return raw bytes"
2061 );
2062 assert_eq!(
2063 bytes_with_path, bytes_no_path,
2064 "both routes must leave the source in the identical, already-decoded state"
2065 );
2066 }
2067}
2068
2069#[cfg(test)]
2070mod truthful_identification_tests {
2071 use super::*;
2074 use crate::archive::{Codec, DecodeOpts, EncodeOpts, Sink};
2075 use crate::format::{CodecCaps, ConfirmFn, FormatId, FormatMeta, MagicRule};
2076 use crate::registry::Registry;
2077 use crate::source::{ReaderSource, SeekRead, Source, SourceCaps, StreamOnly};
2078 use crate::testing::{
2079 MOCK_CODEC, MOCK_CONTAINER, MockCodec, MockContainer, mock_archive_bytes,
2080 };
2081 use std::io::{self, Read, Write};
2082 use std::path::Path;
2083 use std::sync::Arc;
2084
2085 fn yes(_: &[u8], _: bool) -> bool {
2086 true
2087 }
2088
2089 fn no(_: &[u8], _: bool) -> bool {
2090 false
2091 }
2092
2093 fn koly(version: u32, size: u32) -> Vec<u8> {
2096 let mut t = vec![0u8; TAIL_LEN];
2097 t[0..4].copy_from_slice(b"koly");
2098 t[4..8].copy_from_slice(&version.to_be_bytes());
2099 t[8..12].copy_from_slice(&size.to_be_bytes());
2100 t
2101 }
2102
2103 struct SeekCursor(io::Cursor<Vec<u8>>);
2105
2106 impl Read for SeekCursor {
2107 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
2108 self.0.read(buf)
2109 }
2110 }
2111
2112 impl Source for SeekCursor {
2113 fn caps(&self) -> SourceCaps {
2114 SourceCaps {
2115 seekable: true,
2116 len: Some(self.0.get_ref().len() as u64),
2117 }
2118 }
2119
2120 fn as_seek(&mut self) -> Option<&mut dyn SeekRead> {
2121 Some(&mut self.0)
2122 }
2123 }
2124
2125 fn zlib_ish_image() -> Vec<u8> {
2126 let mut bytes = vec![0x78, 0x9c];
2127 bytes.extend(std::iter::repeat_n(0xa5, 1000));
2128 bytes.extend(koly(4, 512));
2129 bytes
2130 }
2131
2132 #[test]
2133 fn foreign_tail_needs_signature_version_and_size() {
2134 assert_eq!(
2135 foreign_from_tail(&koly(4, 512)),
2136 Some(Foreign::AppleDiskImage)
2137 );
2138 assert_eq!(foreign_from_tail(&koly(3, 512)), None, "version 3");
2139 assert_eq!(foreign_from_tail(&koly(4, 256)), None, "header size 256");
2140 assert_eq!(foreign_from_tail(&koly(4, 512)[..511]), None, "511 bytes");
2141 let mut kolx = koly(4, 512);
2142 kolx[3] = b'x';
2143 assert_eq!(foreign_from_tail(&kolx), None, "kolx");
2144 assert_eq!(
2145 Foreign::AppleDiskImage.to_string(),
2146 "an Apple disk image (UDIF)"
2147 );
2148 }
2149
2150 #[test]
2151 fn probe_refuses_a_seekable_source_with_a_koly_tail() {
2152 let src: Box<dyn Source> = Box::new(SeekCursor(io::Cursor::new(zlib_ish_image())));
2153 match probe(src) {
2154 Err(e) => {
2155 assert!(
2156 matches!(e, Error::ForeignFormat(Foreign::AppleDiskImage)),
2157 "{e:?}"
2158 );
2159 assert_eq!(e.exit_code(), 3);
2160 assert_eq!(
2161 e.to_string(),
2162 "this is an Apple disk image (UDIF), which stuffr cannot open"
2163 );
2164 }
2165 Ok(_) => panic!("a koly tail must be refused"),
2166 }
2167 }
2168
2169 #[test]
2170 fn probe_does_not_read_the_tail_of_a_pipe() {
2171 let src: Box<dyn Source> = Box::new(ReaderSource::new(io::Cursor::new(zlib_ish_image())));
2172 let Ok((prefix, _)) = probe(src) else {
2173 panic!("a pipe has no tail to read, so it must probe as before");
2174 };
2175 assert_eq!(&prefix[..2], &[0x78, 0x9c]);
2176 }
2177
2178 #[test]
2179 fn read_tail_leaves_a_seekable_source_at_zero() {
2180 let mut bytes = b"HEAD".to_vec();
2181 bytes.extend(vec![7u8; 600]);
2182 let mut src = SeekCursor(io::Cursor::new(bytes.clone()));
2183 let tail = read_tail(&mut src)
2184 .unwrap()
2185 .expect("600+ bytes have a tail");
2186 assert_eq!(tail.len(), TAIL_LEN);
2187 assert_eq!(tail, bytes[bytes.len() - TAIL_LEN..]);
2188 let mut all = Vec::new();
2189 src.read_to_end(&mut all).unwrap();
2190 assert_eq!(all, bytes, "the position must be back at byte zero");
2191
2192 let mut short = SeekCursor(io::Cursor::new(b"tiny".to_vec()));
2194 assert_eq!(read_tail(&mut short).unwrap(), None);
2195 let mut all = Vec::new();
2196 short.read_to_end(&mut all).unwrap();
2197 assert_eq!(all, b"tiny");
2198
2199 let mut pipe = ReaderSource::new(io::Cursor::new(bytes));
2201 assert_eq!(read_tail(&mut pipe).unwrap(), None);
2202 }
2203
2204 const ALPHA: FormatId = FormatId::new("alpha");
2205 const BETA: FormatId = FormatId::new("beta");
2206 const SHARED_MAGIC: &[MagicRule] = &[
2207 MagicRule {
2208 offset: 0,
2209 bytes: b"SH",
2210 format: ALPHA,
2211 },
2212 MagicRule {
2213 offset: 0,
2214 bytes: b"SH",
2215 format: BETA,
2216 },
2217 ];
2218
2219 #[test]
2220 fn confirmed_candidate_beats_an_unconfirmed_one() {
2221 for (alpha_ok, winner) in [(true, ALPHA), (false, BETA)] {
2225 let mut r = Registry::new();
2226 let (a, b): (ConfirmFn, ConfirmFn) = if alpha_ok { (yes, no) } else { (no, yes) };
2227 r.register_codec(
2228 Arc::new(MockCodec),
2229 FormatMeta::codec(ALPHA, &[], SHARED_MAGIC).with_confirm(a),
2230 );
2231 r.register_codec(
2232 Arc::new(MockCodec),
2233 FormatMeta::codec(BETA, &[], SHARED_MAGIC).with_confirm(b),
2234 );
2235 let (chain, ev) = resolve_chain_checked(&r, None, b"SH..").expect("no ambiguity");
2236 assert_eq!(chain.outermost_codec(), Some(winner));
2237 assert_eq!(ev, Evidence::Confirmed);
2238 }
2239 }
2240
2241 #[test]
2242 fn a_format_with_no_confirm_function_counts_as_confirmed() {
2243 let mut r = Registry::new();
2244 r.register_codec(
2245 Arc::new(MockCodec),
2246 FormatMeta::codec(ALPHA, &[], SHARED_MAGIC).with_confirm(no),
2247 );
2248 r.register_codec(
2249 Arc::new(MockCodec),
2250 FormatMeta::codec(BETA, &[], SHARED_MAGIC),
2251 );
2252 let (chain, ev) = resolve_chain_checked(&r, None, b"SH..").unwrap();
2253 assert_eq!(chain.outermost_codec(), Some(BETA));
2254 assert_eq!(ev, Evidence::Confirmed);
2255 }
2256
2257 #[test]
2258 fn unconfirmed_magic_still_resolves_leniently() {
2259 let mut r = Registry::new();
2260 r.register_codec(
2261 Arc::new(MockCodec),
2262 FormatMeta::codec(ALPHA, &["al"], &SHARED_MAGIC[..1]).with_confirm(no),
2263 );
2264 let chain = resolve_chain(&r, None, b"SH..").expect("lenient resolution keeps it");
2265 assert_eq!(chain.outermost_codec(), Some(ALPHA));
2266 let (_, ev) = resolve_chain_checked(&r, None, b"SH..").unwrap();
2267 assert_eq!(ev, Evidence::Unconfirmed);
2268 }
2269
2270 #[test]
2271 fn extension_without_confirm_is_by_name() {
2272 let mut r = Registry::new();
2273 r.register_codec(Arc::new(MockCodec), FormatMeta::codec(ALPHA, &["al"], &[]));
2274 let (chain, ev) = resolve_chain_checked(&r, Some(Path::new("x.al")), b"whatever").unwrap();
2275 assert_eq!(chain.outermost_codec(), Some(ALPHA));
2276 assert_eq!(ev, Evidence::ByName);
2277 }
2278
2279 #[test]
2280 fn extension_with_failing_confirm_is_unconfirmed() {
2281 let mut r = Registry::new();
2282 r.register_codec(
2283 Arc::new(MockCodec),
2284 FormatMeta::codec(ALPHA, &["al"], &[]).with_confirm(no),
2285 );
2286 let (_, ev) = resolve_chain_checked(&r, Some(Path::new("x.al")), b"whatever").unwrap();
2287 assert_eq!(ev, Evidence::Unconfirmed);
2288
2289 let mut r = Registry::new();
2290 r.register_codec(
2291 Arc::new(MockCodec),
2292 FormatMeta::codec(ALPHA, &["al"], &[]).with_confirm(yes),
2293 );
2294 let (_, ev) = resolve_chain_checked(&r, Some(Path::new("x.al")), b"whatever").unwrap();
2295 assert_eq!(ev, Evidence::Confirmed);
2296 }
2297
2298 #[test]
2299 fn errors_are_unchanged() {
2300 let r = Registry::new();
2301 assert!(matches!(
2302 resolve_chain_checked(&r, None, b"??"),
2303 Err(Error::UnknownFormat { .. })
2304 ));
2305 let mut r = Registry::new();
2306 r.register_codec(
2307 Arc::new(MockCodec),
2308 FormatMeta::codec(ALPHA, &[], SHARED_MAGIC).with_confirm(no),
2309 );
2310 r.register_codec(
2311 Arc::new(MockCodec),
2312 FormatMeta::codec(BETA, &[], SHARED_MAGIC).with_confirm(no),
2313 );
2314 assert!(matches!(
2315 resolve_chain_checked(&r, None, b"SH"),
2316 Err(Error::AmbiguousFormat { .. })
2317 ));
2318 }
2319
2320 const LAYER_MAGIC: &[u8] = b"MOCK";
2323
2324 struct LayerCodec;
2328
2329 impl Codec for LayerCodec {
2330 fn id(&self) -> FormatId {
2331 MOCK_CODEC
2332 }
2333
2334 fn caps(&self) -> CodecCaps {
2335 CodecCaps::round_trip()
2336 }
2337
2338 fn decoder<'a>(
2339 &self,
2340 mut src: Box<dyn Source + 'a>,
2341 _o: &DecodeOpts,
2342 ) -> Result<Box<dyn Source + 'a>> {
2343 let mut discard = [0u8; 4];
2344 src.read_exact(&mut discard)?;
2345 Ok(Box::new(StreamOnly::new(src)))
2346 }
2347
2348 fn encoder(&self, _dst: Box<dyn Write + Send>, _o: &EncodeOpts) -> Result<Box<dyn Sink>> {
2349 unreachable!("identification never encodes")
2350 }
2351 }
2352
2353 fn mock_archive_confirms(p: &[u8], _whole: bool) -> bool {
2356 p.len() >= 4 && {
2357 let n = u16::from_le_bytes([p[2], p[3]]) as usize;
2358 n > 0 && 4 + n <= p.len()
2359 }
2360 }
2361
2362 fn strict_registry() -> Registry {
2363 const CODEC_MAGIC: &[MagicRule] = &[MagicRule {
2364 offset: 0,
2365 bytes: LAYER_MAGIC,
2366 format: MOCK_CODEC,
2367 }];
2368 const CONTAINER_MAGIC: &[MagicRule] = &[MagicRule {
2369 offset: 0,
2370 bytes: b"ME",
2371 format: MOCK_CONTAINER,
2372 }];
2373 let mut r = Registry::new();
2374 r.register_codec(
2375 Arc::new(LayerCodec),
2376 FormatMeta::codec(MOCK_CODEC, &["mock"], CODEC_MAGIC)
2377 .with_confirm(|p, _| p.starts_with(LAYER_MAGIC)),
2378 );
2379 r.register_container(
2380 Arc::new(MockContainer),
2381 FormatMeta::container(MOCK_CONTAINER, &["mar"], CONTAINER_MAGIC)
2382 .with_confirm(mock_archive_confirms),
2383 );
2384 r
2385 }
2386
2387 fn layered(layers: usize, inner: &[u8]) -> Vec<u8> {
2388 let mut out = LAYER_MAGIC.repeat(layers);
2389 out.extend_from_slice(inner);
2390 out
2391 }
2392
2393 fn strict(
2394 r: &Registry,
2395 path: Option<&str>,
2396 prefix: &[u8],
2397 tail: Option<&[u8]>,
2398 ) -> Option<Chain> {
2399 identify_strict(r, path.map(Path::new), prefix, tail, &DecodeOpts::default())
2400 }
2401
2402 #[test]
2403 fn identify_strict_rejects_unconfirmed_and_foreign() {
2404 let r = strict_registry();
2405 assert_eq!(
2406 strict(&r, None, b"ME\xff\xffabc", None),
2407 None,
2408 "a magic hit whose confirmation fails is not openable"
2409 );
2410 let archive = mock_archive_bytes(&[("a.txt", b"alpha")]);
2411 assert_eq!(
2412 strict(&r, None, &archive, None),
2413 Some(Chain::Container {
2414 container: MOCK_CONTAINER
2415 })
2416 );
2417 assert_eq!(
2418 strict(&r, None, &archive, Some(&koly(4, 512))),
2419 None,
2420 "a koly tail outranks a confirmed prefix"
2421 );
2422 assert_eq!(strict(&r, None, b"nothing", None), None, "unknown");
2423 }
2424
2425 #[test]
2426 fn identify_strict_decodes_the_inner_layer_from_the_prefix() {
2427 let r = strict_registry();
2428 let archive = mock_archive_bytes(&[("a.txt", b"alpha")]);
2429 assert_eq!(
2430 strict(&r, None, &layered(1, &archive), None),
2431 Some(Chain::Codec {
2432 codec: MOCK_CODEC,
2433 inner: Box::new(Chain::Container {
2434 container: MOCK_CONTAINER
2435 }),
2436 })
2437 );
2438 assert_eq!(
2439 strict(&r, Some("x.mar.mock"), &layered(1, b"ME\xff\xffabc"), None),
2440 Some(Chain::Codec {
2441 codec: MOCK_CODEC,
2442 inner: Box::new(Chain::Raw),
2443 }),
2444 "an inner layer failing confirmation is Raw, whatever the path says"
2445 );
2446 }
2447
2448 #[test]
2449 fn identify_strict_bounds_the_nesting() {
2450 let r = strict_registry();
2451 let archive = mock_archive_bytes(&[("a.txt", b"alpha")]);
2452 let depth = |c: &Chain| {
2453 let mut n = 0;
2454 let mut c = c;
2455 while let Chain::Codec { inner, .. } = c {
2456 n += 1;
2457 c = inner;
2458 }
2459 n
2460 };
2461 let three = strict(&r, None, &layered(3, &archive), None).unwrap();
2462 assert_eq!(depth(&three), 3);
2463 assert_eq!(three.container(), Some(MOCK_CONTAINER));
2464 let many = strict(&r, None, &layered(10, &archive), None).unwrap();
2465 assert!(depth(&many) < MAX_CHAIN_DEPTH, "{many:?}");
2466 assert_eq!(many.container(), None, "stopped with Raw at the bound");
2467 }
2468
2469 struct Scripted {
2471 yields: &'static [u8],
2472 then_error: bool,
2473 }
2474
2475 struct ScriptedSource {
2476 rest: &'static [u8],
2477 then_error: bool,
2478 }
2479
2480 impl Read for ScriptedSource {
2481 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
2482 if self.rest.is_empty() {
2483 return if self.then_error {
2484 Err(io::Error::new(io::ErrorKind::InvalidData, "bad stream"))
2485 } else {
2486 Ok(0)
2487 };
2488 }
2489 let n = buf.len().min(self.rest.len());
2490 buf[..n].copy_from_slice(&self.rest[..n]);
2491 self.rest = &self.rest[n..];
2492 Ok(n)
2493 }
2494 }
2495
2496 impl Source for ScriptedSource {
2497 fn caps(&self) -> SourceCaps {
2498 SourceCaps::default()
2499 }
2500
2501 fn as_seek(&mut self) -> Option<&mut dyn SeekRead> {
2502 None
2503 }
2504 }
2505
2506 impl Codec for Scripted {
2507 fn id(&self) -> FormatId {
2508 MOCK_CODEC
2509 }
2510
2511 fn caps(&self) -> CodecCaps {
2512 CodecCaps::decode_only()
2513 }
2514
2515 fn decoder<'a>(
2516 &self,
2517 _src: Box<dyn Source + 'a>,
2518 _o: &DecodeOpts,
2519 ) -> Result<Box<dyn Source + 'a>> {
2520 Ok(Box::new(ScriptedSource {
2521 rest: self.yields,
2522 then_error: self.then_error,
2523 }))
2524 }
2525
2526 fn encoder(&self, _dst: Box<dyn Write + Send>, _o: &EncodeOpts) -> Result<Box<dyn Sink>> {
2527 unreachable!("identification never encodes")
2528 }
2529 }
2530
2531 fn scripted_registry(yields: &'static [u8], then_error: bool) -> Registry {
2532 let mut r = strict_registry();
2533 r.register_codec(
2534 Arc::new(Scripted { yields, then_error }),
2535 FormatMeta::codec(
2536 MOCK_CODEC,
2537 &["mock"],
2538 &[MagicRule {
2539 offset: 0,
2540 bytes: LAYER_MAGIC,
2541 format: MOCK_CODEC,
2542 }],
2543 )
2544 .with_confirm(yes),
2545 );
2546 r
2547 }
2548
2549 #[test]
2550 fn identify_strict_drops_a_codec_that_decodes_nothing() {
2551 let raw = Some(Chain::Codec {
2552 codec: MOCK_CODEC,
2553 inner: Box::new(Chain::Raw),
2554 });
2555 assert_eq!(
2556 strict(&scripted_registry(b"", true), None, b"MOCKjunk", None),
2557 None,
2558 "an error before the first byte: nothing here decodes"
2559 );
2560 assert_eq!(
2561 strict(&scripted_registry(b"", false), None, b"MOCKjunk", None),
2562 raw,
2563 "a clean, empty stream is still a stream"
2564 );
2565 assert_eq!(
2566 strict(
2567 &scripted_registry(b"plain text", true),
2568 None,
2569 b"MOCKjunk",
2570 None
2571 ),
2572 raw,
2573 "what arrived before the error is kept"
2574 );
2575 }
2576
2577 struct NeedsMore;
2582
2583 const NEEDS: usize = PROBE_LEN * 2;
2584 const SLOW: FormatId = FormatId::new("slow");
2585 const TAR: FormatId = FormatId::new("tar");
2586
2587 impl Codec for NeedsMore {
2588 fn id(&self) -> FormatId {
2589 SLOW
2590 }
2591
2592 fn caps(&self) -> CodecCaps {
2593 CodecCaps::decode_only()
2594 }
2595
2596 fn decoder<'a>(
2597 &self,
2598 mut src: Box<dyn Source + 'a>,
2599 _o: &DecodeOpts,
2600 ) -> Result<Box<dyn Source + 'a>> {
2601 let mut all = Vec::new();
2602 src.read_to_end(&mut all)?;
2603 Ok(Box::new(ScriptedSource {
2604 rest: if all.len() >= NEEDS { b"payload" } else { b"" },
2605 then_error: all.len() < NEEDS,
2606 }))
2607 }
2608
2609 fn encoder(&self, _dst: Box<dyn Write + Send>, _o: &EncodeOpts) -> Result<Box<dyn Sink>> {
2610 unreachable!("identification never encodes")
2611 }
2612 }
2613
2614 fn needs_more_registry() -> Registry {
2615 let mut r = strict_registry();
2616 r.register_codec(
2617 Arc::new(NeedsMore),
2618 FormatMeta::codec(
2619 SLOW,
2620 &["slow"],
2621 &[MagicRule {
2622 offset: 0,
2623 bytes: b"SLOW",
2624 format: SLOW,
2625 }],
2626 )
2627 .with_confirm(yes),
2628 );
2629 r.register_container(
2630 Arc::new(MockContainer),
2631 FormatMeta::container(TAR, &["tar"], &[]),
2632 );
2633 r
2634 }
2635
2636 fn slow_prefix(len: usize) -> Vec<u8> {
2637 let mut p = b"SLOW".to_vec();
2638 p.resize(len, 0x5a);
2639 p
2640 }
2641
2642 #[test]
2643 fn a_cut_off_prefix_that_decodes_nothing_keeps_the_codec_over_the_paths_inner() {
2644 let r = needs_more_registry();
2645 assert_eq!(
2646 strict(&r, Some("big.tar.slow"), &slow_prefix(PROBE_LEN), None),
2647 Some(Chain::Codec {
2648 codec: SLOW,
2649 inner: Box::new(Chain::Container { container: TAR }),
2650 }),
2651 "the window ran out, not the file: as 0.21.0 listed it"
2652 );
2653 assert_eq!(
2654 strict(&r, Some("big.slow"), &slow_prefix(PROBE_LEN), None),
2655 Some(Chain::Codec {
2656 codec: SLOW,
2657 inner: Box::new(Chain::Raw),
2658 }),
2659 );
2660 }
2661
2662 #[test]
2663 fn a_whole_file_that_decodes_nothing_before_an_error_is_not_openable() {
2664 let r = needs_more_registry();
2665 assert_eq!(
2666 strict(&r, Some("small.tar.slow"), &slow_prefix(100), None),
2667 None,
2668 "the prefix is the whole file, and it decodes to nothing but an error"
2669 );
2670 }
2671
2672 #[test]
2673 fn an_extension_naming_a_refused_hit_falls_to_the_confirmed_ones() {
2674 const ZIP: FormatId = FormatId::new("zip");
2675 const JAR: FormatId = FormatId::new("jar");
2676 const APK: FormatId = FormatId::new("apk");
2677 const PK: &[MagicRule] = &[
2678 MagicRule {
2679 offset: 0,
2680 bytes: b"PK",
2681 format: ZIP,
2682 },
2683 MagicRule {
2684 offset: 0,
2685 bytes: b"PK",
2686 format: JAR,
2687 },
2688 MagicRule {
2689 offset: 0,
2690 bytes: b"PK",
2691 format: APK,
2692 },
2693 ];
2694 let mut r = Registry::new();
2695 r.register_container(
2696 Arc::new(MockContainer),
2697 FormatMeta::container(ZIP, &["zip"], PK)
2698 .with_priority(1)
2699 .with_confirm(yes),
2700 );
2701 r.register_container(
2702 Arc::new(MockContainer),
2703 FormatMeta::container(JAR, &["jar"], PK).with_confirm(yes),
2704 );
2705 r.register_container(
2706 Arc::new(MockContainer),
2707 FormatMeta::container(APK, &["apk"], PK).with_confirm(no),
2708 );
2709 let (chain, ev) = resolve_chain_checked(&r, Some(Path::new("x.apk")), b"PK..").unwrap();
2710 assert_eq!(chain, Chain::Container { container: ZIP });
2711 assert_eq!(ev, Evidence::Confirmed);
2712 }
2713 fn eight_when_whole(p: &[u8], whole: bool) -> bool {
2717 !whole || p.len() >= 8
2718 }
2719
2720 #[test]
2721 fn a_probe_prefix_short_of_the_window_is_confirmed_as_the_whole_stream() {
2722 let mut r = Registry::new();
2723 r.register_codec(
2724 Arc::new(MockCodec),
2725 FormatMeta::codec(ALPHA, &[], &SHARED_MAGIC[..1]).with_confirm(eight_when_whole),
2726 );
2727 let ev = |prefix: &[u8]| resolve_chain_checked(&r, None, prefix).unwrap().1;
2728 assert_eq!(ev(b"SH"), Evidence::Unconfirmed, "a two-byte file is whole");
2729 assert_eq!(ev(b"SH345678"), Evidence::Confirmed);
2730 let mut window = vec![0u8; PROBE_LEN];
2732 window[..2].copy_from_slice(b"SH");
2733 assert_eq!(ev(&window), Evidence::Confirmed);
2734 }
2735
2736 const PLAIN: FormatId = FormatId::new("plain");
2737 const WHOLE: FormatId = FormatId::new("wholecheck");
2738
2739 fn layered_registry(yields: &'static [u8], then_error: bool) -> Registry {
2742 let mut r = scripted_registry(yields, then_error);
2743 r.register_container(
2744 Arc::new(MockContainer),
2745 FormatMeta::container(PLAIN, &["pln"], &[]).with_confirm(|p, _| p.starts_with(b"PL")),
2746 );
2747 r.register_container(
2748 Arc::new(MockContainer),
2749 FormatMeta::container(
2750 WHOLE,
2751 &["wh"],
2752 &[MagicRule {
2753 offset: 0,
2754 bytes: b"WH",
2755 format: WHOLE,
2756 }],
2757 )
2758 .with_confirm(eight_when_whole),
2759 );
2760 r
2761 }
2762
2763 fn over(inner: Chain) -> Option<Chain> {
2764 Some(Chain::Codec {
2765 codec: MOCK_CODEC,
2766 inner: Box::new(inner),
2767 })
2768 }
2769
2770 #[test]
2771 fn a_decoded_layer_is_whole_only_when_it_ended_cleanly_inside_the_window() {
2772 let r = layered_registry(b"WH", false);
2774 assert_eq!(strict(&r, None, b"MOCKjunk", None), over(Chain::Raw));
2775 let chain = over(Chain::Container { container: WHOLE }).unwrap();
2776 assert_eq!(
2777 first_unconfirmed_layer(&r, &chain, b"MOCKjunk", &DecodeOpts::default()),
2778 Some(WHOLE)
2779 );
2780 let r = layered_registry(b"WH", true);
2782 assert_eq!(
2783 strict(&r, None, b"MOCKjunk", None),
2784 over(Chain::Container { container: WHOLE })
2785 );
2786 assert_eq!(
2787 first_unconfirmed_layer(&r, &chain, b"MOCKjunk", &DecodeOpts::default()),
2788 None
2789 );
2790 }
2791
2792 #[test]
2793 fn decoded_bytes_with_no_magic_take_the_paths_container_when_it_confirms() {
2794 let plain = Chain::Container { container: PLAIN };
2795 let r = layered_registry(b"PLAIN DATA", false);
2796 assert_eq!(
2797 strict(&r, Some("x.pln.mock"), b"MOCKjunk", None),
2798 over(plain.clone()),
2799 "a magicless inner layer the name gives and the bytes confirm"
2800 );
2801 assert_eq!(
2802 strict(&r, None, b"MOCKjunk", None),
2803 over(Chain::Raw),
2804 "no name"
2805 );
2806 assert_eq!(
2807 strict(&r, Some("x.mock"), b"MOCKjunk", None),
2808 over(Chain::Raw),
2809 "a name with nothing inside"
2810 );
2811 let r = layered_registry(b"just text", false);
2812 assert_eq!(
2813 strict(&r, Some("x.pln.mock"), b"MOCKjunk", None),
2814 over(Chain::Raw),
2815 "the name alone is not enough when the check refuses the bytes"
2816 );
2817 let (chain, _) =
2819 resolve_chain_checked(&r, Some(Path::new("x.pln.mock")), b"MOCKjunk").unwrap();
2820 assert_eq!(chain, over(plain).unwrap());
2821 assert_eq!(
2822 first_unconfirmed_layer(&r, &chain, b"MOCKjunk", &DecodeOpts::default()),
2823 Some(PLAIN)
2824 );
2825 }
2826}