Skip to content

Commit

Permalink
fix: Correctly quote NODE_OPTIONS when spaces occur in paths
Browse files Browse the repository at this point in the history
Fixes #122
  • Loading branch information
dividedmind committed Mar 18, 2024
1 parent c864d36 commit 841646f
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 5 deletions.
5 changes: 3 additions & 2 deletions src/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import YAML from "yaml";
import config from "./config";
import { info } from "./message";
import { version } from "./metadata";
import NodeOptions from "./util/NodeOptions";
import forwardSignals from "./util/forwardSignals";
import { readPkgUp } from "./util/readPkgUp";

Expand Down Expand Up @@ -101,9 +102,9 @@ function isTsEsmLoaderNeeded(cmd: string, args: string[]) {
}

function addNodeOptions(...options: string[]) {
const envar = (process.env.NODE_OPTIONS ?? "").split(" ");
const envar = new NodeOptions(process.env.NODE_OPTIONS);
envar.push(...options);
process.env.NODE_OPTIONS = envar.join(" ");
process.env.NODE_OPTIONS = envar.toString();
}

/* Heuristic to check if argument is a node script. Currently just checks if it's an existing file
Expand Down
49 changes: 49 additions & 0 deletions src/util/NodeOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
export default class NodeOptions extends Array {
constructor(value?: string) {
super();
if (!value) return;
if (typeof value !== "string") throw new Error("NODE_OPTIONS must be a string");

this.push(...parseNodeOptionsEnvVar(value));
}

toString(): string {
return this.map(maybeQuote).join(" ");
}

static get [Symbol.species]() {
return Array;
}
}

// based on ParseNodeOptionsEnvVar()
// https://github.com/nodejs/node/blob/39f1b899cd536de4d4c9bbf56f24927d8d06999a/src/node_options.cc#L1397
function parseNodeOptionsEnvVar(nodeOptions: string): string[] {
const result: string[] = [];
let buffer = "";
let inString = false;
for (let i = 0; i < nodeOptions.length; i++) {
let c = nodeOptions[i];
if (c === "\\" && inString) {
if (i + 1 === nodeOptions.length) break;
c = nodeOptions[++i];
} else if (c === " " && !inString) {
result.push(buffer);
buffer = "";
continue;
} else if (c === '"') {
inString = !inString;
continue;
}

buffer += c;
}

result.push(buffer);
return result;
}

function maybeQuote(value: string): string {
if (value.includes(" ")) return `"${value.replace("\\", "\\\\").replace('"', '\\"')}"`;
return value;
}
15 changes: 15 additions & 0 deletions src/util/__tests__/NodeOptions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import NodeOptions from "../NodeOptions";

describe(NodeOptions, () => {
it("splits the options correctly", () => {
const options = new NodeOptions('--option "spaced argument"');
expect(Array.from(options)).toEqual(["--option", "spaced argument"]);
});

it("stringifies spaced options properly", () => {
const opts = new NodeOptions("");
opts.push("--option", 'spaced \\argu"ment');
expect(opts.toString()).toBe('--option "spaced \\\\argu\\"ment"');
expect(Array.from(new NodeOptions(opts.toString()))).toEqual(Array.from(opts));
});
});
7 changes: 4 additions & 3 deletions test/smoketest.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import tmp from "tmp";
* it doesn't crash and produces an AppMap. */

// Create a temporary directory to work in.
const tmpDir = tmp.dirSync({ unsafeCleanup: true }).name;
// To test NODE_OPTIONS handling, use a space in the directory name and use mjs.
const tmpDir = tmp.dirSync({ unsafeCleanup: true, template: "appmap node test XXXXXX" }).name;

// Package up the appmap-node package.
runCommand("yarn", "pack", "-o", join(tmpDir, "appmap-node.tgz"));
Expand All @@ -26,7 +27,7 @@ runCommand("npm", "init", "-y");
runCommand("npm", "install", join(tmpDir, "appmap-node.tgz"));

writeFileSync(
"index.js",
"index.mjs",
`
function main() {
console.log("Hello world!");
Expand All @@ -35,7 +36,7 @@ main();
`,
);

runCommand("npm", "exec", "appmap-node", "index.js");
runCommand("npm", "exec", "appmap-node", "index.mjs");

// verify that appmap has been created
const files = glob.globSync("tmp/**/*.appmap.json");
Expand Down

0 comments on commit 841646f

Please sign in to comment.