fn is_path_safe(entry_path: &Path) -> bool {
for cmp in entry_path.components() {
std::path::Component::Prefix(_) | std::path::Component::RootDir => {
std::path::Component::ParentDir => {
std::path::Component::CurDir => {}
std::path::Component::Normal(_) => {
pub(crate) fn extract_archive(archive: &Path, target_dir: &Path) -> Result<(), String> {
fs::remove_dir_all(target_dir).ok();
fs::create_dir_all(target_dir).map_err(|e| e.to_string())?;
let file_name = archive.file_name().and_then(|n| n.to_str()).unwrap_or("");
let is_xz = file_name.ends_with(".xz");
let is_gz = file_name.ends_with(".gz") || file_name.ends_with(".tgz");
return Err(format!("Unsupported archive extension: {file_name}"));
let file = fs::File::open(archive)
.map_err(|e| format!("Failed to open archive file {}: {e}", archive.display()))?;
let xz_decoder = lzma_rust2::XzReader::new(file, true);
let mut tar_archive = tar::Archive::new(xz_decoder);
unpack_and_verify(&mut tar_archive, target_dir)?;
let gz_decoder = flate2::read::GzDecoder::new(file);
let mut tar_archive = tar::Archive::new(gz_decoder);
unpack_and_verify(&mut tar_archive, target_dir)?;
// If there is a single nested folder inside target_dir, move its children out
let entries = fs::read_dir(target_dir)
.map_err(|e| e.to_string())?
if entries.len() == 1 && entries[0].path().is_dir() {
let nested_dir = entries[0].path();
let nested_entries = fs::read_dir(&nested_dir).map_err(|e| e.to_string())?;
for entry in nested_entries {
let entry = entry.map_err(|e| e.to_string())?;
let name = entry.file_name();
fs::rename(entry.path(), target_dir.join(name)).map_err(|e| e.to_string())?;
fs::remove_dir(&nested_dir).ok();
fn unpack_and_verify<R: std::io::Read>(
archive: &mut tar::Archive<R>,
) -> Result<(), String> {
.map_err(|e| format!("Failed to read archive entries: {e}"))?;
for entry_res in entries {
let mut entry = entry_res.map_err(|e| format!("Failed to read archive entry: {e}"))?;
.map_err(|e| format!("Failed to get entry path: {e}"))?
if !is_path_safe(&entry_path) {
"Path traversal detected in archive entry: {}",
.map_err(|e| format!("Failed to unpack entry {}: {e}", entry_path.display()))?;
fn test_extract_tar_gz_and_flatten() {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
let temp_dir = std::env::temp_dir().join(format!("archive-test-{}", nanos));
fs::create_dir_all(&temp_dir).unwrap();
let archive_path = temp_dir.join("test.tar.gz");
let file = fs::File::create(&archive_path).unwrap();
let enc = flate2::write::GzEncoder::new(file, flate2::Compression::default());
let mut builder = tar::Builder::new(enc);
let mut header = tar::Header::new_gnu();
header.set_path("nested_dir/").unwrap();
header.set_entry_type(tar::EntryType::Directory);
builder.append(&header, &[][..]).unwrap();
let mut header2 = tar::Header::new_gnu();
header2.set_path("nested_dir/file.txt").unwrap();
let content = b"hello archive";
header2.set_size(content.len() as u64);
header2.set_entry_type(tar::EntryType::Regular);
builder.append(&header2, &content[..]).unwrap();
builder.into_inner().unwrap().finish().unwrap();
let target_dir = temp_dir.join("target");
extract_archive(&archive_path, &target_dir).unwrap();
let extracted_file = target_dir.join("file.txt");
assert!(extracted_file.is_file());
let data = fs::read_to_string(&extracted_file).unwrap();
assert_eq!(data, "hello archive");
fs::remove_dir_all(&temp_dir).ok();
fn test_extract_tar_xz_and_flatten() {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
let temp_dir = std::env::temp_dir().join(format!("archive-test-xz-{}", nanos));
fs::create_dir_all(&temp_dir).unwrap();
let archive_path = temp_dir.join("test.tar.xz");
let file = fs::File::create(&archive_path).unwrap();
// Let's use XzWriter to compress the tarball
let enc = lzma_rust2::XzWriter::new(file, lzma_rust2::XzOptions::default()).unwrap();
let mut builder = tar::Builder::new(enc);
let mut header = tar::Header::new_gnu();
header.set_path("nested_dir/").unwrap();
header.set_entry_type(tar::EntryType::Directory);
builder.append(&header, &[][..]).unwrap();
let mut header2 = tar::Header::new_gnu();
header2.set_path("nested_dir/file.txt").unwrap();
let content = b"hello xz archive";
header2.set_size(content.len() as u64);
header2.set_entry_type(tar::EntryType::Regular);
builder.append(&header2, &content[..]).unwrap();
builder.into_inner().unwrap().finish().unwrap();
let target_dir = temp_dir.join("target");
extract_archive(&archive_path, &target_dir).unwrap();
let extracted_file = target_dir.join("file.txt");
assert!(extracted_file.is_file());
let data = fs::read_to_string(&extracted_file).unwrap();
assert_eq!(data, "hello xz archive");
fs::remove_dir_all(&temp_dir).ok();
fn test_extract_path_traversal() {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
let temp_dir = std::env::temp_dir().join(format!("archive-test-{}", nanos));
fs::create_dir_all(&temp_dir).unwrap();
let archive_path = temp_dir.join("evil.tar.gz");
let file = fs::File::create(&archive_path).unwrap();
let enc = flate2::write::GzEncoder::new(file, flate2::Compression::default());
let mut builder = tar::Builder::new(enc);
let mut header = tar::Header::new_gnu();
// Manually write the raw path to bypass set_path validation
let bytes = header.as_mut_bytes();
let path_str = "../evil.txt";
bytes[..path_str.len()].copy_from_slice(path_str.as_bytes());
header.set_size(content.len() as u64);
header.set_entry_type(tar::EntryType::Regular);
builder.append(&header, &content[..]).unwrap();
builder.into_inner().unwrap().finish().unwrap();
let target_dir = temp_dir.join("target");
let result = extract_archive(&archive_path, &target_dir);
assert!(result.is_err());
assert!(result.unwrap_err().contains("Path traversal detected"));
fs::remove_dir_all(&temp_dir).ok();
fn test_extract_unsupported_format() {
let archive_path = Path::new("unsupported.zip");
let target_dir = Path::new("target");
let result = extract_archive(archive_path, target_dir);
assert!(result.is_err());
.contains("Unsupported archive extension")