Skip to content

Commit 985041f

Browse files
committed
--version should just print the command name, not the path
This will fix the parsing for old autoconf Closes: uutils#8880
1 parent e55b823 commit 985041f

File tree

5 files changed

+335
-10
lines changed

5 files changed

+335
-10
lines changed

fuzz/fuzz_targets/fuzz_date.rs

Lines changed: 184 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,189 @@
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+
16
#![no_main]
27
use libfuzzer_sys::fuzz_target;
3-
4-
use std::ffi::OsString;
58
use uu_date::uumain;
69

7-
fuzz_target!(|data: &[u8]| {
8-
let delim: u8 = 0; // Null byte
9-
let args = data
10-
.split(|b| *b == delim)
11-
.filter_map(|e| std::str::from_utf8(e).ok())
12-
.map(OsString::from);
13-
uumain(args);
10+
use rand::prelude::IndexedRandom;
11+
use rand::Rng;
12+
use std::{env, ffi::OsString};
13+
14+
use uufuzz::CommandResult;
15+
use uufuzz::{compare_result, generate_and_run_uumain, generate_random_string, run_gnu_cmd_with_env};
16+
17+
// Use absolute path to GNU date to avoid picking up other versions
18+
static CMD_PATH: &str = "/usr/bin/date";
19+
20+
fn generate_date_args(rng: &mut impl Rng) -> Vec<OsString> {
21+
let mut args = vec![OsString::from("date")];
22+
23+
// Choose random strategy
24+
match rng.random_range(0..=5) {
25+
0 => {
26+
// Test pure numeric -d inputs (new GNU compatibility feature)
27+
args.push(OsString::from("-d"));
28+
29+
// Generate numeric strings of different lengths (1-4 digits)
30+
let len = rng.random_range(1..=4);
31+
let mut numeric_str = String::new();
32+
33+
for _ in 0..len {
34+
numeric_str.push_str(&rng.random_range(0..=9).to_string());
35+
}
36+
37+
args.push(OsString::from(numeric_str));
38+
39+
// Sometimes add a format string
40+
if rng.random_bool(0.5) {
41+
args.push(OsString::from("+%F %T %Z"));
42+
}
43+
}
44+
1 => {
45+
// Test edge cases for pure numeric inputs
46+
args.push(OsString::from("-d"));
47+
48+
// Generate 4-digit numbers that might be invalid times
49+
let val = rng.random_range(0..=9999);
50+
args.push(OsString::from(format!("{:04}", val)));
51+
52+
// Add format string
53+
args.push(OsString::from("+%F %T %Z"));
54+
}
55+
2 => {
56+
// Test with human-readable dates
57+
args.push(OsString::from("-d"));
58+
59+
// Use only absolute date formats to avoid timing issues
60+
let dates = [
61+
"2025-10-15",
62+
"2025-01-01",
63+
"2024-12-31",
64+
"1970-01-01",
65+
"2000-01-01 12:00:00",
66+
"2025-06-15 15:30:45",
67+
];
68+
69+
let date = dates.choose(rng).unwrap();
70+
args.push(OsString::from(*date));
71+
}
72+
3 => {
73+
// Test with single letter military timezones (including J which should fail)
74+
args.push(OsString::from("-d"));
75+
76+
let letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
77+
let letter = letters.chars().nth(rng.random_range(0..26)).unwrap();
78+
args.push(OsString::from(letter.to_string()));
79+
80+
// Add format string to see timezone
81+
args.push(OsString::from("+%F %T %Z"));
82+
}
83+
4 => {
84+
// Test with ISO-like date strings
85+
args.push(OsString::from("-d"));
86+
87+
// Generate a random date string
88+
let year = rng.random_range(1970..=2100);
89+
let month = rng.random_range(1..=12);
90+
let day = rng.random_range(1..=31);
91+
let hour = rng.random_range(0..=23);
92+
let minute = rng.random_range(0..=59);
93+
94+
let date_str = if rng.random_bool(0.5) {
95+
format!("{:04}-{:02}-{:02}", year, month, day)
96+
} else {
97+
format!(
98+
"{:04}-{:02}-{:02} {:02}:{:02}",
99+
year, month, day, hour, minute
100+
)
101+
};
102+
103+
args.push(OsString::from(date_str));
104+
}
105+
_ => {
106+
// Test with random/malformed inputs
107+
args.push(OsString::from("-d"));
108+
109+
// Generate random string that might or might not be valid
110+
let random_str = generate_random_string(rng.random_range(1..=20));
111+
args.push(OsString::from(random_str));
112+
}
113+
}
114+
115+
// Sometimes add UTC flag
116+
if rng.random_bool(0.3) {
117+
args.insert(1, OsString::from("-u"));
118+
}
119+
120+
args
121+
}
122+
123+
fuzz_target!(|_data: &[u8]| {
124+
let mut rng = rand::rng();
125+
126+
// Use simple offset-based timezones to avoid database dependencies
127+
let tz = match rng.random_range(0..=3) {
128+
0 => "UTC0",
129+
1 => "UTC-5", // Like EST
130+
2 => "UTC+9", // Like JST
131+
_ => "UTC+1", // Like CET
132+
};
133+
134+
unsafe {
135+
env::set_var("TZ", tz);
136+
}
137+
138+
let args = generate_date_args(&mut rng);
139+
140+
let rust_result = generate_and_run_uumain(&args, uumain, None);
141+
142+
let gnu_result = match run_gnu_cmd_with_env(CMD_PATH, &args[1..], false, None, &[("TZ", tz)]) {
143+
Ok(result) => result,
144+
Err(error_result) => CommandResult {
145+
stdout: String::new(),
146+
stderr: error_result.stderr,
147+
exit_code: error_result.exit_code,
148+
},
149+
};
150+
151+
// Include TZ in the command description for debugging
152+
let cmd_str = format!(
153+
"TZ={} date {}",
154+
tz,
155+
args[1..]
156+
.iter()
157+
.map(|s| s.to_string_lossy().to_string())
158+
.collect::<Vec<_>>()
159+
.join(" ")
160+
);
161+
162+
// For stderr-only differences, don't panic but log them
163+
// This handles cases where our error messages differ from GNU
164+
let stderr_differs = rust_result.stderr.trim() != gnu_result.stderr.trim();
165+
let stdout_differs = rust_result.stdout.trim() != gnu_result.stdout.trim();
166+
let exit_code_differs = rust_result.exit_code != gnu_result.exit_code;
167+
168+
// Skip if only stderr differs (common for error messages)
169+
if stderr_differs && !stdout_differs && !exit_code_differs {
170+
// Just log this case, don't compare
171+
return;
172+
}
173+
174+
// Known issue: -u flag with non-UTC timezone has bugs
175+
// TODO: Fix this in the date implementation
176+
if args.contains(&OsString::from("-u")) && tz != "UTC0" {
177+
// Skip this known bug for now
178+
return;
179+
}
180+
181+
compare_result(
182+
"",
183+
&cmd_str,
184+
None,
185+
&rust_result,
186+
&gnu_result,
187+
false, // Don't fail on stderr differences
188+
);
14189
});

fuzz/uufuzz/src/lib.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,16 @@ pub fn run_gnu_cmd(
221221
args: &[OsString],
222222
check_gnu: bool,
223223
pipe_input: Option<&str>,
224+
) -> Result<CommandResult, CommandResult> {
225+
run_gnu_cmd_with_env(cmd_path, args, check_gnu, pipe_input, &[])
226+
}
227+
228+
pub fn run_gnu_cmd_with_env(
229+
cmd_path: &str,
230+
args: &[OsString],
231+
check_gnu: bool,
232+
pipe_input: Option<&str>,
233+
env_vars: &[(&str, &str)],
224234
) -> Result<CommandResult, CommandResult> {
225235
if check_gnu {
226236
match is_gnu_cmd(cmd_path) {
@@ -245,6 +255,11 @@ pub fn run_gnu_cmd(
245255
// uutils' coreutils is not locale-aware, and aims to mirror/be compatible with GNU Core Utilities's LC_ALL=C behavior
246256
command.env("LC_ALL", "C");
247257

258+
// Apply additional environment variables
259+
for (key, value) in env_vars {
260+
command.env(key, value);
261+
}
262+
248263
let output = if let Some(input_str) = pipe_input {
249264
// We have an pipe input
250265
command

src/uucore/src/lib/lib.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,14 @@ static UTIL_NAME: LazyLock<String> = LazyLock::new(|| {
328328
let is_man = usize::from(ARGV[base_index].eq("manpage"));
329329
let argv_index = base_index + is_man;
330330

331-
ARGV[argv_index].to_string_lossy().into_owned()
331+
// Strip directory path to show only utility name
332+
// (e.g., "mkdir" instead of "./target/debug/mkdir")
333+
// in version output, error messages, and other user-facing output
334+
std::path::Path::new(&ARGV[argv_index])
335+
.file_name()
336+
.unwrap_or(&ARGV[argv_index])
337+
.to_string_lossy()
338+
.into_owned()
332339
});
333340

334341
/// Derive the utility name.

tests/by-util/test_date.rs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -835,3 +835,90 @@ fn test_date_numeric_d_invalid_numbers() {
835835
.fails()
836836
.stderr_contains("invalid date");
837837
}
838+
839+
// Fuzzer-discovered failures - these tests document known issues
840+
// All tests marked with #[ignore] are bugs that need to be fixed
841+
842+
#[test]
843+
#[ignore]
844+
fn test_date_fuzz_military_timezones() {
845+
// Military timezones: A-Z (except J) represent UTC offsets
846+
// A=+1, B=+2, ..., I=+9, K=+10, L=+11, M=+12, N=-1, ..., Y=-12, Z=UTC
847+
// J is reserved for local time
848+
let test_cases = vec![
849+
("UTC0", "A", "23:00:00"), // A = UTC+1
850+
("UTC0", "U", "08:00:00"), // U = UTC+8
851+
("UTC+1", "M", "11:00:00"), // M = UTC+12
852+
("UTC0", "N", "01:00:00"), // N = UTC-1
853+
("UTC+9", "W", "01:00:00"), // W = UTC+10
854+
("UTC+1", "F", "17:00:00"), // F = UTC+6
855+
("UTC+1", "K", "13:00:00"), // K = UTC+10
856+
("UTC+1", "J", "00:00:00"), // J is not defined, GNU uses midnight
857+
];
858+
859+
for (tz, letter, expected_time) in test_cases {
860+
new_ucmd!()
861+
.env("TZ", tz)
862+
.arg("-d")
863+
.arg(letter)
864+
.arg("+%F %T %Z")
865+
.succeeds()
866+
.stdout_contains(expected_time);
867+
}
868+
}
869+
870+
#[test]
871+
#[ignore]
872+
fn test_date_fuzz_numeric_with_offset_tz() {
873+
// Pure numeric dates should work correctly with offset timezones
874+
let test_cases = vec![
875+
("UTC+1", "2", "02:00:00"),
876+
("UTC+1", "6", "06:00:00"),
877+
];
878+
879+
for (tz, num, expected_time) in test_cases {
880+
new_ucmd!()
881+
.env("TZ", tz)
882+
.arg("-d")
883+
.arg(num)
884+
.succeeds()
885+
.stdout_contains(expected_time);
886+
}
887+
}
888+
889+
#[test]
890+
#[ignore]
891+
fn test_date_fuzz_empty_string() {
892+
// Empty string should be treated as midnight today
893+
new_ucmd!()
894+
.env("TZ", "UTC+1")
895+
.arg("-d")
896+
.arg("")
897+
.succeeds()
898+
.stdout_contains("00:00:00");
899+
}
900+
901+
#[test]
902+
#[ignore]
903+
fn test_date_fuzz_date_with_offset_tz() {
904+
// Date parsing should work correctly with offset timezones
905+
new_ucmd!()
906+
.env("TZ", "UTC+1")
907+
.arg("-d")
908+
.arg("2025-10-15")
909+
.succeeds()
910+
.stdout_contains("Oct 15")
911+
.stdout_contains("00:00:00");
912+
}
913+
914+
#[test]
915+
#[ignore]
916+
fn test_date_fuzz_relative_m9() {
917+
// Relative date string "m9" should be parsed correctly
918+
new_ucmd!()
919+
.env("TZ", "UTC+9")
920+
.arg("-d")
921+
.arg("m9")
922+
.succeeds()
923+
.stdout_contains("12:00:00");
924+
}

tests/by-util/test_mkdir.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,47 @@ fn test_invalid_arg() {
2424
new_ucmd!().arg("--definitely-invalid").fails_with_code(1);
2525
}
2626

27+
#[test]
28+
fn test_version_no_path() {
29+
use std::process::Command;
30+
use uutests::get_tests_binary;
31+
32+
// This test verifies that when an individual utility binary is invoked with its full path,
33+
// the version output shows just "mkdir", not the full path like "/path/to/mkdir".
34+
//
35+
// Note: The multicall binary (coreutils) doesn't have this issue because it reads
36+
// the utility name from ARGV[1], not ARGV[0]. This bug only affects individual binaries.
37+
38+
let tests_binary = get_tests_binary!();
39+
let mkdir_binary_path = std::path::Path::new(tests_binary)
40+
.parent()
41+
.unwrap()
42+
.join("mkdir");
43+
44+
// If the individual mkdir binary exists, test it
45+
if mkdir_binary_path.exists() {
46+
// Invoke the individual mkdir binary with its full path
47+
let output = Command::new(&mkdir_binary_path)
48+
.arg("--version")
49+
.output()
50+
.expect("Failed to execute mkdir binary");
51+
52+
let stdout = String::from_utf8_lossy(&output.stdout);
53+
let first_line = stdout.lines().next().unwrap_or("");
54+
55+
assert!(stdout.starts_with("mkdir (uutils coreutils)"));
56+
} else {
57+
// If only multicall binary exists, test that (it should already pass)
58+
let output = Command::new(tests_binary)
59+
.args(["mkdir", "--version"])
60+
.output()
61+
.expect("Failed to execute mkdir via multicall binary");
62+
63+
let stdout = String::from_utf8_lossy(&output.stdout);
64+
assert!(stdout.starts_with("mkdir (uutils coreutils)"));
65+
}
66+
}
67+
2768
#[test]
2869
fn test_no_arg() {
2970
new_ucmd!()

0 commit comments

Comments
 (0)