| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237 |
- /** Minimal GDB/MI output parser (plain Node, no dependencies).
- *
- * Handles result records (`TOKEN^done,...`), out-of-band records
- * (`*stopped,...`, `=notify,...`), stream records (`~`, `&`, `@`),
- * the `(gdb)` prompt and anything else (treated as inferior output).
- * Tolerant by design: unparseable lines surface as output, never throw.
- */
- export type MIFields = { [name: string]: MIValue };
- export type MIValue = string | MIFields | MIValue[];
- export interface MIRecord {
- token: string;
- /** `^` result, `*` exec-async, `+` status-async, `=` notify, `~` console, `&` log, `@` target. */
- marker: string;
- /** Class after the marker (`done`, `error`, `stopped`, ...) or '' for streams. */
- cls: string;
- /** Raw remainder after `marker + cls` (for streams: the payload). */
- rest: string;
- fields: MIFields;
- }
- export function classifyLine(line: string): MIRecord | { prompt: true } | { output: string } {
- const trimmed = line.replace(/\r$/, '');
- if (trimmed === '(gdb)') return { prompt: true };
- const m = /^([0-9]*)([\^*+=~&@])(.*)$/.exec(trimmed);
- if (!m) return { output: line };
- const [, token, marker, rest] = m;
- if (marker === '~' || marker === '&') {
- return { token, marker, cls: '', rest, fields: { text: unescapeMIString(rest.trim()) } };
- }
- if (marker === '@') {
- return { token, marker, cls: '', rest, fields: { text: rest } };
- }
- const comma = rest.indexOf(',');
- const cls = comma < 0 ? rest : rest.slice(0, comma);
- const body = comma < 0 ? '' : rest.slice(comma + 1);
- let fields: MIFields = {};
- try {
- fields = body ? parseResultList(body) : {};
- } catch {
- fields = {};
- }
- return { token, marker, cls, rest, fields };
- }
- function isRecord(r: unknown): r is MIRecord {
- return !!r && typeof r === 'object' && 'marker' in (r as object);
- }
- export { isRecord };
- /** Parse `name=value,name=value,...` at top level. */
- export function parseResultList(s: string): MIFields {
- const out: MIFields = {};
- const p = new Cursor(s);
- p.skipSpaces();
- while (!p.eof()) {
- const name = p.readName();
- if (name === null || !p.eat('=')) break;
- out[name] = p.readValue();
- p.skipSpaces();
- if (!p.eat(',')) break;
- p.skipSpaces();
- }
- return out;
- }
- class Cursor {
- pos = 0;
- constructor(public s: string) {}
- eof(): boolean {
- return this.pos >= this.s.length;
- }
- skipSpaces(): void {
- while (this.s[this.pos] === ' ') this.pos++;
- }
- eat(c: string): boolean {
- if (this.s[this.pos] === c) { this.pos++; return true; }
- return false;
- }
- readName(): string | null {
- const m = /^[A-Za-z_][A-Za-z0-9_\-]*/.exec(this.s.slice(this.pos));
- if (!m) return null;
- this.pos += m[0].length;
- return m[0];
- }
- readValue(): MIValue {
- this.skipSpaces();
- const c = this.s[this.pos];
- if (c === '"') return this.readCString();
- if (c === '{') return this.readTuple();
- if (c === '[') return this.readList();
- return this.readConst();
- }
- readCString(): string {
- // Opening quote already peeked.
- this.pos++;
- let out = '';
- while (this.pos < this.s.length) {
- const c = this.s[this.pos];
- if (c === '"') { this.pos++; return out; }
- if (c === '\\') {
- const esc = this.s.slice(this.pos, this.pos + 4);
- const decoded = decodeEscape(esc);
- out += decoded.text;
- this.pos += decoded.length;
- continue;
- }
- out += c;
- this.pos++;
- }
- return out;
- }
- readTuple(): MIFields {
- this.pos++; // {
- const out: MIFields = {};
- this.skipSpaces();
- if (this.eat('}')) return out;
- for (;;) {
- this.skipSpaces();
- const name = this.readName();
- if (name === null || !this.eat('=')) break;
- out[name] = this.readValue();
- this.skipSpaces();
- if (this.eat(',')) continue;
- break;
- }
- this.eat('}');
- return out;
- }
- readList(): MIValue[] {
- this.pos++; // [
- const out: MIValue[] = [];
- this.skipSpaces();
- if (this.eat(']')) return out;
- for (;;) {
- this.skipSpaces();
- out.push(this.readListItem());
- this.skipSpaces();
- if (this.eat(',')) continue;
- break;
- }
- this.eat(']');
- return out;
- }
- readListItem(): MIValue {
- // Items are values, but result-shaped `name=value` items occur as well.
- const save = this.pos;
- const name = this.readName();
- if (name !== null && this.eat('=')) {
- const v = this.readValue();
- const o: MIFields = {};
- o[name] = v;
- return o;
- }
- this.pos = save;
- return this.readValue();
- }
- readConst(): string {
- const start = this.pos;
- while (this.pos < this.s.length && ![',', '}', ']'].includes(this.s[this.pos])) this.pos++;
- return this.s.slice(start, this.pos);
- }
- }
- function decodeEscape(esc: string): { text: string; length: number } {
- const c = esc[1];
- switch (c) {
- case 'n': return { text: '\n', length: 2 };
- case 'r': return { text: '\r', length: 2 };
- case 't': return { text: '\t', length: 2 };
- case 'a': return { text: '\x07', length: 2 };
- case 'b': return { text: '\b', length: 2 };
- case 'f': return { text: '\f', length: 2 };
- case 'v': return { text: '\v', length: 2 };
- case 'e': return { text: '\x1b', length: 2 };
- case '\\': return { text: '\\', length: 2 };
- case '"': return { text: '"', length: 2 };
- case "'": return { text: "'", length: 2 };
- case '0': case '1': case '2': case '3':
- case '4': case '5': case '6': case '7': {
- const m = /^\\([0-7]{1,3})/.exec(esc);
- if (m) return { text: String.fromCharCode(parseInt(m[1], 8)), length: m[0].length };
- return { text: c, length: 2 };
- }
- default: return { text: c ?? '', length: 2 };
- }
- }
- /** Unescape the inside of an MI `"..."` stream payload. */
- export function unescapeMIString(quoted: string): string {
- const inner = quoted.startsWith('"') && quoted.endsWith('"') && quoted.length >= 2
- ? quoted.slice(1, -1)
- : quoted;
- let out = '';
- let i = 0;
- while (i < inner.length) {
- if (inner[i] === '\\') {
- const decoded = decodeEscape(inner.slice(i, i + 4));
- out += decoded.text;
- i += decoded.length;
- } else {
- out += inner[i];
- i++;
- }
- }
- return out;
- }
- // ---- Convenience accessors (all total: undefined on shape mismatch) ----
- export function asString(v: MIValue | undefined): string | undefined {
- return typeof v === 'string' ? v : undefined;
- }
- export function asFields(v: MIValue | undefined): MIFields | undefined {
- return v !== undefined && typeof v === 'object' && !Array.isArray(v) ? v as MIFields : undefined;
- }
- export function asList(v: MIValue | undefined): MIValue[] {
- return Array.isArray(v) ? v : [];
- }
- export function fieldString(fields: MIFields, name: string): string | undefined {
- return asString(fields[name]);
- }
|