From 28391f29948e42fb0d3d913bc034fcdf5aae72f2 Mon Sep 17 00:00:00 2001 From: Valentin Isipchuk Date: Wed, 27 May 2026 21:23:52 +0700 Subject: [PATCH 1/4] feat: absolute paths support for archives creation --- src/uu/tar/src/operations/create.rs | 17 +++++-- src/uu/tar/src/tar.rs | 8 +++- tests/by-util/test_tar.rs | 70 ++++++++++++++++++++++++++--- 3 files changed, 84 insertions(+), 11 deletions(-) diff --git a/src/uu/tar/src/operations/create.rs b/src/uu/tar/src/operations/create.rs index a33d801..9827d12 100644 --- a/src/uu/tar/src/operations/create.rs +++ b/src/uu/tar/src/operations/create.rs @@ -18,6 +18,7 @@ use uucore::error::UResult; /// /// * `archive_path` - Path where the tar archive should be created /// * `files` - Slice of file paths to add to the archive +/// * `allow_absolute` - Allow absolute paths while creating archive /// * `verbose` - Whether to print verbose output during creation /// /// # Errors @@ -26,7 +27,12 @@ use uucore::error::UResult; /// - The archive file cannot be created /// - Any input file cannot be read /// - Files cannot be added due to I/O or permission errors -pub fn create_archive(archive_path: &Path, files: &[&Path], verbose: bool) -> UResult<()> { +pub fn create_archive( + archive_path: &Path, + files: &[&Path], + allow_absolute: bool, + verbose: bool, +) -> UResult<()> { // Create the output file let file = File::create(archive_path).map_err(|e| TarError::CannotCreateArchive { path: archive_path.to_path_buf(), @@ -35,6 +41,8 @@ pub fn create_archive(archive_path: &Path, files: &[&Path], verbose: bool) -> UR // Create Builder instance let mut builder = Builder::new(file); + builder.preserve_absolute(allow_absolute); + let mut out = BufWriter::new(io::stdout().lock()); // Add each file or directory to the archive @@ -64,7 +72,7 @@ pub fn create_archive(archive_path: &Path, files: &[&Path], verbose: bool) -> UR } // Normalize path if needed (so far, handles only absolute paths) - let normalized_name = if let Some(normalized) = normalize_path(path) { + let normalized_name = if let Some(normalized) = normalize_path(path, allow_absolute) { let original_components: Vec = path.components().collect(); let normalized_components: Vec = normalized.components().collect(); if original_components.len() > normalized_components.len() { @@ -129,7 +137,10 @@ fn get_tree(path: &Path) -> Result, std::io::Error> { Ok(paths) } -fn normalize_path(path: &Path) -> Option { +fn normalize_path(path: &Path, allow_absolute: bool) -> Option { + if allow_absolute { + return Some(path.to_path_buf()); + } if path.is_absolute() { Some( path.components() diff --git a/src/uu/tar/src/tar.rs b/src/uu/tar/src/tar.rs index 16e29d1..991d4fa 100644 --- a/src/uu/tar/src/tar.rs +++ b/src/uu/tar/src/tar.rs @@ -131,6 +131,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { }; let verbose = matches.get_flag("verbose"); + let allow_absolute = matches.get_flag("absolute-names"); // Handle extract operation if matches.get_flag("extract") { @@ -159,7 +160,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { )); } - return operations::create::create_archive(archive_path, &files, verbose); + return operations::create::create_archive(archive_path, &files, allow_absolute, verbose); } // Handle list operation @@ -199,6 +200,10 @@ pub fn uu_app() -> Command { // Archive file arg!(-f --file "Use archive file or device ARCHIVE") .value_parser(clap::value_parser!(PathBuf)), + arg!( + -P --"absolute-names" + "Don't strip leading '/'s from file names" + ), // Compression options // arg!(-z --gzip "Filter through gzip"), // arg!(-j --bzip2 "Filter through bzip2"), @@ -207,7 +212,6 @@ pub fn uu_app() -> Command { arg!(-v --verbose "Verbosely list files processed"), // arg!(-h --dereference "Follow symlinks"), // arg!(-p --"preserve-permissions" "Extract information about file permissions"), - // arg!(-P --"absolute-names" "Don't strip leading '/' from file names"), // Help arg!(--help "Print help information").action(ArgAction::Help), // Files to process diff --git a/tests/by-util/test_tar.rs b/tests/by-util/test_tar.rs index 51395a3..1a51e27 100644 --- a/tests/by-util/test_tar.rs +++ b/tests/by-util/test_tar.rs @@ -171,11 +171,48 @@ fn test_create_absolute_path() { file_abs_path.push("file1.txt"); at.write(&file_abs_path.display().to_string(), "content1"); - ucmd.args(&["-cf", "archive.tar", &file_abs_path.display().to_string()]) + + // Trim leading '/' + ucmd.args(&[ + "-cf", + "archive-trimed.tar", + &file_abs_path.display().to_string(), + ]) + .succeeds() + .stdout_contains("Removing leading"); + + assert!(at.file_exists("archive-trimed.tar")); + + let expected_trimmed_path = file_abs_path + .components() + .filter(|c| !matches!(c, path::Component::RootDir | path::Component::Prefix(_))) + .map(|c| c.as_os_str().display().to_string()) + .collect::>() + .join(std::path::MAIN_SEPARATOR_STR); + + new_ucmd!() + .args(&["-tf", "archive-trimed.tar"]) + .current_dir(at.as_string()) .succeeds() - .stdout_contains("Removing leading"); + .stdout_contains(expected_trimmed_path); - assert!(at.file_exists("archive.tar")); + let (at, mut ucmd) = at_and_ucmd!(); + // Preserve leading '/' + ucmd.args(&[ + "-cPf", + "archive-preserved.tar", + &file_abs_path.display().to_string(), + ]) + .succeeds() + .no_output(); + + assert!(at.file_exists("archive-preserved.tar")); + + new_ucmd!() + .args(&["-tf", "archive-preserved.tar"]) + .current_dir(at.as_string()) + .succeeds() + .stdout_contains(file_abs_path.display().to_string()); } // Extract operation tests @@ -554,11 +591,15 @@ fn test_extract_created_from_absolute_path() { file_abs_path.push("file1.txt"); at.write(&file_abs_path.display().to_string(), "content1"); - ucmd.args(&["-cf", "archive.tar", &file_abs_path.display().to_string()]) - .succeeds(); + ucmd.args(&[ + "-cf", + "archive-trimed.tar", + &file_abs_path.display().to_string(), + ]) + .succeeds(); new_ucmd!() - .args(&["-xf", "archive.tar"]) + .args(&["-xf", "archive-trimed.tar"]) .current_dir(at.as_string()) .succeeds(); @@ -569,6 +610,23 @@ fn test_extract_created_from_absolute_path() { .collect::>() .join(std::path::MAIN_SEPARATOR_STR); + assert!(at.file_exists(&expected_path)); + + new_ucmd!() + .args(&[ + "-cPf", + "archive-preserved.tar", + &file_abs_path.display().to_string(), + ]) + .current_dir(at.as_string()) + .succeeds(); + + at.remove(&expected_path); + new_ucmd!() + .args(&["-xf", "archive-preserved.tar"]) + .current_dir(at.as_string()) + .succeeds(); + assert!(at.file_exists(expected_path)); } From a14f4e6eb41d9cb5765b091b84f9e3167e0a8eaa Mon Sep 17 00:00:00 2001 From: Valentin Isipchuk Date: Thu, 28 May 2026 17:20:52 +0700 Subject: [PATCH 2/4] fix: failed test --- tests/by-util/test_tar.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/by-util/test_tar.rs b/tests/by-util/test_tar.rs index 1a51e27..5492601 100644 --- a/tests/by-util/test_tar.rs +++ b/tests/by-util/test_tar.rs @@ -188,7 +188,7 @@ fn test_create_absolute_path() { .filter(|c| !matches!(c, path::Component::RootDir | path::Component::Prefix(_))) .map(|c| c.as_os_str().display().to_string()) .collect::>() - .join(std::path::MAIN_SEPARATOR_STR); + .join("/"); new_ucmd!() .args(&["-tf", "archive-trimed.tar"]) @@ -208,11 +208,16 @@ fn test_create_absolute_path() { assert!(at.file_exists("archive-preserved.tar")); + let expected_full_path = file_abs_path + .display() + .to_string() + .replace(std::path::MAIN_SEPARATOR_STR, "/"); + new_ucmd!() .args(&["-tf", "archive-preserved.tar"]) .current_dir(at.as_string()) .succeeds() - .stdout_contains(file_abs_path.display().to_string()); + .stdout_contains(expected_full_path); } // Extract operation tests From 87fff24fc66f5f7b5ec3d9a65392c37233068f9f Mon Sep 17 00:00:00 2001 From: Valentin Isipchuk Date: Thu, 28 May 2026 17:44:41 +0700 Subject: [PATCH 3/4] fix: failed test --- tests/by-util/test_tar.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/by-util/test_tar.rs b/tests/by-util/test_tar.rs index 5492601..3fb603f 100644 --- a/tests/by-util/test_tar.rs +++ b/tests/by-util/test_tar.rs @@ -194,7 +194,7 @@ fn test_create_absolute_path() { .args(&["-tf", "archive-trimed.tar"]) .current_dir(at.as_string()) .succeeds() - .stdout_contains(expected_trimmed_path); + .stdout_contains(&expected_trimmed_path); let (at, mut ucmd) = at_and_ucmd!(); // Preserve leading '/' @@ -208,16 +208,18 @@ fn test_create_absolute_path() { assert!(at.file_exists("archive-preserved.tar")); - let expected_full_path = file_abs_path - .display() - .to_string() - .replace(std::path::MAIN_SEPARATOR_STR, "/"); + let expected_prefix = file_abs_path + .components() + .find(|c| matches!(c, path::Component::RootDir | path::Component::Prefix(_))) + .map(|c| c.as_os_str().display().to_string()) + .unwrap(); new_ucmd!() .args(&["-tf", "archive-preserved.tar"]) .current_dir(at.as_string()) .succeeds() - .stdout_contains(expected_full_path); + .stdout_contains(expected_prefix) + .stdout_contains(expected_trimmed_path); } // Extract operation tests From 3fab307ec498ec2e97bce3323adfd591730ce1ce Mon Sep 17 00:00:00 2001 From: Valentin Isipchuk Date: Sun, 31 May 2026 11:11:47 +0700 Subject: [PATCH 4/4] refactor: simplified condition for path normalization --- src/uu/tar/src/operations/create.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/uu/tar/src/operations/create.rs b/src/uu/tar/src/operations/create.rs index 9827d12..39b6d63 100644 --- a/src/uu/tar/src/operations/create.rs +++ b/src/uu/tar/src/operations/create.rs @@ -138,10 +138,7 @@ fn get_tree(path: &Path) -> Result, std::io::Error> { } fn normalize_path(path: &Path, allow_absolute: bool) -> Option { - if allow_absolute { - return Some(path.to_path_buf()); - } - if path.is_absolute() { + if path.is_absolute() && !allow_absolute { Some( path.components() .filter(|c| !matches!(c, RootDir | ParentDir | Prefix(_)))