import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; 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 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 }, ]; type ShellWrapperCommand = { isWrapper: boolean; command: string | null; }; type ShellWrapperCandidate = { argv: string[]; token0: string; state: TState; }; function resolveShellWrapperCandidate(params: { argv: string[]; depth: number; state: TState; onDispatchUnwrap?: (state: TState, wrappedArgv: string[]) => TState; }): ShellWrapperCandidate | null { if (!isWithinDispatchClassificationDepth(params.depth)) { return null; } const token0 = params.argv[0]?.trim(); if (!token0) { return null; } const dispatchUnwrap = unwrapKnownDispatchWrapperInvocation(params.argv); if (dispatchUnwrap.kind === "blocked") { return null; } if (dispatchUnwrap.kind === "unwrapped") { return resolveShellWrapperCandidate({ ...params, argv: dispatchUnwrap.argv, depth: params.depth + 1, state: params.onDispatchUnwrap?.(params.state, params.argv) ?? params.state, }); } const shellMultiplexerUnwrap = unwrapKnownShellMultiplexerInvocation(params.argv); if (shellMultiplexerUnwrap.kind === "blocked") { return null; } if (shellMultiplexerUnwrap.kind === "unwrapped") { return resolveShellWrapperCandidate({ ...params, argv: shellMultiplexerUnwrap.argv, depth: params.depth + 1, }); } return { argv: params.argv, token0, state: params.state }; } function resolveShellWrapperSpecAndArgvInternal( argv: string[], depth: number, ): { argv: string[]; wrapper: ShellWrapperSpec; payload: string } | null { const candidate = resolveShellWrapperCandidate({ argv, depth, state: null }); if (!candidate) { return null; } const wrapper = findShellWrapperSpec(normalizeExecutableToken(candidate.token0)); if (!wrapper) { return null; } const payload = extractShellWrapperPayload(candidate.argv, wrapper); if (!payload) { return null; } return { argv: candidate.argv, wrapper, payload }; } 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 isShellWrapperInvocationInternal(argv: string[], depth: number): boolean { const candidate = resolveShellWrapperCandidate({ argv, depth, state: null }); return candidate ? isShellWrapperExecutable(candidate.token0) : false; } export function isShellWrapperInvocation(argv: string[]): boolean { return isShellWrapperInvocationInternal(argv, 0); } 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; } 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 = normalizeLowercaseStringOrEmpty(item); 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); } throw new Error("Unsupported shell wrapper kind"); } function hasEnvManipulationBeforeShellWrapperInternal( argv: string[], depth: number, envManipulationSeen: boolean, ): boolean { const candidate = resolveShellWrapperCandidate({ argv, depth, state: envManipulationSeen, onDispatchUnwrap: (state, wrappedArgv) => state || hasDispatchEnvManipulation(wrappedArgv), }); if (!candidate) { return false; } const wrapper = findShellWrapperSpec(normalizeExecutableToken(candidate.token0)); if (!wrapper) { return false; } const payload = extractShellWrapperPayload(candidate.argv, wrapper); if (!payload) { return false; } return candidate.state; } export function hasEnvManipulationBeforeShellWrapper(argv: string[]): boolean { return hasEnvManipulationBeforeShellWrapperInternal(argv, 0, false); } function extractShellWrapperCommandInternal( argv: string[], rawCommand: string | null, depth: number, ): ShellWrapperCommand { const resolved = resolveShellWrapperSpecAndArgvInternal(argv, depth); if (!resolved) { return { isWrapper: false, command: null }; } return { isWrapper: true, command: rawCommand ?? resolved.payload }; } export function resolveShellWrapperTransportArgv(argv: string[]): string[] | null { return resolveShellWrapperSpecAndArgvInternal(argv, 0)?.argv ?? null; } 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); }