use std::fs; use std::io::Cursor; use std::os::unix::ffi::OsStrExt; use std::path::{Path, PathBuf}; use flate2::read::GzDecoder; #[derive(Debug, Clone, Copy)] pub enum ArchiveFormat { TarGz, TarXz, TarBz2, Tar, } impl ArchiveFormat { pub fn detect(url: &str, data: &[u8]) -> Self { if url.ends_with(".tar.gz") || url.ends_with(".tgz") { return ArchiveFormat::TarGz; } if url.ends_with(".tar.xz") || url.ends_with(".txz") { return ArchiveFormat::TarXz; } if url.ends_with(".tar.bz2") || url.ends_with(".tbz2") { return ArchiveFormat::TarBz2; } if url.ends_with(".tar") { return ArchiveFormat::Tar; } if data.len() >= 2 && data[0] == 0x1f && data[1] == 0x8b { return ArchiveFormat::TarGz; } if data.len() >= 6 && &data[0..6] == b"\xfd7zXZ\x00" { return ArchiveFormat::TarXz; } if data.len() >= 3 && &data[0..3] == b"BZh" { return ArchiveFormat::TarBz2; } ArchiveFormat::TarGz } } pub fn extract_tarball(data: &[u8], dest: &Path) -> Result { let format = ArchiveFormat::detect("", data); let temp_dir = dest.join("_extract_temp"); fs::create_dir_all(&temp_dir)?; match format { ArchiveFormat::TarGz => extract_tar_gz(data, &temp_dir)?, ArchiveFormat::TarXz => extract_tar_xz(data, &temp_dir)?, ArchiveFormat::TarBz2 => extract_tar_bz2(data, &temp_dir)?, ArchiveFormat::Tar => extract_tar(data, &temp_dir)?, } strip_single_toplevel(&temp_dir, dest) } fn extract_tar_gz(data: &[u8], dest: &Path) -> Result<(), ArchiveError> { let decoder = GzDecoder::new(Cursor::new(data)); let mut archive = tar::Archive::new(decoder); archive.unpack(dest)?; Ok(()) } fn extract_tar_xz(data: &[u8], dest: &Path) -> Result<(), ArchiveError> { let decoder = xz2::read::XzDecoder::new(Cursor::new(data)); let mut archive = tar::Archive::new(decoder); archive.unpack(dest)?; Ok(()) } fn extract_tar_bz2(data: &[u8], dest: &Path) -> Result<(), ArchiveError> { let decoder = bzip2::read::BzDecoder::new(Cursor::new(data)); let mut archive = tar::Archive::new(decoder); archive.unpack(dest)?; Ok(()) } fn extract_tar(data: &[u8], dest: &Path) -> Result<(), ArchiveError> { let mut archive = tar::Archive::new(Cursor::new(data)); archive.unpack(dest)?; Ok(()) } fn strip_single_toplevel(temp_dir: &Path, dest: &Path) -> Result { let entries: Vec<_> = fs::read_dir(temp_dir)? .filter_map(|e| e.ok()) .filter(|e| e.file_name().as_os_str().as_bytes()[0] != b'.') .collect(); let source_dir = if entries.len() == 1 && entries[0].file_type()?.is_dir() { entries[0].path() } else { temp_dir.to_path_buf() }; let final_dest = dest.join("content"); if final_dest.exists() { fs::remove_dir_all(&final_dest)?; } if source_dir == *temp_dir { fs::rename(temp_dir, &final_dest)?; } else { copy_dir_recursive(&source_dir, &final_dest)?; fs::remove_dir_all(temp_dir)?; } Ok(final_dest) } fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<(), std::io::Error> { fs::create_dir_all(dst)?; for entry in fs::read_dir(src)? { let entry = entry?; let path = entry.path(); let dest_path = dst.join(entry.file_name()); let metadata = fs::symlink_metadata(&path)?; if metadata.is_symlink() { let target = fs::read_link(&path)?; #[cfg(unix)] { std::os::unix::fs::symlink(&target, &dest_path)?; } #[cfg(windows)] { if target.is_dir() { std::os::windows::fs::symlink_dir(&target, &dest_path)?; } else { std::os::windows::fs::symlink_file(&target, &dest_path)?; } } } else if metadata.is_dir() { copy_dir_recursive(&path, &dest_path)?; } else { fs::copy(&path, &dest_path)?; } } Ok(()) } pub fn extract_tarball_to_temp(data: &[u8]) -> Result<(PathBuf, tempfile::TempDir), ArchiveError> { let temp_dir = tempfile::tempdir()?; let extracted_path = extract_tarball(data, temp_dir.path())?; Ok((extracted_path, temp_dir)) } #[derive(Debug)] pub enum ArchiveError { IoError(std::io::Error), } impl std::fmt::Display for ArchiveError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { ArchiveError::IoError(e) => write!(f, "I/O error: {}", e), } } } impl std::error::Error for ArchiveError {} impl From for ArchiveError { fn from(e: std::io::Error) -> Self { ArchiveError::IoError(e) } }