pub mod tar;
use std::fs;
use std::os::unix::fs::MetadataExt;
use std::path::Path;
use std::io::{Write, Read};
use flate2::read::GzDecoder;
use flate2::write::GzEncoder;
use flate2::Compression;
use std::io::{self, BufRead};
#[cfg(unix)]
use std::ffi::CStr;
pub use tar::{read_tar, write_tar, Tar, TarEntry, TarHeader};
#[cfg(unix)]
fn get_username_from_uid(uid: u32) -> Option<String> {
unsafe {
let passwd = libc::getpwuid(uid);
if passwd.is_null() {
return None;
}
let name_ptr = (*passwd).pw_name;
if name_ptr.is_null() {
return None;
}
CStr::from_ptr(name_ptr)
.to_str()
.ok()
.map(|s| s.to_string())
}
}
#[cfg(unix)]
fn get_groupname_from_gid(gid: u32) -> Option<String> {
unsafe {
let group = libc::getgrgid(gid);
if group.is_null() {
return None;
}
let name_ptr = (*group).gr_name;
if name_ptr.is_null() {
return None;
}
CStr::from_ptr(name_ptr)
.to_str()
.ok()
.map(|s| s.to_string())
}
}
#[cfg(not(unix))]
fn get_username_from_uid(_uid: u32) -> Option<String> {
None
}
#[cfg(not(unix))]
fn get_groupname_from_gid(_gid: u32) -> Option<String> {
None
}
fn is_gzipped(filename: &str) -> bool {
filename.ends_with(".tar.gz") || filename.ends_with(".tgz")
}
fn ungzip(filename: &str, data: Vec<u8>) -> Result<Vec<u8>, std::io::Error> {
if is_gzipped(filename) {
let mut decoder = GzDecoder::new(&data[..]);
let mut decompressed = Vec::new();
decoder.read_to_end(&mut decompressed)?;
Ok(decompressed)
} else {
Ok(data)
}
}
fn gzip(filename: &str, data: Vec<u8>) -> Result<Vec<u8>, std::io::Error> {
if is_gzipped(filename) {
let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
encoder.write_all(&data)?;
encoder.finish()
} else {
Ok(data)
}
}
fn add_file_to_entries(file_path: &Path, base_path: &Path, entries: &mut Vec<TarEntry>) {
let data = match fs::read(file_path) {
Ok(d) => d,
Err(e) => {
eprintln!("Error reading {}: {}", file_path.display(), e);
return;
}
};
let relative_path = file_path.strip_prefix(base_path)
.unwrap_or(file_path)
.to_string_lossy()
.to_string();
let mut header = TarHeader::new(
relative_path,
0o644,
data.len() as u64
);
match fs::metadata(file_path) {
Ok(m) => {
header.mode = m.mode() as u32;
header.mtime = m.modified().unwrap_or(std::time::SystemTime::UNIX_EPOCH)
.duration_since(std::time::SystemTime::UNIX_EPOCH).unwrap_or_default().as_secs();
header.gid = m.gid();
header.uid = m.uid();
if let Some(uname) = get_username_from_uid(m.uid()) {
header.uname = uname;
}
if let Some(gname) = get_groupname_from_gid(m.gid()) {
header.gname = gname;
}
},
Err(e) => {
eprintln!("Error getting metadata for {}: {}", file_path.display(), e);
return;
}
}; let header_bytes = header.to_bytes();
entries.push(TarEntry {
header,
data,
header_bytes,
});
}
fn collect_files_from_dir(dir_path: &Path, base_path: &Path, entries: &mut Vec<TarEntry>) {
let read_dir = match fs::read_dir(dir_path) {
Ok(d) => d,
Err(e) => {
eprintln!("Error reading directory {}: {}", dir_path.display(), e);
return;
}
};
for entry_result in read_dir {
let entry = match entry_result {
Ok(e) => e,
Err(e) => {
eprintln!("Error reading directory entry: {}", e);
continue;
}
};
let path = entry.path();
if path.is_dir() {
collect_files_from_dir(&path, base_path, entries);
} else if path.is_file() {
add_file_to_entries(&path, base_path, entries);
}
}
}
pub fn pack(tarfile: &str, files: &[&str]) {
let mut entries = Vec::new();
for file_path in files {
let path = Path::new(file_path);
if !path.exists() {
eprintln!("Warning: File not found: {}", file_path);
continue;
}
if path.is_dir() {
collect_files_from_dir(path, path, &mut entries);
} else {
let base = path.parent().unwrap_or_else(|| Path::new(""));
add_file_to_entries(path, base, &mut entries);
}
}
let tar_data = write_tar(&entries);
let result = gzip(tarfile, tar_data)
.and_then(|data| fs::write(tarfile, data));
match result {
Ok(_) => println!("Created tar archive: {}", tarfile),
Err(e) => {
eprintln!("Error writing tar file: {}", e);
std::process::exit(1);
}
}
}
pub fn unpack(tarfile: &str, output_dir: &str) {
unpack_with_options(tarfile, output_dir, false, true);
}
pub fn unpack_with_options(tarfile: &str, output_dir: &str, overwrite: bool, use_prompt: bool) {
let mut overwrite = overwrite;
let file_data = match fs::read(tarfile) {
Ok(d) => d,
Err(e) => {
eprintln!("Error reading tar file: {}", e);
std::process::exit(1);
}
};
let tar_data = match ungzip(tarfile, file_data) {
Ok(data) => data,
Err(e) => {
eprintln!("Error decompressing gzip: {}", e);
std::process::exit(1);
}
};
let entries = read_tar(&tar_data);
let output_path = Path::new(output_dir);
if !output_path.exists() {
if let Err(e) = fs::create_dir_all(output_path) {
eprintln!("Error creating output directory: {}", e);
std::process::exit(1);
}
}
for entry in entries {
let file_path = output_path.join(&entry.header.name);
let mut flag_overwrite = false;
if file_path.exists() {
if !overwrite {
if use_prompt {
println!("❓File '{}' already exists. Overwrite? ([Y]es/[N]o/[A]ll): ", entry.header.name);
let stdin = io::stdin();
let mut line = String::new();
stdin.lock().read_line(&mut line).unwrap_or(0);
let answer = line.trim().to_lowercase();
if answer == "a" || answer == "all" {
println!("⚡ Overwriting all files...");
overwrite = true;
} else if answer == "y" || answer == "yes" {
} else {
println!("- Skipping: {}", entry.header.name);
continue;
}
} else {
println!("- Skipping: {}", entry.header.name);
continue;
}
}
flag_overwrite = true;
}
if let Some(parent) = file_path.parent() {
if !parent.exists() {
if let Err(e) = fs::create_dir_all(parent) {
eprintln!("❌ Error creating directory {}: {}", parent.display(), e);
continue;
}
}
}
match fs::File::create(&file_path) {
Ok(mut file) => {
if let Err(e) = file.write_all(&entry.data) {
eprintln!("❌ Error writing {}: {}", entry.header.name, e);
} else {
let overwrite_msg = if flag_overwrite { " (overwritten)" } else { "" };
println!("- Extracted: {}{}", entry.header.name, overwrite_msg);
}
}
Err(e) => {
eprintln!("❌ Error creating {}: {}", entry.header.name, e);
}
}
}
println!("Extraction complete to: {}", output_dir);
}
pub fn list(tarfile: &str) -> Result<Vec<TarHeader>, std::io::Error> {
let file_data = fs::read(tarfile)?;
let tar_data = ungzip(tarfile, file_data)?;
let entries = read_tar(&tar_data);
let headers: Vec<TarHeader> = entries.into_iter().map(|e| e.header).collect();
Ok(headers)
}
pub fn list_entry(tarfile: &str) -> Result<Vec<TarEntry>, std::io::Error> {
let file_data = fs::read(tarfile)?;
let is_gzipped = tarfile.ends_with(".tar.gz") || tarfile.ends_with(".tgz");
let tar_data = if is_gzipped {
let mut decoder = GzDecoder::new(&file_data[..]);
let mut decompressed = Vec::new();
decoder.read_to_end(&mut decompressed)?;
decompressed
} else {
file_data
};
let entries = read_tar(&tar_data);
Ok(entries)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::path::Path;
#[test]
fn test_pack() {
let test_file1 = "test_file1.txt";
let test_file2 = "test_file2.txt";
let test_tar = "test_pack.tar";
fs::write(test_file1, "Hello, World!").unwrap();
fs::write(test_file2, "Test content 2").unwrap();
let files = vec![test_file1, test_file2];
pack(test_tar, &files);
assert!(Path::new(test_tar).exists());
let tar_data = fs::read(test_tar).unwrap();
let entries = read_tar(&tar_data);
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].header.name, test_file1);
assert_eq!(entries[1].header.name, test_file2);
fs::remove_file(test_file1).unwrap();
fs::remove_file(test_file2).unwrap();
fs::remove_file(test_tar).unwrap();
}
#[test]
fn test_unpack() {
let test_file = "test_unpack_file.txt";
let test_content = "Unpack test content";
let test_tar = "test_unpack.tar";
let output_dir = "test_unpack_output";
fs::write(test_file, test_content).unwrap();
let files = vec![test_file];
pack(test_tar, &files);
unpack_with_options(test_tar, output_dir, false, false);
let extracted_file = Path::new(output_dir).join(test_file);
assert!(extracted_file.exists());
let content = fs::read_to_string(&extracted_file).unwrap();
assert_eq!(content, test_content);
fs::remove_file(test_file).unwrap();
fs::remove_file(test_tar).unwrap();
fs::remove_dir_all(output_dir).unwrap();
}
#[test]
fn test_list() {
let test_file1 = "test_list_file1.txt";
let test_file2 = "test_list_file2.txt";
let test_tar = "test_list.tar";
fs::write(test_file1, "Content 1").unwrap();
fs::write(test_file2, "Content 2 longer").unwrap();
let files = vec![test_file1, test_file2];
pack(test_tar, &files);
let headers = list(test_tar).unwrap();
assert_eq!(headers.len(), 2);
assert_eq!(headers[0].name, test_file1);
assert_eq!(headers[0].size, 9);
assert_eq!(headers[1].name, test_file2);
assert_eq!(headers[1].size, 16);
let tar_data = fs::read(test_tar).unwrap();
let entries = read_tar(&tar_data);
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].header.name, test_file1);
assert_eq!(entries[0].header.size, 9);
assert_eq!(entries[1].header.name, test_file2);
assert_eq!(entries[1].header.size, 16);
fs::remove_file(test_file1).unwrap();
fs::remove_file(test_file2).unwrap();
fs::remove_file(test_tar).unwrap();
}
#[test]
fn test_tar_gz() {
let test_file1 = "test_gz_file1.txt";
let test_file2 = "test_gz_file2.txt";
let test_tar_gz = "test_pack.tar.gz";
let output_dir = "test_gz_output";
fs::write(test_file1, "GZ test content 1").unwrap();
fs::write(test_file2, "GZ test content 2 longer").unwrap();
let files = vec![test_file1, test_file2];
pack(test_tar_gz, &files);
assert!(Path::new(test_tar_gz).exists());
let headers = list(test_tar_gz).unwrap();
assert_eq!(headers.len(), 2);
assert_eq!(headers[0].name, test_file1);
assert_eq!(headers[0].size, 17);
assert_eq!(headers[1].name, test_file2);
assert_eq!(headers[1].size, 24);
unpack_with_options(test_tar_gz, output_dir, false, false);
let extracted_file1 = Path::new(output_dir).join(test_file1);
let extracted_file2 = Path::new(output_dir).join(test_file2);
assert!(extracted_file1.exists());
assert!(extracted_file2.exists());
let content1 = fs::read_to_string(&extracted_file1).unwrap();
let content2 = fs::read_to_string(&extracted_file2).unwrap();
assert_eq!(content1, "GZ test content 1");
assert_eq!(content2, "GZ test content 2 longer");
fs::remove_file(test_file1).unwrap();
fs::remove_file(test_file2).unwrap();
fs::remove_file(test_tar_gz).unwrap();
fs::remove_dir_all(output_dir).unwrap();
}
#[test]
fn test_pack_directory() {
let test_dir = "test_pack_dir";
let test_tar = "test_pack_dir.tar";
fs::create_dir_all(format!("{}/subdir", test_dir)).unwrap();
fs::write(format!("{}/file1.txt", test_dir), "File 1 content").unwrap();
fs::write(format!("{}/file2.txt", test_dir), "File 2 content").unwrap();
fs::write(format!("{}/subdir/file3.txt", test_dir), "File 3 in subdir").unwrap();
let files = vec![test_dir];
pack(test_tar, &files);
assert!(Path::new(test_tar).exists());
let tar_data = fs::read(test_tar).unwrap();
let entries = read_tar(&tar_data);
assert_eq!(entries.len(), 3);
let names: Vec<String> = entries.iter().map(|e| e.header.name.clone()).collect();
assert!(names.contains(&"file1.txt".to_string()));
assert!(names.contains(&"file2.txt".to_string()));
assert!(names.contains(&"subdir/file3.txt".to_string()));
fs::remove_dir_all(test_dir).unwrap();
fs::remove_file(test_tar).unwrap();
}
#[test]
fn test_pack_and_unpack_directory() {
let test_dir = "test_dir_full";
let test_tar = "test_dir_full.tar";
let output_dir = "test_dir_full_output";
fs::create_dir_all(format!("{}/a/b/c", test_dir)).unwrap();
fs::write(format!("{}/root.txt", test_dir), "Root file").unwrap();
fs::write(format!("{}/a/file_a.txt", test_dir), "File in a").unwrap();
fs::write(format!("{}/a/b/file_b.txt", test_dir), "File in b").unwrap();
fs::write(format!("{}/a/b/c/file_c.txt", test_dir), "File in c").unwrap();
let files = vec![test_dir];
pack(test_tar, &files);
unpack_with_options(test_tar, output_dir, false, false);
assert!(Path::new(output_dir).join("root.txt").exists());
assert!(Path::new(output_dir).join("a/file_a.txt").exists());
assert!(Path::new(output_dir).join("a/b/file_b.txt").exists());
assert!(Path::new(output_dir).join("a/b/c/file_c.txt").exists());
let content = fs::read_to_string(Path::new(output_dir).join("a/b/c/file_c.txt")).unwrap();
assert_eq!(content, "File in c");
fs::remove_dir_all(test_dir).unwrap();
fs::remove_file(test_tar).unwrap();
fs::remove_dir_all(output_dir).unwrap();
}
#[test]
fn test_pack_mixed_files_and_directories() {
let test_file = "test_mixed_file.txt";
let test_dir = "test_mixed_dir";
let test_tar = "test_mixed.tar";
fs::write(test_file, "Single file").unwrap();
fs::create_dir_all(format!("{}/subdir", test_dir)).unwrap();
fs::write(format!("{}/dir_file.txt", test_dir), "File in dir").unwrap();
fs::write(format!("{}/subdir/sub_file.txt", test_dir), "File in subdir").unwrap();
let files = vec![test_file, test_dir];
pack(test_tar, &files);
let tar_data = fs::read(test_tar).unwrap();
let entries = read_tar(&tar_data);
assert_eq!(entries.len(), 3);
let names: Vec<String> = entries.iter().map(|e| e.header.name.clone()).collect();
assert!(names.contains(&test_file.to_string()));
assert!(names.contains(&"dir_file.txt".to_string()));
assert!(names.contains(&"subdir/sub_file.txt".to_string()));
fs::remove_file(test_file).unwrap();
fs::remove_dir_all(test_dir).unwrap();
fs::remove_file(test_tar).unwrap();
}
#[test]
fn test_pack_directory_gzipped() {
let test_dir = "test_pack_dir_gz";
let test_tar_gz = "test_pack_dir.tar.gz";
let output_dir = "test_pack_dir_gz_output";
fs::create_dir_all(format!("{}/nested/deep", test_dir)).unwrap();
fs::write(format!("{}/file1.txt", test_dir), "First file").unwrap();
fs::write(format!("{}/nested/file2.txt", test_dir), "Second file").unwrap();
fs::write(format!("{}/nested/deep/file3.txt", test_dir), "Third file").unwrap();
let files = vec![test_dir];
pack(test_tar_gz, &files);
assert!(Path::new(test_tar_gz).exists());
let headers = list(test_tar_gz).unwrap();
assert_eq!(headers.len(), 3);
unpack_with_options(test_tar_gz, output_dir, false, false);
assert!(Path::new(output_dir).join("file1.txt").exists());
assert!(Path::new(output_dir).join("nested/file2.txt").exists());
assert!(Path::new(output_dir).join("nested/deep/file3.txt").exists());
let content = fs::read_to_string(Path::new(output_dir).join("nested/deep/file3.txt")).unwrap();
assert_eq!(content, "Third file");
fs::remove_dir_all(test_dir).unwrap();
fs::remove_file(test_tar_gz).unwrap();
fs::remove_dir_all(output_dir).unwrap();
}
#[test]
fn security_test_unpack_path_traversal() {
use crate::tar::{TarEntry, TarHeader};
let test_tar = "test_security_traversal.tar";
let output_dir = "test_security_output";
let mut entries = Vec::new();
let malicious_paths = vec![
"../outside.txt",
"../../etc/outside2.txt",
"subdir/../../../outside3.txt",
];
for malicious_path in malicious_paths {
let header = TarHeader::new(malicious_path.to_string(), 0o644, 9);
let data = b"malicious".to_vec();
let header_bytes = header.to_bytes();
entries.push(TarEntry { header, data, header_bytes });
}
let tar_data = write_tar(&entries);
fs::write(test_tar, tar_data).unwrap();
unpack_with_options(test_tar, output_dir, false, false);
fs::remove_file(test_tar).unwrap();
if Path::new(output_dir).exists() {
fs::remove_dir_all(output_dir).ok();
}
fs::remove_file("outside.txt").ok();
fs::remove_file("../outside.txt").ok();
fs::remove_file("outside2.txt").ok();
fs::remove_file("outside3.txt").ok();
}
#[test]
fn security_test_unpack_absolute_path() {
use crate::tar::{TarEntry, TarHeader};
let test_tar = "test_security_absolute.tar";
let output_dir = "test_security_abs_output";
let header = TarHeader::new("/tmp/absolute_file.txt".to_string(), 0o644, 8);
let data = b"absolute".to_vec();
let header_bytes = header.to_bytes();
let entry = TarEntry { header, data, header_bytes };
let tar_data = write_tar(&[entry]);
fs::write(test_tar, tar_data).unwrap();
unpack_with_options(test_tar, output_dir, false, false);
fs::remove_file(test_tar).unwrap();
if Path::new(output_dir).exists() {
fs::remove_dir_all(output_dir).ok();
}
fs::remove_file("/tmp/absolute_file.txt").ok();
}
#[test]
fn security_test_unpack_large_file_size() {
use crate::tar::{TarEntry, TarHeader};
let test_tar = "test_security_large.tar";
let output_dir = "test_security_large_output";
let header = TarHeader::new("fake_large.txt".to_string(), 0o644, 5);
let data = b"small".to_vec();
let header_bytes = header.to_bytes();
let entry = TarEntry { header, data, header_bytes };
let tar_data = write_tar(&[entry]);
fs::write(test_tar, tar_data).unwrap();
unpack_with_options(test_tar, output_dir, false, false);
let extracted_file = Path::new(output_dir).join("fake_large.txt");
if extracted_file.exists() {
let content = fs::read(&extracted_file).unwrap();
assert_eq!(content.len(), 5);
}
fs::remove_file(test_tar).unwrap();
if Path::new(output_dir).exists() {
fs::remove_dir_all(output_dir).unwrap();
}
}
#[test]
fn security_test_unpack_empty_filename() {
use crate::tar::{TarEntry, TarHeader};
let test_tar = "test_security_empty_name.tar";
let output_dir = "test_security_empty_output";
let header = TarHeader::new("".to_string(), 0o644, 4);
let data = b"data".to_vec();
let header_bytes = header.to_bytes();
let entry = TarEntry { header, data, header_bytes };
let tar_data = write_tar(&[entry]);
fs::write(test_tar, tar_data).unwrap();
unpack_with_options(test_tar, output_dir, false, false);
fs::remove_file(test_tar).unwrap();
if Path::new(output_dir).exists() {
fs::remove_dir_all(output_dir).ok();
}
}
#[test]
fn security_test_unpack_special_characters() {
use crate::tar::{TarEntry, TarHeader};
let test_tar = "test_security_special.tar";
let output_dir = "test_security_special_output";
let special_names = vec![
"file\0with\0nulls.txt",
"file\nwith\nnewlines.txt",
"file;with;semicolons.txt",
"file|with|pipes.txt",
];
let mut entries = Vec::new();
for name in special_names {
let header = TarHeader::new(name.to_string(), 0o644, 7);
let data = b"special".to_vec();
let header_bytes = header.to_bytes();
entries.push(TarEntry { header, data, header_bytes });
}
let tar_data = write_tar(&entries);
fs::write(test_tar, tar_data).unwrap();
unpack_with_options(test_tar, output_dir, false, false);
fs::remove_file(test_tar).unwrap();
if Path::new(output_dir).exists() {
fs::remove_dir_all(output_dir).ok();
}
}
#[test]
fn security_test_pack_symlink_handling() {
#[cfg(unix)]
{
use std::os::unix::fs::symlink;
let test_dir = "test_security_symlink_dir";
let test_tar = "test_security_symlink.tar";
fs::create_dir_all(test_dir).unwrap();
fs::write(format!("{}/target.txt", test_dir), "target content").unwrap();
let symlink_path = format!("{}/link.txt", test_dir);
let target_path = format!("{}/target.txt", test_dir);
symlink(&target_path, &symlink_path).ok();
let files = vec![test_dir];
pack(test_tar, &files);
assert!(Path::new(test_tar).exists());
fs::remove_file(&symlink_path).ok();
fs::remove_dir_all(test_dir).unwrap();
fs::remove_file(test_tar).unwrap();
}
}
#[test]
fn security_test_unpack_overwrites_existing() {
use crate::tar::{TarEntry, TarHeader};
let test_tar = "test_security_overwrite.tar";
let output_dir = "test_security_overwrite_output";
fs::create_dir_all(output_dir).unwrap();
let sensitive_file = Path::new(output_dir).join("important.txt");
fs::write(&sensitive_file, "SENSITIVE DATA").unwrap();
let header = TarHeader::new("important.txt".to_string(), 0o644, 9);
let data = b"overwrite".to_vec();
let header_bytes = header.to_bytes();
let entry = TarEntry { header, data, header_bytes };
let tar_data = write_tar(&[entry]);
fs::write(test_tar, tar_data).unwrap();
unpack_with_options(test_tar, output_dir, true, false);
let content = fs::read_to_string(&sensitive_file).unwrap();
assert_eq!(content, "overwrite");
fs::remove_file(test_tar).unwrap();
fs::remove_dir_all(output_dir).unwrap();
}
}