Skip to main content

uufuzz/
lib.rs

1// This file is part of the uutils coreutils package.
2//
3// For the full copyright and license information, please view the LICENSE
4// file that was distributed with this source code.
5
6use console::Style;
7use pretty_print::{
8    print_diff, print_end_with_status, print_or_empty, print_section, print_with_style,
9};
10use rand::RngExt;
11use rand::prelude::IndexedRandom;
12use rustix::io::dup;
13use rustix::io::read;
14use rustix::stdio::{dup2_stderr, dup2_stdin, dup2_stdout};
15use std::env::temp_dir;
16use std::ffi::OsString;
17use std::fs::File;
18use std::io::{self, Seek, SeekFrom, Write, pipe};
19use std::process::{Command, Stdio};
20use std::sync::atomic::Ordering;
21use std::sync::{Once, atomic::AtomicBool};
22use std::thread;
23
24pub mod pretty_print;
25
26/// Represents the result of running a command, including its standard output,
27/// standard error, and exit code.
28#[derive(Debug)]
29pub struct CommandResult {
30    /// The standard output (stdout) of the command as a string.
31    pub stdout: String,
32
33    /// The standard error (stderr) of the command as a string.
34    pub stderr: String,
35
36    /// The exit code of the command.
37    pub exit_code: i32,
38}
39
40static CHECK_GNU: Once = Once::new();
41static IS_GNU: AtomicBool = AtomicBool::new(false);
42
43pub fn is_gnu_cmd(cmd_path: &str) -> io::Result<()> {
44    CHECK_GNU.call_once(|| {
45        let version_output = Command::new(cmd_path).arg("--version").output().unwrap();
46
47        println!("version_output {version_output:#?}");
48
49        let version_str = String::from_utf8_lossy(&version_output.stdout).to_string();
50        if version_str.contains("GNU coreutils") {
51            IS_GNU.store(true, Ordering::Relaxed);
52        }
53    });
54
55    if IS_GNU.load(Ordering::Relaxed) {
56        Ok(())
57    } else {
58        panic!("Not the GNU implementation");
59    }
60}
61
62pub fn generate_and_run_uumain<F>(
63    args: &[OsString],
64    uumain_function: F,
65    pipe_input: Option<&str>,
66) -> CommandResult
67where
68    F: FnOnce(std::vec::IntoIter<OsString>) -> i32 + Send + 'static,
69{
70    // Duplicate the stdout and stderr file descriptors to restore later
71    let original_stdout_fd_owned = dup(io::stdout()).expect("Failed to duplicate STDOUT_FILENO");
72    let original_stderr_fd_owned = dup(io::stderr()).expect("Failed to duplicate STDERR_FILENO");
73
74    println!("Running test {:?}", &args[0..]);
75    let (read_pipe_stdout, write_pipe_stdout) = pipe().expect("Failed to create pipes");
76    let (read_pipe_stderr, write_pipe_stderr) = pipe().expect("Failed to create pipes");
77
78    // Redirect stdout and stderr to their respective pipes
79    dup2_stdout(&write_pipe_stdout).expect("Failed to redirect STDOUT_FILENO");
80    dup2_stderr(&write_pipe_stderr).expect("Failed to redirect STDERR_FILENO");
81
82    // Handle stdin redirection if needed
83    let original_stdin_fd_owned = if let Some(input_str) = pipe_input {
84        // we have pipe input
85        let mut input_file = tempfile::tempfile().unwrap();
86        write!(input_file, "{input_str}").unwrap();
87        input_file.seek(SeekFrom::Start(0)).unwrap();
88
89        // Redirect stdin to read from the in-memory file
90        let stdin_fd = dup(io::stdin()).expect("Failed to duplicate STDIN");
91
92        // Redirect stdin to read from the in-memory file
93        dup2_stdin(&input_file).expect("Failed to set up stdin redirection");
94
95        Some(stdin_fd)
96    } else {
97        None
98    };
99
100    let (uumain_exit_status, captured_stdout, captured_stderr) = thread::scope(|s| {
101        let out = s.spawn(|| read_from_fd(read_pipe_stdout));
102        let err = s.spawn(|| read_from_fd(read_pipe_stderr));
103        #[allow(clippy::unnecessary_to_owned)]
104        // TODO: clippy wants us to use args.iter().cloned() ?
105        let status = uumain_function(args.to_owned().into_iter());
106        // Reset the exit code global variable in case we run another test after this one
107        // See https://github.com/uutils/coreutils/issues/5777
108        uucore::error::set_exit_code(0);
109        io::stdout().flush().unwrap();
110        io::stderr().flush().unwrap();
111        // Drop write ends to close them, allowing readers to get EOF
112        drop(write_pipe_stdout);
113        drop(write_pipe_stderr);
114        // Restore stdout/stderr
115        let _ = dup2_stdout(&original_stdout_fd_owned);
116        let _ = dup2_stderr(&original_stderr_fd_owned);
117        (status, out.join().unwrap(), err.join().unwrap())
118    });
119
120    // Restore the original stdin if it was modified
121    if let Some(fd) = original_stdin_fd_owned {
122        dup2_stdin(&fd).expect("Failed to restore the original STDIN");
123    }
124
125    CommandResult {
126        stdout: captured_stdout,
127        stderr: captured_stderr
128            .split_once(':')
129            .map(|x| x.1)
130            .unwrap_or("")
131            .trim()
132            .to_string(),
133        exit_code: uumain_exit_status,
134    }
135}
136
137fn read_from_fd(fd: impl std::os::fd::AsFd) -> String {
138    let mut captured_output = Vec::new();
139    let mut read_buffer = [0; 1024];
140
141    loop {
142        match read(&fd, &mut read_buffer) {
143            Ok(0) => break,
144            Ok(bytes_read) => {
145                captured_output.extend_from_slice(&read_buffer[..bytes_read]);
146            }
147            Err(_) => {
148                eprintln!("Failed to read from the pipe");
149                break;
150            }
151        }
152    }
153
154    String::from_utf8_lossy(&captured_output).into_owned()
155}
156
157pub fn run_gnu_cmd(
158    cmd_path: &str,
159    args: &[OsString],
160    check_gnu: bool,
161    pipe_input: Option<&str>,
162) -> Result<CommandResult, CommandResult> {
163    if check_gnu {
164        // if the check passes, do nothing
165        if let Err(e) = is_gnu_cmd(cmd_path) {
166            // Convert the io::Error into the function's error type
167            return Err(CommandResult {
168                stdout: String::new(),
169                stderr: e.to_string(),
170                exit_code: -1,
171            });
172        }
173    }
174
175    let mut command = Command::new(cmd_path);
176    for arg in args {
177        command.arg(arg);
178    }
179
180    // See https://github.com/uutils/coreutils/issues/6794
181    // uutils' coreutils is not locale-aware, and aims to mirror/be compatible with GNU Core Utilities's LC_ALL=C behavior
182    command.env("LC_ALL", "C");
183
184    let output = if let Some(input_str) = pipe_input {
185        // We have an pipe input
186        command
187            .stdin(Stdio::piped())
188            .stdout(Stdio::piped())
189            .stderr(Stdio::piped());
190
191        let mut child = command.spawn().expect("Failed to execute command");
192        let child_stdin = child.stdin.as_mut().unwrap();
193        child_stdin
194            .write_all(input_str.as_bytes())
195            .expect("Failed to write to stdin");
196
197        match child.wait_with_output() {
198            Ok(output) => output,
199            Err(e) => {
200                return Err(CommandResult {
201                    stdout: String::new(),
202                    stderr: e.to_string(),
203                    exit_code: -1,
204                });
205            }
206        }
207    } else {
208        // Just run with args
209        match command.output() {
210            Ok(output) => output,
211            Err(e) => {
212                return Err(CommandResult {
213                    stdout: String::new(),
214                    stderr: e.to_string(),
215                    exit_code: -1,
216                });
217            }
218        }
219    };
220    let exit_code = output.status.code().unwrap_or(-1);
221    // Here we get stdout and stderr as Strings
222    let stdout = String::from_utf8_lossy(&output.stdout).to_string();
223    let stderr = String::from_utf8_lossy(&output.stderr).to_string();
224    let stderr = stderr
225        .split_once(':')
226        .map(|x| x.1)
227        .unwrap_or("")
228        .trim()
229        .to_string();
230
231    if output.status.success() || !check_gnu {
232        Ok(CommandResult {
233            stdout,
234            stderr,
235            exit_code,
236        })
237    } else {
238        Err(CommandResult {
239            stdout,
240            stderr,
241            exit_code,
242        })
243    }
244}
245
246/// Compare results from two different implementations of a command.
247///
248/// # Arguments
249/// * `test_type` - The command.
250/// * `input` - The input provided to the command.
251/// * `rust_result` - The result of running the command with the Rust implementation.
252/// * `gnu_result` - The result of running the command with the GNU implementation.
253/// * `fail_on_stderr_diff` - Whether to fail the test if there is a difference in stderr output.
254pub fn compare_result(
255    test_type: &str,
256    input: &str,
257    pipe_input: Option<&str>,
258    rust_result: &CommandResult,
259    gnu_result: &CommandResult,
260    fail_on_stderr_diff: bool,
261) {
262    print_section(format!("Compare result for: {test_type} {input}"));
263
264    if let Some(pipe) = pipe_input {
265        println!("Pipe: {pipe}");
266    }
267
268    let mut discrepancies = Vec::new();
269    let mut should_panic = false;
270
271    if rust_result.stdout.trim() != gnu_result.stdout.trim() {
272        discrepancies.push("stdout differs");
273        println!("Rust stdout:");
274        print_or_empty(rust_result.stdout.as_str());
275        println!("GNU stdout:");
276        print_or_empty(gnu_result.stdout.as_ref());
277        print_diff(&rust_result.stdout, &gnu_result.stdout);
278        should_panic = true;
279    }
280
281    if rust_result.stderr.trim() != gnu_result.stderr.trim() {
282        discrepancies.push("stderr differs");
283        println!("Rust stderr:");
284        print_or_empty(rust_result.stderr.as_str());
285        println!("GNU stderr:");
286        print_or_empty(gnu_result.stderr.as_str());
287        print_diff(&rust_result.stderr, &gnu_result.stderr);
288        if fail_on_stderr_diff {
289            should_panic = true;
290        }
291    }
292
293    if rust_result.exit_code != gnu_result.exit_code {
294        discrepancies.push("exit code differs");
295        println!(
296            "Different exit code: (Rust: {}, GNU: {})",
297            rust_result.exit_code, gnu_result.exit_code
298        );
299        should_panic = true;
300    }
301
302    if discrepancies.is_empty() {
303        print_end_with_status("Same behavior", true);
304    } else {
305        print_with_style(
306            format!("Discrepancies detected: {}", discrepancies.join(", ")),
307            Style::new().red(),
308        );
309        if should_panic {
310            print_end_with_status(
311                format!("Test failed and will panic for: {test_type} {input}"),
312                false,
313            );
314            panic!("Test failed for: {test_type} {input}");
315        } else {
316            print_end_with_status(
317                format!("Test completed with discrepancies for: {test_type} {input}"),
318                false,
319            );
320        }
321    }
322    println!();
323}
324
325pub fn generate_random_string(max_length: usize) -> String {
326    let mut rng = rand::rng();
327    let valid_utf8: Vec<char> =
328        "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789🔩🪛🪓⚙️🔗🧰"
329            .chars()
330            .collect();
331    let invalid_utf8 = [0xC3, 0x28]; // Invalid UTF-8 sequence
332    let mut result = String::new();
333
334    for _ in 0..rng.random_range(0..=max_length) {
335        if rng.random_bool(0.9) {
336            let ch = valid_utf8.choose(&mut rng).unwrap();
337            result.push(*ch);
338        } else {
339            let ch = invalid_utf8.choose(&mut rng).unwrap();
340            if let Some(c) = char::from_u32(*ch as u32) {
341                result.push(c);
342            }
343        }
344    }
345
346    result
347}
348
349#[allow(dead_code)]
350pub fn generate_random_file() -> io::Result<String> {
351    let mut rng = rand::rng();
352    let file_name: String = (0..10)
353        .map(|_| rng.random_range(b'a'..=b'z') as char)
354        .collect();
355    let mut file_path = temp_dir();
356    file_path.push(file_name);
357
358    let mut file = File::create(&file_path)?;
359
360    let content_length = rng.random_range(10..1000);
361    let content: String = (0..content_length)
362        .map(|_| rng.random_range(b' '..=b'~') as char)
363        .collect();
364
365    file.write_all(content.as_bytes())?;
366
367    Ok(file_path.to_str().unwrap().to_string())
368}
369
370#[allow(dead_code)]
371pub fn replace_fuzz_binary_name(cmd: &str, result: &mut CommandResult) {
372    let fuzz_bin_name = format!("fuzz/target/x86_64-unknown-linux-gnu/release/fuzz_{cmd}");
373
374    result.stdout = result.stdout.replace(&fuzz_bin_name, cmd);
375    result.stderr = result.stderr.replace(&fuzz_bin_name, cmd);
376}
377
378#[cfg(test)]
379mod tests {
380    use super::*;
381    use std::ffi::OsString;
382
383    #[test]
384    fn test_command_result_creation() {
385        let result = CommandResult {
386            stdout: "Hello, world!".to_string(),
387            stderr: "".to_string(),
388            exit_code: 0,
389        };
390
391        assert_eq!(result.stdout, "Hello, world!");
392        assert_eq!(result.stderr, "");
393        assert_eq!(result.exit_code, 0);
394    }
395
396    #[test]
397    fn test_generate_random_string() {
398        let result = generate_random_string(10);
399        // Check character count, not byte count (emojis are multi-byte)
400        assert!(result.chars().count() <= 10);
401
402        // Test that empty string can be generated (max_length = 0)
403        let empty_result = generate_random_string(0);
404        assert_eq!(empty_result.chars().count(), 0);
405    }
406
407    #[test]
408    fn test_replace_fuzz_binary_name() {
409        let mut result = CommandResult {
410            stdout: "fuzz/target/x86_64-unknown-linux-gnu/release/fuzz_echo: error".to_string(),
411            stderr: "fuzz/target/x86_64-unknown-linux-gnu/release/fuzz_echo failed".to_string(),
412            exit_code: 1,
413        };
414
415        replace_fuzz_binary_name("echo", &mut result);
416
417        assert_eq!(result.stdout, "echo: error");
418        assert_eq!(result.stderr, "echo failed");
419        assert_eq!(result.exit_code, 1);
420    }
421
422    #[test]
423    fn test_run_gnu_cmd_nonexistent() {
424        let args = vec![OsString::from("--version")];
425        let result = run_gnu_cmd("nonexistent_command_12345", &args, false, None);
426
427        // Should return an error since the command doesn't exist
428        assert!(result.is_err());
429        let error_result = result.unwrap_err();
430        assert_ne!(error_result.exit_code, 0);
431    }
432
433    #[test]
434    fn test_run_gnu_cmd_basic() {
435        // Test with a simple command that should exist on most systems
436        let args = vec![OsString::from("--version")];
437        let result = run_gnu_cmd("echo", &args, false, None);
438
439        // Should succeed (echo --version might not be standard but echo should exist)
440
441        if let Err(e) = result {
442            // Command failed but at least ran
443            assert_ne!(e.exit_code, -1); // -1 would indicate the command couldn't be found
444        }
445    }
446
447    #[test]
448    fn test_run_gnu_cmd_with_pipe_input() {
449        let args: Vec<OsString> = vec![];
450        let pipe_input = "hello world";
451        let result = run_gnu_cmd("cat", &args, false, Some(pipe_input));
452        // cat might not be available in test environment, that's ok
453        if let Ok(cmd_result) = result {
454            assert_eq!(cmd_result.stdout.trim(), "hello world");
455        }
456    }
457
458    #[test]
459    fn test_generate_random_file() {
460        let result = generate_random_file();
461        // File creation might fail due to permissions, that's acceptable for this test
462        if let Ok(path) = result {
463            assert!(!path.is_empty());
464            // Clean up - try to remove the file
465            let _ = std::fs::remove_file(&path);
466        }
467    }
468}