From 3262e28170ddd4e813b50a1fb745b686a4e69b14 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 18 May 2025 16:57:57 +0200 Subject: [PATCH 1/4] uufuzz: create a crate with the common functions --- fuzz/uufuzz/Cargo.toml | 15 +++++++++++++++ fuzz/uufuzz/src/lib.rs | 14 ++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 fuzz/uufuzz/Cargo.toml create mode 100644 fuzz/uufuzz/src/lib.rs diff --git a/fuzz/uufuzz/Cargo.toml b/fuzz/uufuzz/Cargo.toml new file mode 100644 index 00000000000..7f949b7a32d --- /dev/null +++ b/fuzz/uufuzz/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "uufuzz" +edition = "2024" +description = "uutils ~ 'core' uutils fuzzing library" +repository = "https://github.com/uutils/coreutils/tree/main/fuzz/uufuzz" +# readme = "README.md" +authors.workspace = true +categories.workspace = true +edition.workspace = true +homepage.workspace = true +keywords.workspace = true +license.workspace = true +version.workspace = true + +[dependencies] diff --git a/fuzz/uufuzz/src/lib.rs b/fuzz/uufuzz/src/lib.rs new file mode 100644 index 00000000000..b93cf3ffd9c --- /dev/null +++ b/fuzz/uufuzz/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} From 73f5c548a7e76a62f9e5f417eac384e0aa087862 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 18 May 2025 16:58:31 +0200 Subject: [PATCH 2/4] uufuzz: move the fuzz-common functions --- fuzz/uufuzz/src/lib.rs | 442 +++++++++++++++++++++++++++++++- fuzz/uufuzz/src/pretty_print.rs | 69 +++++ 2 files changed, 502 insertions(+), 9 deletions(-) create mode 100644 fuzz/uufuzz/src/pretty_print.rs diff --git a/fuzz/uufuzz/src/lib.rs b/fuzz/uufuzz/src/lib.rs index b93cf3ffd9c..e887bfc6755 100644 --- a/fuzz/uufuzz/src/lib.rs +++ b/fuzz/uufuzz/src/lib.rs @@ -1,14 +1,438 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right +// 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. + +use console::Style; +use libc::STDIN_FILENO; +use libc::{STDERR_FILENO, STDOUT_FILENO, close, dup, dup2, pipe}; +use pretty_print::{ + print_diff, print_end_with_status, print_or_empty, print_section, print_with_style, +}; +use rand::Rng; +use rand::prelude::IndexedRandom; +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}; +use std::sync::atomic::Ordering; +use std::sync::{Once, atomic::AtomicBool}; +use std::{io, thread}; + +pub mod pretty_print; + +/// Represents the result of running a command, including its standard output, +/// standard error, and exit code. +pub struct CommandResult { + /// The standard output (stdout) of the command as a string. + pub stdout: String, + + /// The standard error (stderr) of the command as a string. + pub stderr: String, + + /// The exit code of the command. + pub exit_code: i32, +} + +static CHECK_GNU: Once = Once::new(); +static IS_GNU: AtomicBool = AtomicBool::new(false); + +pub fn is_gnu_cmd(cmd_path: &str) -> Result<(), std::io::Error> { + CHECK_GNU.call_once(|| { + let version_output = Command::new(cmd_path).arg("--version").output().unwrap(); + + println!("version_output {version_output:#?}"); + + let version_str = String::from_utf8_lossy(&version_output.stdout).to_string(); + if version_str.contains("GNU coreutils") { + IS_GNU.store(true, Ordering::Relaxed); + } + }); + + if IS_GNU.load(Ordering::Relaxed) { + Ok(()) + } else { + panic!("Not the GNU implementation"); + } +} + +pub fn generate_and_run_uumain( + args: &[OsString], + uumain_function: F, + pipe_input: Option<&str>, +) -> CommandResult +where + F: FnOnce(std::vec::IntoIter) -> i32 + Send + 'static, +{ + // Duplicate the stdout and stderr file descriptors + let original_stdout_fd = unsafe { dup(STDOUT_FILENO) }; + let original_stderr_fd = unsafe { dup(STDERR_FILENO) }; + if original_stdout_fd == -1 || original_stderr_fd == -1 { + return CommandResult { + stdout: "".to_string(), + stderr: "Failed to duplicate STDOUT_FILENO or STDERR_FILENO".to_string(), + exit_code: -1, + }; + } + + println!("Running test {:?}", &args[0..]); + let mut pipe_stdout_fds = [-1; 2]; + let mut pipe_stderr_fds = [-1; 2]; + + // Create pipes for stdout and stderr + if unsafe { pipe(pipe_stdout_fds.as_mut_ptr()) } == -1 + || unsafe { pipe(pipe_stderr_fds.as_mut_ptr()) } == -1 + { + return CommandResult { + stdout: "".to_string(), + stderr: "Failed to create pipes".to_string(), + exit_code: -1, + }; + } + + // Redirect stdout and stderr to their respective pipes + if unsafe { dup2(pipe_stdout_fds[1], STDOUT_FILENO) } == -1 + || unsafe { dup2(pipe_stderr_fds[1], STDERR_FILENO) } == -1 + { + unsafe { + close(pipe_stdout_fds[0]); + close(pipe_stdout_fds[1]); + close(pipe_stderr_fds[0]); + close(pipe_stderr_fds[1]); + } + return CommandResult { + stdout: "".to_string(), + stderr: "Failed to redirect STDOUT_FILENO or STDERR_FILENO".to_string(), + exit_code: -1, + }; + } + + let original_stdin_fd = if let Some(input_str) = pipe_input { + // we have pipe input + let mut input_file = tempfile::tempfile().unwrap(); + write!(input_file, "{input_str}").unwrap(); + input_file.seek(SeekFrom::Start(0)).unwrap(); + + // Redirect stdin to read from the in-memory file + let original_stdin_fd = unsafe { dup(STDIN_FILENO) }; + if original_stdin_fd == -1 || unsafe { dup2(input_file.as_raw_fd(), STDIN_FILENO) } == -1 { + return CommandResult { + stdout: "".to_string(), + stderr: "Failed to set up stdin redirection".to_string(), + exit_code: -1, + }; + } + Some(original_stdin_fd) + } else { + None + }; + + let (uumain_exit_status, captured_stdout, captured_stderr) = thread::scope(|s| { + let out = s.spawn(|| read_from_fd(pipe_stdout_fds[0])); + let err = s.spawn(|| read_from_fd(pipe_stderr_fds[0])); + #[allow(clippy::unnecessary_to_owned)] + // TODO: clippy wants us to use args.iter().cloned() ? + let status = uumain_function(args.to_owned().into_iter()); + // Reset the exit code global variable in case we run another test after this one + // See https://github.com/uutils/coreutils/issues/5777 + uucore::error::set_exit_code(0); + io::stdout().flush().unwrap(); + io::stderr().flush().unwrap(); + unsafe { + close(pipe_stdout_fds[1]); + close(pipe_stderr_fds[1]); + close(STDOUT_FILENO); + close(STDERR_FILENO); + } + (status, out.join().unwrap(), err.join().unwrap()) + }); + + // Restore the original stdout and stderr + if unsafe { dup2(original_stdout_fd, STDOUT_FILENO) } == -1 + || unsafe { dup2(original_stderr_fd, STDERR_FILENO) } == -1 + { + return CommandResult { + stdout: "".to_string(), + stderr: "Failed to restore the original STDOUT_FILENO or STDERR_FILENO".to_string(), + exit_code: -1, + }; + } + unsafe { + close(original_stdout_fd); + close(original_stderr_fd); + } + + // Restore the original stdin if it was modified + if let Some(fd) = original_stdin_fd { + if unsafe { dup2(fd, STDIN_FILENO) } == -1 { + return CommandResult { + stdout: "".to_string(), + stderr: "Failed to restore the original STDIN".to_string(), + exit_code: -1, + }; + } + unsafe { close(fd) }; + } + + CommandResult { + stdout: captured_stdout, + stderr: captured_stderr + .split_once(':') + .map(|x| x.1) + .unwrap_or("") + .trim() + .to_string(), + exit_code: uumain_exit_status, + } +} + +fn read_from_fd(fd: RawFd) -> String { + let mut captured_output = Vec::new(); + let mut read_buffer = [0; 1024]; + loop { + let bytes_read = unsafe { + libc::read( + fd, + read_buffer.as_mut_ptr() as *mut libc::c_void, + read_buffer.len(), + ) + }; + + if bytes_read == -1 { + eprintln!("Failed to read from the pipe"); + break; + } + if bytes_read == 0 { + break; + } + captured_output.extend_from_slice(&read_buffer[..bytes_read as usize]); + } + + unsafe { libc::close(fd) }; + + String::from_utf8_lossy(&captured_output).into_owned() +} + +pub fn run_gnu_cmd( + cmd_path: &str, + args: &[OsString], + check_gnu: bool, + pipe_input: Option<&str>, +) -> Result { + if check_gnu { + match is_gnu_cmd(cmd_path) { + Ok(_) => {} // if the check passes, do nothing + Err(e) => { + // Convert the io::Error into the function's error type + return Err(CommandResult { + stdout: String::new(), + stderr: e.to_string(), + exit_code: -1, + }); + } + } + } + + let mut command = Command::new(cmd_path); + for arg in args { + command.arg(arg); + } + + // See https://github.com/uutils/coreutils/issues/6794 + // uutils' coreutils is not locale-aware, and aims to mirror/be compatible with GNU Core Utilities's LC_ALL=C behavior + command.env("LC_ALL", "C"); + + let output = if let Some(input_str) = pipe_input { + // We have an pipe input + command + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + + let mut child = command.spawn().expect("Failed to execute command"); + let child_stdin = child.stdin.as_mut().unwrap(); + child_stdin + .write_all(input_str.as_bytes()) + .expect("Failed to write to stdin"); + + match child.wait_with_output() { + Ok(output) => output, + Err(e) => { + return Err(CommandResult { + stdout: String::new(), + stderr: e.to_string(), + exit_code: -1, + }); + } + } + } else { + // Just run with args + match command.output() { + Ok(output) => output, + Err(e) => { + return Err(CommandResult { + stdout: String::new(), + stderr: e.to_string(), + exit_code: -1, + }); + } + } + }; + let exit_code = output.status.code().unwrap_or(-1); + // Here we get stdout and stderr as Strings + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + let stderr = stderr + .split_once(':') + .map(|x| x.1) + .unwrap_or("") + .trim() + .to_string(); + + if output.status.success() || !check_gnu { + Ok(CommandResult { + stdout, + stderr, + exit_code, + }) + } else { + Err(CommandResult { + stdout, + stderr, + exit_code, + }) + } } -#[cfg(test)] -mod tests { - use super::*; +/// Compare results from two different implementations of a command. +/// +/// # Arguments +/// * `test_type` - The command. +/// * `input` - The input provided to the command. +/// * `rust_result` - The result of running the command with the Rust implementation. +/// * `gnu_result` - The result of running the command with the GNU implementation. +/// * `fail_on_stderr_diff` - Whether to fail the test if there is a difference in stderr output. +pub fn compare_result( + test_type: &str, + input: &str, + pipe_input: Option<&str>, + rust_result: &CommandResult, + gnu_result: &CommandResult, + fail_on_stderr_diff: bool, +) { + print_section(format!("Compare result for: {test_type} {input}")); - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); + if let Some(pipe) = pipe_input { + println!("Pipe: {pipe}"); } + + let mut discrepancies = Vec::new(); + let mut should_panic = false; + + if rust_result.stdout.trim() != gnu_result.stdout.trim() { + discrepancies.push("stdout differs"); + println!("Rust stdout:"); + print_or_empty(rust_result.stdout.as_str()); + println!("GNU stdout:"); + print_or_empty(gnu_result.stdout.as_ref()); + print_diff(&rust_result.stdout, &gnu_result.stdout); + should_panic = true; + } + + if rust_result.stderr.trim() != gnu_result.stderr.trim() { + discrepancies.push("stderr differs"); + println!("Rust stderr:"); + print_or_empty(rust_result.stderr.as_str()); + println!("GNU stderr:"); + print_or_empty(gnu_result.stderr.as_str()); + print_diff(&rust_result.stderr, &gnu_result.stderr); + if fail_on_stderr_diff { + should_panic = true; + } + } + + if rust_result.exit_code != gnu_result.exit_code { + discrepancies.push("exit code differs"); + println!( + "Different exit code: (Rust: {}, GNU: {})", + rust_result.exit_code, gnu_result.exit_code + ); + should_panic = true; + } + + if discrepancies.is_empty() { + print_end_with_status("Same behavior", true); + } else { + print_with_style( + format!("Discrepancies detected: {}", discrepancies.join(", ")), + Style::new().red(), + ); + if should_panic { + print_end_with_status( + format!("Test failed and will panic for: {test_type} {input}"), + false, + ); + panic!("Test failed for: {test_type} {input}"); + } else { + print_end_with_status( + format!("Test completed with discrepancies for: {test_type} {input}"), + false, + ); + } + } + println!(); +} + +pub fn generate_random_string(max_length: usize) -> String { + let mut rng = rand::rng(); + let valid_utf8: Vec = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + .chars() + .collect(); + let invalid_utf8 = [0xC3, 0x28]; // Invalid UTF-8 sequence + let mut result = String::new(); + + for _ in 0..rng.random_range(0..=max_length) { + if rng.random_bool(0.9) { + let ch = valid_utf8.choose(&mut rng).unwrap(); + result.push(*ch); + } else { + let ch = invalid_utf8.choose(&mut rng).unwrap(); + if let Some(c) = char::from_u32(*ch as u32) { + result.push(c); + } + } + } + + result +} + +#[allow(dead_code)] +pub fn generate_random_file() -> Result { + let mut rng = rand::rng(); + let file_name: String = (0..10) + .map(|_| rng.random_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.random_range(10..1000); + let content: String = (0..content_length) + .map(|_| (rng.random_range(b' '..=b'~') as char)) + .collect(); + + file.write_all(content.as_bytes())?; + + Ok(file_path.to_str().unwrap().to_string()) +} + +#[allow(dead_code)] +pub fn replace_fuzz_binary_name(cmd: &str, result: &mut CommandResult) { + let fuzz_bin_name = format!("fuzz/target/x86_64-unknown-linux-gnu/release/fuzz_{cmd}"); + + result.stdout = result.stdout.replace(&fuzz_bin_name, cmd); + result.stderr = result.stderr.replace(&fuzz_bin_name, cmd); } diff --git a/fuzz/uufuzz/src/pretty_print.rs b/fuzz/uufuzz/src/pretty_print.rs new file mode 100644 index 00000000000..ecdfccfd035 --- /dev/null +++ b/fuzz/uufuzz/src/pretty_print.rs @@ -0,0 +1,69 @@ +// 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. + +use std::fmt; + +use console::{Style, style}; +use similar::TextDiff; + +pub fn print_section(s: S) { + println!("{}", style(format!("=== {s}")).bold()); +} + +pub fn print_subsection(s: S) { + println!("{}", style(format!("--- {s}")).bright()); +} + +#[allow(dead_code)] +pub fn print_test_begin(msg: S) { + println!( + "{} {} {}", + style("===").bold(), // Kind of gray + style("TEST").black().on_yellow().bold(), + style(msg).bold() + ); +} + +pub fn print_end_with_status(msg: S, ok: bool) { + let ok = if ok { + style(" OK ").black().on_green().bold() + } else { + style(" KO ").black().on_red().bold() + }; + + println!( + "{} {ok} {}", + style("===").bold(), // Kind of gray + style(msg).bold() + ); +} + +pub fn print_or_empty(s: &str) { + let to_print = if s.is_empty() { "(empty)" } else { s }; + + println!("{}", style(to_print).dim()); +} + +pub fn print_with_style(msg: S, style: Style) { + println!("{}", style.apply_to(msg)); +} + +pub fn print_diff(got: &str, expected: &str) { + let diff = TextDiff::from_lines(got, expected); + + print_subsection("START diff"); + + for change in diff.iter_all_changes() { + let (sign, style) = match change.tag() { + similar::ChangeTag::Equal => (" ", Style::new().dim()), + similar::ChangeTag::Delete => ("-", Style::new().red()), + similar::ChangeTag::Insert => ("+", Style::new().green()), + }; + print!("{}{}", style.apply_to(sign).bold(), style.apply_to(change)); + } + + print_subsection("END diff"); + println!(); +} From 5ab109f5293604f6ea9a0ed69e164fa00bbf7547 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 18 May 2025 17:33:59 +0200 Subject: [PATCH 3/4] uufuzz: polish the crate --- .../cspell.dictionaries/workspace.wordlist.txt | 1 + fuzz/uufuzz/Cargo.toml | 16 +++++++++------- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.vscode/cspell.dictionaries/workspace.wordlist.txt b/.vscode/cspell.dictionaries/workspace.wordlist.txt index 3757980d334..d917d30a4de 100644 --- a/.vscode/cspell.dictionaries/workspace.wordlist.txt +++ b/.vscode/cspell.dictionaries/workspace.wordlist.txt @@ -331,6 +331,7 @@ utmpx uucore uucore_procs uudoc +uufuzz uumain uutil uutests diff --git a/fuzz/uufuzz/Cargo.toml b/fuzz/uufuzz/Cargo.toml index 7f949b7a32d..20c3e8847d2 100644 --- a/fuzz/uufuzz/Cargo.toml +++ b/fuzz/uufuzz/Cargo.toml @@ -1,15 +1,17 @@ [package] name = "uufuzz" -edition = "2024" +authors = ["uutils developers"] description = "uutils ~ 'core' uutils fuzzing library" repository = "https://github.com/uutils/coreutils/tree/main/fuzz/uufuzz" -# readme = "README.md" -authors.workspace = true -categories.workspace = true +version = "0.0.30" edition.workspace = true -homepage.workspace = true -keywords.workspace = true license.workspace = true -version.workspace = true + [dependencies] +console = "0.15.0" +libc = "0.2.153" +rand = { version = "0.9.0", features = ["small_rng"] } +similar = "2.5.0" +uucore = { path = "../../src/uucore/", features = ["parser"] } +tempfile = "3.15.0" From 15c4e2c7982c4d79c190a390b6009844db21b909 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 18 May 2025 17:34:18 +0200 Subject: [PATCH 4/4] adjust the fuzzer to use uufuzz --- fuzz/Cargo.lock | 17 +- fuzz/Cargo.toml | 12 +- fuzz/fuzz_targets/fuzz_cksum.rs | 13 +- fuzz/fuzz_targets/fuzz_common/mod.rs | 438 ------------------ fuzz/fuzz_targets/fuzz_common/pretty_print.rs | 69 --- fuzz/fuzz_targets/fuzz_cut.rs | 3 +- fuzz/fuzz_targets/fuzz_echo.rs | 7 +- fuzz/fuzz_targets/fuzz_env.rs | 5 +- fuzz/fuzz_targets/fuzz_expr.rs | 7 +- fuzz/fuzz_targets/fuzz_printf.rs | 7 +- fuzz/fuzz_targets/fuzz_seq.rs | 7 +- fuzz/fuzz_targets/fuzz_sort.rs | 7 +- fuzz/fuzz_targets/fuzz_split.rs | 3 +- fuzz/fuzz_targets/fuzz_test.rs | 12 +- fuzz/fuzz_targets/fuzz_tr.rs | 3 +- fuzz/fuzz_targets/fuzz_wc.rs | 3 +- 16 files changed, 45 insertions(+), 568 deletions(-) delete mode 100644 fuzz/fuzz_targets/fuzz_common/mod.rs delete mode 100644 fuzz/fuzz_targets/fuzz_common/pretty_print.rs diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 37b66a3fa31..d7d066515c3 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -1347,12 +1347,8 @@ dependencies = [ name = "uucore-fuzz" version = "0.0.0" dependencies = [ - "console", - "libc", "libfuzzer-sys", "rand 0.9.1", - "similar", - "tempfile", "uu_cksum", "uu_cut", "uu_date", @@ -1367,6 +1363,7 @@ dependencies = [ "uu_tr", "uu_wc", "uucore", + "uufuzz", ] [[package]] @@ -1378,6 +1375,18 @@ dependencies = [ "uuhelp_parser", ] +[[package]] +name = "uufuzz" +version = "0.0.30" +dependencies = [ + "console", + "libc", + "rand 0.9.1", + "similar", + "tempfile", + "uucore", +] + [[package]] name = "uuhelp_parser" version = "0.0.30" diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 255d11d5b72..48da8e846b4 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -1,20 +1,22 @@ [package] name = "uucore-fuzz" version = "0.0.0" +description = "uutils ~ 'core' uutils fuzzers" +repository = "https://github.com/uutils/coreutils/tree/main/fuzz/" +edition.workspace = true publish = false + +[workspace.package] edition = "2024" +license = "MIT" [package.metadata] cargo-fuzz = true [dependencies] -console = "0.15.0" libfuzzer-sys = "0.4.7" -libc = "0.2.153" -tempfile = "3.15.0" rand = { version = "0.9.0", features = ["small_rng"] } -similar = "2.5.0" - +uufuzz = { path = "uufuzz/" } uucore = { path = "../src/uucore/", features = ["parser"] } uu_date = { path = "../src/uu/date/" } uu_test = { path = "../src/uu/test/" } diff --git a/fuzz/fuzz_targets/fuzz_cksum.rs b/fuzz/fuzz_targets/fuzz_cksum.rs index 3b5ddb8bb18..be93a96050e 100644 --- a/fuzz/fuzz_targets/fuzz_cksum.rs +++ b/fuzz/fuzz_targets/fuzz_cksum.rs @@ -6,20 +6,19 @@ #![no_main] use libfuzzer_sys::fuzz_target; +use rand::Rng; +use std::env::temp_dir; use std::ffi::OsString; +use std::fs::{self, File}; +use std::io::Write; +use std::process::Command; use uu_cksum::uumain; -mod fuzz_common; -use crate::fuzz_common::{ +use uufuzz::{ CommandResult, compare_result, generate_and_run_uumain, generate_random_file, generate_random_string, pretty_print::{print_or_empty, print_test_begin}, replace_fuzz_binary_name, run_gnu_cmd, }; -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"; diff --git a/fuzz/fuzz_targets/fuzz_common/mod.rs b/fuzz/fuzz_targets/fuzz_common/mod.rs deleted file mode 100644 index e887bfc6755..00000000000 --- a/fuzz/fuzz_targets/fuzz_common/mod.rs +++ /dev/null @@ -1,438 +0,0 @@ -// 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. - -use console::Style; -use libc::STDIN_FILENO; -use libc::{STDERR_FILENO, STDOUT_FILENO, close, dup, dup2, pipe}; -use pretty_print::{ - print_diff, print_end_with_status, print_or_empty, print_section, print_with_style, -}; -use rand::Rng; -use rand::prelude::IndexedRandom; -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}; -use std::sync::atomic::Ordering; -use std::sync::{Once, atomic::AtomicBool}; -use std::{io, thread}; - -pub mod pretty_print; - -/// Represents the result of running a command, including its standard output, -/// standard error, and exit code. -pub struct CommandResult { - /// The standard output (stdout) of the command as a string. - pub stdout: String, - - /// The standard error (stderr) of the command as a string. - pub stderr: String, - - /// The exit code of the command. - pub exit_code: i32, -} - -static CHECK_GNU: Once = Once::new(); -static IS_GNU: AtomicBool = AtomicBool::new(false); - -pub fn is_gnu_cmd(cmd_path: &str) -> Result<(), std::io::Error> { - CHECK_GNU.call_once(|| { - let version_output = Command::new(cmd_path).arg("--version").output().unwrap(); - - println!("version_output {version_output:#?}"); - - let version_str = String::from_utf8_lossy(&version_output.stdout).to_string(); - if version_str.contains("GNU coreutils") { - IS_GNU.store(true, Ordering::Relaxed); - } - }); - - if IS_GNU.load(Ordering::Relaxed) { - Ok(()) - } else { - panic!("Not the GNU implementation"); - } -} - -pub fn generate_and_run_uumain( - args: &[OsString], - uumain_function: F, - pipe_input: Option<&str>, -) -> CommandResult -where - F: FnOnce(std::vec::IntoIter) -> i32 + Send + 'static, -{ - // Duplicate the stdout and stderr file descriptors - let original_stdout_fd = unsafe { dup(STDOUT_FILENO) }; - let original_stderr_fd = unsafe { dup(STDERR_FILENO) }; - if original_stdout_fd == -1 || original_stderr_fd == -1 { - return CommandResult { - stdout: "".to_string(), - stderr: "Failed to duplicate STDOUT_FILENO or STDERR_FILENO".to_string(), - exit_code: -1, - }; - } - - println!("Running test {:?}", &args[0..]); - let mut pipe_stdout_fds = [-1; 2]; - let mut pipe_stderr_fds = [-1; 2]; - - // Create pipes for stdout and stderr - if unsafe { pipe(pipe_stdout_fds.as_mut_ptr()) } == -1 - || unsafe { pipe(pipe_stderr_fds.as_mut_ptr()) } == -1 - { - return CommandResult { - stdout: "".to_string(), - stderr: "Failed to create pipes".to_string(), - exit_code: -1, - }; - } - - // Redirect stdout and stderr to their respective pipes - if unsafe { dup2(pipe_stdout_fds[1], STDOUT_FILENO) } == -1 - || unsafe { dup2(pipe_stderr_fds[1], STDERR_FILENO) } == -1 - { - unsafe { - close(pipe_stdout_fds[0]); - close(pipe_stdout_fds[1]); - close(pipe_stderr_fds[0]); - close(pipe_stderr_fds[1]); - } - return CommandResult { - stdout: "".to_string(), - stderr: "Failed to redirect STDOUT_FILENO or STDERR_FILENO".to_string(), - exit_code: -1, - }; - } - - let original_stdin_fd = if let Some(input_str) = pipe_input { - // we have pipe input - let mut input_file = tempfile::tempfile().unwrap(); - write!(input_file, "{input_str}").unwrap(); - input_file.seek(SeekFrom::Start(0)).unwrap(); - - // Redirect stdin to read from the in-memory file - let original_stdin_fd = unsafe { dup(STDIN_FILENO) }; - if original_stdin_fd == -1 || unsafe { dup2(input_file.as_raw_fd(), STDIN_FILENO) } == -1 { - return CommandResult { - stdout: "".to_string(), - stderr: "Failed to set up stdin redirection".to_string(), - exit_code: -1, - }; - } - Some(original_stdin_fd) - } else { - None - }; - - let (uumain_exit_status, captured_stdout, captured_stderr) = thread::scope(|s| { - let out = s.spawn(|| read_from_fd(pipe_stdout_fds[0])); - let err = s.spawn(|| read_from_fd(pipe_stderr_fds[0])); - #[allow(clippy::unnecessary_to_owned)] - // TODO: clippy wants us to use args.iter().cloned() ? - let status = uumain_function(args.to_owned().into_iter()); - // Reset the exit code global variable in case we run another test after this one - // See https://github.com/uutils/coreutils/issues/5777 - uucore::error::set_exit_code(0); - io::stdout().flush().unwrap(); - io::stderr().flush().unwrap(); - unsafe { - close(pipe_stdout_fds[1]); - close(pipe_stderr_fds[1]); - close(STDOUT_FILENO); - close(STDERR_FILENO); - } - (status, out.join().unwrap(), err.join().unwrap()) - }); - - // Restore the original stdout and stderr - if unsafe { dup2(original_stdout_fd, STDOUT_FILENO) } == -1 - || unsafe { dup2(original_stderr_fd, STDERR_FILENO) } == -1 - { - return CommandResult { - stdout: "".to_string(), - stderr: "Failed to restore the original STDOUT_FILENO or STDERR_FILENO".to_string(), - exit_code: -1, - }; - } - unsafe { - close(original_stdout_fd); - close(original_stderr_fd); - } - - // Restore the original stdin if it was modified - if let Some(fd) = original_stdin_fd { - if unsafe { dup2(fd, STDIN_FILENO) } == -1 { - return CommandResult { - stdout: "".to_string(), - stderr: "Failed to restore the original STDIN".to_string(), - exit_code: -1, - }; - } - unsafe { close(fd) }; - } - - CommandResult { - stdout: captured_stdout, - stderr: captured_stderr - .split_once(':') - .map(|x| x.1) - .unwrap_or("") - .trim() - .to_string(), - exit_code: uumain_exit_status, - } -} - -fn read_from_fd(fd: RawFd) -> String { - let mut captured_output = Vec::new(); - let mut read_buffer = [0; 1024]; - loop { - let bytes_read = unsafe { - libc::read( - fd, - read_buffer.as_mut_ptr() as *mut libc::c_void, - read_buffer.len(), - ) - }; - - if bytes_read == -1 { - eprintln!("Failed to read from the pipe"); - break; - } - if bytes_read == 0 { - break; - } - captured_output.extend_from_slice(&read_buffer[..bytes_read as usize]); - } - - unsafe { libc::close(fd) }; - - String::from_utf8_lossy(&captured_output).into_owned() -} - -pub fn run_gnu_cmd( - cmd_path: &str, - args: &[OsString], - check_gnu: bool, - pipe_input: Option<&str>, -) -> Result { - if check_gnu { - match is_gnu_cmd(cmd_path) { - Ok(_) => {} // if the check passes, do nothing - Err(e) => { - // Convert the io::Error into the function's error type - return Err(CommandResult { - stdout: String::new(), - stderr: e.to_string(), - exit_code: -1, - }); - } - } - } - - let mut command = Command::new(cmd_path); - for arg in args { - command.arg(arg); - } - - // See https://github.com/uutils/coreutils/issues/6794 - // uutils' coreutils is not locale-aware, and aims to mirror/be compatible with GNU Core Utilities's LC_ALL=C behavior - command.env("LC_ALL", "C"); - - let output = if let Some(input_str) = pipe_input { - // We have an pipe input - command - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()); - - let mut child = command.spawn().expect("Failed to execute command"); - let child_stdin = child.stdin.as_mut().unwrap(); - child_stdin - .write_all(input_str.as_bytes()) - .expect("Failed to write to stdin"); - - match child.wait_with_output() { - Ok(output) => output, - Err(e) => { - return Err(CommandResult { - stdout: String::new(), - stderr: e.to_string(), - exit_code: -1, - }); - } - } - } else { - // Just run with args - match command.output() { - Ok(output) => output, - Err(e) => { - return Err(CommandResult { - stdout: String::new(), - stderr: e.to_string(), - exit_code: -1, - }); - } - } - }; - let exit_code = output.status.code().unwrap_or(-1); - // Here we get stdout and stderr as Strings - let stdout = String::from_utf8_lossy(&output.stdout).to_string(); - let stderr = String::from_utf8_lossy(&output.stderr).to_string(); - let stderr = stderr - .split_once(':') - .map(|x| x.1) - .unwrap_or("") - .trim() - .to_string(); - - if output.status.success() || !check_gnu { - Ok(CommandResult { - stdout, - stderr, - exit_code, - }) - } else { - Err(CommandResult { - stdout, - stderr, - exit_code, - }) - } -} - -/// Compare results from two different implementations of a command. -/// -/// # Arguments -/// * `test_type` - The command. -/// * `input` - The input provided to the command. -/// * `rust_result` - The result of running the command with the Rust implementation. -/// * `gnu_result` - The result of running the command with the GNU implementation. -/// * `fail_on_stderr_diff` - Whether to fail the test if there is a difference in stderr output. -pub fn compare_result( - test_type: &str, - input: &str, - pipe_input: Option<&str>, - rust_result: &CommandResult, - gnu_result: &CommandResult, - fail_on_stderr_diff: bool, -) { - print_section(format!("Compare result for: {test_type} {input}")); - - if let Some(pipe) = pipe_input { - println!("Pipe: {pipe}"); - } - - let mut discrepancies = Vec::new(); - let mut should_panic = false; - - if rust_result.stdout.trim() != gnu_result.stdout.trim() { - discrepancies.push("stdout differs"); - println!("Rust stdout:"); - print_or_empty(rust_result.stdout.as_str()); - println!("GNU stdout:"); - print_or_empty(gnu_result.stdout.as_ref()); - print_diff(&rust_result.stdout, &gnu_result.stdout); - should_panic = true; - } - - if rust_result.stderr.trim() != gnu_result.stderr.trim() { - discrepancies.push("stderr differs"); - println!("Rust stderr:"); - print_or_empty(rust_result.stderr.as_str()); - println!("GNU stderr:"); - print_or_empty(gnu_result.stderr.as_str()); - print_diff(&rust_result.stderr, &gnu_result.stderr); - if fail_on_stderr_diff { - should_panic = true; - } - } - - if rust_result.exit_code != gnu_result.exit_code { - discrepancies.push("exit code differs"); - println!( - "Different exit code: (Rust: {}, GNU: {})", - rust_result.exit_code, gnu_result.exit_code - ); - should_panic = true; - } - - if discrepancies.is_empty() { - print_end_with_status("Same behavior", true); - } else { - print_with_style( - format!("Discrepancies detected: {}", discrepancies.join(", ")), - Style::new().red(), - ); - if should_panic { - print_end_with_status( - format!("Test failed and will panic for: {test_type} {input}"), - false, - ); - panic!("Test failed for: {test_type} {input}"); - } else { - print_end_with_status( - format!("Test completed with discrepancies for: {test_type} {input}"), - false, - ); - } - } - println!(); -} - -pub fn generate_random_string(max_length: usize) -> String { - let mut rng = rand::rng(); - let valid_utf8: Vec = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" - .chars() - .collect(); - let invalid_utf8 = [0xC3, 0x28]; // Invalid UTF-8 sequence - let mut result = String::new(); - - for _ in 0..rng.random_range(0..=max_length) { - if rng.random_bool(0.9) { - let ch = valid_utf8.choose(&mut rng).unwrap(); - result.push(*ch); - } else { - let ch = invalid_utf8.choose(&mut rng).unwrap(); - if let Some(c) = char::from_u32(*ch as u32) { - result.push(c); - } - } - } - - result -} - -#[allow(dead_code)] -pub fn generate_random_file() -> Result { - let mut rng = rand::rng(); - let file_name: String = (0..10) - .map(|_| rng.random_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.random_range(10..1000); - let content: String = (0..content_length) - .map(|_| (rng.random_range(b' '..=b'~') as char)) - .collect(); - - file.write_all(content.as_bytes())?; - - Ok(file_path.to_str().unwrap().to_string()) -} - -#[allow(dead_code)] -pub fn replace_fuzz_binary_name(cmd: &str, result: &mut CommandResult) { - let fuzz_bin_name = format!("fuzz/target/x86_64-unknown-linux-gnu/release/fuzz_{cmd}"); - - result.stdout = result.stdout.replace(&fuzz_bin_name, cmd); - result.stderr = result.stderr.replace(&fuzz_bin_name, cmd); -} diff --git a/fuzz/fuzz_targets/fuzz_common/pretty_print.rs b/fuzz/fuzz_targets/fuzz_common/pretty_print.rs deleted file mode 100644 index ecdfccfd035..00000000000 --- a/fuzz/fuzz_targets/fuzz_common/pretty_print.rs +++ /dev/null @@ -1,69 +0,0 @@ -// 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. - -use std::fmt; - -use console::{Style, style}; -use similar::TextDiff; - -pub fn print_section(s: S) { - println!("{}", style(format!("=== {s}")).bold()); -} - -pub fn print_subsection(s: S) { - println!("{}", style(format!("--- {s}")).bright()); -} - -#[allow(dead_code)] -pub fn print_test_begin(msg: S) { - println!( - "{} {} {}", - style("===").bold(), // Kind of gray - style("TEST").black().on_yellow().bold(), - style(msg).bold() - ); -} - -pub fn print_end_with_status(msg: S, ok: bool) { - let ok = if ok { - style(" OK ").black().on_green().bold() - } else { - style(" KO ").black().on_red().bold() - }; - - println!( - "{} {ok} {}", - style("===").bold(), // Kind of gray - style(msg).bold() - ); -} - -pub fn print_or_empty(s: &str) { - let to_print = if s.is_empty() { "(empty)" } else { s }; - - println!("{}", style(to_print).dim()); -} - -pub fn print_with_style(msg: S, style: Style) { - println!("{}", style.apply_to(msg)); -} - -pub fn print_diff(got: &str, expected: &str) { - let diff = TextDiff::from_lines(got, expected); - - print_subsection("START diff"); - - for change in diff.iter_all_changes() { - let (sign, style) = match change.tag() { - similar::ChangeTag::Equal => (" ", Style::new().dim()), - similar::ChangeTag::Delete => ("-", Style::new().red()), - similar::ChangeTag::Insert => ("+", Style::new().green()), - }; - print!("{}{}", style.apply_to(sign).bold(), style.apply_to(change)); - } - - print_subsection("END diff"); - println!(); -} diff --git a/fuzz/fuzz_targets/fuzz_cut.rs b/fuzz/fuzz_targets/fuzz_cut.rs index 828a7c6190b..4a5215f8aec 100644 --- a/fuzz/fuzz_targets/fuzz_cut.rs +++ b/fuzz/fuzz_targets/fuzz_cut.rs @@ -11,8 +11,7 @@ use uu_cut::uumain; use rand::Rng; use std::ffi::OsString; -mod fuzz_common; -use crate::fuzz_common::{ +use uufuzz::{ CommandResult, compare_result, generate_and_run_uumain, generate_random_string, run_gnu_cmd, }; static CMD_PATH: &str = "cut"; diff --git a/fuzz/fuzz_targets/fuzz_echo.rs b/fuzz/fuzz_targets/fuzz_echo.rs index a36a7ebadfc..e6b0ba9a6aa 100644 --- a/fuzz/fuzz_targets/fuzz_echo.rs +++ b/fuzz/fuzz_targets/fuzz_echo.rs @@ -6,11 +6,8 @@ use rand::Rng; use rand::prelude::IndexedRandom; use std::ffi::OsString; -mod fuzz_common; -use crate::fuzz_common::CommandResult; -use crate::fuzz_common::{ - compare_result, generate_and_run_uumain, generate_random_string, run_gnu_cmd, -}; +use uufuzz::CommandResult; +use uufuzz::{compare_result, generate_and_run_uumain, generate_random_string, run_gnu_cmd}; static CMD_PATH: &str = "echo"; diff --git a/fuzz/fuzz_targets/fuzz_env.rs b/fuzz/fuzz_targets/fuzz_env.rs index f38dced076e..284089f8378 100644 --- a/fuzz/fuzz_targets/fuzz_env.rs +++ b/fuzz/fuzz_targets/fuzz_env.rs @@ -10,11 +10,10 @@ use uu_env::uumain; use std::ffi::OsString; -mod fuzz_common; -use crate::fuzz_common::{ +use rand::Rng; +use uufuzz::{ CommandResult, compare_result, generate_and_run_uumain, generate_random_string, run_gnu_cmd, }; -use rand::Rng; static CMD_PATH: &str = "env"; diff --git a/fuzz/fuzz_targets/fuzz_expr.rs b/fuzz/fuzz_targets/fuzz_expr.rs index a2c232ab333..77ecffabc1b 100644 --- a/fuzz/fuzz_targets/fuzz_expr.rs +++ b/fuzz/fuzz_targets/fuzz_expr.rs @@ -12,11 +12,8 @@ use rand::Rng; use rand::prelude::IndexedRandom; use std::{env, ffi::OsString}; -mod fuzz_common; -use crate::fuzz_common::CommandResult; -use crate::fuzz_common::{ - compare_result, generate_and_run_uumain, generate_random_string, run_gnu_cmd, -}; +use uufuzz::CommandResult; +use uufuzz::{compare_result, generate_and_run_uumain, generate_random_string, run_gnu_cmd}; static CMD_PATH: &str = "expr"; fn generate_expr(max_depth: u32) -> String { diff --git a/fuzz/fuzz_targets/fuzz_printf.rs b/fuzz/fuzz_targets/fuzz_printf.rs index e8d74e2bedd..885ebb815bf 100644 --- a/fuzz/fuzz_targets/fuzz_printf.rs +++ b/fuzz/fuzz_targets/fuzz_printf.rs @@ -13,11 +13,8 @@ use rand::seq::IndexedRandom; use std::env; use std::ffi::OsString; -mod fuzz_common; -use crate::fuzz_common::CommandResult; -use crate::fuzz_common::{ - compare_result, generate_and_run_uumain, generate_random_string, run_gnu_cmd, -}; +use uufuzz::CommandResult; +use uufuzz::{compare_result, generate_and_run_uumain, generate_random_string, run_gnu_cmd}; static CMD_PATH: &str = "printf"; diff --git a/fuzz/fuzz_targets/fuzz_seq.rs b/fuzz/fuzz_targets/fuzz_seq.rs index d36f0720a65..35721865e8c 100644 --- a/fuzz/fuzz_targets/fuzz_seq.rs +++ b/fuzz/fuzz_targets/fuzz_seq.rs @@ -11,11 +11,8 @@ use uu_seq::uumain; use rand::Rng; use std::ffi::OsString; -mod fuzz_common; -use crate::fuzz_common::CommandResult; -use crate::fuzz_common::{ - compare_result, generate_and_run_uumain, generate_random_string, run_gnu_cmd, -}; +use uufuzz::CommandResult; +use uufuzz::{compare_result, generate_and_run_uumain, generate_random_string, run_gnu_cmd}; static CMD_PATH: &str = "seq"; fn generate_seq() -> String { diff --git a/fuzz/fuzz_targets/fuzz_sort.rs b/fuzz/fuzz_targets/fuzz_sort.rs index e94938c3903..8b38f39ec1b 100644 --- a/fuzz/fuzz_targets/fuzz_sort.rs +++ b/fuzz/fuzz_targets/fuzz_sort.rs @@ -12,11 +12,8 @@ use rand::Rng; use std::env; use std::ffi::OsString; -mod fuzz_common; -use crate::fuzz_common::CommandResult; -use crate::fuzz_common::{ - compare_result, generate_and_run_uumain, generate_random_string, run_gnu_cmd, -}; +use uufuzz::CommandResult; +use uufuzz::{compare_result, generate_and_run_uumain, generate_random_string, run_gnu_cmd}; static CMD_PATH: &str = "sort"; fn generate_sort_args() -> String { diff --git a/fuzz/fuzz_targets/fuzz_split.rs b/fuzz/fuzz_targets/fuzz_split.rs index 9a925b222ad..70860ece731 100644 --- a/fuzz/fuzz_targets/fuzz_split.rs +++ b/fuzz/fuzz_targets/fuzz_split.rs @@ -11,8 +11,7 @@ use uu_split::uumain; use rand::Rng; use std::ffi::OsString; -mod fuzz_common; -use crate::fuzz_common::{ +use uufuzz::{ CommandResult, compare_result, generate_and_run_uumain, generate_random_string, run_gnu_cmd, }; static CMD_PATH: &str = "split"; diff --git a/fuzz/fuzz_targets/fuzz_test.rs b/fuzz/fuzz_targets/fuzz_test.rs index 39926b26f76..894a1dcd56b 100644 --- a/fuzz/fuzz_targets/fuzz_test.rs +++ b/fuzz/fuzz_targets/fuzz_test.rs @@ -12,11 +12,8 @@ use rand::Rng; use rand::prelude::IndexedRandom; use std::ffi::OsString; -mod fuzz_common; -use crate::fuzz_common::CommandResult; -use crate::fuzz_common::{ - compare_result, generate_and_run_uumain, generate_random_string, run_gnu_cmd, -}; +use uufuzz::CommandResult; +use uufuzz::{compare_result, generate_and_run_uumain, generate_random_string, run_gnu_cmd}; #[allow(clippy::upper_case_acronyms)] #[derive(PartialEq, Debug, Clone)] @@ -146,10 +143,7 @@ fn generate_test_arg() -> String { let random_str = generate_random_string(rng.random_range(1..=10)); let random_str2 = generate_random_string(rng.random_range(1..=10)); - arg.push_str(&format!( - "{random_str} {} {random_str2}", - test_arg.arg, - )); + arg.push_str(&format!("{random_str} {} {random_str2}", test_arg.arg,)); } else if test_arg.arg_type == ArgType::STRING { let random_str = generate_random_string(rng.random_range(1..=10)); arg.push_str(&format!("{} {random_str}", test_arg.arg)); diff --git a/fuzz/fuzz_targets/fuzz_tr.rs b/fuzz/fuzz_targets/fuzz_tr.rs index d260e378088..5055ec0d748 100644 --- a/fuzz/fuzz_targets/fuzz_tr.rs +++ b/fuzz/fuzz_targets/fuzz_tr.rs @@ -10,8 +10,7 @@ use uu_tr::uumain; use rand::Rng; -mod fuzz_common; -use crate::fuzz_common::{ +use uufuzz::{ CommandResult, compare_result, generate_and_run_uumain, generate_random_string, run_gnu_cmd, }; static CMD_PATH: &str = "tr"; diff --git a/fuzz/fuzz_targets/fuzz_wc.rs b/fuzz/fuzz_targets/fuzz_wc.rs index 39dfb1ee862..dbc046522bb 100644 --- a/fuzz/fuzz_targets/fuzz_wc.rs +++ b/fuzz/fuzz_targets/fuzz_wc.rs @@ -11,8 +11,7 @@ use uu_wc::uumain; use rand::Rng; use std::ffi::OsString; -mod fuzz_common; -use crate::fuzz_common::{ +use uufuzz::{ CommandResult, compare_result, generate_and_run_uumain, generate_random_string, run_gnu_cmd, }; static CMD_PATH: &str = "wc";