Expand description
Building a cmd.exe command line for a batch shim.
§Why this module has to exist at all
npm, pnpm, npx, yarn and tsc are .cmd files on Windows, not
executables. Two separate things then go wrong:
std::process::Command’s program resolution only ever appends.exe, soCommand::new("npm")does not findnpm.cmdand fails with “not found”.crate::launchresolves throughwhich, which honoursPATHEXT.- A batch file is not executed by the loader;
cmd.exeinterprets it. That means the arguments are parsed twice – once whencmd.exereads the command line we hand it, and again inside the script when%*is substituted into a line and that line is re-parsed. Getting this wrong is CVE-2024-24576: an argument containing&becomes a second command.
§The algorithm
This mirrors the escaping the Rust standard library adopted for the
CVE-2024-24576 fix, which is the only construction that is known to survive
both parses. Reimplementing it here rather than deferring to std is what
lets prk run -- npm test work: std applies it only when it resolved the
program to a .bat/.cmd, and it never resolves npm to npm.cmd at all.
The pieces, each of which is load-bearing:
| Piece | Defeats |
|---|---|
| Wrap the whole command in one outer quote pair | cmd.exe’s argument splitting |
/s | The conditional “should I strip the outer quotes” rule, which is genuinely hard to predict |
/d | An AutoRun registry value running before the command |
/v:OFF | !DELAYED! expansion |
/e:ON | Needed for the % construction below to evaluate |
| Quote any argument that is not purely alphanumeric | & | < > ( ) ^ and whitespace splitting |
Double an inner " rather than backslash-escaping it | A \" would end the quoted region as far as cmd.exe is concerned, exposing everything after it |
Replace % with %%cd:~,% | %PATH% expanding to its value |
The % construction deserves a sentence. %cd:~,% is a substring of the
built-in cd variable with an empty start and end index, so it expands to
nothing. Splicing that no-op in front of every % leaves the text unchanged
but leaves cmd.exe with no %NAME% pair to match, so nothing expands.
§What cannot be escaped
\r and \n terminate a cmd.exe command line; there is no encoding that
carries them through. They are rejected rather than silently dropped –
silently dropping half an argument is how a prk run invocation quietly
does something other than what it was asked to.
§Portability of this module
Everything here operates on UTF-16 code units and is compiled on every
platform, so the adversarial-argument tests run on Linux and macOS CI too.
Only crate::launch restricts its use to Windows.
Structs§
- Utf16
Display - A
Vec<u16>rendered for a test assertion or a diagnostic.
Enums§
- CmdLine
Error - An argument that cannot be carried through
cmd.exeat all.
Constants§
- CMD_
SWITCHES - Switches passed to
cmd.exeahead of the command itself.
Functions§
- append_
argument - Appends one escaped argument to a
cmd.execommand line. - batch_
command_ line - Builds the argument string for
cmd.exe, runningscriptwithargs.