feat(rust): extract .tgz archives (Rust-only short suffix)

This commit is contained in:
2026-10-10 23:52:47 +02:00
parent b8fdf508c8
commit 550062f46b
+22 -3
View File
@@ -4,7 +4,7 @@
//!
//! Extraction backends: `sevenz-rust2` for .7z, `zip` for .zip, `unrar` for
//! .rar — pure Rust, no external binaries. Tarballs and bare compressed
//! files (.tar/.tar.gz/.tar.bz2/.tar.xz/.gz/.bz2/.xz) are handled by the
//! files (.tar/.tar.gz/.tgz/.tar.bz2/.tar.xz/.gz/.bz2/.xz) are handled by the
//! system `tar`/`gzip`/`bzip2`/`xz` binaries, exactly like the python
//! version shelled out to `7z`: merged output capture, exit-code parity
//! (0 ok, 1 warnings), and the 4h per-archive timeout. The tar/flate2/
@@ -37,8 +37,11 @@ use std::time::{Duration, Instant};
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
/// Archive suffixes recognized by the extractors (python `ARCHIVE_SUFFIXES`).
pub const ARCHIVE_SUFFIXES: [&str; 7] = [".zip", ".7z", ".rar", ".tar", ".gz", ".bz2", ".xz"];
/// Archive suffixes recognized by the extractors (python `ARCHIVE_SUFFIXES`,
/// plus `.tgz` — a Rust-only extension; the python version never extracted
/// those). `.tgz` is a gzip-compressed tar, so it maps onto the tarball path.
pub const ARCHIVE_SUFFIXES: [&str; 8] =
[".zip", ".7z", ".rar", ".tar", ".gz", ".bz2", ".xz", ".tgz"];
/// Stop extracting below this free space (python `MIN_FREE_BYTES`).
pub const MIN_FREE_BYTES: u64 = 20 * 1024 * 1024 * 1024;
@@ -388,6 +391,9 @@ impl Extractor {
}
}
Some(".tar") => extract_tar_cmd(archive, &actual, None),
// .tgz is a gzip-compressed tar: same as .tar.gz, but the tar flag
// cannot be inferred from the name (Rust-only extension).
Some(".tgz") => extract_tar_cmd(archive, &actual, Some(".gz")),
_ => Err(format!("no extractor for {name}")),
};
match res {
@@ -957,6 +963,10 @@ mod tests {
let targz = gzip_stored(&make_ustar("delta.txt", b"tarred payload"));
fs::write(s.join("logs.tar.gz"), targz).unwrap();
// a .tgz (gzip-compressed tar under the short suffix — Rust-only)
let tgz = gzip_stored(&make_ustar("epsilon.txt", b"tgz payload"));
fs::write(s.join("bundle.tgz"), tgz).unwrap();
// garbage zip -> must land in .broken
fs::write(s.join("bad.zip"), b"this is not a zip archive at all").unwrap();
@@ -1081,6 +1091,10 @@ mod tests {
fs::read_to_string(o.join("src1/logs/delta.txt")).unwrap(),
"tarred payload"
);
assert_eq!(
fs::read_to_string(o.join("src1/bundle/epsilon.txt")).unwrap(),
"tgz payload"
);
// no archives left in the extracted tree (outside .broken)
for p in walk_files_sorted(o) {
@@ -1164,6 +1178,7 @@ mod tests {
"The Twin Lance",
),
("src1/logs.tar.gz", "src1/logs", "logs"),
("src1/bundle.tgz", "src1/bundle", "bundle"),
]
.iter()
.map(|(a, d, u)| (a.to_string(), d.to_string(), u.to_string()))
@@ -1255,6 +1270,10 @@ mod tests {
);
assert_eq!(display_unit("Pack.zip"), "Pack");
assert!(is_archive("c.7z.7z"));
// .tgz: Rust-only short suffix for a gzip-compressed tar
assert_eq!(archive_suffixes("x.tgz"), vec![".tgz"]);
assert_eq!(display_unit("x.tgz"), "x");
assert!(is_archive("X.TGZ"));
}
#[test]