dirname: add fuzzer to test GNU compatibility

This commit is contained in:
Sylvestre Ledru
2026-01-17 15:22:06 +01:00
parent 4e7fa9fc94
commit 7c5383dd89
4 changed files with 229 additions and 0 deletions
+1
View File
@@ -96,6 +96,7 @@ jobs:
- { name: fuzz_parse_time, should_pass: true }
- { name: fuzz_seq_parse_number, should_pass: false }
- { name: fuzz_non_utf8_paths, should_pass: true }
- { name: fuzz_dirname, should_pass: true }
steps:
- uses: actions/checkout@v6
+10
View File
@@ -1617,6 +1617,15 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "uu_dirname"
version = "0.6.0"
dependencies = [
"clap",
"fluent",
"uucore",
]
[[package]]
name = "uu_echo"
version = "0.6.0"
@@ -1798,6 +1807,7 @@ dependencies = [
"uu_cksum",
"uu_cut",
"uu_date",
"uu_dirname",
"uu_echo",
"uu_env",
"uu_expr",
+7
View File
@@ -41,6 +41,7 @@ uu_split = { path = "../src/uu/split" }
uu_tr = { path = "../src/uu/tr" }
uu_env = { path = "../src/uu/env" }
uu_cksum = { path = "../src/uu/cksum" }
uu_dirname = { path = "../src/uu/dirname" }
[[bin]]
name = "fuzz_date"
@@ -149,3 +150,9 @@ name = "fuzz_non_utf8_paths"
path = "fuzz_targets/fuzz_non_utf8_paths.rs"
test = false
doc = false
[[bin]]
name = "fuzz_dirname"
path = "fuzz_targets/fuzz_dirname.rs"
test = false
doc = false
+211
View File
@@ -0,0 +1,211 @@
// 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.
#![no_main]
use libfuzzer_sys::fuzz_target;
use uu_dirname::uumain;
use rand::Rng;
use rand::prelude::IndexedRandom;
use std::ffi::OsString;
use uufuzz::CommandResult;
use uufuzz::{compare_result, generate_and_run_uumain, generate_random_string, run_gnu_cmd};
static CMD_PATH: &str = "dirname";
fn generate_dirname_args() -> Vec<String> {
let mut rng = rand::rng();
let mut args = Vec::new();
// 20% chance to include -z/--zero flag
if rng.random_bool(0.2) {
if rng.random_bool(0.5) {
args.push("-z".to_string());
} else {
args.push("--zero".to_string());
}
}
// 30% chance to use one of the specific issue #8924 cases
if rng.random_bool(0.3) {
let issue_cases = [
"foo//.",
"foo/./",
"foo/bar/./",
"bar//.",
"test/./",
"a/b/./",
"x//.",
"dir/subdir/./",
];
args.push(issue_cases.choose(&mut rng).unwrap().to_string());
} else {
// Generate 1-3 path arguments normally
let num_paths = rng.random_range(1..=3);
for _ in 0..num_paths {
args.push(generate_path());
}
}
args
}
fn generate_path() -> String {
let mut rng = rand::rng();
// Different types of paths to test
let path_type = rng.random_range(0..15);
match path_type {
// Simple paths
0 => generate_random_string(rng.random_range(1..=20)),
// Paths with slashes
1 => {
let mut path = String::new();
let components = rng.random_range(1..=5);
for i in 0..components {
if i > 0 {
path.push('/');
}
path.push_str(&generate_random_string(rng.random_range(1..=10)));
}
path
}
// Root path
2 => "/".to_string(),
// Absolute paths
3 => {
let mut path = "/".to_string();
let components = rng.random_range(1..=4);
for _ in 0..components {
path.push_str(&generate_random_string(rng.random_range(1..=8)));
path.push('/');
}
// Remove trailing slash sometimes
if rng.random_bool(0.5) && path.len() > 1 {
path.pop();
}
path
}
// Paths ending with "/." (specific case from issue #8924)
4 => {
let base = if rng.random_bool(0.3) {
"/".to_string()
} else {
format!("/{}", generate_random_string(rng.random_range(1..=10)))
};
format!("{}.", base)
}
// Paths with multiple slashes
5 => {
let base = generate_random_string(rng.random_range(1..=10));
format!(
"///{}//{}",
base,
generate_random_string(rng.random_range(1..=8))
)
}
// Paths with dots
6 => {
let components = [".", "..", "...", "...."];
let chosen = components.choose(&mut rng).unwrap();
if rng.random_bool(0.5) {
format!("/{}", chosen)
} else {
chosen.to_string()
}
}
// Single character paths
7 => {
let chars = ['a', 'x', '1', '-', '_', '.'];
chars.choose(&mut rng).unwrap().to_string()
}
// Empty string (edge case)
8 => "".to_string(),
// Issue #8924 specific cases: paths like "foo//."
9 => {
let base = generate_random_string(rng.random_range(1..=10));
format!("{}//.", base)
}
// Issue #8924 specific cases: paths like "foo/./"
10 => {
let base = generate_random_string(rng.random_range(1..=10));
format!("{}/./", base)
}
// Issue #8924 specific cases: paths like "foo/bar/./"
11 => {
let base1 = generate_random_string(rng.random_range(1..=8));
let base2 = generate_random_string(rng.random_range(1..=8));
format!("{}/{}/./", base1, base2)
}
// More complex patterns with ./ and multiple slashes
12 => {
let base = generate_random_string(rng.random_range(1..=10));
let patterns = ["/./", "//./", "//.//", "/.//"];
let pattern = patterns.choose(&mut rng).unwrap();
format!("{}{}", base, pattern)
}
// Patterns with .. and multiple slashes
13 => {
let base = generate_random_string(rng.random_range(1..=10));
let patterns = ["/..", "//..", "/../", "//..//"];
let pattern = patterns.choose(&mut rng).unwrap();
format!("{}{}", base, pattern)
}
// Complex paths with special cases
_ => {
let special_endings = [".", "..", "/.", "/..", "//", "/", "/./.", "//.", "./"];
let base = generate_random_string(rng.random_range(1..=15));
let ending = special_endings.choose(&mut rng).unwrap();
format!("{}{}", base, ending)
}
}
}
fuzz_target!(|_data: &[u8]| {
let dirname_args = generate_dirname_args();
let mut args = vec![OsString::from("dirname")];
args.extend(dirname_args.iter().map(OsString::from));
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(
"dirname",
&format!("{:?}", &args[1..]),
None,
&rust_result,
&gnu_result,
false,
);
});