Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/core/args_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
/// than `parsed_args` (nothing was consumed). Otherwise restores all consumed `--` at
/// their original positions by returning the user-args suffix of `raw_args` verbatim.
pub fn restore_double_dash(parsed_args: &[String]) -> Vec<String> {
let raw_args: Vec<String> = std::env::args().collect();
// args_os(): env::args() aborts when argv holds non-UTF-8 bytes (see #3025).
let raw_args: Vec<String> = std::env::args_os()
.map(|a| a.to_string_lossy().into_owned())
.collect();
restore_double_dash_with_raw(parsed_args, &raw_args)
}

Expand Down
14 changes: 10 additions & 4 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1281,7 +1281,13 @@ enum SbtCommands {
}

fn run_fallback(parse_error: clap::Error) -> Result<i32> {
let args: Vec<String> = std::env::args().skip(1).collect();
// argv can hold non-UTF-8 bytes (env::args() aborts on those). Keep the raw
// OsStrings for execution and derive lossy text only for display/lookup.
let raw_args: Vec<OsString> = std::env::args_os().skip(1).collect();
let args: Vec<String> = raw_args
.iter()
.map(|a| a.to_string_lossy().into_owned())
.collect();

// No args → show Clap's error (user ran just "rtk" with bad syntax)
if args.is_empty() {
Expand Down Expand Up @@ -1323,14 +1329,14 @@ fn run_fallback(parse_error: clap::Error) -> Result<i32> {
let result = if filter.filter_stderr {
// Merge stderr into stdout so the filter can strip banners emitted by tools like liquibase
core::utils::resolved_command(&args[0])
.args(&args[1..])
.args(&raw_args[1..])
.stdin(std::process::Stdio::inherit())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped()) // captured for merging
.output()
} else {
core::utils::resolved_command(&args[0])
.args(&args[1..])
.args(&raw_args[1..])
.stdin(std::process::Stdio::inherit())
.stdout(std::process::Stdio::piped()) // capture
.stderr(std::process::Stdio::inherit()) // stderr always direct
Expand Down Expand Up @@ -1399,7 +1405,7 @@ fn run_fallback(parse_error: clap::Error) -> Result<i32> {
} else {
// No TOML match: original passthrough behaviour (Stdio::inherit, streaming)
let status = core::utils::resolved_command(&args[0])
.args(&args[1..])
.args(&raw_args[1..])
.stdin(std::process::Stdio::inherit())
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
Expand Down
53 changes: 53 additions & 0 deletions tests/non_utf8_argv_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//! Regression tests for #3025: non-UTF-8 bytes in argv must not abort the process.
//!
//! Unix-only: argv is arbitrary bytes here, whereas Windows argv is UTF-16.

#![cfg(unix)]

use std::ffi::OsString;
use std::os::unix::ffi::OsStringExt;
use std::process::Command;

/// Leading bytes of a double-encoded UTF-8 sequence — invalid UTF-8 on their own.
fn invalid_utf8_arg() -> OsString {
OsString::from_vec(vec![0xc3, 0xa2, 0xe2, 0x80])
}

#[test]
fn non_utf8_arg_does_not_abort() {
let out = Command::new(env!("CARGO_BIN_EXE_rtk"))
.arg("grep")
.arg("-c")
.arg(invalid_utf8_arg())
.arg("/dev/null")
.output()
.expect("rtk should spawn");

let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!stderr.contains("panicked"),
"rtk panicked on non-UTF-8 argv: {stderr}"
);
assert!(
out.status.code().is_some(),
"rtk was killed by a signal (abort) on non-UTF-8 argv"
);
}

/// The fallback path executes the wrapped command, so argv bytes must reach it
/// verbatim — a lossy String conversion would silently replace them with U+FFFD.
#[test]
fn non_utf8_arg_reaches_the_wrapped_command_verbatim() {
let out = Command::new(env!("CARGO_BIN_EXE_rtk"))
.arg("printf")
.arg("%s")
.arg(invalid_utf8_arg())
.output()
.expect("rtk should spawn");

assert_eq!(
out.stdout,
invalid_utf8_arg().into_vec(),
"argv bytes were altered before reaching the wrapped command"
);
}