fn append_arg(cmd: &mut String, arg: &str) {
let quote = arg.is_empty() || arg.bytes().any(|c| c == b' ' || c == b'\t');
if quote {
cmd.push('"');
}
let mut backslashes: usize = 0;
for c in arg.chars() {
if c == '\\' {
backslashes += 1;
} else {
if c == '"' {
cmd.extend(std::iter::repeat_n('\\', backslashes + 1));
}
backslashes = 0;
}
cmd.push(c);
}
if quote {
cmd.extend(std::iter::repeat_n('\\', backslashes));
cmd.push('"');
}
}
pub(super) fn escape_arguments(args: &[&str]) -> String {
let mut cmd = String::new();
for (i, arg) in args.iter().enumerate() {
if i > 0 {
cmd.push(' ');
}
append_arg(&mut cmd, arg);
}
cmd
}
const BAT_UNQUOTED_SAFE: &str = r"#$*+-./:?@\_";
#[cfg_attr(not(test), allow(dead_code))]
const BAT_PERCENT_ESCAPE: &str = "%%cd:~,%";
fn append_bat_arg(cmd: &mut String, arg: &str) {
let mut quote = arg.is_empty() || arg.ends_with('\\');
if !quote {
for c in arg.chars() {
let needs_quotes = if c.is_ascii() {
!(c.is_ascii_alphanumeric() || BAT_UNQUOTED_SAFE.contains(c))
} else {
c.is_control()
};
if needs_quotes {
quote = true;
break;
}
}
}
if quote {
cmd.push('"');
}
let mut backslashes: usize = 0;
for c in arg.chars() {
if c == '\\' {
backslashes += 1;
} else {
if c == '"' {
cmd.extend(std::iter::repeat_n('\\', backslashes));
cmd.push('"');
} else if c == '%' || c == '\r' {
cmd.push_str(BAT_PERCENT_ESCAPE);
}
backslashes = 0;
}
cmd.push(c);
}
if quote {
cmd.extend(std::iter::repeat_n('\\', backslashes));
cmd.push('"');
}
}
pub(super) fn escape_bat_arguments(args: &[&str]) -> String {
let mut cmd = String::new();
for (i, arg) in args.iter().enumerate() {
if i > 0 {
cmd.push(' ');
}
append_bat_arg(&mut cmd, arg);
}
cmd
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_escape_arguments() {
assert_eq!(escape_arguments(&["aaa", "bbb", "ccc"]), "aaa bbb ccc");
assert_eq!(escape_arguments(&[r"C:\"]), r"C:\");
assert_eq!(escape_arguments(&[r"2slashes\\"]), r"2slashes\\");
assert_eq!(escape_arguments(&[r" C:\"]), r#"" C:\\""#);
assert_eq!(escape_arguments(&[r" 2slashes\\"]), r#"" 2slashes\\\\""#);
assert_eq!(escape_arguments(&["aaa"]), "aaa");
assert_eq!(escape_arguments(&["aaa", "v*"]), "aaa v*");
assert_eq!(escape_arguments(&[r#"aa"bb"#]), r#"aa\"bb"#);
assert_eq!(escape_arguments(&["a b c"]), r#""a b c""#);
assert_eq!(escape_arguments(&[r#"" \" \"#, r"\"]), r#""\" \\\" \\" \"#);
assert_eq!(escape_arguments(&[""]), r#""""#);
assert_eq!(escape_arguments(&["", ""]), r#""" """#);
assert_eq!(escape_arguments(&["a", "", "b"]), r#"a "" b"#);
assert_eq!(escape_arguments(&["a\tb"]), "\"a\tb\"");
assert_eq!(
escape_arguments(&["\u{03c0}\u{042f}\u{97f3}\u{00e6}\u{221e}"]),
"\u{03c0}\u{042f}\u{97f3}\u{00e6}\u{221e}"
);
assert_eq!(escape_arguments(&[r#"a\\\"b"#]), r#"a\\\\\\\"b"#);
assert_eq!(escape_arguments(&[r"a\b\c"]), r"a\b\c");
assert_eq!(escape_arguments(&[r#"a\"#, r#"b"c"#]), r#"a\ b\"c"#);
}
#[test]
fn test_append_arg() {
let mut cmd = String::new();
append_arg(&mut cmd, "simple");
assert_eq!(cmd, "simple");
let mut cmd = String::new();
append_arg(&mut cmd, "with space");
assert_eq!(cmd, "\"with space\"");
let mut cmd = String::new();
append_arg(&mut cmd, "");
assert_eq!(cmd, "\"\"");
let mut cmd = String::new();
append_arg(&mut cmd, r"path\to\file");
assert_eq!(cmd, r"path\to\file");
let mut cmd = String::new();
append_arg(&mut cmd, r"path with\spaces\");
assert_eq!(cmd, r#""path with\spaces\\""#);
let mut cmd = String::new();
append_arg(&mut cmd, r#"say "hello""#);
assert_eq!(cmd, r#""say \"hello\"""#);
}
#[test]
fn test_escape_bat_arguments() {
assert_eq!(escape_bat_arguments(&["aaa", "bbb", "ccc"]), "aaa bbb ccc");
assert_eq!(escape_bat_arguments(&["hello123"]), "hello123");
assert_eq!(escape_bat_arguments(&["file.txt"]), "file.txt");
assert_eq!(escape_bat_arguments(&["path/to/file"]), "path/to/file");
assert_eq!(escape_bat_arguments(&["C:/Windows"]), "C:/Windows");
assert_eq!(escape_bat_arguments(&["user@host"]), "user@host");
assert_eq!(escape_bat_arguments(&["a+b"]), "a+b");
assert_eq!(escape_bat_arguments(&["a-b"]), "a-b");
assert_eq!(escape_bat_arguments(&["a*"]), "a*");
assert_eq!(escape_bat_arguments(&["a?"]), "a?");
assert_eq!(escape_bat_arguments(&["#tag"]), "#tag");
assert_eq!(escape_bat_arguments(&["$var"]), "$var");
assert_eq!(escape_bat_arguments(&["a b c"]), r#""a b c""#);
assert_eq!(escape_bat_arguments(&["hello world"]), r#""hello world""#);
assert_eq!(escape_bat_arguments(&[""]), r#""""#);
assert_eq!(escape_bat_arguments(&["a", "", "b"]), r#"a "" b"#);
assert_eq!(escape_bat_arguments(&[r"C:\"]), r#""C:\\""#);
assert_eq!(escape_bat_arguments(&[r"path\"]), r#""path\\""#);
assert_eq!(escape_bat_arguments(&[r"double\\"]), r#""double\\\\""#);
assert_eq!(escape_bat_arguments(&[r"a\b\c"]), r"a\b\c");
assert_eq!(
escape_bat_arguments(&["%PATH%"]),
format!(r#""{e}%PATH{e}%""#, e = BAT_PERCENT_ESCAPE)
);
assert_eq!(
escape_bat_arguments(&["100%"]),
format!(r#""100{e}%""#, e = BAT_PERCENT_ESCAPE)
);
assert_eq!(
escape_bat_arguments(&["line\rbreak"]),
format!(r#""line{e}{cr}break""#, e = BAT_PERCENT_ESCAPE, cr = '\r')
);
assert_eq!(
escape_bat_arguments(&[r#"say "hello""#]),
r#""say ""hello""""#
);
assert_eq!(escape_bat_arguments(&[r#"""#]), r#""""""#);
assert_eq!(escape_bat_arguments(&[r#"a\"b"#]), r#""a\\""b""#);
assert_eq!(escape_bat_arguments(&[r#"a\\"b"#]), r#""a\\\\""b""#);
assert_eq!(escape_bat_arguments(&["a&b"]), r#""a&b""#);
assert_eq!(escape_bat_arguments(&["a|b"]), r#""a|b""#);
assert_eq!(escape_bat_arguments(&["a<b"]), r#""a<b""#);
assert_eq!(escape_bat_arguments(&["a>b"]), r#""a>b""#);
assert_eq!(escape_bat_arguments(&["a^b"]), r#""a^b""#);
assert_eq!(escape_bat_arguments(&["a(b)"]), r#""a(b)""#);
assert_eq!(escape_bat_arguments(&["a;b"]), r#""a;b""#);
assert_eq!(escape_bat_arguments(&["a,b"]), r#""a,b""#);
assert_eq!(escape_bat_arguments(&["a=b"]), r#""a=b""#);
assert_eq!(escape_bat_arguments(&["a!b"]), r#""a!b""#);
assert_eq!(escape_bat_arguments(&["a`b"]), r#""a`b""#);
assert_eq!(escape_bat_arguments(&["a'b"]), r#""a'b""#);
assert_eq!(escape_bat_arguments(&["a[b]"]), r#""a[b]""#);
assert_eq!(escape_bat_arguments(&["a{b}"]), r#""a{b}""#);
assert_eq!(escape_bat_arguments(&["a~b"]), r#""a~b""#);
assert_eq!(escape_bat_arguments(&["a\tb"]), "\"a\tb\"");
assert_eq!(
escape_bat_arguments(&["\u{03c0}\u{042f}\u{97f3}"]),
"\u{03c0}\u{042f}\u{97f3}"
);
assert_eq!(escape_bat_arguments(&["a\x01b"]), "\"a\x01b\"");
assert_eq!(escape_bat_arguments(&["a\nb"]), "\"a\nb\"");
assert_eq!(
escape_bat_arguments(&["hello world", r"C:\path\", "%VAR%"]),
format!(
r#""hello world" "C:\path\\" "{e}%VAR{e}%""#,
e = BAT_PERCENT_ESCAPE
)
);
}
#[test]
fn test_append_bat_arg() {
let mut cmd = String::new();
append_bat_arg(&mut cmd, "simple");
assert_eq!(cmd, "simple");
let mut cmd = String::new();
append_bat_arg(&mut cmd, "with space");
assert_eq!(cmd, "\"with space\"");
let mut cmd = String::new();
append_bat_arg(&mut cmd, "");
assert_eq!(cmd, "\"\"");
let mut cmd = String::new();
append_bat_arg(&mut cmd, r"trailing\");
assert_eq!(cmd, r#""trailing\\""#);
let mut cmd = String::new();
append_bat_arg(&mut cmd, "%VAR%");
assert_eq!(cmd, format!(r#""{e}%VAR{e}%""#, e = BAT_PERCENT_ESCAPE));
let mut cmd = String::new();
append_bat_arg(&mut cmd, r#"say "hi""#);
assert_eq!(cmd, r#""say ""hi""""#);
}
#[test]
fn test_bat_escaping_prevents_injection() {
let escaped = escape_bat_arguments(&["%PATH%"]);
assert!(!escaped.contains("%PATH%") || escaped.contains(BAT_PERCENT_ESCAPE));
let escaped = escape_bat_arguments(&["%COMSPEC%", "/c", "malicious"]);
assert!(escaped.contains(BAT_PERCENT_ESCAPE));
let escaped = escape_bat_arguments(&["%%nested%%"]);
assert!(escaped.matches(BAT_PERCENT_ESCAPE).count() >= 4);
}
}