Cleanup code, add some tests

This commit is contained in:
Sander Declerck 2025-10-23 13:23:08 +02:00
parent c74c23b0ff
commit 08c1328b52
No known key found for this signature in database
2 changed files with 91 additions and 8 deletions

View file

@ -1,22 +1,33 @@
import { spawn, execSync } from "child_process";
import os from "os";
function escapeArg(arg) {
// Shell metacharacters that need escaping
// These characters have special meaning in shells and need to be quoted
const shellMetaChars = /[ "&'|;<>()$`\\!*?[\]{}~#]/;
function sanitizeShellArgument(arg) {
// If argument contains shell metacharacters, wrap in double quotes
// and escape characters that are special even inside double quotes
if (shellMetaChars.test(arg)) {
if (hasShellMetaChars(arg)) {
// Inside double quotes, we need to escape: " $ ` \
return '"' + arg.replace(/(["`$\\])/g, "\\$1") + '"';
return '"' + escapeDoubleQuoteContent(arg) + '"';
}
return arg;
}
function hasShellMetaChars(arg) {
// Shell metacharacters that need escaping
// These characters have special meaning in shells and need to be quoted
// Whenever one of these characters is present, we should quote the argument
// Characters: space, ", &, ', |, ;, <, >, (, ), $, `, \, !, *, ?, [, ], {, }, ~, #
const shellMetaChars = /[ "&'|;<>()$`\\!*?[\]{}~#]/;
return shellMetaChars.test(arg);
}
function escapeDoubleQuoteContent(arg) {
// Escape special characters for shell safety
// This escapes ", $, `, and \ by prefixing them with a backslash
return arg.replace(/(["`$\\])/g, "\\$1");
}
function buildCommand(command, args) {
const escapedArgs = args.map(escapeArg);
const escapedArgs = args.map(sanitizeShellArgument);
return `${command} ${escapedArgs.join(" ")}`;
}

View file

@ -112,4 +112,76 @@ describe("safeSpawn", () => {
);
assert.strictEqual(spawnCalls[0].options.shell, true);
});
it("should escape dollar signs to prevent variable expansion", async () => {
await safeSpawn("echo", ["$HOME/test"]);
assert.strictEqual(spawnCalls.length, 1);
assert.strictEqual(spawnCalls[0].command, 'echo "\\$HOME/test"');
});
it("should escape backticks to prevent command substitution", async () => {
await safeSpawn("echo", ["file`whoami`.txt"]);
assert.strictEqual(spawnCalls.length, 1);
assert.strictEqual(spawnCalls[0].command, 'echo "file\\`whoami\\`.txt"');
});
it("should escape backslashes properly", async () => {
await safeSpawn("echo", ["path\\with\\backslash"]);
assert.strictEqual(spawnCalls.length, 1);
assert.strictEqual(
spawnCalls[0].command,
'echo "path\\\\with\\\\backslash"'
);
});
it("should handle multiple special characters in one argument", async () => {
await safeSpawn("cmd", ['test "quoted" $var `cmd` & more']);
assert.strictEqual(spawnCalls.length, 1);
assert.strictEqual(
spawnCalls[0].command,
'cmd "test \\"quoted\\" \\$var \\`cmd\\` & more"'
);
});
it("should handle pipe character", async () => {
await safeSpawn("echo", ["foo|bar"]);
assert.strictEqual(spawnCalls.length, 1);
assert.strictEqual(spawnCalls[0].command, 'echo "foo|bar"');
});
it("should handle parentheses", async () => {
await safeSpawn("echo", ["(test)"]);
assert.strictEqual(spawnCalls.length, 1);
assert.strictEqual(spawnCalls[0].command, 'echo "(test)"');
});
it("should handle angle brackets for redirection", async () => {
await safeSpawn("echo", ["foo>output.txt"]);
assert.strictEqual(spawnCalls.length, 1);
assert.strictEqual(spawnCalls[0].command, 'echo "foo>output.txt"');
});
it("should handle wildcard characters", async () => {
await safeSpawn("echo", ["*.txt"]);
assert.strictEqual(spawnCalls.length, 1);
assert.strictEqual(spawnCalls[0].command, 'echo "*.txt"');
});
it("should handle multiple arguments with mixed escaping needs", async () => {
await safeSpawn("cmd", ["safe", "needs space", "$dangerous", "also-safe"]);
assert.strictEqual(spawnCalls.length, 1);
assert.strictEqual(
spawnCalls[0].command,
'cmd safe "needs space" "\\$dangerous" also-safe'
);
});
});