1pub const SNIFF_LEN: usize = 512;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum Format {
8 Gzip,
9 Zstd,
10 Xz,
11 Bzip2,
12 Lz4,
13 CompressZ,
15 Zip,
16 Tar,
17 Plain,
18}
19
20const LZ4_FRAME: [u8; 4] = [0x04, 0x22, 0x4D, 0x18];
21const LZ4_LEGACY: [u8; 4] = [0x02, 0x21, 0x4C, 0x18];
23const BZ_BLOCK: [u8; 6] = [0x31, 0x41, 0x59, 0x26, 0x53, 0x59];
25const BZ_END: [u8; 6] = [0x17, 0x72, 0x45, 0x38, 0x50, 0x90];
26
27fn is_skippable(b: &[u8]) -> bool {
28 b.len() >= 4 && (0x50..=0x5F).contains(&b[0]) && b[1..4] == [0x2A, 0x4D, 0x18]
29}
30
31pub fn skippable_prefix(head: &[u8]) -> Option<usize> {
35 let mut at = 0usize;
36 while is_skippable(&head[at..]) {
37 let len = head.get(at + 4..at + 8)?;
38 let len = u32::from_le_bytes(len.try_into().unwrap()) as usize;
39 at = at.checked_add(8 + len).filter(|&next| next <= head.len())?;
40 }
41 Some(at)
42}
43
44fn after_skippable(head: &[u8]) -> Format {
46 let Some(at) = skippable_prefix(head) else { return Format::Zstd };
47 match head.get(at..at + 4) {
48 Some(m) if m == LZ4_FRAME || m == LZ4_LEGACY => Format::Lz4,
49 _ => Format::Zstd,
50 }
51}
52
53pub fn sniff(head: &[u8]) -> Format {
57 if head.len() >= 2 && head[0] == 0x1f && head[1] == 0x8b {
58 return Format::Gzip;
59 }
60 if head.len() >= 2 && head[0] == 0x1f && head[1] == 0x9d {
62 return Format::CompressZ;
63 }
64 if head.len() >= 4
68 && head[..2] == *b"PK"
69 && matches!(head[2..4], [3, 4] | [5, 6] | [7, 8])
70 {
71 return Format::Zip;
72 }
73 if head.len() >= 4 && head[..4] == [0x28, 0xB5, 0x2F, 0xFD] {
74 return Format::Zstd;
75 }
76 if head.len() >= 4 && (head[..4] == LZ4_FRAME || head[..4] == LZ4_LEGACY) {
77 return Format::Lz4;
78 }
79 if is_skippable(head) {
87 return after_skippable(head);
88 }
89 if head.len() >= 6 && head[..6] == [0xFD, b'7', b'z', b'X', b'Z', 0x00] {
90 return Format::Xz;
91 }
92 if head.len() >= 10
96 && head[..3] == *b"BZh"
97 && (b'1'..=b'9').contains(&head[3])
98 && (head[4..10] == BZ_BLOCK || head[4..10] == BZ_END)
99 {
100 return Format::Bzip2;
101 }
102 if head.len() >= 262 && &head[257..262] == b"ustar" {
103 return Format::Tar;
104 }
105 Format::Plain
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 #[test]
113 fn detects_gzip_from_magic() {
114 assert_eq!(sniff(&[0x1f, 0x8b, 0x08, 0x00]), Format::Gzip);
115 }
116
117 #[test]
118 fn detects_ustar_tar_at_offset_257() {
119 let mut b = vec![0u8; 512];
120 b[257..262].copy_from_slice(b"ustar");
121 assert_eq!(sniff(&b), Format::Tar);
122 }
123
124 #[test]
125 fn detects_gnu_tar_variant() {
126 let mut b = vec![0u8; 512];
127 b[257..265].copy_from_slice(b"ustar \0");
128 assert_eq!(sniff(&b), Format::Tar);
129 }
130
131 #[test]
132 fn detects_zstd_from_magic() {
133 assert_eq!(sniff(&[0x28, 0xB5, 0x2F, 0xFD, 0x00]), Format::Zstd);
134 }
135
136 #[test]
141 fn a_skippable_frame_is_zstd() {
142 for low in 0x50..=0x5F {
143 assert_eq!(sniff(&[low, 0x2A, 0x4D, 0x18, 4, 0, 0, 0]), Format::Zstd, "magic {low:#x}");
144 }
145 assert_eq!(sniff(&[0x60, 0x2A, 0x4D, 0x18, 4, 0, 0, 0]), Format::Plain, "just past the range");
146 }
147
148 #[test]
149 fn zstd_magic_needs_all_four_bytes() {
150 assert_eq!(sniff(&[0x28, 0xB5, 0x2F]), Format::Plain);
152 }
153
154 #[test]
155 fn zstd_wins_over_a_coincidental_ustar() {
156 let mut b = vec![0u8; 512];
157 b[..4].copy_from_slice(&[0x28, 0xB5, 0x2F, 0xFD]);
158 b[257..262].copy_from_slice(b"ustar");
159 assert_eq!(sniff(&b), Format::Zstd);
160 }
161
162 #[test]
163 fn detects_xz_from_magic() {
164 assert_eq!(sniff(&[0xFD, b'7', b'z', b'X', b'Z', 0x00, 0x00]), Format::Xz);
165 }
166
167 #[test]
168 fn xz_magic_needs_all_six_bytes() {
169 assert_eq!(sniff(&[0xFD, b'7', b'z', b'X', b'Z']), Format::Plain);
170 }
171
172 #[test]
173 fn xz_wins_over_a_coincidental_ustar() {
174 let mut b = vec![0u8; 512];
175 b[..6].copy_from_slice(&[0xFD, b'7', b'z', b'X', b'Z', 0x00]);
176 b[257..262].copy_from_slice(b"ustar");
177 assert_eq!(sniff(&b), Format::Xz);
178 }
179
180 fn bz2_head(level: u8) -> Vec<u8> {
181 let mut b = b"BZh".to_vec();
182 b.push(level);
183 b.extend_from_slice(&BZ_BLOCK);
184 b
185 }
186
187 #[test]
188 fn detects_bzip2_from_magic() {
189 for level in b'1'..=b'9' {
190 assert_eq!(sniff(&bz2_head(level)), Format::Bzip2, "BZh{}", level as char);
191 }
192 }
193
194 #[test]
196 fn an_empty_bzip2_stream_is_bzip2() {
197 let mut b = b"BZh9".to_vec();
198 b.extend_from_slice(&BZ_END);
199 assert_eq!(sniff(&b), Format::Bzip2);
200 }
201
202 #[test]
205 fn bzip2_needs_more_than_its_printable_prefix() {
206 assert_eq!(sniff(b"BZh9 is not a block size\n"), Format::Plain);
207 assert_eq!(sniff(b"BZh0"), Format::Plain);
208 let mut zero = b"BZh0".to_vec();
209 zero.extend_from_slice(&BZ_BLOCK);
210 assert_eq!(sniff(&zero), Format::Plain, "level 0 does not exist");
211 }
212
213 #[test]
214 fn bzip2_wins_over_a_coincidental_ustar() {
215 let mut b = vec![0u8; 512];
216 b[..10].copy_from_slice(&bz2_head(b'9'));
217 b[257..262].copy_from_slice(b"ustar");
218 assert_eq!(sniff(&b), Format::Bzip2);
219 }
220
221 #[test]
222 fn detects_lz4_frame_and_legacy_magics() {
223 assert_eq!(sniff(&[0x04, 0x22, 0x4D, 0x18, 0x64]), Format::Lz4);
224 assert_eq!(sniff(&[0x02, 0x21, 0x4C, 0x18, 0x00]), Format::Lz4);
225 assert_eq!(sniff(&[0x04, 0x22, 0x4D]), Format::Plain, "needs all four bytes");
226 }
227
228 #[test]
229 fn lz4_wins_over_a_coincidental_ustar() {
230 let mut b = vec![0u8; 512];
231 b[..4].copy_from_slice(&LZ4_FRAME);
232 b[257..262].copy_from_slice(b"ustar");
233 assert_eq!(sniff(&b), Format::Lz4);
234 }
235
236 #[test]
239 fn a_skippable_frame_before_lz4_is_lz4() {
240 let mut b = vec![0x50, 0x2A, 0x4D, 0x18, 4, 0, 0, 0, 1, 2, 3, 4];
241 b.extend_from_slice(&LZ4_FRAME);
242 assert_eq!(sniff(&b), Format::Lz4);
243 }
244
245 #[test]
246 fn a_skippable_frame_before_zstd_is_still_zstd() {
247 let mut b = vec![0x5F, 0x2A, 0x4D, 0x18, 0, 0, 0, 0];
248 b.extend_from_slice(&[0x50, 0x2A, 0x4D, 0x18, 2, 0, 0, 0, 9, 9]);
249 b.extend_from_slice(&[0x28, 0xB5, 0x2F, 0xFD]);
250 assert_eq!(sniff(&b), Format::Zstd, "two skippable frames, then zstd");
251 }
252
253 #[test]
256 fn a_skippable_frame_past_the_window_is_zstd_not_plain() {
257 let mut b = vec![0x50, 0x2A, 0x4D, 0x18, 0xFF, 0xFF, 0xFF, 0xFF];
258 b.resize(SNIFF_LEN, 0);
259 assert_eq!(sniff(&b), Format::Zstd);
260 assert_eq!(sniff(&[0x50, 0x2A, 0x4D, 0x18, 4]), Format::Zstd, "truncated length");
261 }
262
263 #[test]
264 fn detects_compress_z_from_magic() {
265 assert_eq!(sniff(&[0x1f, 0x9d, 0x90]), Format::CompressZ);
266 assert_eq!(sniff(&[0x1f]), Format::Plain);
267 }
268
269 #[test]
270 fn detects_zip_from_its_record_signatures() {
271 assert_eq!(sniff(b"PK\x03\x04\x14\x00"), Format::Zip, "local file header");
272 assert_eq!(sniff(b"PK\x05\x06\x00\x00"), Format::Zip, "empty zip");
273 assert_eq!(sniff(b"PK\x07\x08PK\x03\x04"), Format::Zip, "split marker");
274 assert_eq!(sniff(b"PK is a log line\n"), Format::Plain);
275 assert_eq!(sniff(b"PK\x01\x02"), Format::Plain, "a central record does not start a file");
276 }
277
278 #[test]
279 fn zip_wins_over_a_coincidental_ustar() {
280 let mut b = vec![0u8; 512];
281 b[..4].copy_from_slice(b"PK\x03\x04");
282 b[257..262].copy_from_slice(b"ustar");
283 assert_eq!(sniff(&b), Format::Zip);
284 }
285
286 #[test]
287 fn plain_text_is_plain() {
288 assert_eq!(sniff(b"2026-08-24T00:00:00Z GET /index.php\n"), Format::Plain);
289 }
290
291 #[test]
292 fn short_input_is_plain_not_panic() {
293 assert_eq!(sniff(b""), Format::Plain);
294 assert_eq!(sniff(&[0x1f]), Format::Plain);
295 }
296
297 #[test]
298 fn gzip_wins_over_a_coincidental_ustar() {
299 let mut b = vec![0u8; 512];
301 b[0] = 0x1f; b[1] = 0x8b;
302 b[257..262].copy_from_slice(b"ustar");
303 assert_eq!(sniff(&b), Format::Gzip);
304 }
305}