Skip to main content

archive_core/
peel.rs

1//! Peel one outer BARE compression layer (gzip / bzip2), if present.
2//!
3//! This handles a single compressed file — e.g. a `disk.dd.gz` evidence image.
4//! Multi-member archives (`.tgz`/`.tbz2`/`.zip`/`.7z`) are handled by
5//! [`crate::archive`], which reuses [`decode_gzip`]/[`decode_bzip2`] for their
6//! outer layer.
7
8use crate::detect::{sniff, Format};
9use crate::error::{ArchiveError, Result};
10
11/// In-memory cap on a single decompression's output (bomb guard). Streaming /
12/// temp-spill for genuinely large inner evidence is the next hardening step.
13pub(crate) const MAX_INFLATED: u64 = 4 << 30; // 4 GiB
14
15/// The result of attempting to peel one bare-compression layer.
16#[derive(Debug)]
17#[non_exhaustive]
18pub enum PeelOutcome {
19    /// Not a bare compression wrapper — pass it through unchanged.
20    NotPacked,
21    /// A bare gzip/bzip2 wrapper peeled to its inner byte stream. A consumer
22    /// re-runs detection on `inner` to continue down the stack.
23    Peeled { format: Format, inner: Vec<u8> },
24}
25
26/// Peel one outer BARE gzip/bzip2 compression layer from `data`. Archives
27/// (`.tgz`/`.tbz2`/`.zip`/`.7z`) are NOT peeled here — they are member lists;
28/// use [`crate::archive::open`]. `name` is an optional file-name hint.
29pub 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
43/// Inflate a gzip stream to bytes, failing loud past [`MAX_INFLATED`].
44pub(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    // Read one byte past the cap so an over-cap stream is *detected*, not
49    // silently truncated.
50    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
63/// Inflate a bzip2 stream to bytes, failing loud past [`MAX_INFLATED`].
64pub(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        // A zip is a member list, not a bare wrapper — peel_bytes leaves it.
126        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}