diff --git a/benches/bench_tar.rs b/benches/bench_tar.rs index 555a1f8..3706a88 100644 --- a/benches/bench_tar.rs +++ b/benches/bench_tar.rs @@ -150,7 +150,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(); }); } @@ -164,7 +165,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..bc61e47 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,35 @@ 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. 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.map(|id| id.to_string()).unwrap_or_default(), + } +} + +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..442e8eb 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,16 @@ 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(); +} + +#[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), ""); } diff --git a/src/uu/tar/src/tar.rs b/src/uu/tar/src/tar.rs index 73c8225..3fc5397 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 recursive = !matches.get_flag("no-recursion"); let explicit_compression = if matches.get_flag("gzip") { Some(CompressionMode::Gzip) @@ -232,11 +233,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) }; } @@ -279,6 +286,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!(--"no-recursion" "Avoid descending automatically in directories"), // arg!(-h --dereference "Follow symlinks"), // arg!(-p --"preserve-permissions" "Extract information about file permissions"), diff --git a/tests/by-util/test_tar.rs b/tests/by-util/test_tar.rs index b766295..9a8c2b4 100644 --- a/tests/by-util/test_tar.rs +++ b/tests/by-util/test_tar.rs @@ -1020,6 +1020,55 @@ 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(); + let lines: Vec<&str> = listing.lines().collect(); + assert_eq!(lines.len(), 2, "{listing}"); + for line in lines { + assert!(line.contains(" 1234/5678 "), "{line}"); + } +} + #[test] fn test_list_nonexistent_archive() { new_ucmd!()