1use std::io::{self, BufRead, Read};
20
21use stuffr::FormatId;
22use stuffr::pack_stream::{PackError, PackReader};
23
24use super::member::{Lookahead, PEEK};
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum Flavour {
29 Gnu,
31 Bsd,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub(crate) enum Detected {
38 Gzip,
39 Compress,
41 Bzip2,
42 Xz,
43 Lzip,
44 Pack,
46 Zip,
48 Lzh,
52 Unknown,
53}
54
55pub(crate) fn detect<R: Read>(r: &mut Lookahead<R>, flavour: Flavour) -> io::Result<Detected> {
58 let buf = r.fill_buf()?;
59 let m = &buf[..buf.len().min(PEEK)];
60 let d = match flavour {
61 Flavour::Gnu => detect_gnu(m),
62 Flavour::Bsd => detect_bsd(m),
63 };
64 Ok(if available(d) { d } else { Detected::Unknown })
65}
66
67fn detect_gnu(m: &[u8]) -> Detected {
70 match m {
71 [0x1f, 0x8b | 0x9e, ..] => Detected::Gzip,
72 [b'P', b'K', 3, 4] => Detected::Zip,
73 [0x1f, 0x9d, ..] => Detected::Compress,
74 [0x1f, 0x1e, ..] => Detected::Pack,
75 _ => Detected::Unknown,
76 }
77}
78
79fn detect_bsd(m: &[u8]) -> Detected {
81 match m {
82 [0x1f, 0x8b | 0x9e, ..] => Detected::Gzip,
83 [b'B', b'Z', b'h', d, ..] if d.is_ascii_digit() => Detected::Bzip2,
84 [0x1f, 0x9d, ..] => Detected::Compress,
85 [0x1f, 0x1e, ..] => Detected::Pack,
86 [0xfd, b'7', b'z', b'X'] => Detected::Xz,
87 [b'L', b'Z', b'I', b'P'] => Detected::Lzip,
88 _ => Detected::Unknown,
89 }
90}
91
92fn available(d: Detected) -> bool {
95 match codec_id(d) {
96 Some(id) => stuffr::registry().codec(FormatId::new(id)).is_some(),
97 None => !matches!(d, Detected::Lzh),
98 }
99}
100
101fn codec_id(d: Detected) -> Option<&'static str> {
102 match d {
103 Detected::Compress => Some("compress"),
104 Detected::Bzip2 => Some("bzip2"),
105 Detected::Xz => Some("xz"),
106 Detected::Lzip => Some("lzip"),
107 _ => None,
108 }
109}
110
111pub(crate) fn foreign_reader(
122 d: Detected,
123 r: Box<dyn BufRead + Send>,
124) -> io::Result<Box<dyn Read + Send>> {
125 if d == Detected::Pack {
126 return match PackReader::new(r) {
127 Ok(p) => Ok(Box::new(p)),
128 Err(PackError::Io(e)) => Err(e),
129 Err(e @ (PackError::Truncated | PackError::BadHeader(_))) => {
130 Err(io::Error::new(io::ErrorKind::UnexpectedEof, e))
131 }
132 Err(PackError::Corrupt("Symbol table truncated")) => Err(io::Error::new(
133 io::ErrorKind::UnexpectedEof,
134 PackError::Corrupt("Symbol table truncated"),
135 )),
136 Err(e) => Err(io::Error::new(io::ErrorKind::InvalidData, e)),
137 };
138 }
139 let Some(id) = codec_id(d) else {
140 return Err(io::Error::new(
141 io::ErrorKind::InvalidInput,
142 "not a foreign format",
143 ));
144 };
145 let Some(codec) = stuffr::registry().codec(FormatId::new(id)) else {
146 return Err(io::Error::new(
147 io::ErrorKind::Unsupported,
148 "format not in this build",
149 ));
150 };
151 let src = Box::new(stuffr::StreamOnly::new(r));
152 match codec.decoder(src, &stuffr::DecodeOpts::default()) {
153 Ok(s) => Ok(Box::new(s)),
154 Err(stuffr::Error::Io(e)) => Err(e),
155 Err(e) => Err(io::Error::other(e)),
156 }
157}
158
159pub(crate) struct ZipEntry<R> {
163 pub method: u16,
164 pub flags: u16,
165 pub crc: u32,
166 pub compressed_size: u32,
167 pub uncompressed_size: u32,
168 pub name: Vec<u8>,
169 reader: R,
170}
171
172pub(crate) fn zip_first_entry<R: BufRead>(mut r: R) -> io::Result<ZipEntry<R>> {
175 let mut h = [0u8; 30];
176 r.read_exact(&mut h)?;
177 if &h[..4] != b"PK\x03\x04" {
178 return Err(io::Error::new(
179 io::ErrorKind::InvalidData,
180 "not a zip local header",
181 ));
182 }
183 let u16_at = |i: usize| u16::from_le_bytes([h[i], h[i + 1]]);
184 let u32_at = |i: usize| u32::from_le_bytes([h[i], h[i + 1], h[i + 2], h[i + 3]]);
185 let (flags, method) = (u16_at(6), u16_at(8));
186 let (crc, compressed_size, uncompressed_size) = (u32_at(14), u32_at(18), u32_at(22));
187 let (nlen, xlen) = (usize::from(u16_at(26)), u64::from(u16_at(28)));
188 let mut name = vec![0u8; nlen];
189 r.read_exact(&mut name)?;
190 let skipped = io::copy(&mut (&mut r).take(xlen), &mut io::sink())?;
191 if skipped < xlen {
192 return Err(io::ErrorKind::UnexpectedEof.into());
193 }
194 Ok(ZipEntry {
195 method,
196 flags,
197 crc,
198 compressed_size,
199 uncompressed_size,
200 name,
201 reader: r,
202 })
203}
204
205impl<R: BufRead> ZipEntry<R> {
206 pub(crate) fn data(self) -> io::Result<Box<dyn Read>>
210 where
211 R: 'static,
212 {
213 match self.method {
214 0 if self.flags & 8 == 0 => Ok(Box::new(self.reader.take(self.compressed_size.into()))),
215 0 => Err(io::Error::new(
216 io::ErrorKind::Unsupported,
217 "stored entry with a data descriptor",
218 )),
219 8 => Ok(Box::new(flate2::bufread::DeflateDecoder::new(self.reader))),
220 _ => Err(io::Error::new(
221 io::ErrorKind::Unsupported,
222 "zip method not stored or deflated",
223 )),
224 }
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231 use std::io::Write;
232
233 fn la(b: &[u8]) -> Lookahead<&[u8]> {
234 Lookahead::new(b)
235 }
236
237 fn has(id: &'static str) -> bool {
238 stuffr::registry().codec(FormatId::new(id)).is_some()
239 }
240
241 #[test]
242 fn detect_every_magic_per_flavour() {
243 use Detected::*;
244 let compress = if has("compress") { Compress } else { Unknown };
245 let bzip2 = if has("bzip2") { Bzip2 } else { Unknown };
246 let xz = if has("xz") { Xz } else { Unknown };
247 let lzip = if has("lzip") { Lzip } else { Unknown };
248 let table: &[(&[u8], Flavour, Detected)] = &[
249 (b"\x1f\x8b\x08\x00", Flavour::Gnu, Gzip),
250 (b"\x1f\x9e\x08\x00", Flavour::Gnu, Gzip),
251 (b"PK\x03\x04", Flavour::Gnu, Zip),
252 (b"\x1f\x9d\x90\x00", Flavour::Gnu, compress),
253 (b"\x1f\x1e\x00\x00", Flavour::Gnu, Pack),
254 (b"\x1f\xa0\x00\x00", Flavour::Gnu, Unknown), (b"\x1f\x8b\x08\x00", Flavour::Bsd, Gzip),
256 (b"\x1f\x9e\x08\x00", Flavour::Bsd, Gzip),
257 (b"\x1f\x9d\x90\x00", Flavour::Bsd, compress),
258 (b"BZh9", Flavour::Bsd, bzip2),
259 (b"\xfd7zXZ\x00", Flavour::Bsd, xz),
260 (b"\xfd7zX", Flavour::Bsd, xz),
261 (b"\x1f\x1e\x00\x00", Flavour::Bsd, Pack),
262 (b"LZIP\x01", Flavour::Bsd, lzip),
263 ];
264 for (bytes, fl, want) in table {
265 assert_eq!(
266 detect(&mut la(bytes), *fl).unwrap(),
267 *want,
268 "{bytes:02x?} {fl:?}"
269 );
270 }
271 }
272
273 #[test]
274 fn a_magic_the_flavour_lacks_is_unknown() {
275 let cases: &[(&[u8], Flavour)] = &[
276 (b"BZh9", Flavour::Gnu),
277 (b"\xfd7zXZ\x00", Flavour::Gnu),
278 (b"LZIP\x01", Flavour::Gnu),
279 (b"PK\x03\x04", Flavour::Bsd),
280 (b"\x1f\xa0\x00\x00", Flavour::Bsd),
281 (b"BZhx", Flavour::Bsd), ];
283 for (bytes, fl) in cases {
284 assert_eq!(
285 detect(&mut la(bytes), *fl).unwrap(),
286 Detected::Unknown,
287 "{bytes:02x?} {fl:?}"
288 );
289 }
290 }
291
292 #[test]
293 fn detect_does_not_consume() {
294 let mut r = la(b"\x1f\x8b\x08\x00rest");
295 detect(&mut r, Flavour::Gnu).unwrap();
296 let mut all = Vec::new();
297 r.read_to_end(&mut all).unwrap();
298 assert_eq!(all, b"\x1f\x8b\x08\x00rest");
299 }
300
301 #[test]
302 fn detect_sees_a_whole_magic_through_tiny_reads() {
303 struct OneByte<'a>(&'a [u8]);
304 impl Read for OneByte<'_> {
305 fn read(&mut self, o: &mut [u8]) -> io::Result<usize> {
306 let n = self.0.len().min(1).min(o.len());
307 o[..n].copy_from_slice(&self.0[..n]);
308 self.0 = &self.0[n..];
309 Ok(n)
310 }
311 }
312 let mut r = Lookahead::new(OneByte(b"PK\x03\x04xxxx"));
313 assert_eq!(detect(&mut r, Flavour::Gnu).unwrap(), Detected::Zip);
314 let mut r = Lookahead::new(OneByte(b"LZIP\x01"));
315 let want = if has("lzip") {
316 Detected::Lzip
317 } else {
318 Detected::Unknown
319 };
320 assert_eq!(detect(&mut r, Flavour::Bsd).unwrap(), want);
321 }
322
323 #[test]
324 fn two_byte_and_empty_inputs() {
325 for fl in [Flavour::Gnu, Flavour::Bsd] {
326 assert_eq!(detect(&mut la(b""), fl).unwrap(), Detected::Unknown);
327 assert_eq!(detect(&mut la(b"\x1f"), fl).unwrap(), Detected::Unknown);
328 assert_eq!(detect(&mut la(b"\x1f\x8b"), fl).unwrap(), Detected::Gzip);
329 assert_eq!(detect(&mut la(b"\x1f\x1e"), fl).unwrap(), Detected::Pack);
330 assert_eq!(detect(&mut la(b"PK"), fl).unwrap(), Detected::Unknown);
331 }
332 }
333
334 fn encode(id: &'static str, data: &[u8]) -> Vec<u8> {
335 use std::sync::{Arc, Mutex};
336 struct Shared(Arc<Mutex<Vec<u8>>>);
337 impl Write for Shared {
338 fn write(&mut self, b: &[u8]) -> io::Result<usize> {
339 self.0.lock().unwrap().extend_from_slice(b);
340 Ok(b.len())
341 }
342 fn flush(&mut self) -> io::Result<()> {
343 Ok(())
344 }
345 }
346 let out = Arc::new(Mutex::new(Vec::new()));
347 let codec = stuffr::registry().codec(FormatId::new(id)).unwrap();
348 let mut enc = codec
349 .encoder(
350 Box::new(Shared(out.clone())),
351 &stuffr::EncodeOpts::default(),
352 )
353 .unwrap();
354 enc.write_all(data).unwrap();
355 enc.finish().unwrap();
356 out.lock().unwrap().clone()
357 }
358
359 fn sample() -> Vec<u8> {
360 (0..5000u32)
361 .flat_map(|i| format!("line {i} of the sample\n").into_bytes())
362 .collect()
363 }
364
365 #[test]
366 fn foreign_reader_round_trips() {
367 let data = sample();
368 let mut cases: Vec<(Detected, Flavour, &str)> = vec![
369 (Detected::Bzip2, Flavour::Bsd, "bzip2"),
370 (Detected::Xz, Flavour::Bsd, "xz"),
371 (Detected::Lzip, Flavour::Bsd, "lzip"),
372 ];
373 if has("compress")
375 && stuffr::registry()
376 .codec(FormatId::new("compress"))
377 .unwrap()
378 .caps()
379 .encode
380 {
381 cases.push((Detected::Compress, Flavour::Gnu, "compress"));
382 }
383 for (d, fl, id) in cases {
384 if !has(id) {
385 continue;
386 }
387 let packed = encode(id, &data);
388 let mut r = Lookahead::new(io::Cursor::new(packed));
389 assert_eq!(detect(&mut r, fl).unwrap(), d, "{id}");
390 let mut out = Vec::new();
391 foreign_reader(d, Box::new(r))
392 .unwrap()
393 .read_to_end(&mut out)
394 .unwrap();
395 assert_eq!(out, data, "{id}");
396 }
397 }
398
399 #[test]
400 fn foreign_reader_decodes_pack() {
401 let p = b"\x1f\x1e\x00\x00\x00\x08\x03\x01\x00\x00AB\x80".to_vec();
403 let mut r = Lookahead::new(io::Cursor::new(p));
404 assert_eq!(detect(&mut r, Flavour::Gnu).unwrap(), Detected::Pack);
405 let mut out = Vec::new();
406 foreign_reader(Detected::Pack, Box::new(r))
407 .unwrap()
408 .read_to_end(&mut out)
409 .unwrap();
410 assert_eq!(out, b"BAAAAAAA");
411 }
412
413 #[test]
418 fn foreign_reader_fails_on_bad_input() {
419 let data = sample();
420 let mut formats: Vec<(Detected, Vec<u8>)> = Vec::new();
421 for (d, id) in [
422 (Detected::Bzip2, "bzip2"),
423 (Detected::Xz, "xz"),
424 (Detected::Lzip, "lzip"),
425 (Detected::Compress, "compress"),
426 ] {
427 if has(id)
428 && stuffr::registry()
429 .codec(FormatId::new(id))
430 .is_some_and(|c| c.caps().encode)
431 {
432 formats.push((d, encode(id, &data)));
433 }
434 }
435 let decode = |d: Detected, b: Vec<u8>| -> io::Result<Vec<u8>> {
436 let mut out = Vec::new();
437 foreign_reader(d, Box::new(io::Cursor::new(b)))?.read_to_end(&mut out)?;
438 Ok(out)
439 };
440 for (d, packed) in formats {
441 if d != Detected::Compress {
444 for cut in [3, packed.len() / 2] {
445 assert!(
446 decode(d, packed[..cut].to_vec()).is_err(),
447 "{d:?} cut at {cut}"
448 );
449 }
450 }
451 let mut bad = packed.clone();
453 let at = packed.len() / 2;
454 for b in &mut bad[at..at + 16.min(packed.len() - at)] {
455 *b ^= 0x5a;
456 }
457 assert!(
458 decode(d, bad).map_or(true, |o| o != data),
459 "{d:?}: corruption went unnoticed"
460 );
461 }
462 let kind_and_words = |b: &[u8]| {
464 let e = foreign_reader(Detected::Pack, Box::new(io::Cursor::new(b.to_vec())))
465 .err()
466 .unwrap();
467 let words = e.get_ref().map(ToString::to_string).unwrap_or_default();
468 (e.kind(), words)
469 };
470 assert_eq!(
471 kind_and_words(b"\x1f\x1e\x00\x00"),
472 (
473 io::ErrorKind::UnexpectedEof,
474 "header shorter than 7 bytes".into()
475 )
476 );
477 assert_eq!(
478 kind_and_words(b"\x1f\x1e\x00\x00\x00\x08\x03\x01"),
479 (
480 io::ErrorKind::UnexpectedEof,
481 "File appears to be truncated".into()
482 )
483 );
484 assert_eq!(
485 kind_and_words(b"\x1f\x1e\x00\x00\x00\x08\x03\x01\x00\x00A"),
486 (
487 io::ErrorKind::UnexpectedEof,
488 "Symbol table truncated".into()
489 )
490 );
491 assert_eq!(
492 kind_and_words(b"\x1f\x1e\x00\x00\x00\x08\x1a"),
493 (
494 io::ErrorKind::InvalidData,
495 "Huffman tree has insane levels".into()
496 )
497 );
498 let mut r = foreign_reader(
500 Detected::Pack,
501 Box::new(io::Cursor::new(
502 b"\x1f\x1e\x00\x00\x00\x0a\x03\x01\x00\x00AB\x80".to_vec(),
503 )),
504 )
505 .unwrap();
506 let mut out = Vec::new();
507 let e = r.read_to_end(&mut out).unwrap_err();
508 assert_eq!(out, b"BAAAAAAA");
509 assert_eq!(
510 e.get_ref().map(ToString::to_string).unwrap(),
511 "Premature EOF"
512 );
513 }
514
515 #[test]
516 fn foreign_reader_refuses_what_is_not_foreign() {
517 for d in [
518 Detected::Gzip,
519 Detected::Zip,
520 Detected::Lzh,
521 Detected::Unknown,
522 ] {
523 assert!(foreign_reader(d, Box::new(io::Cursor::new(Vec::new()))).is_err());
524 }
525 }
526
527 fn zip_entry(method: u16, flags: u16, data: &[u8], csize: u32, name: &[u8]) -> Vec<u8> {
528 let mut z = b"PK\x03\x04".to_vec();
529 z.extend(20u16.to_le_bytes());
530 z.extend(flags.to_le_bytes());
531 z.extend(method.to_le_bytes());
532 z.extend([0u8; 4]); z.extend(0xdeadbeefu32.to_le_bytes());
534 z.extend(csize.to_le_bytes());
535 z.extend(7u32.to_le_bytes());
536 z.extend((name.len() as u16).to_le_bytes());
537 z.extend(2u16.to_le_bytes());
538 z.extend(name);
539 z.extend([0xaa, 0xbb]); z.extend(data);
541 z.extend(b"PK\x01\x02trailing central directory");
542 z
543 }
544
545 #[test]
546 fn zip_first_entry_stored_and_deflated() {
547 let z = zip_entry(0, 0, b"payload", 7, b"a.txt");
548 let e = zip_first_entry(io::Cursor::new(z)).unwrap();
549 assert_eq!(
550 (e.method, e.flags, e.crc, e.compressed_size),
551 (0, 0, 0xdeadbeef, 7)
552 );
553 assert_eq!(e.name, b"a.txt");
554 let mut out = Vec::new();
555 e.data().unwrap().read_to_end(&mut out).unwrap();
556 assert_eq!(out, b"payload");
557
558 let mut enc =
559 flate2::write::DeflateEncoder::new(Vec::new(), flate2::Compression::default());
560 enc.write_all(b"deflated payload").unwrap();
561 let d = enc.finish().unwrap();
562 let z = zip_entry(8, 0, &d, d.len() as u32, b"b");
563 let e = zip_first_entry(io::Cursor::new(z)).unwrap();
564 assert_eq!(e.method, 8);
565 let mut out = Vec::new();
566 e.data().unwrap().read_to_end(&mut out).unwrap();
567 assert_eq!(out, b"deflated payload");
568 }
569
570 #[test]
571 fn zip_first_entry_reports_other_methods_and_truncation() {
572 let z = zip_entry(12, 1, b"x", 1, b"c");
573 let e = zip_first_entry(io::Cursor::new(z)).unwrap();
574 assert_eq!((e.method, e.flags), (12, 1));
575 assert_eq!(e.data().err().unwrap().kind(), io::ErrorKind::Unsupported);
576 let z = zip_entry(0, 0, b"", 0, b"name");
577 let err = zip_first_entry(io::Cursor::new(&z[..20])).err().unwrap();
578 assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
579 }
580}