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::{PeekSource, Source};
25
26pub const PROBE_LEN: usize = 4096;
28
29pub const MAX_CHAIN_DEPTH: usize = 4;
36
37pub fn probe(mut src: Box<dyn Source>) -> Result<(Vec<u8>, Box<dyn Source>)> {
42 if src.caps().seekable {
43 let mut prefix = vec![0u8; PROBE_LEN];
44 let Some(seek) = src.as_seek() else {
54 return Err(Error::NotSeekable {
55 format: FormatId::new("<probe>"),
56 });
57 };
58 let mut filled = 0;
59 while filled < PROBE_LEN {
60 match std::io::Read::read(seek, &mut prefix[filled..])? {
61 0 => break,
62 n => filled += n,
63 }
64 }
65 prefix.truncate(filled);
66 std::io::Seek::seek(seek, std::io::SeekFrom::Start(0))?;
67 return Ok((prefix, src));
68 }
69
70 let peek = PeekSource::fill(src, PROBE_LEN)?;
71 let prefix = peek.prefix().to_vec();
72 Ok((prefix, Box::new(peek)))
73}
74
75#[non_exhaustive]
77#[derive(Clone, PartialEq, Eq, Debug)]
78pub enum Chain {
79 Codec { codec: FormatId, inner: Box<Chain> },
81 Container { container: FormatId },
83 Raw,
85}
86
87impl Chain {
88 pub fn container(&self) -> Option<FormatId> {
103 match self {
104 Chain::Container { container } => Some(*container),
105 Chain::Codec { inner, .. } => inner.container(),
106 _ => None,
107 }
108 }
109
110 pub fn outermost_codec(&self) -> Option<FormatId> {
120 match self {
121 Chain::Codec { codec, .. } => Some(*codec),
122 _ => None,
123 }
124 }
125
126 pub fn describe(&self) -> String {
128 match self {
129 Chain::Raw => "raw".to_string(),
130 Chain::Container { container } => container.to_string(),
131 Chain::Codec { codec, inner } => match inner.as_ref() {
132 Chain::Raw => codec.to_string(),
133 other => format!("{} over {}", other.describe(), codec),
134 },
135 }
136 }
137}
138
139fn hex_prefix(bytes: &[u8]) -> String {
140 if bytes.is_empty() {
141 return "empty input".to_string();
142 }
143 bytes
144 .iter()
145 .take(8)
146 .map(|b| format!("{b:02x}"))
147 .collect::<Vec<_>>()
148 .join(" ")
149}
150
151fn extensions(path: &Path) -> Vec<String> {
154 let name = path
155 .file_name()
156 .and_then(|s| s.to_str())
157 .unwrap_or_default();
158 name.split('.')
159 .skip(1)
160 .map(|s| s.to_ascii_lowercase())
161 .collect()
162}
163
164fn inner_from_path(reg: &Registry, path: Option<&Path>, outer: FormatId) -> Chain {
167 let Some(path) = path else {
168 return Chain::Raw;
169 };
170 let exts = extensions(path);
171
172 if let Some(last) = exts.last()
179 && compound_alias(reg, last) == Some(outer)
180 && let Some(id) = reg.by_extension("tar")
181 && reg.container(id).is_some()
182 {
183 return Chain::Container { container: id };
184 }
185
186 if exts.len() >= 2
188 && let Some(id) = reg.by_extension(&exts[exts.len() - 2])
189 && reg.container(id).is_some()
190 {
191 return Chain::Container { container: id };
192 }
193 Chain::Raw
194}
195
196fn compound_alias(reg: &Registry, ext: &str) -> Option<FormatId> {
226 let stripped = ext.strip_prefix(['t', 'T'])?;
227 if stripped.is_empty() {
228 return None;
229 }
230 let stripped = stripped.to_ascii_lowercase();
231 let spelling = COMPOUND_SPELLINGS
232 .iter()
233 .find(|(alias, _)| *alias == stripped)
234 .map_or(stripped.as_str(), |(_, real)| *real);
235 let id = reg.by_extension(spelling)?;
236 reg.container(id).is_none().then_some(id)
239}
240
241const COMPOUND_SPELLINGS: &[(&str, &str)] = &[("bz", "bz2")];
262
263pub fn chain_for_new_path(reg: &Registry, path: &Path) -> Chain {
274 let Some(ext) = path.extension().and_then(|e| e.to_str()) else {
275 return Chain::Raw;
276 };
277 let Some(outer) = reg.by_extension(ext).or_else(|| compound_alias(reg, ext)) else {
278 return Chain::Raw;
279 };
280 if reg.container(outer).is_some() {
282 return Chain::Container { container: outer };
283 }
284 let inner = inner_from_path(reg, Some(path), outer);
287 Chain::Codec {
288 codec: outer,
289 inner: Box::new(inner),
290 }
291}
292
293pub fn resolve_chain(reg: &Registry, path: Option<&Path>, prefix: &[u8]) -> Result<Chain> {
300 let mut candidates = reg.match_magic(prefix);
301
302 if candidates.len() > 1 {
303 let by_ext =
309 path.and_then(|p| extensions(p).iter().rev().find_map(|e| reg.by_extension(e)));
310 match by_ext.filter(|id| candidates.contains(id)) {
311 Some(id) => candidates = vec![id],
312 None => {
313 let top = reg.priority_of(candidates[0]);
314 let tied: Vec<FormatId> = candidates
315 .iter()
316 .copied()
317 .filter(|id| reg.priority_of(*id) == top)
318 .collect();
319 if tied.len() == 1 {
320 candidates = tied;
321 } else {
322 let names: Vec<&str> = tied.iter().map(|id| id.as_str()).collect();
323 return Err(Error::AmbiguousFormat {
324 candidates: names.join(", "),
325 });
326 }
327 }
328 }
329 }
330
331 let outer = candidates.first().copied().or_else(|| {
332 path.and_then(|p| extensions(p).iter().rev().find_map(|e| reg.by_extension(e)))
333 });
334
335 let Some(outer) = outer else {
336 return Err(Error::UnknownFormat {
337 seen: hex_prefix(prefix),
338 });
339 };
340
341 let kind = if reg.container(outer).is_some() {
342 FormatKind::Container
343 } else if reg.codec(outer).is_some() {
344 FormatKind::Codec
345 } else {
346 return Err(Error::FormatNotEnabled(outer));
347 };
348
349 Ok(match kind {
350 FormatKind::Container => Chain::Container { container: outer },
351 FormatKind::Codec => Chain::Codec {
352 codec: outer,
353 inner: Box::new(inner_from_path(reg, path, outer)),
354 },
355 })
356}
357
358fn wrap_layers(layers: &[FormatId], innermost: Chain) -> Chain {
361 layers
362 .iter()
363 .rev()
364 .fold(innermost, |inner, codec| Chain::Codec {
365 codec: *codec,
366 inner: Box::new(inner),
367 })
368}
369
370fn decode_through_chain(
376 reg: &Registry,
377 chain: &Chain,
378 src: Box<dyn Source>,
379 opts: &DecodeOpts,
380) -> Result<Box<dyn Source>> {
381 match chain {
382 Chain::Codec { codec, inner } => {
383 let decoded = reg.require_codec(*codec)?.decoder(src, opts)?;
384 decode_through_chain(reg, inner, decoded, opts)
385 }
386 Chain::Container { .. } | Chain::Raw => Ok(src),
387 }
388}
389
390pub fn resolve_chain_deep(
418 reg: &Registry,
419 path: Option<&Path>,
420 src: Box<dyn Source>,
421) -> Result<(Chain, Box<dyn Source>)> {
422 resolve_chain_deep_with(reg, path, src, &DecodeOpts::default())
423}
424
425pub fn resolve_chain_deep_with(
442 reg: &Registry,
443 path: Option<&Path>,
444 src: Box<dyn Source>,
445 opts: &DecodeOpts,
446) -> Result<(Chain, Box<dyn Source>)> {
447 let (prefix, src) = probe(src)?;
448 let chain = resolve_chain(reg, path, &prefix)?;
449
450 if !matches!(&chain, Chain::Codec { inner, .. } if matches!(inner.as_ref(), Chain::Raw)) {
455 let decoded = decode_through_chain(reg, &chain, src, opts)?;
456 return Ok((chain, decoded));
457 }
458
459 let Chain::Codec { codec, .. } = chain else {
463 unreachable!("guarded above: chain is Chain::Codec with a Raw inner")
464 };
465 let mut layers: Vec<FormatId> = vec![codec];
466 let mut current_codec = codec;
467 let mut src = src;
468 let mut depth = 1usize;
469
470 loop {
471 if depth >= MAX_CHAIN_DEPTH {
472 return Err(Error::ChainTooDeep {
473 depth: MAX_CHAIN_DEPTH,
474 });
475 }
476
477 let decoded = reg.require_codec(current_codec)?.decoder(src, opts)?;
478 let (next_prefix, next_src) = probe(decoded)?;
479 depth += 1;
480
481 let next_chain = match resolve_chain(reg, None, &next_prefix) {
482 Ok(chain) => chain,
483 Err(Error::UnknownFormat { .. }) => {
484 return Ok((wrap_layers(&layers, Chain::Raw), next_src));
485 }
486 Err(e) => return Err(e),
487 };
488
489 let Chain::Codec { codec: nested, .. } = next_chain else {
490 return Ok((wrap_layers(&layers, next_chain), next_src));
491 };
492
493 layers.push(nested);
494 current_codec = nested;
495 src = next_src;
496 }
497}
498
499#[cfg(test)]
500mod tests {
501 use super::*;
502 use crate::archive::{Codec, DecodeOpts, EncodeOpts, Sink};
503 use crate::format::{CodecCaps, FormatId, FormatMeta, MagicRule};
504 use crate::registry::Registry;
505 use crate::source::{ReaderSource, Source, StreamOnly};
506 use crate::testing::{
507 MOCK_CODEC, MOCK_CONTAINER, MockCodec, MockContainer, SharedBuf, mock_archive_bytes,
508 };
509 use std::io;
510 use std::io::{Read, Write};
511 use std::path::Path;
512 use std::sync::Arc;
513
514 const GZIP: FormatId = FormatId::new("gzip");
515 const TAR: FormatId = FormatId::new("tar");
516
517 const GZIP_MAGIC: &[MagicRule] = &[MagicRule {
518 offset: 0,
519 bytes: &[0x1f, 0x8b],
520 format: GZIP,
521 }];
522 const TAR_MAGIC: &[MagicRule] = &[MagicRule {
523 offset: 257,
524 bytes: b"ustar",
525 format: TAR,
526 }];
527
528 fn registry() -> Registry {
529 let mut r = Registry::new();
530 r.register_codec(
531 Arc::new(MockCodec),
532 FormatMeta::codec(GZIP, &["gz", "tgz"], GZIP_MAGIC),
533 );
534 r.register_container(
535 Arc::new(MockContainer),
536 FormatMeta::container(TAR, &["tar"], TAR_MAGIC),
537 );
538 r
539 }
540
541 #[test]
545 fn an_output_path_resolves_to_the_chain_its_name_promises() {
546 let reg = registry();
547 let c = |p: &str| chain_for_new_path(®, Path::new(p)).describe();
548 assert_eq!(c("x.tar"), "tar");
549 assert_eq!(c("x.gz"), "gzip");
550 assert_eq!(c("x.tar.gz"), "tar over gzip");
551 assert_eq!(
552 c("x.tgz"),
553 "tar over gzip",
554 "the compound alias must resolve too"
555 );
556 assert_eq!(c("x.TAR.GZ"), "tar over gzip", "extensions are caseless");
557 assert_eq!(c("x.txt"), "raw");
558 assert_eq!(c("x"), "raw");
559 }
560
561 #[test]
569 fn tbz_means_tar_over_bzip2_without_making_plain_bz_writable() {
570 const BZIP2: FormatId = FormatId::new("bzip2");
571 const BZIP2_MAGIC: &[MagicRule] = &[MagicRule {
572 offset: 0,
573 bytes: b"BZh",
574 format: BZIP2,
575 }];
576 let mut reg = Registry::new();
577 reg.register_codec(
579 Arc::new(MockCodec),
580 FormatMeta::codec(BZIP2, &["bz2"], BZIP2_MAGIC),
581 );
582 reg.register_container(
583 Arc::new(MockContainer),
584 FormatMeta::container(TAR, &["tar"], TAR_MAGIC),
585 );
586
587 let c = |p: &str| chain_for_new_path(®, Path::new(p)).describe();
588 assert_eq!(
589 c("f.tbz"),
590 "tar over bzip2",
591 "GNU tar and libarchive both read .tbz as tar.bz2"
592 );
593 assert_eq!(c("f.TBZ"), "tar over bzip2", "and extensions are caseless");
594 assert_eq!(
595 c("f.tbz2"),
596 "tar over bzip2",
597 "the spelling that always worked must keep working"
598 );
599 assert_eq!(
600 c("f.tar.bz2"),
601 "tar over bzip2",
602 "and so must the long form"
603 );
604
605 assert_eq!(
610 c("f.bz"),
611 "raw",
612 "`bz` must resolve to nothing outside the t-prefixed compound form"
613 );
614
615 let chain = resolve_chain(®, Some(Path::new("f.tbz")), b"BZh\0").unwrap();
619 assert_eq!(
620 chain.describe(),
621 "tar over bzip2",
622 "reading a .tbz must agree with writing one"
623 );
624 }
625
626 #[test]
629 fn the_outermost_codec_is_the_layer_a_writer_creates_first() {
630 let reg = registry();
631 let pair = |p: &str| {
632 let c = chain_for_new_path(®, Path::new(p));
633 (c.container(), c.outermost_codec())
634 };
635 assert_eq!(pair("x.tar"), (Some(TAR), None));
636 assert_eq!(pair("x.gz"), (None, Some(GZIP)));
637 assert_eq!(pair("x.tar.gz"), (Some(TAR), Some(GZIP)));
638 assert_eq!(pair("x.tgz"), (Some(TAR), Some(GZIP)));
639 assert_eq!(pair("x.txt"), (None, None));
640 }
641
642 #[test]
643 fn probe_does_not_consume_the_stream() {
644 let src: Box<dyn Source> = Box::new(ReaderSource::new(std::io::Cursor::new(
645 b"HEADERbody".to_vec(),
646 )));
647 let (prefix, mut rest) = probe(src).unwrap();
648 assert_eq!(&prefix[..6], b"HEADER");
649
650 let mut all = Vec::new();
651 rest.read_to_end(&mut all).unwrap();
652 assert_eq!(all, b"HEADERbody", "the consumer must still see byte zero");
653 }
654
655 #[test]
656 fn probe_reads_at_most_the_window() {
657 let big = vec![b'x'; PROBE_LEN * 3];
658 let src: Box<dyn Source> = Box::new(ReaderSource::new(std::io::Cursor::new(big)));
659 let (prefix, _) = probe(src).unwrap();
660 assert_eq!(prefix.len(), PROBE_LEN);
661 }
662
663 #[test]
664 fn magic_identifies_a_bare_codec_stream() {
665 let r = registry();
666 let chain = resolve_chain(&r, Some(Path::new("blob.gz")), &[0x1f, 0x8b, 0x08]).unwrap();
667 assert_eq!(
668 chain,
669 Chain::Codec {
670 codec: GZIP,
671 inner: Box::new(Chain::Raw)
672 }
673 );
674 assert_eq!(chain.describe(), "gzip");
675 }
676
677 #[test]
678 fn a_double_extension_nests_the_container_inside_the_codec() {
679 let r = registry();
680 let chain = resolve_chain(&r, Some(Path::new("out.tar.gz")), &[0x1f, 0x8b, 0x08]).unwrap();
681 assert_eq!(
682 chain,
683 Chain::Codec {
684 codec: GZIP,
685 inner: Box::new(Chain::Container { container: TAR })
686 }
687 );
688 assert_eq!(chain.describe(), "tar over gzip");
689 }
690
691 #[test]
692 fn a_codec_on_stdin_resolves_to_raw_inner() {
693 let r = registry();
696 let chain = resolve_chain(&r, None, &[0x1f, 0x8b, 0x08]).unwrap();
697 assert_eq!(
698 chain,
699 Chain::Codec {
700 codec: GZIP,
701 inner: Box::new(Chain::Raw)
702 }
703 );
704 }
705
706 #[test]
707 fn container_magic_resolves_directly() {
708 let r = registry();
709 let mut prefix = vec![0u8; 512];
710 prefix[257..262].copy_from_slice(b"ustar");
711 let chain = resolve_chain(&r, Some(Path::new("x.tar")), &prefix).unwrap();
712 assert_eq!(chain, Chain::Container { container: TAR });
713 assert_eq!(chain.describe(), "tar");
714 }
715
716 #[test]
717 fn extension_is_the_fallback_when_magic_says_nothing() {
718 let r = registry();
719 let chain = resolve_chain(&r, Some(Path::new("mystery.tar")), b"no magic here").unwrap();
720 assert_eq!(chain, Chain::Container { container: TAR });
721 }
722
723 #[test]
729 fn chain_container_sees_through_codec_layers() {
730 let tar = FormatId::new("tar");
731 assert_eq!(Chain::Container { container: tar }.container(), Some(tar));
732 assert_eq!(
733 Chain::Codec {
734 codec: GZIP,
735 inner: Box::new(Chain::Container { container: tar }),
736 }
737 .container(),
738 Some(tar),
739 "tar over gzip still has a container nobody has opened"
740 );
741 assert_eq!(
742 Chain::Codec {
743 codec: GZIP,
744 inner: Box::new(Chain::Raw),
745 }
746 .container(),
747 None,
748 "a bare codec stream has no container, so an empty warning list is a real finding"
749 );
750 assert_eq!(Chain::Raw.container(), None);
751 }
752
753 #[test]
754 fn tgz_single_extension_still_nests_tar_inside_gzip() {
755 let r = registry();
756 let chain = resolve_chain(&r, Some(Path::new("a.tgz")), &[0x1f, 0x8b]).unwrap();
757 assert_eq!(
758 chain,
759 Chain::Codec {
760 codec: GZIP,
761 inner: Box::new(Chain::Container { container: TAR })
762 }
763 );
764 }
765
766 #[test]
767 fn a_compound_extension_resolves_without_a_registered_compound_alias() {
768 let mut r = Registry::new();
773 r.register_codec(
774 Arc::new(MockCodec),
775 FormatMeta::codec(GZIP, &["gz"], GZIP_MAGIC), );
777 r.register_container(
778 Arc::new(MockContainer),
779 FormatMeta::container(TAR, &["tar"], TAR_MAGIC),
780 );
781
782 let chain = resolve_chain(&r, Some(Path::new("a.tgz")), &[0x1f, 0x8b]).unwrap();
783 assert_eq!(
784 chain,
785 Chain::Codec {
786 codec: GZIP,
787 inner: Box::new(Chain::Container { container: TAR })
788 }
789 );
790 }
791
792 #[test]
793 fn probe_does_not_consume_a_seekable_stream_either() {
794 let mut f = tempfile::NamedTempFile::new().unwrap();
797 std::io::Write::write_all(&mut f, b"HEADERbody").unwrap();
798 let (_, path) = f.keep().unwrap();
799 let src: Box<dyn Source> = Box::new(crate::source::FileSource::open(&path).unwrap());
800
801 let (prefix, mut rest) = probe(src).unwrap();
802 assert_eq!(&prefix[..6], b"HEADER");
803
804 let mut all = Vec::new();
805 rest.read_to_end(&mut all).unwrap();
806 assert_eq!(
807 all, b"HEADERbody",
808 "a seekable source must be rewound to byte zero"
809 );
810 }
811
812 #[test]
813 fn probe_bounds_the_window_on_a_seekable_stream_too() {
814 let big = vec![b'x'; PROBE_LEN * 3];
815 let mut f = tempfile::NamedTempFile::new().unwrap();
816 std::io::Write::write_all(&mut f, &big).unwrap();
817 let (_, path) = f.keep().unwrap();
818 let src: Box<dyn Source> = Box::new(crate::source::FileSource::open(&path).unwrap());
819
820 let (prefix, _) = probe(src).unwrap();
821 assert_eq!(prefix.len(), PROBE_LEN);
822 }
823
824 #[test]
825 fn unknown_input_names_the_bytes_it_actually_saw() {
826 let r = registry();
828 let err = resolve_chain(&r, None, &[0xde, 0xad, 0xbe, 0xef]).unwrap_err();
829 let msg = err.to_string();
830 assert!(msg.contains("de ad be ef"), "message was: {msg}");
831 assert!(matches!(err, crate::Error::UnknownFormat { .. }));
832 }
833
834 #[test]
835 fn unknown_empty_input_still_produces_a_usable_message() {
836 let r = registry();
837 let err = resolve_chain(&r, None, b"").unwrap_err();
838 assert!(err.to_string().contains("empty"));
839 }
840
841 #[test]
842 fn equally_ranked_candidates_with_no_extension_are_an_error_not_a_guess() {
843 const A: &[MagicRule] = &[MagicRule {
846 offset: 0,
847 bytes: b"PK",
848 format: FormatId::new("alpha"),
849 }];
850 const B: &[MagicRule] = &[MagicRule {
851 offset: 0,
852 bytes: b"PK",
853 format: FormatId::new("beta"),
854 }];
855 let mut r = Registry::new();
856 r.register_container(
857 Arc::new(MockContainer),
858 FormatMeta::container(FormatId::new("alpha"), &["alpha"], A),
859 );
860 r.register_container(
861 Arc::new(MockContainer),
862 FormatMeta::container(FormatId::new("beta"), &["beta"], B),
863 );
864
865 let err = resolve_chain(&r, None, b"PKrest").unwrap_err();
866 assert!(matches!(err, crate::Error::AmbiguousFormat { .. }));
867 let msg = err.to_string();
868 assert!(
869 msg.contains("alpha") && msg.contains("beta"),
870 "must name the candidates: {msg}"
871 );
872 assert_eq!(err.exit_code(), 2);
880 }
881
882 #[test]
883 fn a_higher_priority_candidate_resolves_without_an_extension() {
884 const A: &[MagicRule] = &[MagicRule {
885 offset: 0,
886 bytes: b"PK",
887 format: FormatId::new("alpha"),
888 }];
889 const B: &[MagicRule] = &[MagicRule {
890 offset: 0,
891 bytes: b"PK",
892 format: FormatId::new("beta"),
893 }];
894 let mut r = Registry::new();
895 r.register_container(
896 Arc::new(MockContainer),
897 FormatMeta::container(FormatId::new("alpha"), &["alpha"], A).with_priority(-5),
898 );
899 r.register_container(
900 Arc::new(MockContainer),
901 FormatMeta::container(FormatId::new("beta"), &["beta"], B),
902 );
903
904 let chain = resolve_chain(&r, None, b"PKrest").unwrap();
905 assert_eq!(
906 chain,
907 Chain::Container {
908 container: FormatId::new("beta")
909 }
910 );
911 }
912
913 #[test]
914 fn an_extension_still_disambiguates_equal_candidates() {
915 const A: &[MagicRule] = &[MagicRule {
916 offset: 0,
917 bytes: b"PK",
918 format: FormatId::new("alpha"),
919 }];
920 const B: &[MagicRule] = &[MagicRule {
921 offset: 0,
922 bytes: b"PK",
923 format: FormatId::new("beta"),
924 }];
925 let mut r = Registry::new();
926 r.register_container(
927 Arc::new(MockContainer),
928 FormatMeta::container(FormatId::new("alpha"), &["alpha"], A),
929 );
930 r.register_container(
931 Arc::new(MockContainer),
932 FormatMeta::container(FormatId::new("beta"), &["beta"], B),
933 );
934
935 let chain = resolve_chain(&r, Some(Path::new("x.beta")), b"PKrest").unwrap();
936 assert_eq!(
937 chain,
938 Chain::Container {
939 container: FormatId::new("beta")
940 }
941 );
942 }
943
944 #[test]
945 fn an_extension_outside_the_tied_set_does_not_break_the_tie() {
946 const A: &[MagicRule] = &[MagicRule {
952 offset: 0,
953 bytes: b"PK",
954 format: FormatId::new("alpha"),
955 }];
956 const B: &[MagicRule] = &[MagicRule {
957 offset: 0,
958 bytes: b"PK",
959 format: FormatId::new("beta"),
960 }];
961 const C: &[MagicRule] = &[MagicRule {
962 offset: 0,
963 bytes: b"GZ",
964 format: FormatId::new("gamma"),
965 }];
966 let mut r = Registry::new();
967 r.register_container(
968 Arc::new(MockContainer),
969 FormatMeta::container(FormatId::new("alpha"), &["alpha"], A),
970 );
971 r.register_container(
972 Arc::new(MockContainer),
973 FormatMeta::container(FormatId::new("beta"), &["beta"], B),
974 );
975 r.register_container(
976 Arc::new(MockContainer),
977 FormatMeta::container(FormatId::new("gamma"), &["gamma"], C),
978 );
979
980 let err = resolve_chain(&r, Some(Path::new("x.gamma")), b"PKrest").unwrap_err();
981 assert!(matches!(err, crate::Error::AmbiguousFormat { .. }));
982 let msg = err.to_string();
983 assert!(
984 msg.contains("alpha") && msg.contains("beta"),
985 "must name the tied candidates: {msg}"
986 );
987 assert!(
988 !msg.contains("gamma"),
989 "must not name the irrelevant extension: {msg}"
990 );
991 }
992
993 #[test]
994 fn an_extension_reaches_a_lower_priority_format_that_shares_the_magic() {
995 const ZIP_MAGIC: &[MagicRule] = &[MagicRule {
999 offset: 0,
1000 bytes: b"PK",
1001 format: FormatId::new("zip"),
1002 }];
1003 const APK_MAGIC: &[MagicRule] = &[MagicRule {
1004 offset: 0,
1005 bytes: b"PK",
1006 format: FormatId::new("apk"),
1007 }];
1008 let mut r = Registry::new();
1009 r.register_container(
1010 Arc::new(MockContainer),
1011 FormatMeta::container(FormatId::new("zip"), &["zip"], ZIP_MAGIC),
1012 );
1013 r.register_container(
1014 Arc::new(MockContainer),
1015 FormatMeta::container(FormatId::new("apk"), &["apk"], APK_MAGIC).with_priority(-5),
1016 );
1017
1018 let chain = resolve_chain(&r, None, b"PKrest").unwrap();
1020 assert_eq!(
1021 chain,
1022 Chain::Container {
1023 container: FormatId::new("zip")
1024 }
1025 );
1026
1027 let chain = resolve_chain(&r, Some(Path::new("archive.apk")), b"PKrest").unwrap();
1030 assert_eq!(
1031 chain,
1032 Chain::Container {
1033 container: FormatId::new("apk")
1034 }
1035 );
1036 }
1037
1038 struct LiesAboutSeekability(std::io::Cursor<Vec<u8>>);
1044
1045 impl Read for LiesAboutSeekability {
1046 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
1047 std::io::Read::read(&mut self.0, buf)
1048 }
1049 }
1050
1051 impl Source for LiesAboutSeekability {
1052 fn caps(&self) -> crate::source::SourceCaps {
1053 crate::source::SourceCaps {
1054 seekable: true,
1055 len: Some(self.0.get_ref().len() as u64),
1056 }
1057 }
1058
1059 fn as_seek(&mut self) -> Option<&mut dyn crate::source::SeekRead> {
1060 None
1061 }
1062 }
1063
1064 #[test]
1065 fn a_source_that_claims_seekable_but_has_no_seek_view_errors_instead_of_panicking() {
1066 let src: Box<dyn Source> = Box::new(LiesAboutSeekability(std::io::Cursor::new(
1067 b"payload".to_vec(),
1068 )));
1069 match probe(src) {
1073 Err(err) => assert!(
1074 matches!(err, crate::Error::NotSeekable { .. }),
1075 "must be NotSeekable, not a panic: {err:?}"
1076 ),
1077 Ok(_) => panic!("expected NotSeekable, got Ok"),
1078 }
1079 }
1080
1081 const NESTED_MAGIC: &[u8] = b"MOCK";
1092
1093 struct PrefixedMockCodec;
1094
1095 impl Codec for PrefixedMockCodec {
1096 fn id(&self) -> FormatId {
1097 MOCK_CODEC
1098 }
1099
1100 fn caps(&self) -> CodecCaps {
1101 CodecCaps {
1102 encode: true,
1103 decode: true,
1104 ..Default::default()
1105 }
1106 }
1107
1108 fn decoder(&self, mut src: Box<dyn Source>, _o: &DecodeOpts) -> Result<Box<dyn Source>> {
1109 let mut discard = vec![0u8; NESTED_MAGIC.len()];
1110 src.read_exact(&mut discard)?;
1111 Ok(Box::new(StreamOnly::new(src)))
1112 }
1113
1114 fn encoder(
1115 &self,
1116 mut dst: Box<dyn Write + Send>,
1117 _o: &EncodeOpts,
1118 ) -> Result<Box<dyn Sink>> {
1119 dst.write_all(NESTED_MAGIC)?;
1120 Ok(Box::new(PassthroughSink(dst)))
1121 }
1122 }
1123
1124 struct PassthroughSink(Box<dyn Write + Send>);
1125
1126 impl Write for PassthroughSink {
1127 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
1128 self.0.write(buf)
1129 }
1130 fn flush(&mut self) -> std::io::Result<()> {
1131 self.0.flush()
1132 }
1133 }
1134
1135 impl Sink for PassthroughSink {
1136 fn finish(mut self: Box<Self>) -> Result<()> {
1137 self.0.flush()?;
1138 Ok(())
1139 }
1140 }
1141
1142 fn mock_registry_with_codec_and_container() -> Registry {
1143 const CODEC_MAGIC: &[MagicRule] = &[MagicRule {
1144 offset: 0,
1145 bytes: NESTED_MAGIC,
1146 format: MOCK_CODEC,
1147 }];
1148 const CONTAINER_MAGIC: &[MagicRule] = &[MagicRule {
1149 offset: 0,
1150 bytes: b"ME",
1151 format: MOCK_CONTAINER,
1152 }];
1153 let mut r = Registry::new();
1154 r.register_codec(
1155 Arc::new(PrefixedMockCodec),
1156 FormatMeta::codec(MOCK_CODEC, &["mock"], CODEC_MAGIC),
1157 );
1158 r.register_container(
1159 Arc::new(MockContainer),
1160 FormatMeta::container(MOCK_CONTAINER, &["mar"], CONTAINER_MAGIC),
1161 );
1162 r
1163 }
1164
1165 fn mock_codec_encode(payload: &[u8]) -> Vec<u8> {
1169 let sink = SharedBuf::new();
1170 let mut s = PrefixedMockCodec
1171 .encoder(Box::new(sink.clone()), &EncodeOpts::default())
1172 .unwrap();
1173 s.write_all(payload).unwrap();
1174 s.finish().unwrap();
1175 sink.contents()
1176 }
1177
1178 #[test]
1179 fn a_container_inside_a_codec_resolves_from_the_stream_with_no_path() {
1180 let reg = mock_registry_with_codec_and_container();
1181 let inner = mock_archive_bytes(&[("a.txt", b"alpha")]);
1182 let outer = mock_codec_encode(&inner);
1183
1184 let (chain, _src) = resolve_chain_deep(
1185 ®,
1186 None,
1187 Box::new(ReaderSource::new(io::Cursor::new(outer))),
1188 )
1189 .expect("resolve");
1190
1191 assert_eq!(
1194 chain,
1195 Chain::Codec {
1196 codec: MOCK_CODEC,
1197 inner: Box::new(Chain::Container {
1198 container: MOCK_CONTAINER
1199 }),
1200 },
1201 "a container nested inside a codec must be found by peeking the decoded stream"
1202 );
1203 }
1204
1205 #[test]
1206 fn nesting_beyond_the_depth_bound_is_a_typed_error_naming_the_depth() {
1207 let reg = mock_registry_with_codec_and_container();
1208 let mut bytes = mock_archive_bytes(&[("a.txt", b"alpha")]);
1209 for _ in 0..MAX_CHAIN_DEPTH + 2 {
1210 bytes = mock_codec_encode(&bytes);
1211 }
1212
1213 match resolve_chain_deep(
1219 ®,
1220 None,
1221 Box::new(ReaderSource::new(io::Cursor::new(bytes))),
1222 ) {
1223 Err(Error::ChainTooDeep { depth }) => assert_eq!(depth, MAX_CHAIN_DEPTH),
1224 Err(other) => panic!("expected ChainTooDeep, got {other:?}"),
1225 Ok(_) => panic!("nesting past the bound must be refused"),
1226 }
1227 }
1228
1229 #[test]
1230 fn nesting_beyond_the_depth_bound_is_refused_via_the_path_route_too() {
1231 let reg = mock_registry_with_codec_and_container();
1239 let mut bytes = mock_archive_bytes(&[("a.txt", b"alpha")]);
1240 for _ in 0..MAX_CHAIN_DEPTH + 2 {
1241 bytes = mock_codec_encode(&bytes);
1242 }
1243
1244 match resolve_chain_deep(
1245 ®,
1246 Some(Path::new("nested.mock")),
1247 Box::new(ReaderSource::new(io::Cursor::new(bytes))),
1248 ) {
1249 Err(Error::ChainTooDeep { depth }) => assert_eq!(depth, MAX_CHAIN_DEPTH),
1250 Err(other) => panic!("expected ChainTooDeep, got {other:?}"),
1251 Ok(_) => panic!("nesting past the bound must be refused, path or no path"),
1252 }
1253 }
1254
1255 #[test]
1256 fn the_returned_source_is_decoded_through_every_codec_layer_on_both_routes() {
1257 let reg = mock_registry_with_codec_and_container();
1273 let inner = mock_archive_bytes(&[("a.txt", b"alpha")]);
1274 let outer = mock_codec_encode(&inner);
1275
1276 let (chain_with_path, mut src_with_path) = resolve_chain_deep(
1277 ®,
1278 Some(Path::new("thing.mar.mock")),
1279 Box::new(ReaderSource::new(io::Cursor::new(outer.clone()))),
1280 )
1281 .expect("resolve with path");
1282 let (chain_no_path, mut src_no_path) = resolve_chain_deep(
1283 ®,
1284 None,
1285 Box::new(ReaderSource::new(io::Cursor::new(outer))),
1286 )
1287 .expect("resolve without path");
1288
1289 let expected_chain = Chain::Codec {
1290 codec: MOCK_CODEC,
1291 inner: Box::new(Chain::Container {
1292 container: MOCK_CONTAINER,
1293 }),
1294 };
1295 assert_eq!(chain_with_path, expected_chain);
1296 assert_eq!(chain_no_path, expected_chain);
1297
1298 let mut bytes_with_path = Vec::new();
1299 src_with_path.read_to_end(&mut bytes_with_path).unwrap();
1300 let mut bytes_no_path = Vec::new();
1301 src_no_path.read_to_end(&mut bytes_no_path).unwrap();
1302
1303 assert_eq!(
1304 bytes_with_path, inner,
1305 "the path route must decode through the codec layer, not return raw bytes"
1306 );
1307 assert_eq!(
1308 bytes_with_path, bytes_no_path,
1309 "both routes must leave the source in the identical, already-decoded state"
1310 );
1311 }
1312}