import { splitLeadingEnvAssignments } from "./shell-env-prefix.js"; import { advanceQuoteEscapeState, readShellChars, type QuoteEscapeState } from "./shell-quote-state.js"; interface ParsedPipeline { segments: string[]; separators: string[]; suffix: string; } interface ProducerRewritePlan { command: string; captureStderr: boolean; } interface ShellSafetyTarget { environmentPrelude: string; command: string; } const SINGLE_QUOTED_SHELL_VALUE_PATTERN = "'(?:'\\\\''|[^'])*'"; const SHELL_ENV_VALUE_PATTERN = `(?:"(?:\\\\.|[^"])*"|${SINGLE_QUOTED_SHELL_VALUE_PATTERN}|[^\\s;]+)`; const LEADING_RTK_DB_PATH_EXPORT_PRELUDE_PATTERN = new RegExp( `^(\\s*export\\s+RTK_DB_PATH=${SHELL_ENV_VALUE_PATTERN}\\s*;\\s*)([\\s\\S]*)$`, "u", ); function splitLeadingRtkDbPathExportPrelude(command: string): ShellSafetyTarget { const match = command.match(LEADING_RTK_DB_PATH_EXPORT_PRELUDE_PATTERN); if (!match) { return { environmentPrelude: "", command }; } return { environmentPrelude: match[1] ?? "", command: match[2] ?? "", }; } function parseSimpleTopLevelPipeline(command: string): ParsedPipeline | null { const segments: string[] = []; const separators: string[] = []; const state: QuoteEscapeState = { quote: null, escaped: false }; let segmentStart = 0; let suffix = ""; for (let index = 0; index < command.length; index += 1) { const { character, nextCharacter } = readShellChars(command, index); const previousCharacter = index > 0 ? (command[index - 1] ?? "") : ""; if (advanceQuoteEscapeState(state, character, "\"'`")) { continue; } if ( (character === "|" && nextCharacter === "|") || (character === "&" && nextCharacter === "&") || character === ";" ) { if (separators.length === 0) { return null; } segments.push(command.slice(segmentStart, index)); suffix = command.slice(index); break; } if (character === "|" && previousCharacter !== ">") { const separatorLength = nextCharacter === "&" ? 2 : 1; segments.push(command.slice(segmentStart, index)); separators.push(command.slice(index, index + separatorLength)); segmentStart = index + separatorLength; if (separatorLength === 2) { index += 1; } continue; } if (character === "&" && nextCharacter !== ">" && previousCharacter !== ">" && previousCharacter !== "<") { return null; } } if (separators.length === 0) { return null; } if (!suffix) { segments.push(command.slice(segmentStart)); } return { segments, separators, suffix }; } function extractProducerRewritePlan(segment: string, firstSeparator: string): ProducerRewritePlan | null { const trimmed = segment.trim(); const { envPrefix, command: commandWithOptionalRedirect } = splitLeadingEnvAssignments(trimmed); if (!/^rtk\s+/i.test(commandWithOptionalRedirect)) { return null; } const stderrMergeMatch = commandWithOptionalRedirect.match(/^(.*?)(?:\s+)?2>\s*&1\s*$/u); if (stderrMergeMatch) { const command = stderrMergeMatch[1]?.trimEnd() ?? ""; return command ? { command: `${envPrefix}${command}`.trim(), captureStderr: true } : null; } return { command: `${envPrefix}${commandWithOptionalRedirect}`.trim(), captureStderr: firstSeparator === "|&", }; } function buildBufferedPipelineCommand( producer: ProducerRewritePlan, remainder: string, ): string { const tempFileVariable = "__pi_rtk_pipe_tmp"; const statusVariable = "__pi_rtk_pipe_status"; const producerRedirect = producer.captureStderr ? `> "$${tempFileVariable}" 2>&1` : `> "$${tempFileVariable}"`; const cleanupTrap = `rm -f "$${tempFileVariable}"`; return [ "{", `${tempFileVariable}="$(mktemp)" || exit $?;`, `${statusVariable}=0;`, `trap '${cleanupTrap}' EXIT HUP INT TERM;`, `${producer.command} ${producerRedirect};`, `${statusVariable}=$?;`, `if [ $${statusVariable} -eq 0 ]; then (${remainder}) < "$${tempFileVariable}"; ${statusVariable}=$?; fi;`, `exit $${statusVariable};`, "}", ].join(" "); } export function applyRewrittenCommandShellSafetyFixups(command: string, platform: string = process.platform): string { if (platform !== "win32") { return command; } const target = splitLeadingRtkDbPathExportPrelude(command); const parsedPipeline = parseSimpleTopLevelPipeline(target.command); if (!parsedPipeline) { return command; } const producer = extractProducerRewritePlan(parsedPipeline.segments[0] ?? "", parsedPipeline.separators[0] ?? ""); if (!producer) { return command; } const remainder = parsedPipeline.segments .slice(1) .map((segment, index) => `${index === 0 ? "" : (parsedPipeline.separators[index] ?? "")}${segment}`) .join("") .trim(); if (!remainder) { return command; } const suffix = parsedPipeline.suffix ? ` ${parsedPipeline.suffix.trimStart()}` : ""; return `${target.environmentPrelude}${buildBufferedPipelineCommand(producer, remainder)}${suffix}`; }