Skip to content

Commit e07ad5b

Browse files
Copilotjgarzik
andcommitted
Fix signal termination handling and add 255 byte limit for -I mode
- Add immediate exit on signal termination (POSIX compliance) - Enforce 4096 byte limit for -I constructed arguments (POSIX requires ≥255) - Add 7 new tests for POSIX compliance gaps: * Single quote handling * Mixed quotes * Empty input * Line continuation with -L * Multiple replstr in -I * Five arguments with replstr in -I * Combining -n and -s options Co-authored-by: jgarzik <494411+jgarzik@users.noreply.github.com>
1 parent d58b803 commit e07ad5b

File tree

2 files changed

+133
-0
lines changed

2 files changed

+133
-0
lines changed

process/tests/xargs/mod.rs

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,3 +211,104 @@ fn xargs_escaped_chars() {
211211
// Test escaped characters
212212
xargs_test("hello\\ world\n", "hello world\n", vec!["echo"]);
213213
}
214+
215+
#[test]
216+
fn xargs_single_quotes() {
217+
// Test single quote (apostrophe) handling
218+
xargs_test("'hello world'\n", "hello world\n", vec!["echo"]);
219+
}
220+
221+
#[test]
222+
fn xargs_mixed_quotes() {
223+
// Test mix of single and double quotes
224+
xargs_test(
225+
"'single' \"double\" plain\n",
226+
"single double plain\n",
227+
vec!["echo"],
228+
);
229+
}
230+
231+
#[test]
232+
fn xargs_empty_input() {
233+
// Empty input should not execute the utility
234+
run_test(TestPlan {
235+
cmd: String::from("xargs"),
236+
args: vec!["echo".to_string()],
237+
stdin_data: String::from(""),
238+
expected_out: String::from(""),
239+
expected_err: String::from(""),
240+
expected_exit_code: 0,
241+
});
242+
}
243+
244+
#[test]
245+
fn xargs_line_continuation() {
246+
// -L with trailing blank should continue to next line
247+
run_test(TestPlan {
248+
cmd: String::from("xargs"),
249+
args: vec!["-L".to_string(), "1".to_string(), "echo".to_string()],
250+
stdin_data: String::from("one \ntwo\nthree\n"),
251+
expected_out: String::from("one two\nthree\n"),
252+
expected_err: String::from(""),
253+
expected_exit_code: 0,
254+
});
255+
}
256+
257+
#[test]
258+
fn xargs_insert_multiple_replstr() {
259+
// -I with multiple occurrences of replstr in same argument
260+
run_test(TestPlan {
261+
cmd: String::from("xargs"),
262+
args: vec![
263+
"-I".to_string(),
264+
"{}".to_string(),
265+
"echo".to_string(),
266+
"{}-{}-{}".to_string(),
267+
],
268+
stdin_data: String::from("test\n"),
269+
expected_out: String::from("test-test-test\n"),
270+
expected_err: String::from(""),
271+
expected_exit_code: 0,
272+
});
273+
}
274+
275+
#[test]
276+
fn xargs_insert_five_args_with_replstr() {
277+
// -I with replstr in 5 arguments (POSIX requires at least 5)
278+
run_test(TestPlan {
279+
cmd: String::from("xargs"),
280+
args: vec![
281+
"-I".to_string(),
282+
"{}".to_string(),
283+
"echo".to_string(),
284+
"{}".to_string(),
285+
"{}".to_string(),
286+
"{}".to_string(),
287+
"{}".to_string(),
288+
"{}".to_string(),
289+
],
290+
stdin_data: String::from("x\n"),
291+
expected_out: String::from("x x x x x\n"),
292+
expected_err: String::from(""),
293+
expected_exit_code: 0,
294+
});
295+
}
296+
297+
#[test]
298+
fn xargs_combine_n_and_s() {
299+
// Combining -n and -s should work together
300+
run_test(TestPlan {
301+
cmd: String::from("xargs"),
302+
args: vec![
303+
"-n".to_string(),
304+
"2".to_string(),
305+
"-s".to_string(),
306+
"20".to_string(),
307+
"echo".to_string(),
308+
],
309+
stdin_data: String::from("a b c d e\n"),
310+
expected_out: String::from("a b\nc d\ne\n"),
311+
expected_err: String::from(""),
312+
expected_exit_code: 0,
313+
});
314+
}

process/xargs.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ use gettextrs::{bind_textdomain_codeset, gettext, setlocale, textdomain, LocaleC
1717
use plib::BUFSZ;
1818

1919
const FALLBACK_ARG_MAX: usize = 131072;
20+
// POSIX requires at least 255 bytes for -I constructed arguments
21+
// We use a higher limit for better usability
22+
const INSERT_ARG_MAX: usize = 4096;
2023

2124
fn get_arg_max() -> usize {
2225
let result = unsafe { libc::sysconf(libc::_SC_ARG_MAX) };
@@ -536,6 +539,20 @@ fn exec_insert_mode(
536539
.map(|arg| arg.replace(replstr, input_arg))
537540
.collect();
538541

542+
// POSIX: Check that constructed arguments don't exceed the limit
543+
// POSIX requires at least 255 bytes, we use INSERT_ARG_MAX (4096)
544+
for arg in &util_args {
545+
if arg.len() > INSERT_ARG_MAX {
546+
return Err(io::Error::new(
547+
io::ErrorKind::InvalidInput,
548+
format!(
549+
"xargs: constructed argument exceeds {} byte limit in insert mode",
550+
INSERT_ARG_MAX
551+
),
552+
));
553+
}
554+
}
555+
539556
exec_util(&args.util, util_args, trace, prompt)
540557
}
541558

@@ -552,6 +569,11 @@ macro_rules! handle_exec_result {
552569
ExecResult::Exited(255) => {
553570
return Ok(SpawnResult { exit_code: 1 });
554571
}
572+
ExecResult::Exited(code) if code >= 128 => {
573+
// POSIX: utility terminated by signal - exit immediately
574+
eprintln!("xargs: command terminated by signal");
575+
return Ok(SpawnResult { exit_code: 1 });
576+
}
555577
ExecResult::Exited(code) if code != 0 => {
556578
$any_failed = true;
557579
}
@@ -682,6 +704,11 @@ fn read_and_spawn(args: &Args) -> io::Result<SpawnResult> {
682704
ExecResult::Exited(255) => {
683705
return Ok(SpawnResult { exit_code: 1 });
684706
}
707+
ExecResult::Exited(code) if code >= 128 => {
708+
// POSIX: utility terminated by signal - exit immediately
709+
eprintln!("xargs: command terminated by signal");
710+
return Ok(SpawnResult { exit_code: 1 });
711+
}
685712
ExecResult::Exited(code) if code != 0 => {
686713
any_failed = true;
687714
}
@@ -703,6 +730,11 @@ fn read_and_spawn(args: &Args) -> io::Result<SpawnResult> {
703730
ExecResult::Exited(255) => {
704731
return Ok(SpawnResult { exit_code: 1 });
705732
}
733+
ExecResult::Exited(code) if code >= 128 => {
734+
// POSIX: utility terminated by signal - exit immediately
735+
eprintln!("xargs: command terminated by signal");
736+
return Ok(SpawnResult { exit_code: 1 });
737+
}
706738
ExecResult::Exited(code) if code != 0 => {
707739
any_failed = true;
708740
}

0 commit comments

Comments
 (0)