#![cfg(windows)]
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
const CHILD: &str = env!("CARGO_BIN_EXE_prick-exec-child");
fn write_shim(dir: &Path, stem: &str) -> PathBuf {
let path = dir.join(format!("{stem}.cmd"));
let body = format!("@echo off\r\n\"{CHILD}\" --print-argv %*\r\n");
std::fs::write(&path, body).expect("writing the shim must succeed");
path
}
fn fixture_with_path(dir: &Path, args: &[&str]) -> Output {
let existing = std::env::var_os("PATH").unwrap_or_default();
let mut search = std::ffi::OsString::from(dir);
search.push(";");
search.push(existing);
Command::new(CHILD)
.args(args)
.env("PATH", search)
.output()
.expect("the fixture binary must be runnable")
}
fn split_argv(stdout: &[u8]) -> Vec<String> {
if stdout.is_empty() {
return Vec::new();
}
stdout.split(|byte| *byte == 0).map(|part| String::from_utf8_lossy(part).into_owned()).collect()
}
#[test]
fn a_shim_is_found_without_naming_its_extension() {
let dir = tempfile::tempdir().expect("a temporary directory");
write_shim(dir.path(), "prickshim");
let out = fixture_with_path(dir.path(), &["--", "prickshim", "hello"]);
assert!(
out.status.success(),
"resolving a .cmd shim failed: {}",
String::from_utf8_lossy(&out.stderr)
);
assert_eq!(split_argv(&out.stdout), ["hello"]);
}
#[test]
fn std_alone_cannot_find_the_shim_which_is_why_this_module_exists() {
let dir = tempfile::tempdir().expect("a temporary directory");
write_shim(dir.path(), "prickshim2");
let existing = std::env::var_os("PATH").unwrap_or_default();
let mut search = std::ffi::OsString::from(dir.path());
search.push(";");
search.push(existing);
let direct = Command::new("prickshim2").env("PATH", search).output();
assert!(direct.is_err(), "std resolved a .cmd without help; the workaround may be obsolete");
}
#[test]
fn adversarial_arguments_reach_the_shim_unchanged() {
let dir = tempfile::tempdir().expect("a temporary directory");
write_shim(dir.path(), "prickargs");
let adversarial = [
r#"a"b"#,
"a&b",
"%PATH%",
"!DELAYED!",
"a b",
"a^b",
"a|b",
"a>b",
"a<b",
"a&&echo pwned",
"(paren)",
r"back\slash",
"100%",
"%%",
"plain",
];
let mut args: Vec<&str> = vec!["--", "prickargs"];
args.extend_from_slice(&adversarial);
let out = fixture_with_path(dir.path(), &args);
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let received = split_argv(&out.stdout);
assert_eq!(received, adversarial, "cmd.exe altered at least one argument");
}
#[test]
fn a_variable_reference_arrives_as_text_rather_than_its_value() {
let dir = tempfile::tempdir().expect("a temporary directory");
write_shim(dir.path(), "prickvar");
let out = fixture_with_path(dir.path(), &["--", "prickvar", "%PATH%", "%USERPROFILE%"]);
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert_eq!(split_argv(&out.stdout), ["%PATH%", "%USERPROFILE%"]);
}
#[test]
fn a_separator_does_not_become_a_second_command() {
let dir = tempfile::tempdir().expect("a temporary directory");
write_shim(dir.path(), "prickinj");
let out = fixture_with_path(dir.path(), &["--", "prickinj", "x&echo INJECTED"]);
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(!stdout.contains("INJECTED\r\n"), "a second command ran: {stdout}");
assert_eq!(split_argv(&out.stdout), ["x&echo INJECTED"]);
}
#[test]
fn an_empty_argument_is_not_dropped_by_cmd() {
let dir = tempfile::tempdir().expect("a temporary directory");
write_shim(dir.path(), "prickempty");
let out = fixture_with_path(dir.path(), &["--", "prickempty", "before", "", "after"]);
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert_eq!(split_argv(&out.stdout), ["before", "", "after"]);
}
#[test]
fn an_argument_containing_a_line_break_is_refused_rather_than_truncated() {
let dir = tempfile::tempdir().expect("a temporary directory");
write_shim(dir.path(), "prickbreak");
let out = fixture_with_path(dir.path(), &["--", "prickbreak", "first\nsecond"]);
assert_ne!(out.status.code(), Some(0), "an unrepresentable argument must not be truncated");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("line break"), "stderr: {stderr}");
}
#[test]
fn a_shim_exit_code_reaches_the_caller() {
let dir = tempfile::tempdir().expect("a temporary directory");
let path = dir.path().join("prickexit.cmd");
std::fs::write(&path, "@echo off\r\nexit /b 42\r\n").expect("write");
let out = fixture_with_path(dir.path(), &["--", "prickexit"]);
assert_eq!(out.status.code(), Some(42));
}
#[test]
fn a_secret_reaches_a_shim_through_the_environment() {
let dir = tempfile::tempdir().expect("a temporary directory");
let path = dir.path().join("prickenv.cmd");
let body = format!("@echo off\r\n\"{CHILD}\" --print-env PRICK_TEST_SECRET\r\n");
std::fs::write(&path, body).expect("write");
let out = fixture_with_path(
dir.path(),
&["--set", "PRICK_TEST_SECRET=through-cmd", "--", "prickenv"],
);
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert_eq!(String::from_utf8_lossy(&out.stdout).trim_end(), "through-cmd");
}
#[test]
fn a_real_executable_does_not_go_through_cmd_at_all() {
let out = Command::new(CHILD)
.args(["--", CHILD, "--print-argv", "a&b", "%PATH%", r#"a"b"#])
.output()
.expect("the fixture binary must be runnable");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert_eq!(split_argv(&out.stdout), ["a&b", "%PATH%", r#"a"b"#]);
}