diff --git a/.github/workflows/GnuTests.yml b/.github/workflows/GnuTests.yml index ab973defca2..0b9d8ce7fe4 100644 --- a/.github/workflows/GnuTests.yml +++ b/.github/workflows/GnuTests.yml @@ -258,6 +258,7 @@ jobs: echo "REF_FAILING = ${REF_FAILING}" echo "CURRENT_RUN_FAILING = ${CURRENT_RUN_FAILING}" echo "REF_SKIP_PASS = ${REF_SKIP}" + echo "CURRENT_RUN_SKIP = ${CURRENT_RUN_SKIP}" # Compare failing and error tests for LINE in ${CURRENT_RUN_FAILING} diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml index 42255d8899a..27ff2afe4d4 100644 --- a/.github/workflows/freebsd.yml +++ b/.github/workflows/freebsd.yml @@ -41,7 +41,7 @@ jobs: - name: Run sccache-cache uses: mozilla-actions/sccache-action@v0.0.7 - name: Prepare, build and test - uses: vmactions/freebsd-vm@v1.1.5 + uses: vmactions/freebsd-vm@v1.1.6 with: usesh: true sync: rsync @@ -135,7 +135,7 @@ jobs: - name: Run sccache-cache uses: mozilla-actions/sccache-action@v0.0.7 - name: Prepare, build and test - uses: vmactions/freebsd-vm@v1.1.5 + uses: vmactions/freebsd-vm@v1.1.6 with: usesh: true sync: rsync diff --git a/.github/workflows/fuzzing.yml b/.github/workflows/fuzzing.yml index 24d0f1c436f..c8e2c801408 100644 --- a/.github/workflows/fuzzing.yml +++ b/.github/workflows/fuzzing.yml @@ -57,6 +57,7 @@ jobs: - { name: fuzz_split, should_pass: false } - { name: fuzz_tr, should_pass: false } - { name: fuzz_env, should_pass: false } + - { name: fuzz_cksum, should_pass: false } - { name: fuzz_parse_glob, should_pass: true } - { name: fuzz_parse_size, should_pass: true } - { name: fuzz_parse_time, should_pass: true } diff --git a/.vscode/cspell.dictionaries/jargon.wordlist.txt b/.vscode/cspell.dictionaries/jargon.wordlist.txt index 6dd5483c6c1..4109630e553 100644 --- a/.vscode/cspell.dictionaries/jargon.wordlist.txt +++ b/.vscode/cspell.dictionaries/jargon.wordlist.txt @@ -10,6 +10,7 @@ bytewise canonicalization canonicalize canonicalizing +capget codepoint codepoints codegen @@ -65,6 +66,7 @@ kibi kibibytes libacl lcase +llistxattr lossily lstat mebi @@ -108,6 +110,7 @@ seedable semver semiprime semiprimes +setcap setfacl shortcode shortcodes diff --git a/Cargo.lock b/Cargo.lock index 435d1a39d73..642b3fddafb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1805,9 +1805,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quote" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] @@ -2080,9 +2080,9 @@ checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4" [[package]] name = "serde" -version = "1.0.216" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] @@ -2098,9 +2098,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.216" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 1991679d8e6..98c50d6dbb6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,8 @@ windows = ["feat_os_windows"] nightly = [] test_unimplemented = [] expensive_tests = [] +# "test_risky_names" == enable tests that create problematic file names (would make a network share inaccessible to Windows, breaks SVN on Mac OS, etc.) +test_risky_names = [] # * only build `uudoc` when `--feature uudoc` is activated uudoc = ["zip", "dep:uuhelp_parser"] ## features diff --git a/build.rs b/build.rs index 91e9d0427ce..d414de09209 100644 --- a/build.rs +++ b/build.rs @@ -33,7 +33,9 @@ pub fn main() { #[allow(clippy::match_same_arms)] match krate.as_ref() { "default" | "macos" | "unix" | "windows" | "selinux" | "zip" => continue, // common/standard feature names - "nightly" | "test_unimplemented" | "expensive_tests" => continue, // crate-local custom features + "nightly" | "test_unimplemented" | "expensive_tests" | "test_risky_names" => { + continue + } // crate-local custom features "uudoc" => continue, // is not a utility "test" => continue, // over-ridden with 'uu_test' to avoid collision with rust core crate 'test' s if s.starts_with(FEATURE_PREFIX) => continue, // crate feature sets diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index ce24e582718..a2bae6dd306 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -81,6 +81,18 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "autocfg" version = "1.3.0" @@ -121,6 +133,39 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +[[package]] +name = "blake2b_simd" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23285ad32269793932e830392f2fe2f83e26488fd3ec778883a93c8323735780" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "blake3" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d82033247fd8e890df8f740e407ad4d038debb9eb1f40533fffb32e7d17dc6f7" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bstr" version = "1.9.1" @@ -146,13 +191,13 @@ checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce" [[package]] name = "cc" -version = "1.0.98" +version = "1.1.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" +checksum = "40545c26d092346d8a8dab71ee48e7685a7a9cba76e634790c215b41a4a7b4cf" dependencies = [ "jobserver", "libc", - "once_cell", + "shlex", ] [[package]] @@ -245,12 +290,27 @@ dependencies = [ "tiny-keccak", ] +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + [[package]] name = "core-foundation-sys" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +[[package]] +name = "cpufeatures" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca741a962e1b0bff6d724a1a0958b686406e853bb14061f218562e1896f95e6" +dependencies = [ + "libc", +] + [[package]] name = "crossbeam-deque" version = "0.8.5" @@ -282,6 +342,16 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "ctrlc" version = "3.4.4" @@ -292,6 +362,42 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "data-encoding" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" + +[[package]] +name = "data-encoding-macro" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1559b6cba622276d6d63706db152618eeb15b89b3e4041446b05876e352e639" +dependencies = [ + "data-encoding", + "data-encoding-macro-internal", +] + +[[package]] +name = "data-encoding-macro-internal" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "332d754c0af53bc87c108fed664d121ecf59207ec4196041f04d6ab9002ad33f" +dependencies = [ + "data-encoding", + "syn 1.0.109", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "dlv-list" version = "0.5.2" @@ -335,6 +441,16 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -358,6 +474,12 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "iana-time-zone" version = "0.1.60" @@ -414,6 +536,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -454,6 +585,16 @@ version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + [[package]] name = "memchr" version = "2.7.2" @@ -748,15 +889,62 @@ checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.65", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", ] +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest", + "keccak", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "similar" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e" +[[package]] +name = "sm3" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebb9a3b702d0a7e33bc4d85a14456633d2b165c2ad839c5fd9a8417c1ab15860" +dependencies = [ + "digest", +] + [[package]] name = "strsim" version = "0.11.1" @@ -814,7 +1002,7 @@ checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.65", ] [[package]] @@ -832,6 +1020,12 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc" +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + [[package]] name = "unicode-ident" version = "1.0.12" @@ -856,6 +1050,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "uu_cksum" +version = "0.0.28" +dependencies = [ + "clap", + "hex", + "regex", + "uucore", +] + [[package]] name = "uu_cut" version = "0.0.28" @@ -990,20 +1194,35 @@ dependencies = [ name = "uucore" version = "0.0.28" dependencies = [ + "blake2b_simd", + "blake3", "clap", + "data-encoding", + "data-encoding-macro", + "digest", "dunce", "glob", + "hex", "itertools", "lazy_static", "libc", + "md-5", + "memchr", "nix 0.29.0", "number_prefix", "once_cell", "os_display", + "regex", + "sha1", + "sha2", + "sha3", + "sm3", + "thiserror", "uucore_procs", "wild", "winapi-util", "windows-sys 0.59.0", + "z85", ] [[package]] @@ -1015,6 +1234,7 @@ dependencies = [ "rand", "similar", "tempfile", + "uu_cksum", "uu_cut", "uu_date", "uu_echo", @@ -1043,6 +1263,12 @@ dependencies = [ name = "uuhelp_parser" version = "0.0.28" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -1070,7 +1296,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.65", "wasm-bindgen-shared", ] @@ -1092,7 +1318,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.65", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1277,3 +1503,9 @@ name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "z85" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a599daf1b507819c1121f0bf87fa37eb19daac6aff3aefefd4e6e2e0f2020fc" diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 3bc5a3433bc..cc2df2d4215 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -27,6 +27,7 @@ uu_cut = { path = "../src/uu/cut/" } uu_split = { path = "../src/uu/split/" } uu_tr = { path = "../src/uu/tr/" } uu_env = { path = "../src/uu/env/" } +uu_cksum = { path = "../src/uu/cksum/" } # Prevent this from interfering with workspaces [workspace] @@ -127,3 +128,9 @@ name = "fuzz_env" path = "fuzz_targets/fuzz_env.rs" test = false doc = false + +[[bin]] +name = "fuzz_cksum" +path = "fuzz_targets/fuzz_cksum.rs" +test = false +doc = false diff --git a/fuzz/fuzz_targets/fuzz_cksum.rs b/fuzz/fuzz_targets/fuzz_cksum.rs new file mode 100644 index 00000000000..411b21aab52 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_cksum.rs @@ -0,0 +1,164 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. +// spell-checker:ignore chdir + +#![no_main] +use libfuzzer_sys::fuzz_target; +use std::ffi::OsString; +use uu_cksum::uumain; +mod fuzz_common; +use crate::fuzz_common::{ + compare_result, generate_and_run_uumain, generate_random_file, generate_random_string, + run_gnu_cmd, CommandResult, +}; +use rand::Rng; +use std::env::temp_dir; +use std::fs::{self, File}; +use std::io::Write; +use std::process::Command; + +static CMD_PATH: &str = "cksum"; + +fn generate_cksum_args() -> Vec { + let mut rng = rand::thread_rng(); + let mut args = Vec::new(); + + let digests = [ + "sysv", "bsd", "crc", "md5", "sha1", "sha224", "sha256", "sha384", "sha512", "blake2b", + "sm3", + ]; + let digest_opts = [ + "--base64", + "--raw", + "--tag", + "--untagged", + "--text", + "--binary", + ]; + + if rng.gen_bool(0.3) { + args.push("-a".to_string()); + args.push(digests[rng.gen_range(0..digests.len())].to_string()); + } + + if rng.gen_bool(0.2) { + args.push(digest_opts[rng.gen_range(0..digest_opts.len())].to_string()); + } + + if rng.gen_bool(0.15) { + args.push("-l".to_string()); + args.push(rng.gen_range(8..513).to_string()); + } + + if rng.gen_bool(0.05) { + for _ in 0..rng.gen_range(0..3) { + args.push(format!("file_{}", generate_random_string(5))); + } + } else { + args.push("-c".to_string()); + } + + if rng.gen_bool(0.25) { + if let Ok(file_path) = generate_random_file() { + args.push(file_path); + } + } + + if args.is_empty() || !args.iter().any(|arg| arg.starts_with("file_")) { + args.push("-a".to_string()); + args.push(digests[rng.gen_range(0..digests.len())].to_string()); + + if let Ok(file_path) = generate_random_file() { + args.push(file_path); + } + } + + args +} + +fn generate_checksum_file( + algo: &str, + file_path: &str, + digest_opts: &[&str], +) -> Result { + let checksum_file_path = temp_dir().join("checksum_file"); + let mut cmd = Command::new(CMD_PATH); + cmd.arg("-a").arg(algo); + + for opt in digest_opts { + cmd.arg(opt); + } + + cmd.arg(file_path); + let output = cmd.output()?; + + let mut checksum_file = File::create(&checksum_file_path)?; + checksum_file.write_all(&output.stdout)?; + + Ok(checksum_file_path.to_str().unwrap().to_string()) +} + +fn select_random_digest_opts<'a>( + rng: &mut rand::rngs::ThreadRng, + digest_opts: &'a [&'a str], +) -> Vec<&'a str> { + digest_opts + .iter() + .filter(|_| rng.gen_bool(0.5)) + .copied() + .collect() +} + +fuzz_target!(|_data: &[u8]| { + let cksum_args = generate_cksum_args(); + let mut args = vec![OsString::from("cksum")]; + args.extend(cksum_args.iter().map(OsString::from)); + + if let Ok(file_path) = generate_random_file() { + let algo = cksum_args + .iter() + .position(|arg| arg == "-a") + .map_or("md5", |index| &cksum_args[index + 1]); + + let all_digest_opts = ["--base64", "--raw", "--tag", "--untagged"]; + let mut rng = rand::thread_rng(); + let selected_digest_opts = select_random_digest_opts(&mut rng, &all_digest_opts); + + if let Ok(checksum_file_path) = + generate_checksum_file(algo, &file_path, &selected_digest_opts) + { + if let Ok(content) = fs::read_to_string(&checksum_file_path) { + println!("File content: {checksum_file_path}={content}"); + } else { + eprintln!("Error reading the checksum file."); + } + println!("args: {:?}", args); + let rust_result = generate_and_run_uumain(&args, uumain, None); + + let gnu_result = match run_gnu_cmd(CMD_PATH, &args[1..], false, None) { + Ok(result) => result, + Err(error_result) => { + eprintln!("Failed to run GNU command:"); + eprintln!("Stderr: {}", error_result.stderr); + eprintln!("Exit Code: {}", error_result.exit_code); + CommandResult { + stdout: String::new(), + stderr: error_result.stderr, + exit_code: error_result.exit_code, + } + } + }; + + compare_result( + "cksum", + &format!("{:?}", &args[1..]), + None, + &rust_result, + &gnu_result, + false, + ); + } + } +}); diff --git a/fuzz/fuzz_targets/fuzz_common.rs b/fuzz/fuzz_targets/fuzz_common.rs index 4cc04c8dc0b..f9d974cf779 100644 --- a/fuzz/fuzz_targets/fuzz_common.rs +++ b/fuzz/fuzz_targets/fuzz_common.rs @@ -8,7 +8,9 @@ use libc::{close, dup, dup2, pipe, STDERR_FILENO, STDOUT_FILENO}; use rand::prelude::SliceRandom; use rand::Rng; use similar::TextDiff; +use std::env::temp_dir; use std::ffi::OsString; +use std::fs::File; use std::io::{Seek, SeekFrom, Write}; use std::os::fd::{AsRawFd, RawFd}; use std::process::{Command, Stdio}; @@ -392,3 +394,23 @@ pub fn generate_random_string(max_length: usize) -> String { result } + +pub fn generate_random_file() -> Result { + let mut rng = rand::thread_rng(); + let file_name: String = (0..10) + .map(|_| rng.gen_range(b'a'..=b'z') as char) + .collect(); + let mut file_path = temp_dir(); + file_path.push(file_name); + + let mut file = File::create(&file_path)?; + + let content_length = rng.gen_range(10..1000); + let content: String = (0..content_length) + .map(|_| (rng.gen_range(b' '..=b'~') as char)) + .collect(); + + file.write_all(content.as_bytes())?; + + Ok(file_path.to_str().unwrap().to_string()) +} diff --git a/src/uu/cp/Cargo.toml b/src/uu/cp/Cargo.toml index 6801e6a0960..3912f3308a5 100644 --- a/src/uu/cp/Cargo.toml +++ b/src/uu/cp/Cargo.toml @@ -30,6 +30,7 @@ uucore = { workspace = true, features = [ "backup-control", "entries", "fs", + "fsxattr", "perms", "mode", "update-control", diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index 32168b09009..b7469404757 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -17,6 +17,8 @@ use std::os::unix::ffi::OsStrExt; #[cfg(unix)] use std::os::unix::fs::{FileTypeExt, PermissionsExt}; use std::path::{Path, PathBuf, StripPrefixError}; +#[cfg(all(unix, not(target_os = "android")))] +use uucore::fsxattr::copy_xattrs; use clap::{builder::ValueParser, crate_version, Arg, ArgAction, ArgMatches, Command}; use filetime::FileTime; @@ -1605,12 +1607,7 @@ pub(crate) fn copy_attributes( handle_preserve(&attributes.xattr, || -> CopyResult<()> { #[cfg(all(unix, not(target_os = "android")))] { - let xattrs = xattr::list(source)?; - for attr in xattrs { - if let Some(attr_value) = xattr::get(source, attr.clone())? { - xattr::set(dest, attr, &attr_value[..])?; - } - } + copy_xattrs(source, dest)?; } #[cfg(not(all(unix, not(target_os = "android"))))] { diff --git a/src/uu/csplit/src/csplit.rs b/src/uu/csplit/src/csplit.rs index 2054e6cff4e..0602f0deec7 100644 --- a/src/uu/csplit/src/csplit.rs +++ b/src/uu/csplit/src/csplit.rs @@ -621,8 +621,9 @@ pub fn uu_app() -> Command { ) .arg( Arg::new(options::QUIET) - .short('s') + .short('q') .long(options::QUIET) + .visible_short_alias('s') .visible_alias("silent") .help("do not print counts of output file sizes") .action(ArgAction::SetTrue), diff --git a/src/uu/csplit/src/patterns.rs b/src/uu/csplit/src/patterns.rs index bd6c4fbfaef..edd632d08fc 100644 --- a/src/uu/csplit/src/patterns.rs +++ b/src/uu/csplit/src/patterns.rs @@ -106,7 +106,7 @@ pub fn get_patterns(args: &[String]) -> Result, CsplitError> { fn extract_patterns(args: &[String]) -> Result, CsplitError> { let mut patterns = Vec::with_capacity(args.len()); let to_match_reg = - Regex::new(r"^(/(?P.+)/|%(?P.+)%)(?P[\+-]\d+)?$").unwrap(); + Regex::new(r"^(/(?P.+)/|%(?P.+)%)(?P[\+-]?\d+)?$").unwrap(); let execute_ntimes_reg = Regex::new(r"^\{(?P\d+)|\*\}$").unwrap(); let mut iter = args.iter().peekable(); @@ -219,14 +219,15 @@ mod tests { "{*}", "/test3.*end$/", "{4}", - "/test4.*end$/+3", - "/test5.*end$/-3", + "/test4.*end$/3", + "/test5.*end$/+3", + "/test6.*end$/-3", ] .into_iter() .map(|v| v.to_string()) .collect(); let patterns = get_patterns(input.as_slice()).unwrap(); - assert_eq!(patterns.len(), 5); + assert_eq!(patterns.len(), 6); match patterns.first() { Some(Pattern::UpToMatch(reg, 0, ExecutePattern::Times(1))) => { let parsed_reg = format!("{reg}"); @@ -256,12 +257,19 @@ mod tests { _ => panic!("expected UpToMatch pattern"), }; match patterns.get(4) { - Some(Pattern::UpToMatch(reg, -3, ExecutePattern::Times(1))) => { + Some(Pattern::UpToMatch(reg, 3, ExecutePattern::Times(1))) => { let parsed_reg = format!("{reg}"); assert_eq!(parsed_reg, "test5.*end$"); } _ => panic!("expected UpToMatch pattern"), }; + match patterns.get(5) { + Some(Pattern::UpToMatch(reg, -3, ExecutePattern::Times(1))) => { + let parsed_reg = format!("{reg}"); + assert_eq!(parsed_reg, "test6.*end$"); + } + _ => panic!("expected UpToMatch pattern"), + }; } #[test] @@ -273,14 +281,15 @@ mod tests { "{*}", "%test3.*end$%", "{4}", - "%test4.*end$%+3", - "%test5.*end$%-3", + "%test4.*end$%3", + "%test5.*end$%+3", + "%test6.*end$%-3", ] .into_iter() .map(|v| v.to_string()) .collect(); let patterns = get_patterns(input.as_slice()).unwrap(); - assert_eq!(patterns.len(), 5); + assert_eq!(patterns.len(), 6); match patterns.first() { Some(Pattern::SkipToMatch(reg, 0, ExecutePattern::Times(1))) => { let parsed_reg = format!("{reg}"); @@ -310,12 +319,19 @@ mod tests { _ => panic!("expected SkipToMatch pattern"), }; match patterns.get(4) { - Some(Pattern::SkipToMatch(reg, -3, ExecutePattern::Times(1))) => { + Some(Pattern::SkipToMatch(reg, 3, ExecutePattern::Times(1))) => { let parsed_reg = format!("{reg}"); assert_eq!(parsed_reg, "test5.*end$"); } _ => panic!("expected SkipToMatch pattern"), }; + match patterns.get(5) { + Some(Pattern::SkipToMatch(reg, -3, ExecutePattern::Times(1))) => { + let parsed_reg = format!("{reg}"); + assert_eq!(parsed_reg, "test6.*end$"); + } + _ => panic!("expected SkipToMatch pattern"), + }; } #[test] diff --git a/src/uu/cut/src/cut.rs b/src/uu/cut/src/cut.rs index 3dde5e66595..5e128425b63 100644 --- a/src/uu/cut/src/cut.rs +++ b/src/uu/cut/src/cut.rs @@ -9,7 +9,7 @@ use bstr::io::BufReadExt; use clap::{builder::ValueParser, crate_version, Arg, ArgAction, ArgMatches, Command}; use std::ffi::OsString; use std::fs::File; -use std::io::{stdin, stdout, BufReader, BufWriter, IsTerminal, Read, Write}; +use std::io::{stdin, stdout, BufRead, BufReader, BufWriter, IsTerminal, Read, Write}; use std::path::Path; use uucore::display::Quotable; use uucore::error::{set_exit_code, FromIo, UResult, USimpleError}; @@ -267,10 +267,46 @@ fn cut_fields_implicit_out_delim( Ok(()) } +// The input delimiter is identical to `newline_char` +fn cut_fields_newline_char_delim( + reader: R, + ranges: &[Range], + newline_char: u8, + out_delim: &[u8], +) -> UResult<()> { + let buf_in = BufReader::new(reader); + let mut out = stdout_writer(); + + let segments: Vec<_> = buf_in.split(newline_char).filter_map(|x| x.ok()).collect(); + let mut print_delim = false; + + for &Range { low, high } in ranges { + for i in low..=high { + // "- 1" is necessary because fields start from 1 whereas a Vec starts from 0 + if let Some(segment) = segments.get(i - 1) { + if print_delim { + out.write_all(out_delim)?; + } else { + print_delim = true; + } + out.write_all(segment.as_slice())?; + } else { + break; + } + } + } + out.write_all(&[newline_char])?; + Ok(()) +} + fn cut_fields(reader: R, ranges: &[Range], opts: &Options) -> UResult<()> { let newline_char = opts.line_ending.into(); let field_opts = opts.field_opts.as_ref().unwrap(); // it is safe to unwrap() here - field_opts will always be Some() for cut_fields() call match field_opts.delimiter { + Delimiter::Slice(delim) if delim == [newline_char] => { + let out_delim = opts.out_delimiter.unwrap_or(delim); + cut_fields_newline_char_delim(reader, ranges, newline_char, out_delim) + } Delimiter::Slice(delim) => { let matcher = ExactMatcher::new(delim); match opts.out_delimiter { diff --git a/src/uu/echo/src/echo.rs b/src/uu/echo/src/echo.rs index 2d2884b1dd7..097e4f2e980 100644 --- a/src/uu/echo/src/echo.rs +++ b/src/uu/echo/src/echo.rs @@ -255,9 +255,26 @@ fn print_escaped(input: &[u8], output: &mut StdoutLock) -> io::Result impl uucore::Args { + let mut result = Vec::new(); + let mut is_first_double_hyphen = true; + + for arg in args { + if arg == "--" && is_first_double_hyphen { + result.push(OsString::from("--")); + is_first_double_hyphen = false; + } + result.push(arg); + } + + result.into_iter() +} + #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().get_matches_from(args); + let matches = uu_app().get_matches_from(handle_double_hyphens(args)); // TODO // "If the POSIXLY_CORRECT environment variable is set, then when echo’s first argument is not -n it outputs option-like arguments instead of treating them as options." diff --git a/src/uu/ls/src/colors.rs b/src/uu/ls/src/colors.rs index 6c580d18a7a..4f97e42e27d 100644 --- a/src/uu/ls/src/colors.rs +++ b/src/uu/ls/src/colors.rs @@ -156,6 +156,26 @@ pub(crate) fn color_name( target_symlink: Option<&PathData>, wrap: bool, ) -> String { + // Check if the file has capabilities + #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] + { + // Skip checking capabilities if LS_COLORS=ca=: + let capabilities = style_manager + .colors + .style_for_indicator(Indicator::Capabilities); + + let has_capabilities = if capabilities.is_none() { + false + } else { + uucore::fsxattr::has_acl(path.p_buf.as_path()) + }; + + // If the file has capabilities, use a specific style for `ca` (capabilities) + if has_capabilities { + return style_manager.apply_style(capabilities, name, wrap); + } + } + if !path.must_dereference { // If we need to dereference (follow) a symlink, we will need to get the metadata if let Some(de) = &path.de { diff --git a/src/uu/mv/src/error.rs b/src/uu/mv/src/error.rs index f989d4e1332..6daa8188ec1 100644 --- a/src/uu/mv/src/error.rs +++ b/src/uu/mv/src/error.rs @@ -12,7 +12,6 @@ pub enum MvError { NoSuchFile(String), CannotStatNotADirectory(String), SameFile(String, String), - SelfSubdirectory(String), SelfTargetSubdirectory(String, String), DirectoryToNonDirectory(String), NonDirectoryToDirectory(String, String), @@ -29,14 +28,9 @@ impl Display for MvError { Self::NoSuchFile(s) => write!(f, "cannot stat {s}: No such file or directory"), Self::CannotStatNotADirectory(s) => write!(f, "cannot stat {s}: Not a directory"), Self::SameFile(s, t) => write!(f, "{s} and {t} are the same file"), - Self::SelfSubdirectory(s) => write!( - f, - "cannot move '{s}' to a subdirectory of itself, '{s}/{s}'" - ), - Self::SelfTargetSubdirectory(s, t) => write!( - f, - "cannot move '{s}' to a subdirectory of itself, '{t}/{s}'" - ), + Self::SelfTargetSubdirectory(s, t) => { + write!(f, "cannot move {s} to a subdirectory of itself, {t}") + } Self::DirectoryToNonDirectory(t) => { write!(f, "cannot overwrite directory {t} with non-directory") } diff --git a/src/uu/mv/src/mv.rs b/src/uu/mv/src/mv.rs index 7debf52c962..675982bacba 100644 --- a/src/uu/mv/src/mv.rs +++ b/src/uu/mv/src/mv.rs @@ -19,13 +19,13 @@ use std::io; use std::os::unix; #[cfg(windows)] use std::os::windows; -use std::path::{Path, PathBuf}; +use std::path::{absolute, Path, PathBuf}; use uucore::backup_control::{self, source_is_target_backup}; use uucore::display::Quotable; use uucore::error::{set_exit_code, FromIo, UResult, USimpleError, UUsageError}; use uucore::fs::{ - are_hardlinks_or_one_way_symlink_to_same_file, are_hardlinks_to_same_file, - path_ends_with_terminator, + are_hardlinks_or_one_way_symlink_to_same_file, are_hardlinks_to_same_file, canonicalize, + path_ends_with_terminator, MissingHandling, ResolveMode, }; #[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))] use uucore::fsxattr; @@ -322,20 +322,6 @@ fn handle_two_paths(source: &Path, target: &Path, opts: &Options) -> UResult<()> }); } - if (source.eq(target) - || are_hardlinks_to_same_file(source, target) - || are_hardlinks_or_one_way_symlink_to_same_file(source, target)) - && opts.backup == BackupMode::NoBackup - { - if source.eq(Path::new(".")) || source.ends_with("/.") || source.is_file() { - return Err( - MvError::SameFile(source.quote().to_string(), target.quote().to_string()).into(), - ); - } else { - return Err(MvError::SelfSubdirectory(source.display().to_string()).into()); - } - } - let target_is_dir = target.is_dir(); let source_is_dir = source.is_dir(); @@ -347,6 +333,8 @@ fn handle_two_paths(source: &Path, target: &Path, opts: &Options) -> UResult<()> return Err(MvError::FailedToAccessNotADirectory(target.quote().to_string()).into()); } + assert_not_same_file(source, target, target_is_dir, opts)?; + if target_is_dir { if opts.no_target_dir { if source.is_dir() { @@ -356,14 +344,6 @@ fn handle_two_paths(source: &Path, target: &Path, opts: &Options) -> UResult<()> } else { Err(MvError::DirectoryToNonDirectory(target.quote().to_string()).into()) } - // Check that source & target do not contain same subdir/dir when both exist - // mkdir dir1/dir2; mv dir1 dir1/dir2 - } else if target.starts_with(source) { - Err(MvError::SelfTargetSubdirectory( - source.display().to_string(), - target.display().to_string(), - ) - .into()) } else { move_files_into_dir(&[source.to_path_buf()], target, opts) } @@ -387,6 +367,88 @@ fn handle_two_paths(source: &Path, target: &Path, opts: &Options) -> UResult<()> } } +fn assert_not_same_file( + source: &Path, + target: &Path, + target_is_dir: bool, + opts: &Options, +) -> UResult<()> { + // we'll compare canonicalized_source and canonicalized_target for same file detection + let canonicalized_source = match canonicalize( + absolute(source)?, + MissingHandling::Normal, + ResolveMode::Logical, + ) { + Ok(source) if source.exists() => source, + _ => absolute(source)?, // file or symlink target doesn't exist but its absolute path is still used for comparison + }; + + // special case if the target exists, is a directory, and the `-T` flag wasn't used + let target_is_dir = target_is_dir && !opts.no_target_dir; + let canonicalized_target = if target_is_dir { + // `mv source_file target_dir` => target_dir/source_file + // canonicalize the path that exists (target directory) and join the source file name + canonicalize( + absolute(target)?, + MissingHandling::Normal, + ResolveMode::Logical, + )? + .join(source.file_name().unwrap_or_default()) + } else { + // `mv source target_dir/target` => target_dir/target + // we canonicalize target_dir and join /target + match absolute(target)?.parent() { + Some(parent) if parent.to_str() != Some("") => { + canonicalize(parent, MissingHandling::Normal, ResolveMode::Logical)? + .join(target.file_name().unwrap_or_default()) + } + // path.parent() returns Some("") or None if there's no parent + _ => absolute(target)?, // absolute paths should always have a parent, but we'll fall back just in case + } + }; + + let same_file = (canonicalized_source.eq(&canonicalized_target) + || are_hardlinks_to_same_file(source, target) + || are_hardlinks_or_one_way_symlink_to_same_file(source, target)) + && opts.backup == BackupMode::NoBackup; + + // get the expected target path to show in errors + // this is based on the argument and not canonicalized + let target_display = match source.file_name() { + Some(file_name) if target_is_dir => { + // join target_dir/source_file in a platform-independent manner + let mut path = target + .display() + .to_string() + .trim_end_matches("/") + .to_owned(); + + path.push('/'); + path.push_str(&file_name.to_string_lossy()); + + path.quote().to_string() + } + _ => target.quote().to_string(), + }; + + if same_file + && (canonicalized_source.eq(&canonicalized_target) + || source.eq(Path::new(".")) + || source.ends_with("/.") + || source.is_file()) + { + return Err(MvError::SameFile(source.quote().to_string(), target_display).into()); + } else if (same_file || canonicalized_target.starts_with(canonicalized_source)) + // don't error if we're moving a symlink of a directory into itself + && !source.is_symlink() + { + return Err( + MvError::SelfTargetSubdirectory(source.quote().to_string(), target_display).into(), + ); + } + Ok(()) +} + fn handle_multiple_paths(paths: &[PathBuf], opts: &Options) -> UResult<()> { if opts.no_target_dir { return Err(UUsageError::new( @@ -425,10 +487,6 @@ fn move_files_into_dir(files: &[PathBuf], target_dir: &Path, options: &Options) return Err(MvError::NotADirectory(target_dir.quote().to_string()).into()); } - let canonicalized_target_dir = target_dir - .canonicalize() - .unwrap_or_else(|_| target_dir.to_path_buf()); - let multi_progress = options.progress_bar.then(MultiProgress::new); let count_progress = if let Some(ref multi_progress) = multi_progress { @@ -479,24 +537,9 @@ fn move_files_into_dir(files: &[PathBuf], target_dir: &Path, options: &Options) // Check if we have mv dir1 dir2 dir2 // And generate an error if this is the case - if let Ok(canonicalized_source) = sourcepath.canonicalize() { - if canonicalized_source == canonicalized_target_dir { - // User tried to move directory to itself, warning is shown - // and process of moving files is continued. - show!(USimpleError::new( - 1, - format!( - "cannot move '{}' to a subdirectory of itself, '{}/{}'", - sourcepath.display(), - uucore::fs::normalize_path(target_dir).display(), - canonicalized_target_dir.components().last().map_or_else( - || target_dir.display().to_string(), - |dir| { PathBuf::from(dir.as_os_str()).display().to_string() } - ) - ) - )); - continue; - } + if let Err(e) = assert_not_same_file(sourcepath, target_dir, true, options) { + show!(e); + continue; } match rename(sourcepath, &targetpath, options, multi_progress.as_ref()) { diff --git a/src/uu/wc/src/wc.rs b/src/uu/wc/src/wc.rs index 1c2d99628f7..6fc1efa0a00 100644 --- a/src/uu/wc/src/wc.rs +++ b/src/uu/wc/src/wc.rs @@ -255,13 +255,17 @@ impl<'a> Input<'a> { } /// Converts input to title that appears in stats. - fn to_title(&self) -> Option> { + fn to_title(&self) -> Option> { match self { - Self::Path(path) => Some(match path.to_str() { - Some(s) if !s.contains('\n') => Cow::Borrowed(s), - _ => Cow::Owned(escape_name_wrapper(path.as_os_str())), - }), - Self::Stdin(StdinKind::Explicit) => Some(Cow::Borrowed(STDIN_REPR)), + Self::Path(path) => { + let path = path.as_os_str(); + if path.to_string_lossy().contains('\n') { + Some(Cow::Owned(quoting_style::escape_name(path, QS_ESCAPE))) + } else { + Some(Cow::Borrowed(path)) + } + } + Self::Stdin(StdinKind::Explicit) => Some(Cow::Borrowed(OsStr::new(STDIN_REPR))), Self::Stdin(StdinKind::Implicit) => None, } } @@ -852,14 +856,17 @@ fn wc(inputs: &Inputs, settings: &Settings) -> UResult<()> { let maybe_title = input.to_title(); let maybe_title_str = maybe_title.as_deref(); if let Err(err) = print_stats(settings, &word_count, maybe_title_str, number_width) { - let title = maybe_title_str.unwrap_or(""); - show!(err.map_err_context(|| format!("failed to print result for {title}"))); + let title = maybe_title_str.unwrap_or(OsStr::new("")); + show!(err.map_err_context(|| format!( + "failed to print result for {}", + title.to_string_lossy() + ))); } } } if settings.total_when.is_total_row_visible(num_inputs) { - let title = are_stats_visible.then_some("total"); + let title = are_stats_visible.then_some(OsStr::new("total")); if let Err(err) = print_stats(settings, &total_word_count, title, number_width) { show!(err.map_err_context(|| "failed to print total".into())); } @@ -873,7 +880,7 @@ fn wc(inputs: &Inputs, settings: &Settings) -> UResult<()> { fn print_stats( settings: &Settings, result: &WordCount, - title: Option<&str>, + title: Option<&OsStr>, number_width: usize, ) -> io::Result<()> { let mut stdout = io::stdout().lock(); @@ -893,8 +900,8 @@ fn print_stats( } if let Some(title) = title { - writeln!(stdout, "{space}{title}") - } else { - writeln!(stdout) + write!(stdout, "{space}")?; + stdout.write_all(&uucore::os_str_as_bytes_lossy(title))?; } + writeln!(stdout) } diff --git a/src/uucore/src/lib/features.rs b/src/uucore/src/lib/features.rs index dfe5b773312..cde1cf264a3 100644 --- a/src/uucore/src/lib/features.rs +++ b/src/uucore/src/lib/features.rs @@ -54,7 +54,7 @@ pub mod process; #[cfg(all(target_os = "linux", feature = "tty"))] pub mod tty; -#[cfg(all(unix, not(target_os = "macos"), feature = "fsxattr"))] +#[cfg(all(unix, feature = "fsxattr"))] pub mod fsxattr; #[cfg(all(unix, not(target_os = "fuchsia"), feature = "signals"))] pub mod signals; diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index 3200145bd73..3a6a537ad49 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -99,7 +99,7 @@ pub use crate::features::wide; #[cfg(feature = "fsext")] pub use crate::features::fsext; -#[cfg(all(unix, not(target_os = "macos"), feature = "fsxattr"))] +#[cfg(all(unix, feature = "fsxattr"))] pub use crate::features::fsxattr; //## core functions diff --git a/tests/by-util/test_csplit.rs b/tests/by-util/test_csplit.rs index 03b8c92fc09..2315715228d 100644 --- a/tests/by-util/test_csplit.rs +++ b/tests/by-util/test_csplit.rs @@ -130,17 +130,21 @@ fn test_up_to_match_sequence() { #[test] fn test_up_to_match_offset() { - let (at, mut ucmd) = at_and_ucmd!(); - ucmd.args(&["numbers50.txt", "/9$/+3"]) - .succeeds() - .stdout_only("24\n117\n"); + for offset in ["3", "+3"] { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", &format!("/9$/{offset}")]) + .succeeds() + .stdout_only("24\n117\n"); - let count = glob(&at.plus_as_string("xx*")) - .expect("there should be splits created") - .count(); - assert_eq!(count, 2); - assert_eq!(at.read("xx00"), generate(1, 12)); - assert_eq!(at.read("xx01"), generate(12, 51)); + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 2); + assert_eq!(at.read("xx00"), generate(1, 12)); + assert_eq!(at.read("xx01"), generate(12, 51)); + at.remove("xx00"); + at.remove("xx01"); + } } #[test] @@ -316,16 +320,19 @@ fn test_skip_to_match_sequence4() { #[test] fn test_skip_to_match_offset() { - let (at, mut ucmd) = at_and_ucmd!(); - ucmd.args(&["numbers50.txt", "%23%+3"]) - .succeeds() - .stdout_only("75\n"); + for offset in ["3", "+3"] { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["numbers50.txt", &format!("%23%{offset}")]) + .succeeds() + .stdout_only("75\n"); - let count = glob(&at.plus_as_string("xx*")) - .expect("there should be splits created") - .count(); - assert_eq!(count, 1); - assert_eq!(at.read("xx00"), generate(26, 51)); + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 1); + assert_eq!(at.read("xx00"), generate(26, 51)); + at.remove("xx00"); + } } #[test] @@ -387,18 +394,23 @@ fn test_option_keep() { #[test] fn test_option_quiet() { - let (at, mut ucmd) = at_and_ucmd!(); - ucmd.args(&["--quiet", "numbers50.txt", "13", "%25%", "/0$/"]) - .succeeds() - .no_stdout(); + for arg in ["-q", "--quiet", "-s", "--silent"] { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&[arg, "numbers50.txt", "13", "%25%", "/0$/"]) + .succeeds() + .no_stdout(); - let count = glob(&at.plus_as_string("xx*")) - .expect("there should be splits created") - .count(); - assert_eq!(count, 3); - assert_eq!(at.read("xx00"), generate(1, 13)); - assert_eq!(at.read("xx01"), generate(25, 30)); - assert_eq!(at.read("xx02"), generate(30, 51)); + let count = glob(&at.plus_as_string("xx*")) + .expect("there should be splits created") + .count(); + assert_eq!(count, 3); + assert_eq!(at.read("xx00"), generate(1, 13)); + assert_eq!(at.read("xx01"), generate(25, 30)); + assert_eq!(at.read("xx02"), generate(30, 51)); + at.remove("xx00"); + at.remove("xx01"); + at.remove("xx02"); + } } #[test] diff --git a/tests/by-util/test_cut.rs b/tests/by-util/test_cut.rs index 1aa3c126a23..dbd26abb287 100644 --- a/tests/by-util/test_cut.rs +++ b/tests/by-util/test_cut.rs @@ -288,11 +288,22 @@ fn test_empty_string_as_delimiter_with_output_delimiter() { #[test] fn test_newline_as_delimiter() { + for (field, expected_output) in [("1", "a:1\n"), ("2", "b:\n")] { + new_ucmd!() + .args(&["-f", field, "-d", "\n"]) + .pipe_in("a:1\nb:") + .succeeds() + .stdout_only_bytes(expected_output); + } +} + +#[test] +fn test_newline_as_delimiter_with_output_delimiter() { new_ucmd!() - .args(&["-f", "1", "-d", "\n"]) - .pipe_in("a:1\nb:") + .args(&["-f1-", "-d", "\n", "--output-delimiter=:"]) + .pipe_in("a\nb\n") .succeeds() - .stdout_only_bytes("a:1\nb:\n"); + .stdout_only_bytes("a:b\n"); } #[test] diff --git a/tests/by-util/test_echo.rs b/tests/by-util/test_echo.rs index 136500b4894..dd6b412a429 100644 --- a/tests/by-util/test_echo.rs +++ b/tests/by-util/test_echo.rs @@ -219,8 +219,7 @@ fn test_hyphen_values_at_start() { .arg("-test") .arg("araba") .arg("-merci") - .run() - .success() + .succeeds() .stdout_does_not_contain("-E") .stdout_is("-test araba -merci\n"); } @@ -231,8 +230,7 @@ fn test_hyphen_values_between() { .arg("test") .arg("-E") .arg("araba") - .run() - .success() + .succeeds() .stdout_is("test -E araba\n"); new_ucmd!() @@ -240,11 +238,20 @@ fn test_hyphen_values_between() { .arg("dum dum dum") .arg("-e") .arg("dum") - .run() - .success() + .succeeds() .stdout_is("dumdum dum dum dum -e dum\n"); } +#[test] +fn test_double_hyphens() { + new_ucmd!().arg("--").succeeds().stdout_only("--\n"); + new_ucmd!() + .arg("--") + .arg("--") + .succeeds() + .stdout_only("-- --\n"); +} + #[test] fn wrapping_octal() { // Some odd behavior of GNU. Values of \0400 and greater do not fit in the diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index 3b2d46b39c2..f65078a0d5a 100644 --- a/tests/by-util/test_ls.rs +++ b/tests/by-util/test_ls.rs @@ -3,6 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. // spell-checker:ignore (words) READMECAREFULLY birthtime doesntexist oneline somebackup lrwx somefile somegroup somehiddenbackup somehiddenfile tabsize aaaaaaaa bbbb cccc dddddddd ncccc neee naaaaa nbcdef nfffff dired subdired tmpfs mdir COLORTERM mexe bcdef mfoo +// spell-checker:ignore (words) fakeroot setcap #![allow( clippy::similar_names, clippy::too_many_lines, @@ -5516,3 +5517,49 @@ fn test_suffix_case_sensitivity() { /* cSpell:enable */ ); } + +#[cfg(all(unix, target_os = "linux"))] +#[test] +fn test_ls_capabilities() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + // Test must be run as root (or with `sudo -E`) + // fakeroot setcap cap_net_bind_service=ep /tmp/file_name + // doesn't trigger an error and fails silently + if scene.cmd("whoami").run().stdout_str() != "root\n" { + return; + } + at.mkdir("test"); + at.mkdir("test/dir"); + at.touch("test/cap_pos"); + at.touch("test/dir/cap_neg"); + at.touch("test/dir/cap_pos"); + + let files = ["test/cap_pos", "test/dir/cap_pos"]; + for file in &files { + scene + .cmd("sudo") + .args(&[ + "-E", + "--non-interactive", + "setcap", + "cap_net_bind_service=ep", + at.plus(file).to_str().unwrap(), + ]) + .succeeds(); + } + + let ls_colors = "di=:ca=30;41"; + + scene + .ucmd() + .env("LS_COLORS", ls_colors) + .arg("--color=always") + .arg("test/cap_pos") + .arg("test/dir") + .succeeds() + .stdout_contains("\x1b[30;41mtest/cap_pos") // spell-checker:disable-line + .stdout_contains("\x1b[30;41mcap_pos") // spell-checker:disable-line + .stdout_does_not_contain("0;41mtest/dir/cap_neg"); // spell-checker:disable-line +} diff --git a/tests/by-util/test_mv.rs b/tests/by-util/test_mv.rs index ac64fae7eb7..1419be4e940 100644 --- a/tests/by-util/test_mv.rs +++ b/tests/by-util/test_mv.rs @@ -6,6 +6,7 @@ // spell-checker:ignore mydir use crate::common::util::TestScenario; use filetime::FileTime; +use rstest::rstest; use std::io::Write; #[test] @@ -467,7 +468,31 @@ fn test_mv_same_symlink() { .arg(file_c) .arg(file_a) .fails() - .stderr_is(format!("mv: '{file_c}' and '{file_a}' are the same file\n",)); + .stderr_is(format!("mv: '{file_c}' and '{file_a}' are the same file\n")); +} + +#[test] +#[cfg(all(unix, not(target_os = "android")))] +fn test_mv_same_broken_symlink() { + let (at, mut ucmd) = at_and_ucmd!(); + + at.symlink_file("missing-target", "broken"); + + ucmd.arg("broken") + .arg("broken") + .fails() + .stderr_is("mv: 'broken' and 'broken' are the same file\n"); +} + +#[test] +#[cfg(all(unix, not(target_os = "android")))] +fn test_mv_symlink_into_target() { + let (at, mut ucmd) = at_and_ucmd!(); + + at.mkdir("dir"); + at.symlink_file("dir", "dir-link"); + + ucmd.arg("dir-link").arg("dir").succeeds(); } #[test] @@ -1389,24 +1414,6 @@ fn test_mv_interactive_error() { .is_empty()); } -#[test] -fn test_mv_into_self() { - let scene = TestScenario::new(util_name!()); - let at = &scene.fixtures; - let dir1 = "dir1"; - let dir2 = "dir2"; - at.mkdir(dir1); - at.mkdir(dir2); - - scene - .ucmd() - .arg(dir1) - .arg(dir2) - .arg(dir2) - .fails() - .stderr_contains("mv: cannot move 'dir2' to a subdirectory of itself, 'dir2/dir2'"); -} - #[test] fn test_mv_arg_interactive_skipped() { let (at, mut ucmd) = at_and_ucmd!(); @@ -1456,27 +1463,32 @@ fn test_mv_into_self_data() { assert!(!at.file_exists(file1)); } -#[test] -fn test_mv_directory_into_subdirectory_of_itself_fails() { +#[rstest] +#[case(vec!["mydir"], vec!["mydir", "mydir"], "mv: cannot move 'mydir' to a subdirectory of itself, 'mydir/mydir'")] +#[case(vec!["mydir"], vec!["mydir/", "mydir/"], "mv: cannot move 'mydir/' to a subdirectory of itself, 'mydir/mydir'")] +#[case(vec!["mydir"], vec!["./mydir", "mydir", "mydir/"], "mv: cannot move './mydir' to a subdirectory of itself, 'mydir/mydir'")] +#[case(vec!["mydir"], vec!["mydir/", "mydir/mydir_2/"], "mv: cannot move 'mydir/' to a subdirectory of itself, 'mydir/mydir_2/'")] +#[case(vec!["mydir/mydir_2"], vec!["mydir", "mydir/mydir_2"], "mv: cannot move 'mydir' to a subdirectory of itself, 'mydir/mydir_2/mydir'\n")] +#[case(vec!["mydir/mydir_2"], vec!["mydir/", "mydir/mydir_2/"], "mv: cannot move 'mydir/' to a subdirectory of itself, 'mydir/mydir_2/mydir'\n")] +#[case(vec!["mydir", "mydir_2"], vec!["mydir/", "mydir_2/", "mydir_2/"], "mv: cannot move 'mydir_2/' to a subdirectory of itself, 'mydir_2/mydir_2'")] +#[case(vec!["mydir"], vec!["mydir/", "mydir"], "mv: cannot move 'mydir/' to a subdirectory of itself, 'mydir/mydir'")] +#[case(vec!["mydir"], vec!["-T", "mydir", "mydir"], "mv: 'mydir' and 'mydir' are the same file")] +#[case(vec!["mydir"], vec!["mydir/", "mydir/../"], "mv: 'mydir/' and 'mydir/../mydir' are the same file")] +fn test_mv_directory_self( + #[case] dirs: Vec<&str>, + #[case] args: Vec<&str>, + #[case] expected_error: &str, +) { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; - let dir1 = "mydir"; - let dir2 = "mydir/mydir_2"; - at.mkdir(dir1); - at.mkdir(dir2); - scene.ucmd().arg(dir1).arg(dir2).fails().stderr_contains( - "mv: cannot move 'mydir' to a subdirectory of itself, 'mydir/mydir_2/mydir'", - ); - - // check that it also errors out with / + for dir in dirs { + at.mkdir_all(dir); + } scene .ucmd() - .arg(format!("{dir1}/")) - .arg(dir2) + .args(&args) .fails() - .stderr_contains( - "mv: cannot move 'mydir/' to a subdirectory of itself, 'mydir/mydir_2/mydir/'", - ); + .stderr_contains(expected_error); } #[test] @@ -1755,23 +1767,3 @@ fn test_mv_error_msg_with_multiple_sources_that_does_not_exist() { .stderr_contains("mv: cannot stat 'a': No such file or directory") .stderr_contains("mv: cannot stat 'b/': No such file or directory"); } - -#[test] -fn test_mv_error_cant_move_itself() { - let scene = TestScenario::new(util_name!()); - let at = &scene.fixtures; - at.mkdir("b"); - scene - .ucmd() - .arg("b") - .arg("b/") - .fails() - .stderr_contains("mv: cannot move 'b' to a subdirectory of itself, 'b/b'"); - scene - .ucmd() - .arg("./b") - .arg("b") - .arg("b/") - .fails() - .stderr_contains("mv: cannot move 'b' to a subdirectory of itself, 'b/b'"); -} diff --git a/tests/by-util/test_wc.rs b/tests/by-util/test_wc.rs index 0bdb5c843a1..e2af757b360 100644 --- a/tests/by-util/test_wc.rs +++ b/tests/by-util/test_wc.rs @@ -283,6 +283,32 @@ fn test_gnu_compatible_quotation() { .stdout_is("0 0 0 'some-dir1/12'$'\\n''34.txt'\n"); } +#[cfg(feature = "test_risky_names")] +#[test] +fn test_non_unicode_names() { + let scene = TestScenario::new(util_name!()); + let target1 = uucore::os_str_from_bytes(b"some-dir1/1\xC0\n.txt") + .expect("Only unix platforms can test non-unicode names"); + let target2 = uucore::os_str_from_bytes(b"some-dir1/2\xC0\t.txt") + .expect("Only unix platforms can test non-unicode names"); + let at = &scene.fixtures; + at.mkdir("some-dir1"); + at.touch(&target1); + at.touch(&target2); + scene + .ucmd() + .args(&[target1, target2]) + .run() + .stdout_is_bytes( + [ + b"0 0 0 'some-dir1/1'$'\\300\\n''.txt'\n".to_vec(), + b"0 0 0 some-dir1/2\xC0\t.txt\n".to_vec(), + b"0 0 0 total\n".to_vec(), + ] + .concat(), + ); +} + #[test] fn test_multiple_default() { new_ucmd!() diff --git a/util/gnu-patches/tests_comm.pl.patch b/util/gnu-patches/tests_comm.pl.patch new file mode 100644 index 00000000000..d3d5595a2c5 --- /dev/null +++ b/util/gnu-patches/tests_comm.pl.patch @@ -0,0 +1,44 @@ +diff --git a/tests/misc/comm.pl b/tests/misc/comm.pl +index 5bd5f56d7..8322d92ba 100755 +--- a/tests/misc/comm.pl ++++ b/tests/misc/comm.pl +@@ -73,18 +73,24 @@ my @Tests = + + # invalid missing command line argument (1) + ['missing-arg1', $inputs[0], {EXIT=>1}, +- {ERR => "$prog: missing operand after 'a'\n" +- . "Try '$prog --help' for more information.\n"}], ++ {ERR => "error: the following required arguments were not provided:\n" ++ . " \n\n" ++ . "Usage: $prog [OPTION]... FILE1 FILE2\n\n" ++ . "For more information, try '--help'.\n"}], + + # invalid missing command line argument (both) + ['missing-arg2', {EXIT=>1}, +- {ERR => "$prog: missing operand\n" +- . "Try '$prog --help' for more information.\n"}], ++ {ERR => "error: the following required arguments were not provided:\n" ++ . " \n" ++ . " \n\n" ++ . "Usage: $prog [OPTION]... FILE1 FILE2\n\n" ++ . "For more information, try '--help'.\n"}], + + # invalid extra command line argument + ['extra-arg', @inputs, 'no-such', {EXIT=>1}, +- {ERR => "$prog: extra operand 'no-such'\n" +- . "Try '$prog --help' for more information.\n"}], ++ {ERR => "error: unexpected argument 'no-such' found\n\n" ++ . "Usage: $prog [OPTION]... FILE1 FILE2\n\n" ++ . "For more information, try '--help'.\n"}], + + # out-of-order input + ['ooo', {IN=>{a=>"1\n3"}}, {IN=>{b=>"3\n2"}}, {EXIT=>1}, +@@ -163,7 +169,7 @@ my @Tests = + + # invalid dual delimiter + ['delim-dual', '--output-delimiter=,', '--output-delimiter=+', @inputs, +- {EXIT=>1}, {ERR => "$prog: multiple output delimiters specified\n"}], ++ {EXIT=>1}, {ERR => "$prog: multiple conflicting output delimiters specified\n"}], + + # valid dual delimiter specification + ['delim-dual2', '--output-delimiter=,', '--output-delimiter=,', @inputs, diff --git a/util/gnu-patches/tests_ls_no_cap.patch b/util/gnu-patches/tests_ls_no_cap.patch new file mode 100644 index 00000000000..5944e3f5661 --- /dev/null +++ b/util/gnu-patches/tests_ls_no_cap.patch @@ -0,0 +1,22 @@ +diff --git a/tests/ls/no-cap.sh b/tests/ls/no-cap.sh +index 3d84c74ff..d1f60e70a 100755 +--- a/tests/ls/no-cap.sh ++++ b/tests/ls/no-cap.sh +@@ -21,13 +21,13 @@ print_ver_ ls + require_strace_ capget + + LS_COLORS=ca=1; export LS_COLORS +-strace -e capget ls --color=always > /dev/null 2> out || fail=1 +-$EGREP 'capget\(' out || skip_ "your ls doesn't call capget" ++strace -e llistxattr ls --color=always > /dev/null 2> out || fail=1 ++$EGREP 'llistxattr\(' out || skip_ "your ls doesn't call llistxattr" + + rm -f out + + LS_COLORS=ca=:; export LS_COLORS +-strace -e capget ls --color=always > /dev/null 2> out || fail=1 +-$EGREP 'capget\(' out && fail=1 ++strace -e llistxattr ls --color=always > /dev/null 2> out || fail=1 ++$EGREP 'llistxattr\(' out && fail=1 + + Exit $fail