fix(shell): actually pass spawn its arguments - #77
Merged
Conversation
`craft.shell.spawn(id, cmd, args)` has advertised an arguments array since it
was written, and the SDK's `spawn(command, args?, options?)` says the same
thing. Native declared no `args` field, so `ignore_unknown_fields` parsed the
array and dropped it without a word: `spawn('p', 'git', ['status'])` ran `git`
bare and reported success.
The arguments had nowhere to go because spawn ran `/bin/sh -c <command>`, and a
shell takes one string rather than an argv. Appending them to that string would
have meant quoting every argument correctly, forever, on two platforms, against
input supplied by a web page — to avoid handing that page a shell. So the
default stops using one. `command` is a program, `args` are its arguments, and
`;`, `|` and `$(...)` inside an argument are now bytes the program receives
rather than syntax something interprets.
`shell: true` asks for the old behaviour explicitly, and the SDK has been
documenting that option all along — it was as dead as `args`, since native had
no field for it either. That path keeps the metacharacter blocklist, because it
is the only path that needs one; passing arguments alongside it is refused
rather than half-honoured, since silently ignoring them is the bug being fixed.
This changes what `spawn('p', 'git status')` does. It used to work, because
`sh` split the string; it now looks for a program named "git status". That is
the same break Node made deliberately, and the fix is
`spawn('p', 'git', ['status'])` — what the signature asked for in the first
place. The blocklist already rejected `;`, `|`, `&`, `>` and `<`, so the shell
was contributing word-splitting and nothing else.
The argv construction moves into a pure function and gets the tests the file
never had. Reintroducing the original behaviour — parsing the arguments and
dropping them, compiling cleanly — fails seven of them by name.
Noted, not fixed: the SDK's `ChildProcess` posts to `process.spawn`, and
`process` is not a routed bridge type at all, so that path answers nothing.
Separate from this issue.
✅ Binary load timeWhat this measures
Both binaries are measured interleaved on this runner and compared by |
✅ Binary Size Report
Size limits
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #69.
craft.shell.spawn(id, cmd, args)has advertised an arguments array since it was written. The SDK'sspawn(command, args?, options?)says the same. Native declared noargsfield, soignore_unknown_fieldsparsed the array and dropped it without a word:Why the arguments had nowhere to go
spawnran/bin/sh -c <command>. A shell takes one string, not an argv — so there was no slot to put them in.The issue offers "append them to the argv", but the argv was
{"/bin/sh", "-c", command}. Appending to that means appending to the command string, which means quoting every argument correctly, forever, on two platforms, against input supplied by a web page — in order to avoid handing that page a shell.So the default stops using one:
/bin/sh -c "git"execve("git", ["git", "status"])args;in an argumentsh -cshell: true— and it was already documentedThe SDK's
SpawnOptionshas declaredshell?: booleanall along, saying "The Craft bridge translates this automatically". It was as dead asargs— native had no field for it either. It's real now.That path keeps the metacharacter blocklist, because it's the only path that needs one. Passing
argsalongside it is refused, not half-honoured — silently ignoring arguments is the exact bug being fixed, and the compatibility path must not reintroduce it.The compatibility break, stated plainly
spawn('p', 'git status')used to work, becauseshsplit the string. It now looks for a program named"git status".That is the same break Node made deliberately, and the fix is
spawn('p', 'git', ['status'])— what the signature asked for in the first place. Worth noting how little the shell was contributing:validateCommandalready rejected;,|,&,>,<,\n,$(and backticks, so it provided word-splitting and essentially nothing else. Callers who need more can passshell: true.Tests
bridge_shell.zighad no tests and no test artifact — which is how it shipped parsing an array and throwing it away. The argv decision is now a pure function with 10 tests, wired intozig build test.Controlled: reintroducing the original behaviour — parsing the arguments and dropping them, compiling cleanly — fails 7 tests by name:
(My first attempt at that control failed to compile rather than failing an assertion, which proves nothing about the test. Redone so the broken version builds.)
Also covered: an older payload with no
argskey still spawns the same program, empty-string arguments survive, argument order is pinned, and the JSON→argv seam is tested end to end with the struct the bridge actually decodes with.Noted, not fixed
The SDK's
ChildProcess.start()posts toprocess.spawn, andprocessis not a routed bridge type anywhere in the dispatcher — that path answers nothing at all. Separate from this issue; flagging rather than widening scope into it.Verified
zig build·zig build test·zig build test-js·x86_64-windowscross-build ·zig fmt --check·tsc --noEmit· 511 bun tests · pickier 38 warnings, unchanged from main.