/** * Undo the CLIXML that Windows PowerShell writes to its OWN stderr when we run * it with -EncodedCommand. * * WHY THIS EXISTS AT ALL. `shell: "powershell"` is implemented in exec-shell.ts * by handing powershell.exe a base64 -EncodedCommand argument — see * wrapForPowerShell there for why base64 and not quoting (metacharacters do not * merely break quoting through cmd.exe's parser, they REPARSE into a different * command). That choice is not negotiable, and it has one measured side effect: * with -EncodedCommand, PowerShell 5.1 stops writing its error/warning/verbose/ * debug/progress streams as plain text and instead SERIALIZES them to its stderr * as CLIXML. The caller then receives, verbatim: * * #< CLIXML * … * Preparing modules for first use. * * on EVERY call (module auto-loading progress), and — much worse — a real error * comes back shredded. Measured on a real Windows box, `Write-Error * "this-is-a-real-error"` arrived as: * * … : this_x000D__x000A_-is-a-real-error_x000D__x000A_ * + CategoryInfo : NotSpecified: (:) [Write-Error], * WriteErrorException_x000D__x000A_… * * i.e. the message split across elements at PowerShell's own console-width wrap * points and `_xNNNN_`-escaped. An AI agent reading that has to reverse-engineer * PowerShell's serializer to recover one sentence. Reassembling it here is what * turns that back into the text a human sees on a console. * * WHAT WAS RULED OUT, MEASURED, so nobody "simplifies" this away: * * - `-OutputFormat Text` DOES NOT WORK. Tested against our exact invocation: * `powershell -NoProfile -NonInteractive -OutputFormat Text -EncodedCommand * ` produced BYTE-IDENTICAL CLIXML. (An earlier probe that appeared to * work had used -Command, not -EncodedCommand — the flag was never the cause. * The trigger is -EncodedCommand itself.) Do not replace this file with a * flag; the flag was tried on the machine that has the bug. * - Dropping -EncodedCommand restores the injection hazard it was chosen for, * and the interior-line-break guarantee that rides on it. * - A temp .ps1 file with -File breaks the launcher's trust model: the command * travels only over the bounded stdin protocol, never disk, never a command * line. * - Prepending `$ProgressPreference='SilentlyContinue'` silences the progress * noise but NOT the error serialization — the half that actually hurts — and * it edits the caller's script, which this layer must not do. * * So the decoding happens HERE, on the transport, because the CLIXML is an * artifact of OUR wrapping choice and the caller's script is untouched by it. * executor.ts feeds stderr through this ONLY when exec-shell.ts says it did the * PowerShell wrapping (ExecShellPlan.clixmlStderr); cmd, sh and bash never see * an instance of this class and are byte-for-byte unaffected. * * THE HARD PART, and the reason this is a state machine rather than an XML * parse: RAW STDERR INTERLEAVES WITH THE SERIALIZED BLOCK. `[Console]::Error. * WriteLine("direct-stderr-line")` bypasses PowerShell's serializer entirely, * and measured output put it BETWEEN the marker and the element: * * #< CLIXML * direct-stderr-line * * * The marker is therefore NOT followed by well-formed XML, and any raw text must * survive verbatim and in its original position. On top of that the pipe splits * chunks anywhere — mid-tag, mid-escape — and remote_exec STREAMS stderr, so * whatever is emitted cannot be recalled. Hence: incremental, per-record, * bounded, and pass-through-on-doubt. * * BYTES, NOT TEXT. Everything below works on latin1 strings — one JS char per * byte — so any byte sequence round-trips exactly and nothing is transcoded by * accident (PowerShell writes this stream in the console output encoding, which * is not necessarily UTF-8 and is not ours to reinterpret). The only place a * code point is created rather than copied is an unescape, and there it is * written back as UTF-8 bytes; every escape that occurs in practice * (_x000D_, _x000A_, _x0009_, _x005F_) is ASCII and identical either way. */ /** * Turn the text content of one CLIXML `` element back into console text. * * Two layers, in the order PowerShell applied them in reverse. First XML entity * references, because the serializer XML-escapes after it escapes control * characters. Then the `_xHHHH_` form, which is how CLIXML carries anything XML * cannot: `_x000D_` is CR, `_x000A_` is LF, `_x0009_` is TAB — and `_x005F_` is * the escape for a LITERAL underscore, which is what makes a single left-to-right * pass correct. A message containing the text `_x000D_` is serialized as * `_x005F_x000D_`; scanning left to right consumes `_x005F_` first, yields `_`, * and leaves `x000D_` as the ordinary characters they were. A right-to-left or * repeated pass would "helpfully" turn that back into a carriage return. */ export declare function decodeClixmlText(text: string): string; /** * Incremental CLIXML-on-stderr decoder for the PowerShell path. * * ONE INSTANCE PER COMMAND. `push` returns the bytes to forward for that chunk * (possibly empty); `flush` returns whatever is still held when the command * ends. Between them nothing is ever dropped except things we positively * identified as PowerShell's own framing. * * WHAT IS TREATED AS POWERSHELL'S FRAMING, and what a hostile command can still * do. An earlier version of this comment claimed the marker "can only" be * PowerShell's because a command would have to be first on stderr to forge it. * That was FALSE, and the code was correspondingly unsafe: PowerShell writes * `#< CLIXML` at offset 0 on every single call — that is the premise of this * whole module — so the decoder stayed armed for the rest of the command and * happily read an `` the COMMAND printed later as its own envelope. A * script whose stderr ended in `SECRET` * had those bytes DELETED. Losing a caller's stderr to our parser is strictly * worse than the noise we remove, so the rule is now positional and narrow: * * 1. An envelope is entered ONLY at a place PowerShell alone writes framing: * directly after a `#< CLIXML` line that sat at stream offset zero, or * directly after an envelope we ourselves closed (optionally across one line * break). One `#< CLIXML` arms the decoder for exactly ONE ``; after * `` it is disarmed until another marker arrives in that position. * A bare `` in the middle of a command's output is therefore just * text now, and is forwarded untouched. * 2. Inside an envelope, anything that is not a record shape we produced is * forwarded verbatim AND marks the envelope "dirty": from that point on this * block is no longer purely PowerShell's, so nothing further is deleted from * it — not the progress records, not even the `` that closes it. * 3. Character data is never deleted anywhere else. Records are decoded, raw * interleaved text (whitespace included) is passed through in place, and the * only bytes dropped are the marker line, the envelope's own tags, the `` * tags around a record we decoded, and progress records in an undirtied * envelope. * * WHAT REMAINS POSSIBLE, stated honestly. A command that writes `#< CLIXML` at * offset 0 of stderr — which requires PowerShell to have written nothing at all * before it — can hand the decoder its own envelope and mangle its OWN bytes in * a way it chose. A command that prints a CLIXML `` * record while PowerShell's own envelope is open loses that record, because at * that point it is byte-for-byte indistinguishable from the module-autoload * progress this exists to remove (a caller's real `Write-Progress` is dropped by * design too). Nothing else: it cannot touch another command's output (one * instance per command), cannot make the decoder delete ordinary text, cannot * make it allocate without bound (MAX_PENDING_BYTES), cannot make it throw (pump * is try/caught into pass-through), and cannot make it reorder anything. */ export declare class PowerShellClixmlDecoder { /** * `header` — deciding whether what follows is PowerShell's marker. * `headerRetry` — after an envelope closed: allow one line break, then re-run * the marker test, so a SECOND serialized block is recognised * instead of leaking its `#< CLIXML` line as text. * `headerEol` — marker matched, swallowing the newline that follows it. * `scan` — armed: everything is raw, watching for this marker's ``: records are decoded, stray text stays raw. * `raw` — give up, forever: forward every byte untouched. */ private mode; /** Unconsumed input, latin1 (one char per byte). */ private buf; /** * Structural bytes we have consumed but not yet committed to dropping — in * practice the `` opening tag, and the single line break allowed * before a SECOND `#< CLIXML` marker (headerRetry), which is only framing if * that marker actually turns up. * * It is held rather than dropped so that a block which turns out NOT to be * something we understand can be forwarded byte-for-byte instead of arriving * headless. It is discarded the moment one record decodes successfully, and * flushed ahead of any raw text so ordering is never disturbed. * * The `#< CLIXML` line itself is NOT held: matched where only PowerShell * writes it, it is unambiguously PowerShell's framing, and * holding it would mean emitting it in front of interleaved raw text in the * measured `#< CLIXML` / direct-stderr-line / `` case — reintroducing * exactly the noise this class removes. */ private pendingPrefix; /** Output collected during the current push/flush. */ private out; /** * Set when this envelope turned out to contain something we did not produce. * * From then on the block is not purely PowerShell's, so nothing more is * deleted from it — including its own `` and any progress record, which * in a block a command is writing into may well be the command's. Cleared when * the next envelope opens. */ private envelopeDirty; push(chunk: Buffer): Buffer; /** * End of stream: forward everything still held, verbatim. * * A CLIXML block can be cut off mid-record (PowerShell killed, output * truncated). Emitting the fragment raw is the only option that loses nothing; * a caller seeing a stray `` tail can still read the message * inside it, whereas a caller seeing nothing cannot. */ flush(): Buffer; private take; /** * Drive the state machine as far as the buffered bytes allow. * * Wrapped whole in a try/catch on purpose: a parser bug here would cost a * caller their stderr, which is strictly worse than the noise we are removing, * so ANY throw degrades to forwarding the bytes untouched. */ private run; /** Forward everything held and stop decoding for the rest of this command. */ private giveUp; /** Forward bytes as-is, flushing any held structural prefix ahead of them. */ private emitRaw; /** A record decoded cleanly: the structural bytes around it are now noise. */ private emitDecoded; private pump; /** * ARMED, between a marker and its envelope. Everything is the command's own * stderr and is forwarded untouched; we are only watching for the envelope to * start. Measured: a `[Console]::Error.WriteLine` really does land in here, * between the marker and `` needs a second `#< CLIXML` in front of it (headerRetry), * so an `` the command prints later is text, not an envelope. * * Returns true if the state changed and pump should keep going. */ private scanForObjs; /** * True for a progress record we are willing to DELETE. * * Progress records are module-autoload noise ("Preparing modules for first * use.") that PowerShell only serializes because it is talking to a pipe, so * in a block that is entirely PowerShell's they carry nothing the caller asked * for. In a block that has already proved to contain something we did not * produce, the next progress record may be the command's own bytes — and this * decoder does not delete those. Noise is recoverable; stderr is not. */ private isDeletableProgress; /** * Inside ``. Consume exactly one child — a record, a stray text run, or * the closing tag — and say whether pump may continue. * * Anything we do not positively recognize is FORWARDED, in place, and marks * the envelope dirty. It is never guessed at and never dropped, and it no * longer ends decoding either: a single `<` in a script's own stderr * (`echo "a&2`, XML output, a nested tool) used to send the rest of the * command down the pass-through path, which handed every LATER error back in * exactly the shredded form this module exists to undo. */ private parseObjsChild; /** * Index just past the `` matching an `` that opened at 0, or -1. * * Depth-counted, because a serialized record nests. `` must not read as its close (the * alternation is ordered so the longer tag wins), and a self-closing `` * changes no depth. */ private findObjEnd; }