Skip to content

Commit 7881596

Browse files
committed
dirname: add fuzzer to test GNU compatibility
1 parent b3ad96f commit 7881596

File tree

4 files changed

+229
-0
lines changed

4 files changed

+229
-0
lines changed

.github/workflows/fuzzing.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ jobs:
9696
- { name: fuzz_parse_time, should_pass: true }
9797
- { name: fuzz_seq_parse_number, should_pass: true }
9898
- { name: fuzz_non_utf8_paths, should_pass: true }
99+
- { name: fuzz_dirname, should_pass: true }
99100

100101
steps:
101102
- uses: actions/checkout@v6

fuzz/Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

fuzz/Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ uu_split = { path = "../src/uu/split" }
4141
uu_tr = { path = "../src/uu/tr" }
4242
uu_env = { path = "../src/uu/env" }
4343
uu_cksum = { path = "../src/uu/cksum" }
44+
uu_dirname = { path = "../src/uu/dirname" }
4445

4546
[[bin]]
4647
name = "fuzz_date"
@@ -149,3 +150,9 @@ name = "fuzz_non_utf8_paths"
149150
path = "fuzz_targets/fuzz_non_utf8_paths.rs"
150151
test = false
151152
doc = false
153+
154+
[[bin]]
155+
name = "fuzz_dirname"
156+
path = "fuzz_targets/fuzz_dirname.rs"
157+
test = false
158+
doc = false

