From d9d6339619c880e11d580e5de6c5f6d5b872ab7d Mon Sep 17 00:00:00 2001 From: Yunus Emre Ozen <94426948+ozenyunus35@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:48:11 +0300 Subject: [PATCH 1/3] list: add --numeric-owner, and list ids when a header has no names `tar -tv` printed an empty owner and group (`/`) for an entry whose header records no user or group name, which is every archive tar-rs writes. Like GNU tar, it now prints the numeric ids then, and `--numeric-owner` always prints them. Closes #203 --- benches/bench_tar.rs | 6 ++-- src/uu/tar/src/operations/list.rs | 39 ++++++++++++-------- src/uu/tar/src/operations/list_tests.rs | 4 +-- src/uu/tar/src/tar.rs | 12 +++++-- tests/by-util/test_tar.rs | 47 +++++++++++++++++++++++++ 5 files changed, 88 insertions(+), 20 deletions(-) diff --git a/benches/bench_tar.rs b/benches/bench_tar.rs index 638813c..0412a44 100644 --- a/benches/bench_tar.rs +++ b/benches/bench_tar.rs @@ -146,7 +146,8 @@ fn list_archive_50_files(bencher: divan::Bencher) { bencher.bench_local(|| { let input = File::open(&archive_path).unwrap(); - operations::list::list_archive(input, &archive_path, false, CompressionMode::None).unwrap(); + operations::list::list_archive(input, &archive_path, false, false, CompressionMode::None) + .unwrap(); }); } @@ -160,7 +161,8 @@ fn list_archive_verbose_50_files(bencher: divan::Bencher) { bencher.bench_local(|| { let input = File::open(&archive_path).unwrap(); - operations::list::list_archive(input, &archive_path, true, CompressionMode::None).unwrap(); + operations::list::list_archive(input, &archive_path, true, false, CompressionMode::None) + .unwrap(); }); } diff --git a/src/uu/tar/src/operations/list.rs b/src/uu/tar/src/operations/list.rs index 8f88b7b..18b7e9f 100644 --- a/src/uu/tar/src/operations/list.rs +++ b/src/uu/tar/src/operations/list.rs @@ -19,6 +19,7 @@ pub fn list_archive( input: impl Read, _archive_path: &Path, verbose: bool, + numeric_owner: bool, compression: CompressionMode, ) -> UResult<()> { let reader = open_archive_reader(input, compression)?; @@ -29,7 +30,7 @@ pub fn list_archive( let entry = entry_result.map_err(TarError::CannotReadEntry)?; if verbose { - let formatted = format_verbose_entry(&entry)?; + let formatted = format_verbose_entry(&entry, numeric_owner)?; writeln!(out, "{formatted}").map_err(TarError::Io)?; } else { let path = entry.path().map_err(TarError::CannotReadEntryPath)?; @@ -41,24 +42,34 @@ pub fn list_archive( Ok(()) } -fn format_verbose_entry(entry: &tar::Entry<'_, R>) -> Result { +/// The owner or group as GNU tar lists it: the name the header records, or the +/// numeric id when there is no name or `--numeric-owner` asks for numbers. +fn owner_label(name: Option<&str>, id: Option, numeric: bool) -> String { + match name { + Some(name) if !numeric && !name.is_empty() => name.to_owned(), + _ => id.unwrap_or(0).to_string(), + } +} + +fn format_verbose_entry( + entry: &tar::Entry<'_, R>, + numeric_owner: bool, +) -> Result { let (mode, entry_type, owner, group, size, mtime) = { let header = entry.header(); ( header.mode().unwrap_or(0), header.entry_type(), - header - .username() - .ok() - .flatten() - .unwrap_or_default() - .to_owned(), - header - .groupname() - .ok() - .flatten() - .unwrap_or_default() - .to_owned(), + owner_label( + header.username().ok().flatten(), + header.uid().ok(), + numeric_owner, + ), + owner_label( + header.groupname().ok().flatten(), + header.gid().ok(), + numeric_owner, + ), header.size().unwrap_or(0), header.mtime().unwrap_or(0), ) diff --git a/src/uu/tar/src/operations/list_tests.rs b/src/uu/tar/src/operations/list_tests.rs index 0bdb442..8cb3e47 100644 --- a/src/uu/tar/src/operations/list_tests.rs +++ b/src/uu/tar/src/operations/list_tests.rs @@ -33,7 +33,7 @@ fn test_list_archive_with_zstd_non_verbose() { write_zstd_tar(&archive_path); let input = fs::File::open(&archive_path).unwrap(); - list_archive(input, &archive_path, false, CompressionMode::Zstd).unwrap(); + list_archive(input, &archive_path, false, false, CompressionMode::Zstd).unwrap(); } #[test] @@ -43,5 +43,5 @@ fn test_list_archive_with_zstd_verbose() { write_zstd_tar(&archive_path); let input = fs::File::open(&archive_path).unwrap(); - list_archive(input, &archive_path, true, CompressionMode::Zstd).unwrap(); + list_archive(input, &archive_path, true, false, CompressionMode::Zstd).unwrap(); } diff --git a/src/uu/tar/src/tar.rs b/src/uu/tar/src/tar.rs index 60ce5a5..9673086 100644 --- a/src/uu/tar/src/tar.rs +++ b/src/uu/tar/src/tar.rs @@ -145,6 +145,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let verbose = matches.get_flag("verbose"); let allow_absolute = matches.get_flag("absolute-names"); + let numeric_owner = matches.get_flag("numeric-owner"); let explicit_compression = if matches.get_flag("gzip") { Some(CompressionMode::Gzip) } else if matches.get_flag("zstd") { @@ -229,11 +230,17 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let compression = explicit_compression.unwrap_or(CompressionMode::Auto); return if archive_path == Path::new("-") { - operations::list::list_archive(io::stdin(), archive_path, verbose, compression) + operations::list::list_archive( + io::stdin(), + archive_path, + verbose, + numeric_owner, + compression, + ) } else { let file = File::open(archive_path).map_err(|e| TarError::from_io_error(e, archive_path))?; - operations::list::list_archive(file, archive_path, verbose, compression) + operations::list::list_archive(file, archive_path, verbose, numeric_owner, compression) }; } @@ -276,6 +283,7 @@ pub fn uu_app() -> Command { arg!(--zstd "Filter through zstd").conflicts_with("gzip"), // Common options arg!(-v --verbose "Verbosely list files processed"), + arg!(--"numeric-owner" "Always use numbers for user/group names"), // arg!(-h --dereference "Follow symlinks"), // arg!(-p --"preserve-permissions" "Extract information about file permissions"), // Help diff --git a/tests/by-util/test_tar.rs b/tests/by-util/test_tar.rs index 975f523..485b52e 100644 --- a/tests/by-util/test_tar.rs +++ b/tests/by-util/test_tar.rs @@ -990,6 +990,53 @@ fn test_list_verbose() { .stdout_contains("7 "); // verbose output includes file size; absent from plain -t listing } +#[test] +fn test_list_verbose_owner_names_and_numeric_owner() { + let at = &at_and_ucmd!().0; + + let mut tar_bytes = Vec::new(); + { + let mut builder = TarRsBuilder::new(&mut tar_bytes); + for (path, names) in [("named.txt", true), ("unnamed.txt", false)] { + let mut header = TarRsHeader::new_gnu(); + header.set_mode(0o644); + header.set_size("content".len() as u64); + header.set_uid(1234); + header.set_gid(5678); + if names { + header.set_username("alice").unwrap(); + header.set_groupname("staff").unwrap(); + } + header.set_cksum(); + builder + .append_data(&mut header, path, Cursor::new("content")) + .unwrap(); + } + builder.finish().unwrap(); + } + at.write_bytes("owners.tar", &tar_bytes); + + // Like GNU tar: the recorded names, or the ids when the header has none. + let listing = new_ucmd!() + .args(&["-tvf", "owners.tar"]) + .current_dir(at.as_string()) + .succeeds() + .stdout_move_str(); + let lines: Vec<&str> = listing.lines().collect(); + assert!(lines[0].contains(" alice/staff ") && lines[0].ends_with("named.txt")); + assert!(lines[1].contains(" 1234/5678 ") && lines[1].ends_with("unnamed.txt")); + + // --numeric-owner always lists the ids. + let listing = new_ucmd!() + .args(&["--numeric-owner", "-tvf", "owners.tar"]) + .current_dir(at.as_string()) + .succeeds() + .stdout_move_str(); + for line in listing.lines() { + assert!(line.contains(" 1234/5678 "), "{line}"); + } +} + #[test] fn test_list_nonexistent_archive() { new_ucmd!() From 3d08274dcf5d349f57ec5528933c778de47ca3e6 Mon Sep 17 00:00:00 2001 From: Yunus Emre Ozen <94426948+ozenyunus35@users.noreply.github.com> Date: Wed, 14 Oct 2026 22:43:44 +0300 Subject: [PATCH 2/3] list: leave an unreadable uid/gid empty instead of printing 0 --- src/uu/tar/src/operations/list.rs | 5 +++-- src/uu/tar/src/operations/list_tests.rs | 11 +++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/uu/tar/src/operations/list.rs b/src/uu/tar/src/operations/list.rs index 18b7e9f..bc61e47 100644 --- a/src/uu/tar/src/operations/list.rs +++ b/src/uu/tar/src/operations/list.rs @@ -43,11 +43,12 @@ pub fn list_archive( } /// The owner or group as GNU tar lists it: the name the header records, or the -/// numeric id when there is no name or `--numeric-owner` asks for numbers. +/// numeric id when there is no name or `--numeric-owner` asks for numbers. An +/// id field that can't be read is left empty rather than shown as 0 (root). fn owner_label(name: Option<&str>, id: Option, numeric: bool) -> String { match name { Some(name) if !numeric && !name.is_empty() => name.to_owned(), - _ => id.unwrap_or(0).to_string(), + _ => id.map(|id| id.to_string()).unwrap_or_default(), } } diff --git a/src/uu/tar/src/operations/list_tests.rs b/src/uu/tar/src/operations/list_tests.rs index 8cb3e47..442e8eb 100644 --- a/src/uu/tar/src/operations/list_tests.rs +++ b/src/uu/tar/src/operations/list_tests.rs @@ -45,3 +45,14 @@ fn test_list_archive_with_zstd_verbose() { let input = fs::File::open(&archive_path).unwrap(); list_archive(input, &archive_path, true, false, CompressionMode::Zstd).unwrap(); } + +#[test] +fn test_owner_label() { + assert_eq!(owner_label(Some("alice"), Some(1000), false), "alice"); + assert_eq!(owner_label(Some("alice"), Some(1000), true), "1000"); + assert_eq!(owner_label(Some(""), Some(1000), false), "1000"); + assert_eq!(owner_label(None, Some(1000), false), "1000"); + // An unreadable id field is left empty, not shown as root. + assert_eq!(owner_label(None, None, false), ""); + assert_eq!(owner_label(Some("alice"), None, true), ""); +} From 01c2ab95cdd2315fffe1ae8589ca51e90a56f1c6 Mon Sep 17 00:00:00 2001 From: Yunus Emre Ozen <94426948+ozenyunus35@users.noreply.github.com> Date: Thu, 15 Oct 2026 01:10:05 +0300 Subject: [PATCH 3/3] list: assert both members are listed with --numeric-owner --- tests/by-util/test_tar.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/by-util/test_tar.rs b/tests/by-util/test_tar.rs index 485b52e..675d31e 100644 --- a/tests/by-util/test_tar.rs +++ b/tests/by-util/test_tar.rs @@ -1032,7 +1032,9 @@ fn test_list_verbose_owner_names_and_numeric_owner() { .current_dir(at.as_string()) .succeeds() .stdout_move_str(); - for line in listing.lines() { + let lines: Vec<&str> = listing.lines().collect(); + assert_eq!(lines.len(), 2, "{listing}"); + for line in lines { assert!(line.contains(" 1234/5678 "), "{line}"); } }