import { MAX_DISPATCH_WRAPPER_DEPTH, hasDispatchEnvManipulation, unwrapKnownDispatchWrapperInvocation, } from "./dispatch-wrapper-resolution.js"; import { normalizeExecutableToken } from "./exec-wrapper-tokens.js"; import { POSIX_INLINE_COMMAND_FLAGS, POWERSHELL_INLINE_COMMAND_FLAGS, resolveInlineCommandMatch, } from "./shell-inline-command.js"; 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 SHELL_MULTIPLEXER_WRAPPER_NAMES = ["busybox", "toybox"] as const; function withWindowsExeAliases(names: readonly string[]): string[] { const expanded = new Set(); for (const name of names) { expanded.add(name); expanded.add(`${name}.exe`); } return Array.from(expanded); } 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)); const POSIX_SHELL_WRAPPER_CANONICAL = new Set(POSIX_SHELL_WRAPPER_NAMES); const WINDOWS_CMD_WRAPPER_CANONICAL = new Set(WINDOWS_CMD_WRAPPER_NAMES); const POWERSHELL_WRAPPER_CANONICAL = new Set(POWERSHELL_WRAPPER_NAMES); const SHELL_MULTIPLEXER_WRAPPER_CANONICAL = new Set(SHELL_MULTIPLEXER_WRAPPER_NAMES); const SHELL_WRAPPER_CANONICAL = new Set([ ...POSIX_SHELL_WRAPPER_NAMES, ...WINDOWS_CMD_WRAPPER_NAMES, ...POWERSHELL_WRAPPER_NAMES, ]); type ShellWrapperKind = "posix" | "cmd" | "powershell"; type ShellWrapperSpec = { kind: ShellWrapperKind; names: ReadonlySet; }; const SHELL_WRAPPER_SPECS: ReadonlyArray = [ { kind: "posix", names: POSIX_SHELL_WRAPPER_CANONICAL }, { kind: "cmd", names: WINDOWS_CMD_WRAPPER_CANONICAL }, { kind: "powershell", names: POWERSHELL_WRAPPER_CANONICAL }, ]; export type ShellWrapperCommand = { isWrapper: boolean; command: string | null; }; function isWithinDispatchClassificationDepth(depth: number): boolean { return depth <= MAX_DISPATCH_WRAPPER_DEPTH; } 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 { for (const spec of SHELL_WRAPPER_SPECS) { if (spec.names.has(baseExecutable)) { return spec; } } return null; } export type ShellMultiplexerUnwrapResult = | { kind: "not-wrapper" } | { kind: "blocked"; wrapper: string } | { kind: "unwrapped"; wrapper: string; argv: string[] }; export function unwrapKnownShellMultiplexerInvocation( argv: string[], ): ShellMultiplexerUnwrapResult { const token0 = argv[0]?.trim(); if (!token0) { return { kind: "not-wrapper" }; } const wrapper = normalizeExecutableToken(token0); if (!SHELL_MULTIPLEXER_WRAPPER_CANONICAL.has(wrapper)) { return { kind: "not-wrapper" }; } let appletIndex = 1; if (argv[appletIndex]?.trim() === "--") { appletIndex += 1; } const applet = argv[appletIndex]?.trim(); if (!applet || !isShellWrapperExecutable(applet)) { return { kind: "blocked", wrapper }; } const unwrapped = argv.slice(appletIndex); if (unwrapped.length === 0) { return { kind: "blocked", wrapper }; } return { kind: "unwrapped", wrapper, argv: unwrapped }; } function extractPosixShellInlineCommand(argv: string[]): string | null { return extractInlineCommandByFlags(argv, POSIX_INLINE_COMMAND_FLAGS, { allowCombinedC: true }); } function extractCmdInlineCommand(argv: string[]): string | null { const idx = argv.findIndex((item) => { const token = item.trim().toLowerCase(); return token === "/c" || token === "/k"; }); if (idx === -1) { return null; } const tail = argv.slice(idx + 1); if (tail.length === 0) { return null; } const cmd = tail.join(" ").trim(); return cmd.length > 0 ? cmd : null; } function extractPowerShellInlineCommand(argv: string[]): string | null { return extractInlineCommandByFlags(argv, POWERSHELL_INLINE_COMMAND_FLAGS); } function extractInlineCommandByFlags( argv: string[], flags: ReadonlySet, options: { allowCombinedC?: boolean } = {}, ): string | null { return resolveInlineCommandMatch(argv, flags, options).command; } function extractShellWrapperPayload(argv: string[], spec: ShellWrapperSpec): string | null { switch (spec.kind) { case "posix": return extractPosixShellInlineCommand(argv); case "cmd": return extractCmdInlineCommand(argv); case "powershell": return extractPowerShellInlineCommand(argv); } } function hasEnvManipulationBeforeShellWrapperInternal( argv: string[], depth: number, envManipulationSeen: boolean, ): boolean { if (!isWithinDispatchClassificationDepth(depth)) { return false; } const token0 = argv[0]?.trim(); if (!token0) { return false; } const dispatchUnwrap = unwrapKnownDispatchWrapperInvocation(argv); if (dispatchUnwrap.kind === "blocked") { return false; } if (dispatchUnwrap.kind === "unwrapped") { const nextEnvManipulationSeen = envManipulationSeen || hasDispatchEnvManipulation(argv); return hasEnvManipulationBeforeShellWrapperInternal( dispatchUnwrap.argv, depth + 1, nextEnvManipulationSeen, ); } const shellMultiplexerUnwrap = unwrapKnownShellMultiplexerInvocation(argv); if (shellMultiplexerUnwrap.kind === "blocked") { return false; } if (shellMultiplexerUnwrap.kind === "unwrapped") { return hasEnvManipulationBeforeShellWrapperInternal( shellMultiplexerUnwrap.argv, depth + 1, envManipulationSeen, ); } const wrapper = findShellWrapperSpec(normalizeExecutableToken(token0)); if (!wrapper) { return false; } const payload = extractShellWrapperPayload(argv, wrapper); if (!payload) { return false; } return envManipulationSeen; } export function hasEnvManipulationBeforeShellWrapper(argv: string[]): boolean { return hasEnvManipulationBeforeShellWrapperInternal(argv, 0, false); } function extractShellWrapperCommandInternal( argv: string[], rawCommand: string | null, depth: number, ): ShellWrapperCommand { if (!isWithinDispatchClassificationDepth(depth)) { return { isWrapper: false, command: null }; } const token0 = argv[0]?.trim(); if (!token0) { return { isWrapper: false, command: null }; } 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 shellMultiplexerUnwrap = unwrapKnownShellMultiplexerInvocation(argv); if (shellMultiplexerUnwrap.kind === "blocked") { return { isWrapper: false, command: null }; } if (shellMultiplexerUnwrap.kind === "unwrapped") { return extractShellWrapperCommandInternal(shellMultiplexerUnwrap.argv, rawCommand, depth + 1); } const wrapper = findShellWrapperSpec(normalizeExecutableToken(token0)); if (!wrapper) { return { isWrapper: false, command: null }; } const payload = extractShellWrapperPayload(argv, wrapper); if (!payload) { return { isWrapper: false, command: null }; } 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, ): ShellWrapperCommand { return extractShellWrapperCommandInternal(argv, normalizeRawCommand(rawCommand), 0); }