1use std::io;
15use std::io::BufRead;
16use std::io::Read;
17use std::io::Write;
18
19use crate::Result;
20use crate::creation::compression::compression_level_to_bzip2;
21use crate::creation::compression::compression_level_to_flate2;
22use crate::creation::compression::compression_level_to_xz;
23use crate::creation::compression::compression_level_to_zstd;
24use crate::formats::tar_metadata_limit::DecoderMemoryExceeded;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
55#[non_exhaustive]
56pub enum CompressionCodec {
57 Gzip,
62
63 Bzip2,
68
69 Xz,
74
75 Zstd,
80}
81
82impl CompressionCodec {
83 #[must_use]
96 pub const fn extension(self) -> &'static str {
97 match self {
98 Self::Gzip => "tar.gz",
99 Self::Bzip2 => "tar.bz2",
100 Self::Xz => "tar.xz",
101 Self::Zstd => "tar.zst",
102 }
103 }
104
105 pub(crate) fn decoder<'r, R>(
122 self,
123 input: R,
124 max_decoder_memory: u64,
125 ) -> Result<Box<dyn Read + Send + 'r>>
126 where
127 R: BufRead + Send + 'r,
128 {
129 Ok(match self {
130 Self::Gzip => Box::new(MultiMember::<_, flate2::bufread::GzDecoder<R>>::new(input)),
131 Self::Bzip2 => Box::new(MultiMember::<_, bzip2::bufread::BzDecoder<R>>::new(input)),
132 Self::Xz => {
133 let stream = xz2::stream::Stream::new_stream_decoder(
134 max_decoder_memory,
135 xz2::stream::CONCATENATED,
136 )
137 .map_err(io::Error::from)?;
138 Box::new(XzLimited {
139 inner: xz2::bufread::XzDecoder::new_stream(input, stream),
140 max: max_decoder_memory,
141 })
142 }
143 Self::Zstd => Box::new(zstd::Decoder::with_buffer(input)?),
144 })
145 }
146
147 pub(crate) fn encoder<W: Write>(self, out: W, level: Option<u8>) -> Result<TarEncoder<W>> {
157 Ok(match self {
158 Self::Gzip => TarEncoder::Gzip(flate2::write::GzEncoder::new(
159 out,
160 compression_level_to_flate2(level),
161 )),
162 Self::Bzip2 => TarEncoder::Bzip2(bzip2::write::BzEncoder::new(
163 out,
164 compression_level_to_bzip2(level),
165 )),
166 Self::Xz => TarEncoder::Xz(xz2::write::XzEncoder::new(
167 out,
168 compression_level_to_xz(level),
169 )),
170 Self::Zstd => {
171 let mut enc = zstd::Encoder::new(out, compression_level_to_zstd(level))?;
172 enc.include_checksum(true)?;
173 TarEncoder::Zstd(enc)
174 }
175 })
176 }
177
178 #[must_use]
189 pub const fn name(self) -> &'static str {
190 match self {
191 Self::Gzip => "gzip",
192 Self::Bzip2 => "bzip2",
193 Self::Xz => "xz",
194 Self::Zstd => "zstd",
195 }
196 }
197}
198
199trait Member<R>: Read {
201 fn start(input: R) -> Self;
202 fn finish(self) -> R;
203}
204
205impl<R: BufRead> Member<R> for flate2::bufread::GzDecoder<R> {
206 fn start(input: R) -> Self {
207 Self::new(input)
208 }
209
210 fn finish(self) -> R {
211 self.into_inner()
212 }
213}
214
215impl<R: BufRead> Member<R> for bzip2::bufread::BzDecoder<R> {
216 fn start(input: R) -> Self {
217 Self::new(input)
218 }
219
220 fn finish(self) -> R {
221 self.into_inner()
222 }
223}
224
225enum MemberState<R, D> {
226 Between(R),
227 Active(D),
228 Done,
229 Failed(io::ErrorKind, String),
232}
233
234enum Step {
235 Data(usize),
236 Eof,
237 Continue,
238}
239
240struct MultiMember<R, D> {
248 state: MemberState<R, D>,
249 seen_member: bool,
250}
251
252impl<R, D> MultiMember<R, D> {
253 const fn new(input: R) -> Self {
254 Self {
255 state: MemberState::Between(input),
256 seen_member: false,
257 }
258 }
259}
260
261fn consume_zero_tail<R: BufRead>(input: &mut R) -> io::Result<()> {
262 loop {
263 let buf = input.fill_buf()?;
264 if buf.is_empty() {
265 return Ok(());
266 }
267 if buf.iter().any(|&b| b != 0) {
268 return Err(io::Error::new(
269 io::ErrorKind::InvalidData,
270 "trailing data after the last compressed member",
271 ));
272 }
273 let n = buf.len();
274 input.consume(n);
275 }
276}
277
278impl<R: BufRead, D: Member<R>> MultiMember<R, D> {
279 fn step(&mut self, out: &mut [u8]) -> io::Result<Step> {
280 match &mut self.state {
281 MemberState::Done => Ok(Step::Eof),
282 MemberState::Failed(kind, message) => Err(io::Error::new(*kind, message.clone())),
283 MemberState::Between(input) => {
284 let first = input.fill_buf()?.first().copied();
285 match first {
286 None | Some(0) if !self.seen_member => Err(io::Error::new(
287 io::ErrorKind::InvalidData,
288 "no compressed member found",
289 )),
290 None => {
291 self.state = MemberState::Done;
292 Ok(Step::Eof)
293 }
294 Some(0) => {
295 consume_zero_tail(input)?;
296 self.state = MemberState::Done;
297 Ok(Step::Eof)
298 }
299 Some(_) => {
300 self.seen_member = true;
301 if let MemberState::Between(input) =
302 std::mem::replace(&mut self.state, MemberState::Done)
303 {
304 self.state = MemberState::Active(D::start(input));
305 }
306 Ok(Step::Continue)
307 }
308 }
309 }
310 MemberState::Active(decoder) => {
311 let n = decoder.read(out)?;
312 if n > 0 {
313 return Ok(Step::Data(n));
314 }
315 if let MemberState::Active(decoder) =
316 std::mem::replace(&mut self.state, MemberState::Done)
317 {
318 self.state = MemberState::Between(decoder.finish());
319 }
320 Ok(Step::Continue)
321 }
322 }
323 }
324}
325
326impl<R: BufRead, D: Member<R>> Read for MultiMember<R, D> {
327 fn read(&mut self, out: &mut [u8]) -> io::Result<usize> {
328 if out.is_empty() {
329 return Ok(0);
330 }
331 loop {
332 match self.step(out) {
333 Ok(Step::Data(n)) => return Ok(n),
334 Ok(Step::Eof) => return Ok(0),
335 Ok(Step::Continue) => {}
336 Err(e) => {
337 if e.kind() != io::ErrorKind::Interrupted
340 && !matches!(self.state, MemberState::Failed(..))
341 {
342 self.state = MemberState::Failed(e.kind(), e.to_string());
343 }
344 return Err(e);
345 }
346 }
347 }
348 }
349}
350
351struct XzLimited<R> {
354 inner: xz2::bufread::XzDecoder<R>,
355 max: u64,
356}
357
358impl<R: BufRead> Read for XzLimited<R> {
359 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
360 self.inner.read(buf).map_err(|e| {
361 let mem_limit = e
362 .get_ref()
363 .and_then(|inner| inner.downcast_ref::<xz2::stream::Error>())
364 .is_some_and(|err| matches!(err, xz2::stream::Error::MemLimit));
365 if mem_limit {
366 io::Error::other(DecoderMemoryExceeded { max: self.max })
367 } else {
368 e
369 }
370 })
371 }
372}
373
374pub(crate) enum TarEncoder<W: Write> {
379 Gzip(flate2::write::GzEncoder<W>),
380 Bzip2(bzip2::write::BzEncoder<W>),
381 Xz(xz2::write::XzEncoder<W>),
382 Zstd(zstd::Encoder<'static, W>),
383}
384
385impl<W: Write> TarEncoder<W> {
386 pub(crate) fn finish(self) -> io::Result<W> {
388 match self {
389 Self::Gzip(e) => e.finish(),
390 Self::Bzip2(e) => e.finish(),
391 Self::Xz(e) => e.finish(),
392 Self::Zstd(e) => e.finish(),
393 }
394 }
395}
396
397impl<W: Write> Write for TarEncoder<W> {
398 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
399 match self {
400 Self::Gzip(e) => e.write(buf),
401 Self::Bzip2(e) => e.write(buf),
402 Self::Xz(e) => e.write(buf),
403 Self::Zstd(e) => e.write(buf),
404 }
405 }
406
407 fn flush(&mut self) -> io::Result<()> {
408 match self {
409 Self::Gzip(e) => e.flush(),
410 Self::Bzip2(e) => e.flush(),
411 Self::Xz(e) => e.flush(),
412 Self::Zstd(e) => e.flush(),
413 }
414 }
415}
416
417#[cfg(test)]
418#[allow(clippy::unwrap_used)]
419mod tests {
420 use super::*;
421
422 #[test]
423 fn test_codec_extension() {
424 assert_eq!(CompressionCodec::Gzip.extension(), "tar.gz");
425 assert_eq!(CompressionCodec::Bzip2.extension(), "tar.bz2");
426 assert_eq!(CompressionCodec::Xz.extension(), "tar.xz");
427 assert_eq!(CompressionCodec::Zstd.extension(), "tar.zst");
428 }
429
430 #[test]
431 fn test_codec_name() {
432 assert_eq!(CompressionCodec::Gzip.name(), "gzip");
433 assert_eq!(CompressionCodec::Bzip2.name(), "bzip2");
434 assert_eq!(CompressionCodec::Xz.name(), "xz");
435 assert_eq!(CompressionCodec::Zstd.name(), "zstd");
436 }
437
438 fn encode(codec: CompressionCodec, data: &[u8]) -> Vec<u8> {
439 use std::io::Write;
440
441 match codec {
442 CompressionCodec::Gzip => {
443 let mut enc =
444 flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
445 enc.write_all(data).unwrap();
446 enc.finish().unwrap()
447 }
448 CompressionCodec::Bzip2 => {
449 let mut enc = bzip2::write::BzEncoder::new(Vec::new(), bzip2::Compression::fast());
450 enc.write_all(data).unwrap();
451 enc.finish().unwrap()
452 }
453 CompressionCodec::Xz => {
454 let mut enc = xz2::write::XzEncoder::new(Vec::new(), 1);
455 enc.write_all(data).unwrap();
456 enc.finish().unwrap()
457 }
458 CompressionCodec::Zstd => zstd::encode_all(data, 1).unwrap(),
459 }
460 }
461
462 const TEST_MEM: u64 = 256 * 1024 * 1024;
463
464 const ALL_CODECS: [CompressionCodec; 4] = [
465 CompressionCodec::Gzip,
466 CompressionCodec::Bzip2,
467 CompressionCodec::Xz,
468 CompressionCodec::Zstd,
469 ];
470
471 #[test]
472 fn test_decoder_round_trips_every_codec() {
473 let data = b"hello decoder ".repeat(1000);
474 for codec in ALL_CODECS {
475 let mut out = Vec::new();
476 codec
477 .decoder(std::io::Cursor::new(encode(codec, &data)), TEST_MEM)
478 .unwrap()
479 .read_to_end(&mut out)
480 .unwrap();
481 assert_eq!(out, data, "{}", codec.name());
482 }
483 }
484
485 #[test]
486 fn test_encoder_round_trips_every_codec_and_level() {
487 let data = b"hello encoder ".repeat(1000);
488 for codec in ALL_CODECS {
489 for level in [None, Some(1), Some(9)] {
490 let mut enc = codec.encoder(Vec::new(), level).unwrap();
491 enc.write_all(&data).unwrap();
492 let encoded = enc.finish().unwrap();
493 let mut out = Vec::new();
494 codec
495 .decoder(std::io::Cursor::new(encoded), TEST_MEM)
496 .unwrap()
497 .read_to_end(&mut out)
498 .unwrap();
499 assert_eq!(out, data, "{} {level:?}", codec.name());
500 }
501 }
502 }
503
504 fn decode_all(codec: CompressionCodec, bytes: Vec<u8>, mem: u64) -> io::Result<Vec<u8>> {
505 let mut out = Vec::new();
506 codec
507 .decoder(std::io::Cursor::new(bytes), mem)
508 .unwrap()
509 .read_to_end(&mut out)?;
510 Ok(out)
511 }
512
513 #[test]
514 fn test_decoder_reads_every_concatenated_member() {
515 for codec in ALL_CODECS {
516 let mut encoded = encode(codec, b"first-");
517 encoded.extend_from_slice(&encode(codec, b"second-"));
518 encoded.extend_from_slice(&encode(codec, b"third"));
519 assert_eq!(
520 decode_all(codec, encoded, TEST_MEM).unwrap(),
521 b"first-second-third",
522 "{}",
523 codec.name()
524 );
525 }
526 }
527
528 #[test]
529 fn test_decoder_accepts_zero_padding_after_last_member() {
530 for codec in [
531 CompressionCodec::Gzip,
532 CompressionCodec::Bzip2,
533 CompressionCodec::Xz,
534 ] {
535 let mut encoded = encode(codec, b"a");
536 encoded.extend_from_slice(&encode(codec, b"b"));
537 encoded.extend_from_slice(&[0u8; 512]);
538 assert_eq!(
539 decode_all(codec, encoded, TEST_MEM).unwrap(),
540 b"ab",
541 "{}",
542 codec.name()
543 );
544 }
545 }
546
547 struct FailsOnce<R> {
548 inner: R,
549 error: Option<io::ErrorKind>,
550 }
551
552 impl<R: Read> Read for FailsOnce<R> {
553 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
554 match self.error.take() {
555 Some(kind) => Err(io::Error::from(kind)),
556 None => self.inner.read(buf),
557 }
558 }
559 }
560
561 impl<R: BufRead> BufRead for FailsOnce<R> {
562 fn fill_buf(&mut self) -> io::Result<&[u8]> {
563 match self.error.take() {
564 Some(kind) => Err(io::Error::from(kind)),
565 None => self.inner.fill_buf(),
566 }
567 }
568
569 fn consume(&mut self, amt: usize) {
570 self.inner.consume(amt);
571 }
572 }
573
574 #[test]
575 fn test_read_error_never_turns_into_a_clean_eof() {
576 let mut encoded = encode(CompressionCodec::Gzip, b"first-");
577 encoded.extend_from_slice(&encode(CompressionCodec::Gzip, b"second"));
578 for kind in [io::ErrorKind::Interrupted, io::ErrorKind::BrokenPipe] {
579 let input = FailsOnce {
580 inner: std::io::Cursor::new(encoded.clone()),
581 error: Some(kind),
582 };
583 let mut decoder = CompressionCodec::Gzip.decoder(input, TEST_MEM).unwrap();
584 let mut out = Vec::new();
585 let mut buf = [0u8; 64];
586 let mut errors = 0;
587 loop {
588 match decoder.read(&mut buf) {
589 Ok(0) => break,
590 Ok(n) => out.extend_from_slice(&buf[..n]),
591 Err(e) => {
592 assert_eq!(e.kind(), kind);
593 errors += 1;
594 if kind != io::ErrorKind::Interrupted {
595 assert_eq!(decoder.read(&mut buf).unwrap_err().kind(), kind);
596 return;
597 }
598 }
599 }
600 }
601 assert_eq!(errors, 1);
602 assert_eq!(out, b"first-second");
603 }
604 }
605
606 #[test]
607 fn test_decoder_rejects_input_without_a_first_member() {
608 for codec in [
609 CompressionCodec::Gzip,
610 CompressionCodec::Bzip2,
611 CompressionCodec::Xz,
612 ] {
613 for input in [Vec::new(), vec![0u8; 64]] {
614 assert!(
615 decode_all(codec, input, TEST_MEM).is_err(),
616 "{}",
617 codec.name()
618 );
619 }
620 }
621 }
622
623 #[test]
624 fn test_decoder_rejects_trailing_garbage() {
625 for codec in [
626 CompressionCodec::Gzip,
627 CompressionCodec::Bzip2,
628 CompressionCodec::Xz,
629 ] {
630 for tail in [&b"TRAILING-BYTES"[..], &b"\0\0\0\0garbage"[..]] {
631 let mut encoded = encode(codec, b"payload");
632 encoded.extend_from_slice(tail);
633 assert!(
634 decode_all(codec, encoded, TEST_MEM).is_err(),
635 "{} {tail:?}",
636 codec.name()
637 );
638 }
639 }
640 }
641
642 #[test]
643 fn test_xz_memlimit_applies_to_later_stream() {
644 let encode_at = |preset: u32, data: &[u8]| {
645 let mut enc = xz2::write::XzEncoder::new(Vec::new(), preset);
646 enc.write_all(data).unwrap();
647 enc.finish().unwrap()
648 };
649 let mut encoded = encode_at(0, b"first");
650 encoded.extend_from_slice(&encode_at(9, b"second"));
651 let err = decode_all(CompressionCodec::Xz, encoded, 16 * 1024 * 1024).unwrap_err();
652 assert!(
653 err.get_ref()
654 .is_some_and(<dyn std::error::Error + Send + Sync>::is::<DecoderMemoryExceeded>)
655 );
656 }
657
658 #[test]
659 fn test_decoder_reads_concatenated_zstd_frames() {
660 let mut encoded = encode(CompressionCodec::Zstd, b"first-");
661 encoded.extend_from_slice(&encode(CompressionCodec::Zstd, b"second"));
662 let mut out = Vec::new();
663 CompressionCodec::Zstd
664 .decoder(std::io::Cursor::new(encoded), TEST_MEM)
665 .unwrap()
666 .read_to_end(&mut out)
667 .unwrap();
668 assert_eq!(out, b"first-second");
669 }
670
671 #[test]
672 fn test_codec_equality() {
673 assert_eq!(CompressionCodec::Gzip, CompressionCodec::Gzip);
674 assert_ne!(CompressionCodec::Gzip, CompressionCodec::Bzip2);
675 }
676
677 #[test]
678 fn test_codec_hash() {
679 use std::collections::HashSet;
680
681 let mut set = HashSet::new();
682 set.insert(CompressionCodec::Gzip);
683 set.insert(CompressionCodec::Bzip2);
684 set.insert(CompressionCodec::Gzip); assert_eq!(set.len(), 2);
687 assert!(set.contains(&CompressionCodec::Gzip));
688 assert!(set.contains(&CompressionCodec::Bzip2));
689 }
690}