refactor(exec): unify wrapper resolution and split approvals tests
This commit is contained in:
@@ -2,32 +2,51 @@ import path from "node:path";
|
||||
|
||||
export const MAX_DISPATCH_WRAPPER_DEPTH = 4;
|
||||
|
||||
export const POSIX_SHELL_WRAPPERS = new Set(["ash", "bash", "dash", "fish", "ksh", "sh", "zsh"]);
|
||||
export const WINDOWS_CMD_WRAPPERS = new Set(["cmd.exe", "cmd"]);
|
||||
export const POWERSHELL_WRAPPERS = new Set(["powershell", "powershell.exe", "pwsh", "pwsh.exe"]);
|
||||
export const DISPATCH_WRAPPER_EXECUTABLES = new Set([
|
||||
const WINDOWS_EXE_SUFFIX = ".exe";
|
||||
|
||||
const POSIX_SHELL_WRAPPER_NAMES = ["ash", "bash", "dash", "fish", "ksh", "sh", "zsh"] as const;
|
||||
const WINDOWS_CMD_WRAPPER_NAMES = ["cmd"] as const;
|
||||
const POWERSHELL_WRAPPER_NAMES = ["powershell", "pwsh"] as const;
|
||||
const DISPATCH_WRAPPER_NAMES = [
|
||||
"chrt",
|
||||
"chrt.exe",
|
||||
"doas",
|
||||
"doas.exe",
|
||||
"env",
|
||||
"env.exe",
|
||||
"ionice",
|
||||
"ionice.exe",
|
||||
"nice",
|
||||
"nice.exe",
|
||||
"nohup",
|
||||
"nohup.exe",
|
||||
"setsid",
|
||||
"setsid.exe",
|
||||
"stdbuf",
|
||||
"stdbuf.exe",
|
||||
"sudo",
|
||||
"sudo.exe",
|
||||
"taskset",
|
||||
"taskset.exe",
|
||||
"timeout",
|
||||
"timeout.exe",
|
||||
] as const;
|
||||
|
||||
function withWindowsExeAliases(names: readonly string[]): string[] {
|
||||
const expanded = new Set<string>();
|
||||
for (const name of names) {
|
||||
expanded.add(name);
|
||||
expanded.add(`${name}${WINDOWS_EXE_SUFFIX}`);
|
||||
}
|
||||
return Array.from(expanded);
|
||||
}
|
||||
|
||||
function stripWindowsExeSuffix(value: string): string {
|
||||
return value.endsWith(WINDOWS_EXE_SUFFIX) ? value.slice(0, -WINDOWS_EXE_SUFFIX.length) : value;
|
||||
}
|
||||
|
||||
export const POSIX_SHELL_WRAPPERS = new Set(POSIX_SHELL_WRAPPER_NAMES);
|
||||
export const WINDOWS_CMD_WRAPPERS = new Set(withWindowsExeAliases(WINDOWS_CMD_WRAPPER_NAMES));
|
||||
export const POWERSHELL_WRAPPERS = new Set(withWindowsExeAliases(POWERSHELL_WRAPPER_NAMES));
|
||||
export const DISPATCH_WRAPPER_EXECUTABLES = new Set(withWindowsExeAliases(DISPATCH_WRAPPER_NAMES));
|
||||
|
||||
const POSIX_SHELL_WRAPPER_CANONICAL = new Set<string>(POSIX_SHELL_WRAPPER_NAMES);
|
||||
const WINDOWS_CMD_WRAPPER_CANONICAL = new Set<string>(WINDOWS_CMD_WRAPPER_NAMES);
|
||||
const POWERSHELL_WRAPPER_CANONICAL = new Set<string>(POWERSHELL_WRAPPER_NAMES);
|
||||
const DISPATCH_WRAPPER_CANONICAL = new Set<string>(DISPATCH_WRAPPER_NAMES);
|
||||
const SHELL_WRAPPER_CANONICAL = new Set<string>([
|
||||
...POSIX_SHELL_WRAPPER_NAMES,
|
||||
...WINDOWS_CMD_WRAPPER_NAMES,
|
||||
...POWERSHELL_WRAPPER_NAMES,
|
||||
]);
|
||||
|
||||
const POSIX_INLINE_COMMAND_FLAGS = new Set(["-lc", "-c", "--command"]);
|
||||
@@ -58,9 +77,9 @@ type ShellWrapperSpec = {
|
||||
};
|
||||
|
||||
const SHELL_WRAPPER_SPECS: ReadonlyArray<ShellWrapperSpec> = [
|
||||
{ kind: "posix", names: POSIX_SHELL_WRAPPERS },
|
||||
{ kind: "cmd", names: WINDOWS_CMD_WRAPPERS },
|
||||
{ kind: "powershell", names: POWERSHELL_WRAPPERS },
|
||||
{ kind: "posix", names: POSIX_SHELL_WRAPPER_CANONICAL },
|
||||
{ kind: "cmd", names: WINDOWS_CMD_WRAPPER_CANONICAL },
|
||||
{ kind: "powershell", names: POWERSHELL_WRAPPER_CANONICAL },
|
||||
];
|
||||
|
||||
export type ShellWrapperCommand = {
|
||||
@@ -75,14 +94,27 @@ export function basenameLower(token: string): string {
|
||||
return base.trim().toLowerCase();
|
||||
}
|
||||
|
||||
export function normalizeExecutableToken(token: string): string {
|
||||
return stripWindowsExeSuffix(basenameLower(token));
|
||||
}
|
||||
|
||||
export function isDispatchWrapperExecutable(token: string): boolean {
|
||||
return DISPATCH_WRAPPER_CANONICAL.has(normalizeExecutableToken(token));
|
||||
}
|
||||
|
||||
export function isShellWrapperExecutable(token: string): boolean {
|
||||
return SHELL_WRAPPER_CANONICAL.has(normalizeExecutableToken(token));
|
||||
}
|
||||
|
||||
function normalizeRawCommand(rawCommand?: string | null): string | null {
|
||||
const trimmed = rawCommand?.trim() ?? "";
|
||||
return trimmed.length > 0 ? trimmed : null;
|
||||
}
|
||||
|
||||
function findShellWrapperSpec(baseExecutable: string): ShellWrapperSpec | null {
|
||||
const canonicalBase = stripWindowsExeSuffix(baseExecutable);
|
||||
for (const spec of SHELL_WRAPPER_SPECS) {
|
||||
if (spec.names.has(baseExecutable)) {
|
||||
if (spec.names.has(canonicalBase)) {
|
||||
return spec;
|
||||
}
|
||||
}
|
||||
@@ -93,7 +125,16 @@ export function isEnvAssignment(token: string): boolean {
|
||||
return /^[A-Za-z_][A-Za-z0-9_]*=.*/.test(token);
|
||||
}
|
||||
|
||||
export function unwrapEnvInvocation(argv: string[]): string[] | null {
|
||||
type WrapperScanDirective = "continue" | "consume-next" | "stop" | "invalid";
|
||||
|
||||
function scanWrapperInvocation(
|
||||
argv: string[],
|
||||
params: {
|
||||
separators?: ReadonlySet<string>;
|
||||
onToken: (token: string, lowerToken: string) => WrapperScanDirective;
|
||||
adjustCommandIndex?: (commandIndex: number, argv: string[]) => number | null;
|
||||
},
|
||||
): string[] | null {
|
||||
let idx = 1;
|
||||
let expectsOptionValue = false;
|
||||
while (idx < argv.length) {
|
||||
@@ -107,27 +148,48 @@ export function unwrapEnvInvocation(argv: string[]): string[] | null {
|
||||
idx += 1;
|
||||
continue;
|
||||
}
|
||||
if (token === "--" || token === "-") {
|
||||
if (params.separators?.has(token)) {
|
||||
idx += 1;
|
||||
break;
|
||||
}
|
||||
if (isEnvAssignment(token)) {
|
||||
idx += 1;
|
||||
continue;
|
||||
const directive = params.onToken(token, token.toLowerCase());
|
||||
if (directive === "stop") {
|
||||
break;
|
||||
}
|
||||
if (token.startsWith("-") && token !== "-") {
|
||||
const lower = token.toLowerCase();
|
||||
if (directive === "invalid") {
|
||||
return null;
|
||||
}
|
||||
if (directive === "consume-next") {
|
||||
expectsOptionValue = true;
|
||||
}
|
||||
idx += 1;
|
||||
}
|
||||
if (expectsOptionValue) {
|
||||
return null;
|
||||
}
|
||||
const commandIndex = params.adjustCommandIndex ? params.adjustCommandIndex(idx, argv) : idx;
|
||||
if (commandIndex === null || commandIndex >= argv.length) {
|
||||
return null;
|
||||
}
|
||||
return argv.slice(commandIndex);
|
||||
}
|
||||
|
||||
export function unwrapEnvInvocation(argv: string[]): string[] | null {
|
||||
return scanWrapperInvocation(argv, {
|
||||
separators: new Set(["--", "-"]),
|
||||
onToken: (token, lower) => {
|
||||
if (isEnvAssignment(token)) {
|
||||
return "continue";
|
||||
}
|
||||
if (!token.startsWith("-") || token === "-") {
|
||||
return "stop";
|
||||
}
|
||||
const [flag] = lower.split("=", 2);
|
||||
if (ENV_FLAG_OPTIONS.has(flag)) {
|
||||
idx += 1;
|
||||
continue;
|
||||
return "continue";
|
||||
}
|
||||
if (ENV_OPTIONS_WITH_VALUE.has(flag)) {
|
||||
if (!lower.includes("=")) {
|
||||
expectsOptionValue = true;
|
||||
}
|
||||
idx += 1;
|
||||
continue;
|
||||
return lower.includes("=") ? "continue" : "consume-next";
|
||||
}
|
||||
if (
|
||||
lower.startsWith("-u") ||
|
||||
@@ -140,195 +202,131 @@ export function unwrapEnvInvocation(argv: string[]): string[] | null {
|
||||
lower.startsWith("--ignore-signal=") ||
|
||||
lower.startsWith("--block-signal=")
|
||||
) {
|
||||
idx += 1;
|
||||
continue;
|
||||
return "continue";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return idx < argv.length ? argv.slice(idx) : null;
|
||||
return "invalid";
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function unwrapNiceInvocation(argv: string[]): string[] | null {
|
||||
let idx = 1;
|
||||
let expectsOptionValue = false;
|
||||
while (idx < argv.length) {
|
||||
const token = argv[idx]?.trim() ?? "";
|
||||
if (!token) {
|
||||
idx += 1;
|
||||
continue;
|
||||
}
|
||||
if (expectsOptionValue) {
|
||||
expectsOptionValue = false;
|
||||
idx += 1;
|
||||
continue;
|
||||
}
|
||||
if (token === "--") {
|
||||
idx += 1;
|
||||
break;
|
||||
}
|
||||
if (token.startsWith("-") && token !== "-") {
|
||||
const lower = token.toLowerCase();
|
||||
return scanWrapperInvocation(argv, {
|
||||
separators: new Set(["--"]),
|
||||
onToken: (token, lower) => {
|
||||
if (!token.startsWith("-") || token === "-") {
|
||||
return "stop";
|
||||
}
|
||||
const [flag] = lower.split("=", 2);
|
||||
if (/^-\d+$/.test(lower)) {
|
||||
idx += 1;
|
||||
continue;
|
||||
return "continue";
|
||||
}
|
||||
if (NICE_OPTIONS_WITH_VALUE.has(flag)) {
|
||||
if (!lower.includes("=") && lower === flag) {
|
||||
expectsOptionValue = true;
|
||||
}
|
||||
idx += 1;
|
||||
continue;
|
||||
return lower.includes("=") || lower !== flag ? "continue" : "consume-next";
|
||||
}
|
||||
if (lower.startsWith("-n") && lower.length > 2) {
|
||||
idx += 1;
|
||||
continue;
|
||||
return "continue";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (expectsOptionValue) {
|
||||
return null;
|
||||
}
|
||||
return idx < argv.length ? argv.slice(idx) : null;
|
||||
return "invalid";
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function unwrapNohupInvocation(argv: string[]): string[] | null {
|
||||
let idx = 1;
|
||||
while (idx < argv.length) {
|
||||
const token = argv[idx]?.trim() ?? "";
|
||||
if (!token) {
|
||||
idx += 1;
|
||||
continue;
|
||||
}
|
||||
if (token === "--") {
|
||||
idx += 1;
|
||||
break;
|
||||
}
|
||||
if (token.startsWith("-") && token !== "-") {
|
||||
const lower = token.toLowerCase();
|
||||
if (lower === "--help" || lower === "--version") {
|
||||
idx += 1;
|
||||
continue;
|
||||
return scanWrapperInvocation(argv, {
|
||||
separators: new Set(["--"]),
|
||||
onToken: (token, lower) => {
|
||||
if (!token.startsWith("-") || token === "-") {
|
||||
return "stop";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return idx < argv.length ? argv.slice(idx) : null;
|
||||
return lower === "--help" || lower === "--version" ? "continue" : "invalid";
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function unwrapStdbufInvocation(argv: string[]): string[] | null {
|
||||
let idx = 1;
|
||||
let expectsOptionValue = false;
|
||||
while (idx < argv.length) {
|
||||
const token = argv[idx]?.trim() ?? "";
|
||||
if (!token) {
|
||||
idx += 1;
|
||||
continue;
|
||||
}
|
||||
if (expectsOptionValue) {
|
||||
expectsOptionValue = false;
|
||||
idx += 1;
|
||||
continue;
|
||||
}
|
||||
if (token === "--") {
|
||||
idx += 1;
|
||||
break;
|
||||
}
|
||||
if (token.startsWith("-") && token !== "-") {
|
||||
const lower = token.toLowerCase();
|
||||
return scanWrapperInvocation(argv, {
|
||||
separators: new Set(["--"]),
|
||||
onToken: (token, lower) => {
|
||||
if (!token.startsWith("-") || token === "-") {
|
||||
return "stop";
|
||||
}
|
||||
const [flag] = lower.split("=", 2);
|
||||
if (STDBUF_OPTIONS_WITH_VALUE.has(flag)) {
|
||||
if (!lower.includes("=")) {
|
||||
expectsOptionValue = true;
|
||||
}
|
||||
idx += 1;
|
||||
continue;
|
||||
return lower.includes("=") ? "continue" : "consume-next";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (expectsOptionValue) {
|
||||
return null;
|
||||
}
|
||||
return idx < argv.length ? argv.slice(idx) : null;
|
||||
return "invalid";
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function unwrapTimeoutInvocation(argv: string[]): string[] | null {
|
||||
let idx = 1;
|
||||
let expectsOptionValue = false;
|
||||
while (idx < argv.length) {
|
||||
const token = argv[idx]?.trim() ?? "";
|
||||
if (!token) {
|
||||
idx += 1;
|
||||
continue;
|
||||
}
|
||||
if (expectsOptionValue) {
|
||||
expectsOptionValue = false;
|
||||
idx += 1;
|
||||
continue;
|
||||
}
|
||||
if (token === "--") {
|
||||
idx += 1;
|
||||
break;
|
||||
}
|
||||
if (token.startsWith("-") && token !== "-") {
|
||||
const lower = token.toLowerCase();
|
||||
return scanWrapperInvocation(argv, {
|
||||
separators: new Set(["--"]),
|
||||
onToken: (token, lower) => {
|
||||
if (!token.startsWith("-") || token === "-") {
|
||||
return "stop";
|
||||
}
|
||||
const [flag] = lower.split("=", 2);
|
||||
if (TIMEOUT_FLAG_OPTIONS.has(flag)) {
|
||||
idx += 1;
|
||||
continue;
|
||||
return "continue";
|
||||
}
|
||||
if (TIMEOUT_OPTIONS_WITH_VALUE.has(flag)) {
|
||||
if (!lower.includes("=")) {
|
||||
expectsOptionValue = true;
|
||||
}
|
||||
idx += 1;
|
||||
continue;
|
||||
return lower.includes("=") ? "continue" : "consume-next";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (expectsOptionValue || idx >= argv.length) {
|
||||
return null;
|
||||
}
|
||||
idx += 1; // duration
|
||||
return idx < argv.length ? argv.slice(idx) : null;
|
||||
return "invalid";
|
||||
},
|
||||
adjustCommandIndex: (commandIndex, currentArgv) => {
|
||||
// timeout consumes a required duration token before the wrapped command.
|
||||
const wrappedCommandIndex = commandIndex + 1;
|
||||
return wrappedCommandIndex < currentArgv.length ? wrappedCommandIndex : null;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function unwrapKnownDispatchWrapperInvocation(argv: string[]): string[] | null | undefined {
|
||||
export type DispatchWrapperUnwrapResult =
|
||||
| { kind: "not-wrapper" }
|
||||
| { kind: "blocked"; wrapper: string }
|
||||
| { kind: "unwrapped"; wrapper: string; argv: string[] };
|
||||
|
||||
function blockDispatchWrapper(wrapper: string): DispatchWrapperUnwrapResult {
|
||||
return { kind: "blocked", wrapper };
|
||||
}
|
||||
|
||||
function unwrapDispatchWrapper(
|
||||
wrapper: string,
|
||||
unwrapped: string[] | null,
|
||||
): DispatchWrapperUnwrapResult {
|
||||
return unwrapped
|
||||
? { kind: "unwrapped", wrapper, argv: unwrapped }
|
||||
: blockDispatchWrapper(wrapper);
|
||||
}
|
||||
|
||||
export function unwrapKnownDispatchWrapperInvocation(argv: string[]): DispatchWrapperUnwrapResult {
|
||||
const token0 = argv[0]?.trim();
|
||||
if (!token0) {
|
||||
return undefined;
|
||||
return { kind: "not-wrapper" };
|
||||
}
|
||||
const base = basenameLower(token0);
|
||||
const normalizedBase = base.endsWith(".exe") ? base.slice(0, -4) : base;
|
||||
switch (normalizedBase) {
|
||||
const wrapper = normalizeExecutableToken(token0);
|
||||
switch (wrapper) {
|
||||
case "env":
|
||||
return unwrapEnvInvocation(argv);
|
||||
return unwrapDispatchWrapper(wrapper, unwrapEnvInvocation(argv));
|
||||
case "nice":
|
||||
return unwrapNiceInvocation(argv);
|
||||
return unwrapDispatchWrapper(wrapper, unwrapNiceInvocation(argv));
|
||||
case "nohup":
|
||||
return unwrapNohupInvocation(argv);
|
||||
return unwrapDispatchWrapper(wrapper, unwrapNohupInvocation(argv));
|
||||
case "stdbuf":
|
||||
return unwrapStdbufInvocation(argv);
|
||||
return unwrapDispatchWrapper(wrapper, unwrapStdbufInvocation(argv));
|
||||
case "timeout":
|
||||
return unwrapTimeoutInvocation(argv);
|
||||
return unwrapDispatchWrapper(wrapper, unwrapTimeoutInvocation(argv));
|
||||
case "chrt":
|
||||
case "doas":
|
||||
case "ionice":
|
||||
case "setsid":
|
||||
case "sudo":
|
||||
case "taskset":
|
||||
return null;
|
||||
return blockDispatchWrapper(wrapper);
|
||||
default:
|
||||
return undefined;
|
||||
return { kind: "not-wrapper" };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -338,32 +336,47 @@ export function unwrapDispatchWrappersForResolution(
|
||||
): string[] {
|
||||
let current = argv;
|
||||
for (let depth = 0; depth < maxDepth; depth += 1) {
|
||||
const unwrapped = unwrapKnownDispatchWrapperInvocation(current);
|
||||
if (unwrapped === undefined) {
|
||||
const unwrap = unwrapKnownDispatchWrapperInvocation(current);
|
||||
if (unwrap.kind !== "unwrapped" || unwrap.argv.length === 0) {
|
||||
break;
|
||||
}
|
||||
if (!unwrapped || unwrapped.length === 0) {
|
||||
break;
|
||||
}
|
||||
current = unwrapped;
|
||||
current = unwrap.argv;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function extractPosixShellInlineCommand(argv: string[]): string | null {
|
||||
const flag = argv[1]?.trim();
|
||||
if (!flag) {
|
||||
return null;
|
||||
for (let i = 1; i < argv.length; i += 1) {
|
||||
const token = argv[i]?.trim();
|
||||
if (!token) {
|
||||
continue;
|
||||
}
|
||||
const lower = token.toLowerCase();
|
||||
if (lower === "--") {
|
||||
break;
|
||||
}
|
||||
if (POSIX_INLINE_COMMAND_FLAGS.has(lower)) {
|
||||
const cmd = argv[i + 1]?.trim();
|
||||
return cmd ? cmd : null;
|
||||
}
|
||||
if (/^-[^-]*c[^-]*$/i.test(token)) {
|
||||
const commandIndex = lower.indexOf("c");
|
||||
const inline = token.slice(commandIndex + 1).trim();
|
||||
if (inline) {
|
||||
return inline;
|
||||
}
|
||||
const cmd = argv[i + 1]?.trim();
|
||||
return cmd ? cmd : null;
|
||||
}
|
||||
}
|
||||
if (!POSIX_INLINE_COMMAND_FLAGS.has(flag.toLowerCase())) {
|
||||
return null;
|
||||
}
|
||||
const cmd = argv[2]?.trim();
|
||||
return cmd ? cmd : null;
|
||||
return null;
|
||||
}
|
||||
|
||||
function extractCmdInlineCommand(argv: string[]): string | null {
|
||||
const idx = argv.findIndex((item) => item.trim().toLowerCase() === "/c");
|
||||
const idx = argv.findIndex((item) => {
|
||||
const token = item.trim().toLowerCase();
|
||||
return token === "/c" || token === "/k";
|
||||
});
|
||||
if (idx === -1) {
|
||||
return null;
|
||||
}
|
||||
@@ -418,15 +431,15 @@ function extractShellWrapperCommandInternal(
|
||||
return { isWrapper: false, command: null };
|
||||
}
|
||||
|
||||
const base0 = basenameLower(token0);
|
||||
if (DISPATCH_WRAPPER_EXECUTABLES.has(base0)) {
|
||||
const unwrapped = unwrapKnownDispatchWrapperInvocation(argv);
|
||||
if (!unwrapped) {
|
||||
return { isWrapper: false, command: null };
|
||||
}
|
||||
return extractShellWrapperCommandInternal(unwrapped, rawCommand, depth + 1);
|
||||
const dispatchUnwrap = unwrapKnownDispatchWrapperInvocation(argv);
|
||||
if (dispatchUnwrap.kind === "blocked") {
|
||||
return { isWrapper: false, command: null };
|
||||
}
|
||||
if (dispatchUnwrap.kind === "unwrapped") {
|
||||
return extractShellWrapperCommandInternal(dispatchUnwrap.argv, rawCommand, depth + 1);
|
||||
}
|
||||
|
||||
const base0 = normalizeExecutableToken(token0);
|
||||
const wrapper = findShellWrapperSpec(base0);
|
||||
if (!wrapper) {
|
||||
return { isWrapper: false, command: null };
|
||||
@@ -440,6 +453,11 @@ function extractShellWrapperCommandInternal(
|
||||
return { isWrapper: true, command: rawCommand ?? payload };
|
||||
}
|
||||
|
||||
export function extractShellWrapperInlineCommand(argv: string[]): string | null {
|
||||
const extracted = extractShellWrapperCommandInternal(argv, null, 0);
|
||||
return extracted.isWrapper ? extracted.command : null;
|
||||
}
|
||||
|
||||
export function extractShellWrapperCommand(
|
||||
argv: string[],
|
||||
rawCommand?: string | null,
|
||||
|
||||
Reference in New Issue
Block a user