1use crate::detect::{sniff, Format};
9use crate::error::{ArchiveError, Result};
10
11pub(crate) const MAX_INFLATED: u64 = 4 << 30; #[derive(Debug)]
17#[non_exhaustive]
18pub enum PeelOutcome {
19 NotPacked,
21 Peeled { format: Format, inner: Vec<u8> },
24}
25
26pub fn peel_bytes(data: &[u8], name: Option<&str>) -> Result<PeelOutcome> {
30 match sniff(name, data) {
31 Format::Gzip => Ok(PeelOutcome::Peeled {
32 format: Format::Gzip,
33 inner: decode_gzip(data)?,
34 }),
35 Format::Bzip2 => Ok(PeelOutcome::Peeled {
36 format: Format::Bzip2,
37 inner: decode_bzip2(data)?,
38 }),
39 _ => Ok(PeelOutcome::NotPacked),
40 }
41}
42
43pub(crate) fn decode_gzip(data: &[u8]) -> Result<Vec<u8>> {
45 use flate2::read::GzDecoder;
46 use std::io::Read;
47 let mut out = Vec::new();
48 let mut limited = GzDecoder::new(data).take(MAX_INFLATED + 1);
51 limited
52 .read_to_end(&mut out)
53 .map_err(|e| ArchiveError::Decode {
54 format: "gzip",
55 detail: e.to_string(),
56 })?;
57 if out.len() as u64 > MAX_INFLATED {
58 return Err(ArchiveError::TooLarge { cap: MAX_INFLATED });
59 }
60 Ok(out)
61}
62
63pub(crate) fn decode_bzip2(data: &[u8]) -> Result<Vec<u8>> {
65 use std::io::Read;
66 let mut out = Vec::new();
67 let mut limited = bzip2_rs::DecoderReader::new(data).take(MAX_INFLATED + 1);
68 limited
69 .read_to_end(&mut out)
70 .map_err(|e| ArchiveError::Decode {
71 format: "bzip2",
72 detail: e.to_string(),
73 })?;
74 if out.len() as u64 > MAX_INFLATED {
75 return Err(ArchiveError::TooLarge { cap: MAX_INFLATED });
76 }
77 Ok(out)
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83 use flate2::write::GzEncoder;
84 use flate2::Compression;
85 use std::io::Write;
86
87 fn gzip(data: &[u8]) -> Vec<u8> {
88 let mut e = GzEncoder::new(Vec::new(), Compression::default());
89 e.write_all(data).unwrap();
90 e.finish().unwrap()
91 }
92
93 #[test]
94 fn peels_bare_gzip_to_inner_bytes() {
95 let inner = b"E01-ish evidence \x00\x01\x02 the quick brown fox".repeat(20);
96 let gz = gzip(&inner);
97 assert_eq!(sniff(Some("evidence.bin"), &gz), Format::Gzip);
98 match peel_bytes(&gz, Some("evidence.dd.gz")).unwrap() {
99 PeelOutcome::Peeled { format, inner: got } => {
100 assert_eq!(format, Format::Gzip);
101 assert_eq!(got, inner);
102 }
103 other => panic!("expected Peeled, got {other:?}"),
104 }
105 }
106
107 const PAYLOAD_BZ2: &[u8] = include_bytes!("../../tests/data/fixtures/payload.bz2");
108
109 #[test]
110 fn peels_bare_bzip2() {
111 let expected = "archive-detour bzip2 test payload — the quick brown fox\n"
112 .repeat(30)
113 .into_bytes();
114 match peel_bytes(PAYLOAD_BZ2, Some("payload.bz2")).unwrap() {
115 PeelOutcome::Peeled { format, inner } => {
116 assert_eq!(format, Format::Bzip2);
117 assert_eq!(inner, expected);
118 }
119 other => panic!("expected Peeled, got {other:?}"),
120 }
121 }
122
123 #[test]
124 fn archive_is_not_bare_peeled() {
125 let zip = b"PK\x03\x04 rest of a zip";
127 assert!(matches!(
128 peel_bytes(zip, Some("eo.zip")).unwrap(),
129 PeelOutcome::NotPacked
130 ));
131 }
132
133 #[test]
134 fn non_packed_passthrough() {
135 let raw = b"\x00\x01\x02 not a known wrapper";
136 assert!(matches!(
137 peel_bytes(raw, Some("disk.raw")).unwrap(),
138 PeelOutcome::NotPacked
139 ));
140 }
141
142 #[test]
143 fn magic_beats_extension() {
144 let seven = [0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C, 0, 0];
145 assert_eq!(sniff(Some("foo.gz"), &seven), Format::SevenZip);
146 }
147}