From 06042058f92563788d0bb767a4a9df44ff9b16ea Mon Sep 17 00:00:00 2001 From: Jeff Bailey Date: Sun, 22 Mar 2026 12:14:55 +0000 Subject: [PATCH] tar: add -t/--list operation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement the list operation. Non-verbose (-t) prints one entry path per line. Verbose (-tv) prints permissions, owner/group, size, modification time, and path — matching GNU tar's ls-l style output. Adds conflict group so -c, -x, and -t are mutually exclusive. Pulls in chrono for timestamp formatting in verbose output. chrono is already in the workspace dependency set and will also be needed for date parsing when --newer=DATE is implemented. Point uucore/uutests at the coreutils git repo while we're iterating quickly on basic functionality. --- Cargo.lock | 160 ++++++++++-------------------- Cargo.toml | 4 +- src/uu/tar/Cargo.toml | 3 +- src/uu/tar/src/operations/list.rs | 91 +++++++++++++++++ src/uu/tar/src/operations/mod.rs | 1 + src/uu/tar/src/tar.rs | 19 +++- tests/by-util/test_tar.rs | 95 ++++++++++++++++++ 7 files changed, 259 insertions(+), 114 deletions(-) create mode 100644 src/uu/tar/src/operations/list.rs diff --git a/Cargo.lock b/Cargo.lock index 1f453a1..2bc1767 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -73,7 +73,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" dependencies = [ - "windows-sys", + "windows-sys 0.60.2", ] [[package]] @@ -84,7 +84,7 @@ checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys", + "windows-sys 0.60.2", ] [[package]] @@ -161,7 +161,7 @@ checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" dependencies = [ "cfg-if", "cpufeatures 0.3.0", - "rand_core 0.10.0", + "rand_core", ] [[package]] @@ -287,16 +287,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "ctor" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "424e0138278faeb2b401f174ad17e715c829512d74f3d1e81eb43365c2e0590e" -dependencies = [ - "ctor-proc-macro", - "dtor 0.1.0", -] - [[package]] name = "ctor" version = "0.8.0" @@ -304,7 +294,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" dependencies = [ "ctor-proc-macro", - "dtor 0.3.0", + "dtor", ] [[package]] @@ -365,16 +355,7 @@ dependencies = [ "cfg-if", "libc", "socket2", - "windows-sys", -] - -[[package]] -name = "dtor" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e58a0764cddb55ab28955347b45be00ade43d4d6f3ba4bf3dc354e4ec9432934" -dependencies = [ - "dtor-proc-macro", + "windows-sys 0.60.2", ] [[package]] @@ -392,6 +373,12 @@ version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "equivalent" version = "1.0.2" @@ -405,7 +392,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.60.2", ] [[package]] @@ -423,7 +410,7 @@ dependencies = [ "cfg-if", "libc", "libredox", - "windows-sys", + "windows-sys 0.60.2", ] [[package]] @@ -526,7 +513,7 @@ dependencies = [ "js-sys", "libc", "r-efi", - "rand_core 0.10.0", + "rand_core", "wasip2", "wasip3", "wasm-bindgen", @@ -662,7 +649,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys", + "windows-sys 0.60.2", ] [[package]] @@ -788,9 +775,9 @@ dependencies = [ [[package]] name = "nix" -version = "0.30.1" +version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" dependencies = [ "bitflags", "cfg-if", @@ -925,15 +912,6 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efca4c95a19a79d1c98f791f10aebd5c1363b473244630bb7dbde1dc98455a24" -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - [[package]] name = "pretty_assertions" version = "1.4.1" @@ -978,16 +956,6 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" -[[package]] -name = "rand" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_chacha", - "rand_core 0.9.3", -] - [[package]] name = "rand" version = "0.10.0" @@ -996,26 +964,7 @@ checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" dependencies = [ "chacha20", "getrandom 0.4.1", - "rand_core 0.10.0", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.3", -] - -[[package]] -name = "rand_core" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" -dependencies = [ - "getrandom 0.3.4", + "rand_core", ] [[package]] @@ -1093,7 +1042,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.60.2", ] [[package]] @@ -1216,7 +1165,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.60.2", ] [[package]] @@ -1261,12 +1210,12 @@ dependencies = [ "clap", "clap_complete", "clap_mangen", - "ctor 0.8.0", + "ctor", "libc", "phf", "phf_codegen", "pretty_assertions", - "rand 0.10.0", + "rand", "regex", "rlimit", "tar", @@ -1289,7 +1238,7 @@ dependencies = [ "getrandom 0.4.1", "once_cell", "rustix", - "windows-sys", + "windows-sys 0.60.2", ] [[package]] @@ -1299,7 +1248,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" dependencies = [ "rustix", - "windows-sys", + "windows-sys 0.60.2", ] [[package]] @@ -1452,6 +1401,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" name = "uu_tar" version = "0.0.1" dependencies = [ + "chrono", "clap", "regex", "tar", @@ -1460,12 +1410,12 @@ dependencies = [ [[package]] name = "uucore" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8038531f506a34ab4612b93f97d5f40759768cd34a83fd2af041b84fcbde474" +version = "0.8.0" +source = "git+https://github.com/uutils/coreutils#f6451c7d20bc65640e92ecdc368470577d7c9e2e" dependencies = [ "clap", "dns-lookup", + "dunce", "fluent", "fluent-bundle", "fluent-syntax", @@ -1479,13 +1429,14 @@ dependencies = [ "unic-langid", "uucore_procs", "wild", + "winapi-util", + "windows-sys 0.61.2", ] [[package]] name = "uucore_procs" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f63e2d5083ff0983193a33e2d57fd271c7e3e3e7df8e46e8f471865647b2cbc" +version = "0.8.0" +source = "git+https://github.com/uutils/coreutils#f6451c7d20bc65640e92ecdc368470577d7c9e2e" dependencies = [ "proc-macro2", "quote", @@ -1493,15 +1444,14 @@ dependencies = [ [[package]] name = "uutests" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25b8a1598ddd20230a5df59d642e2e5741b0b90038bde8684c984515f947fda5" +version = "0.8.0" +source = "git+https://github.com/uutils/coreutils#f6451c7d20bc65640e92ecdc368470577d7c9e2e" dependencies = [ - "ctor 0.6.3", + "ctor", "libc", "nix", "pretty_assertions", - "rand 0.9.2", + "rand", "regex", "rlimit", "tempfile", @@ -1621,6 +1571,15 @@ dependencies = [ "glob", ] +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.60.2", +] + [[package]] name = "windows-core" version = "0.62.2" @@ -1689,6 +1648,15 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-targets" version = "0.53.5" @@ -1864,26 +1832,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" -[[package]] -name = "zerocopy" -version = "0.8.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "zerofrom" version = "0.1.6" diff --git a/Cargo.toml b/Cargo.toml index 47992b8..5a0c695 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,8 +43,8 @@ phf = "0.13.0" phf_codegen = "0.13.0" rand = { version = "0.10.0" } regex = "1.10.4" -uucore = "0.7.0" -uutests = "0.7.0" +uucore = { git = "https://github.com/uutils/coreutils" } +uutests = { git = "https://github.com/uutils/coreutils" } tar = "0.4" tempfile = "3.10.1" textwrap = { version = "0.16.1", features = ["terminal_size"] } diff --git a/src/uu/tar/Cargo.toml b/src/uu/tar/Cargo.toml index d6ed134..45ee8cc 100644 --- a/src/uu/tar/Cargo.toml +++ b/src/uu/tar/Cargo.toml @@ -13,10 +13,11 @@ categories = ["command-line-utilities"] [dependencies] -uucore = { workspace = true } +uucore = { workspace = true, features = ["fs"] } clap = { workspace = true } regex = { workspace = true } tar = { workspace = true } +chrono = { workspace = true } [lib] path = "src/tar.rs" diff --git a/src/uu/tar/src/operations/list.rs b/src/uu/tar/src/operations/list.rs new file mode 100644 index 0000000..6826a50 --- /dev/null +++ b/src/uu/tar/src/operations/list.rs @@ -0,0 +1,91 @@ +// This file is part of the uutils tar package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use crate::errors::TarError; +use chrono::{TimeZone, Utc}; +use std::fs::File; +use std::path::Path; +use tar::Archive; +use uucore::error::UResult; +use uucore::fs::display_permissions_unix; + +/// List the contents of a tar archive, printing one entry per line. +pub fn list_archive(archive_path: &Path, verbose: bool) -> UResult<()> { + let file: File = + File::open(archive_path).map_err(|e| TarError::from_io_error(e, archive_path))?; + let mut archive = Archive::new(file); + + for entry_result in archive + .entries() + .map_err(|e| TarError::InvalidArchive(format!("Failed to read archive entries: {e}")))? + { + let entry = entry_result + .map_err(|e| TarError::InvalidArchive(format!("Failed to read entry: {e}")))?; + + if verbose { + // Collect all header fields into owned values before borrowing entry for the path, + // since both header() and path() require a borrow of entry. + 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(), + header.size().unwrap_or(0), + header.mtime().unwrap_or(0), + ) + }; + + let path = entry + .path() + .map_err(|e| TarError::InvalidArchive(format!("Failed to read entry path: {e}")))?; + + let type_char = match entry_type { + tar::EntryType::Directory => 'd', + tar::EntryType::Symlink => 'l', + tar::EntryType::Char => 'c', + tar::EntryType::Block => 'b', + tar::EntryType::Fifo => 'p', + _ => '-', + }; + // Tar headers store the type separately from the mode bits, so we get the + // 9-character rwx string from uucore and prepend our own type character. + let perm_str = display_permissions_unix(mode, false); + let permissions = format!("{type_char}{perm_str}"); + + // TODO: GNU tar displays mtime in the user's local timezone; we + // currently format in UTC. Convert to local time for compatibility. + let dt: chrono::DateTime = Utc + .timestamp_opt(mtime as i64, 0) + .single() + .unwrap_or_else(Utc::now); + let date_str = dt.format("%Y-%m-%d %H:%M"); + + println!( + "{permissions} {owner}/{group} {size:>8} {date_str} {}", + path.display() + ); + } else { + let path = entry + .path() + .map_err(|e| TarError::InvalidArchive(format!("Failed to read entry path: {e}")))?; + + println!("{}", path.display()); + } + } + + Ok(()) +} diff --git a/src/uu/tar/src/operations/mod.rs b/src/uu/tar/src/operations/mod.rs index b11efb7..a17f6f0 100644 --- a/src/uu/tar/src/operations/mod.rs +++ b/src/uu/tar/src/operations/mod.rs @@ -5,3 +5,4 @@ pub mod create; pub mod extract; +pub mod list; diff --git a/src/uu/tar/src/tar.rs b/src/uu/tar/src/tar.rs index 79bc01e..483038c 100644 --- a/src/uu/tar/src/tar.rs +++ b/src/uu/tar/src/tar.rs @@ -12,7 +12,7 @@ use uucore::error::UResult; use uucore::format_usage; const ABOUT: &str = "an archiving utility"; -const USAGE: &str = "tar key [FILE...]\n tar {-c|-x} [-v] -f ARCHIVE [FILE...]"; +const USAGE: &str = "tar key [FILE...]\n tar {-c|-t|-x} [-v] -f ARCHIVE [FILE...]"; /// Determines whether a string looks like a POSIX tar keystring. /// @@ -162,10 +162,19 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { return operations::create::create_archive(archive_path, &files, verbose); } + // Handle list operation + if matches.get_flag("list") { + let archive_path = matches.get_one::("file").ok_or_else(|| { + uucore::error::USimpleError::new(64, "option requires an argument -- 'f'") + })?; + + return operations::list::list_archive(archive_path, verbose); + } + // If no operation specified, show error Err(uucore::error::USimpleError::new( 2, - "You must specify one of the '-c' or '-x' options", + "You must specify one of the '-c', '-x', or '-t' options", )) } @@ -179,14 +188,14 @@ pub fn uu_app() -> Command { .disable_help_flag(true) .args([ // Main operation modes - arg!(-c --create "Create a new archive").conflicts_with("extract"), + arg!(-c --create "Create a new archive").conflicts_with_all(["extract", "list"]), // arg!(-d --diff "Find differences between archive and file system").alias("compare"), // arg!(-r --append "Append files to end of archive"), - // arg!(-t --list "List contents of archive"), + arg!(-t --list "List contents of archive").conflicts_with_all(["create", "extract"]), // arg!(-u --update "Only append files newer than copy in archive"), arg!(-x --extract "Extract files from archive") .alias("get") - .conflicts_with("create"), + .conflicts_with_all(["create", "list"]), // Archive file arg!(-f --file "Use archive file or device ARCHIVE") .value_parser(clap::value_parser!(PathBuf)), diff --git a/tests/by-util/test_tar.rs b/tests/by-util/test_tar.rs index 49df72e..6b8e506 100644 --- a/tests/by-util/test_tar.rs +++ b/tests/by-util/test_tar.rs @@ -657,3 +657,98 @@ fn test_posix_b_matches_dash_prefix_failure() { .code_is(64) .stderr_contains("unexpected argument '-b'"); } + +// List operation tests + +#[test] +fn test_list_single_file() { + let (at, mut ucmd) = at_and_ucmd!(); + at.write("file.txt", "test content"); + ucmd.args(&["-cf", "archive.tar", "file.txt"]).succeeds(); + + new_ucmd!() + .args(&["-tf", "archive.tar"]) + .current_dir(at.as_string()) + .succeeds() + .stdout_contains("file.txt"); +} + +#[test] +fn test_list_multiple_files() { + let (at, mut ucmd) = at_and_ucmd!(); + at.write("file1.txt", "content1"); + at.write("file2.txt", "content2"); + at.write("file3.txt", "content3"); + + ucmd.args(&["-cf", "archive.tar", "file1.txt", "file2.txt", "file3.txt"]) + .succeeds(); + + new_ucmd!() + .args(&["-tf", "archive.tar"]) + .current_dir(at.as_string()) + .succeeds() + .stdout_contains("file1.txt") + .stdout_contains("file2.txt") + .stdout_contains("file3.txt"); +} + +#[test] +fn test_list_directory() { + let (at, mut ucmd) = at_and_ucmd!(); + at.mkdir("testdir"); + at.write("testdir/file1.txt", "content1"); + at.mkdir("testdir/subdir"); + at.write("testdir/subdir/file2.txt", "content2"); + + ucmd.args(&["-cf", "archive.tar", "testdir"]).succeeds(); + + new_ucmd!() + .args(&["-tf", "archive.tar"]) + .current_dir(at.as_string()) + .succeeds() + .stdout_contains("testdir") + .stdout_contains("testdir/file1.txt") + .stdout_contains("testdir/subdir") + .stdout_contains("testdir/subdir/file2.txt"); +} + +#[test] +fn test_list_verbose() { + let (at, mut ucmd) = at_and_ucmd!(); + at.write("file.txt", "content"); + ucmd.args(&["-cf", "archive.tar", "file.txt"]).succeeds(); + + new_ucmd!() + .args(&["-tvf", "archive.tar"]) + .current_dir(at.as_string()) + .succeeds() + .stdout_contains("file.txt") + .stdout_contains("7 "); // verbose output includes file size; absent from plain -t listing +} + +#[test] +fn test_list_nonexistent_archive() { + new_ucmd!() + .args(&["-tf", "nonexistent.tar"]) + .fails() + .code_is(2) + .stderr_contains("nonexistent.tar"); +} + +#[test] +fn test_list_conflicts_with_create() { + new_ucmd!() + .args(&["-ctf", "archive.tar", "file.txt"]) + .fails() + .code_is(2) + .stderr_contains("cannot be used with"); +} + +#[test] +fn test_list_conflicts_with_extract() { + new_ucmd!() + .args(&["-xtf", "archive.tar"]) + .fails() + .code_is(2) + .stderr_contains("cannot be used with"); +}