diff --git a/src/artifacts/ReleaseExposureReader.test.ts b/src/artifacts/ReleaseExposureReader.test.ts index cffe9c7951..99a25c0cd8 100644 --- a/src/artifacts/ReleaseExposureReader.test.ts +++ b/src/artifacts/ReleaseExposureReader.test.ts @@ -3068,6 +3068,11 @@ describe("ReleaseExposureReader Node shebang scripts", () => { ["#!/usr/bin/env node"], ["#!/usr/bin/env node\r"], ["#!/usr/bin/env -S node --no-warnings"], + ["#!/usr/bin/env -S -u NODE_OPTIONS node"], + ["#!/usr/bin/env -u NODE_OPTIONS node"], + ["#!/usr/bin/env -C /srv/app node"], + ["#!/usr/bin/env -Snode --no-warnings"], + ["#!/usr/bin/env --unset NODE_OPTIONS -- node"], ["#!/usr/local/bin/node --no-warnings"], ["#! /usr/bin/nodejs"], ])("reads the map of %j in a directory", async (shebang) => { diff --git a/src/artifacts/ReleaseExposureReader.ts b/src/artifacts/ReleaseExposureReader.ts index 0b0d3735d8..4218f99619 100644 --- a/src/artifacts/ReleaseExposureReader.ts +++ b/src/artifacts/ReleaseExposureReader.ts @@ -1308,12 +1308,47 @@ const hasNodeShebang = (prefix: Buffer): boolean => { const [line = ""] = prefix.toString("latin1", 2).split("\n", 1); const [interpreter = "", ...args] = line.trim().split(/[ \t]+/u); const program = - basename(interpreter) === "env" - ? args.find((arg) => !arg.startsWith("-") && !arg.includes("=")) - : interpreter; + basename(interpreter) === "env" ? envProgram(args) : interpreter; return program !== undefined && /^node(?:js)?$/u.test(basename(program)); }; +/** `env` options whose argument is the next field, as in `-u NODE_OPTIONS`. */ +const ENV_OPTION_WITH_ARGUMENT = new Set([ + "-u", + "--unset", + "-C", + "--chdir", + "-P", +]); + +/** + * The program an `env` shebang runs: the first field that is neither an + * option, an option's argument, nor a `NAME=value` setting. `-S` splits the + * rest of the line, so its attached text is read as the next fields. + */ +const envProgram = (args: string[]): string | undefined => { + const fields = [...args]; + while (fields.length > 0) { + const field = fields.shift() ?? ""; + if (field === "--") return fields[0]; + if (ENV_OPTION_WITH_ARGUMENT.has(field)) { + fields.shift(); + continue; + } + if (/^-[0iv]*S./u.test(field)) { + fields.unshift(field.slice(field.indexOf("S") + 1)); + continue; + } + if (field.startsWith("--split-string=")) { + fields.unshift(field.slice("--split-string=".length)); + continue; + } + if (field.startsWith("-") || field.includes("=")) continue; + return field; + } + return undefined; +}; + /** * A name is not a format: a package member called `payload.json` or * `payload.js.map` can be a PE or an ASAR, and a script or map declaration