fuzz/fuzz_targets/fuzz_dirname.rs

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
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+
6+
#![no_main]
7+
use libfuzzer_sys::fuzz_target;
8+
use uu_dirname::uumain;
9+
10+
use rand::Rng;
11+
use rand::prelude::IndexedRandom;
12+
use std::ffi::OsString;
13+
14+
use uufuzz::CommandResult;
15+
use uufuzz::{compare_result, generate_and_run_uumain, generate_random_string, run_gnu_cmd};
16+
17+
static CMD_PATH: &str = "dirname";
18+
19+
fn generate_dirname_args() -> Vec<String> {
20+
let mut rng = rand::rng();
21+
let mut args = Vec::new();
22+
23+
// 20% chance to include -z/--zero flag
24+
if rng.random_bool(0.2) {
25+
if rng.random_bool(0.5) {
26+
args.push("-z".to_string());
27+
} else {
28+
args.push("--zero".to_string());
29+
}
30+
}
31+
32+
// 30% chance to use one of the specific issue #8924 cases
33+
if rng.random_bool(0.3) {
34+
let issue_cases = [
35+
"foo//.",
36+
"foo/./",
37+
"foo/bar/./",
38+
"bar//.",
39+
"test/./",
40+
"a/b/./",
41+
"x//.",
42+
"dir/subdir/./",
43+
];
44+
args.push(issue_cases.choose(&mut rng).unwrap().to_string());
45+
} else {
46+
// Generate 1-3 path arguments normally
47+
let num_paths = rng.random_range(1..=3);
48+
for _ in 0..num_paths {
49+
args.push(generate_path());
50+
}
51+
}
52+
53+
args
54+
}
55+
56+
fn generate_path() -> String {
57+
let mut rng = rand::rng();
58+
59+
// Different types of paths to test
60+
let path_type = rng.random_range(0..15);
61+
62+
match path_type {
63+
// Simple paths
64+
0 => generate_random_string(rng.random_range(1..=20)),
65+
66+
// Paths with slashes
67+
1 => {
68+
let mut path = String::new();
69+
let components = rng.random_range(1..=5);
70+
for i in 0..components {
71+
if i > 0 {
72+
path.push('/');
73+
}
74+
path.push_str(&generate_random_string(rng.random_range(1..=10)));
75+
}
76+
path
77+
}
78+
79+
// Root path
80+
2 => "/".to_string(),
81+
82+
// Absolute paths
83+
3 => {
84+
let mut path = "/".to_string();
85+
let components = rng.random_range(1..=4);
86+
for _ in 0..components {
87+
path.push_str(&generate_random_string(rng.random_range(1..=8)));
88+
path.push('/');
89+
}
90+
// Remove trailing slash sometimes
91+
if rng.random_bool(0.5) && path.len() > 1 {
92+
path.pop();
93+
}
94+
path
95+
}
96+
97+
// Paths ending with "/." (specific case from issue #8924)
98+
4 => {
99+
let base = if rng.random_bool(0.3) {
100+
"/".to_string()
101+
} else {
102+
format!("/{}", generate_random_string(rng.random_range(1..=10)))
103+
};
104+
format!("{}.", base)
105+
}
106+
107+
// Paths with multiple slashes
108+
5 => {
109+
let base = generate_random_string(rng.random_range(1..=10));
110+
format!(
111+
"///{}//{}",
112+
base,
113+
generate_random_string(rng.random_range(1..=8))
114+
)
115+
}
116+
117+
// Paths with dots
118+
6 => {
119+
let components = [".", "..", "...", "...."];
120+
let chosen = components.choose(&mut rng).unwrap();
121+
if rng.random_bool(0.5) {
122+
format!("/{}", chosen)
123+
} else {
124+
chosen.to_string()
125+
}
126+
}
127+
128+
// Single character paths
129+
7 => {
130+
let chars = ['a', 'x', '1', '-', '_', '.'];
131+
chars.choose(&mut rng).unwrap().to_string()
132+
}
133+
134+
// Empty string (edge case)
135+
8 => "".to_string(),
136+
137+
// Issue #8924 specific cases: paths like "foo//."
138+
9 => {
139+
let base = generate_random_string(rng.random_range(1..=10));
140+
format!("{}//.", base)
141+
}
142+
143+
// Issue #8924 specific cases: paths like "foo/./"
144+
10 => {
145+
let base = generate_random_string(rng.random_range(1..=10));
146+
format!("{}/./", base)
147+
}
148+
149+
// Issue #8924 specific cases: paths like "foo/bar/./"
150+
11 => {
151+
let base1 = generate_random_string(rng.random_range(1..=8));
152+
let base2 = generate_random_string(rng.random_range(1..=8));
153+
format!("{}/{}/./", base1, base2)
154+
}
155+
156+
// More complex patterns with ./ and multiple slashes
157+
12 => {
158+
let base = generate_random_string(rng.random_range(1..=10));
159+
let patterns = ["/./", "//./", "//.//", "/.//"];
160+
let pattern = patterns.choose(&mut rng).unwrap();
161+
format!("{}{}", base, pattern)
162+
}
163+
164+
// Patterns with .. and multiple slashes
165+
13 => {
166+
let base = generate_random_string(rng.random_range(1..=10));
167+
let patterns = ["/..", "//..", "/../", "//..//"];
168+
let pattern = patterns.choose(&mut rng).unwrap();
169+
format!("{}{}", base, pattern)
170+
}
171+
172+
// Complex paths with special cases
173+
_ => {
174+
let special_endings = [".", "..", "/.", "/..", "//", "/", "/./.", "//.", "./"];
175+
let base = generate_random_string(rng.random_range(1..=15));
176+
let ending = special_endings.choose(&mut rng).unwrap();
177+
format!("{}{}", base, ending)
178+
}
179+
}
180+
}
181+
182+
fuzz_target!(|_data: &[u8]| {
183+
let dirname_args = generate_dirname_args();
184+
let mut args = vec![OsString::from("dirname")];
185+
args.extend(dirname_args.iter().map(OsString::from));
186+
187+
let rust_result = generate_and_run_uumain(&args, uumain, None);
188+
189+
let gnu_result = match run_gnu_cmd(CMD_PATH, &args[1..], false, None) {
190+
Ok(result) => result,
191+
Err(error_result) => {
192+
eprintln!("Failed to run GNU command:");
193+
eprintln!("Stderr: {}", error_result.stderr);
194+
eprintln!("Exit Code: {}", error_result.exit_code);
195+
CommandResult {
196+
stdout: String::new(),
197+
stderr: error_result.stderr,
198+
exit_code: error_result.exit_code,
199+
}
200+
}
201+
};
202+
203+
compare_result(
204+
"dirname",
205+
&format!("{:?}", &args[1..]),
206+
None,
207+
&rust_result,
208+
&gnu_result,
209+
false,
210+
);
211+
});

0 commit comments

Comments
 (0)