* Parse a PowerShell command using the native AST parser. * Spawns pwsh to parse the command and returns structured results. * Results are memoized by command string. * * @param command - The PowerShell command to parse * @returns Parsed command structure, or a result with valid=false on failur
( command: string, )
| 1134 | * @returns Parsed command structure, or a result with valid=false on failure |
| 1135 | */ |
| 1136 | async function parsePowerShellCommandImpl( |
| 1137 | command: string, |
| 1138 | ): Promise<ParsedPowerShellCommand> { |
| 1139 | // SECURITY: MAX_COMMAND_LENGTH is a UTF-8 BYTE budget (see derivation at the |
| 1140 | // constant definition). command.length counts UTF-16 code units; a CJK |
| 1141 | // character is 1 code unit but 3 UTF-8 bytes, so .length under-reports by |
| 1142 | // up to 3× and allows argv overflow on Windows → CreateProcess fails → |
| 1143 | // valid:false → deny rules degrade to ask. Finding #36. |
| 1144 | const commandBytes = Buffer.byteLength(command, 'utf8') |
| 1145 | if (commandBytes > MAX_COMMAND_LENGTH) { |
| 1146 | logForDebugging( |
| 1147 | `PowerShell parser: command too long (${commandBytes} bytes, max ${MAX_COMMAND_LENGTH})`, |
| 1148 | ) |
| 1149 | return makeInvalidResult( |
| 1150 | command, |
| 1151 | `Command too long for parsing (${commandBytes} bytes). Maximum supported length is ${MAX_COMMAND_LENGTH} bytes.`, |
| 1152 | 'CommandTooLong', |
| 1153 | ) |
| 1154 | } |
| 1155 | |
| 1156 | const pwshPath = await getCachedPowerShellPath() |
| 1157 | if (!pwshPath) { |
| 1158 | return makeInvalidResult( |
| 1159 | command, |
| 1160 | 'PowerShell is not available', |
| 1161 | 'NoPowerShell', |
| 1162 | ) |
| 1163 | } |
| 1164 | |
| 1165 | const script = buildParseScript(command) |
| 1166 | |
| 1167 | // Pass the script to PowerShell via -EncodedCommand. |
| 1168 | // -EncodedCommand takes a Base64-encoded UTF-16LE string and executes it, |
| 1169 | // which avoids: (1) stdin interactive-mode issues where -File - produces |
| 1170 | // PS prompts and ANSI escapes in stdout, (2) command-line escaping issues, |
| 1171 | // (3) temp files. The script itself is large but well within OS arg limits |
| 1172 | // (Windows: 32K chars, Unix: typically 2MB+). |
| 1173 | const encodedScript = toUtf16LeBase64(script) |
| 1174 | const args = [ |
| 1175 | '-NoProfile', |
| 1176 | '-NonInteractive', |
| 1177 | '-NoLogo', |
| 1178 | '-EncodedCommand', |
| 1179 | encodedScript, |
| 1180 | ] |
| 1181 | |
| 1182 | // Spawn pwsh with one retry on timeout. On loaded CI runners (Windows |
| 1183 | // especially), pwsh spawn + .NET JIT + ParseInput occasionally exceeds 5s |
| 1184 | // even after CAN_SPAWN_PARSE_SCRIPT() warms the JIT. execa kills the process |
| 1185 | // but exitCode is undefined, which the old code reported as the misleading |
| 1186 | // "pwsh exited with code 1:" with empty stderr. A single retry absorbs |
| 1187 | // transient load spikes; a double timeout is reported as PwshTimeout. |
| 1188 | const parseTimeoutMs = getParseTimeoutMs() |
| 1189 | let stdout = '' |
| 1190 | let stderr = '' |
| 1191 | let code: number | null = null |
| 1192 | let timedOut = false |
| 1193 | for (let attempt = 0; attempt < 2; attempt++) { |
no test coverage detected