// SPDX-License-Identifier: GPL-2.0-or-later // Copyright (C) 2026 Bryan Joshua Pedini /** * Go string quoting/unquoting compatible with strconv.Quote / strconv.Unquote, * as used by `token list` (DESCRIPTION column), slog's text handler and the * configuration error messages. */ /** Characters Go's strconv.IsPrint accepts: letters, marks, numbers, punctuation, symbols, and ASCII space. */ const PRINTABLE = /^[\p{L}\p{M}\p{N}\p{P}\p{S} ]$/u; /** * Go strconv.Quote of a byte string (Buffer) or a JS string (encoded as UTF-8). * Note: relies on Node's Unicode tables; avoid code points newer than Go's tables in tests. */ export function goQuote(input: string | Buffer): string { const bytes = typeof input === 'string' ? Buffer.from(input, 'utf8') : input; let out = '"'; let i = 0; while (i < bytes.length) { const b = bytes[i]; if (b < 0x80) { i++; const c = String.fromCharCode(b); switch (c) { case '"': out += '\\"'; continue; case '\\': out += '\\\\'; continue; case '\x07': out += '\\a'; continue; case '\b': out += '\\b'; continue; case '\f': out += '\\f'; continue; case '\n': out += '\\n'; continue; case '\r': out += '\\r'; continue; case '\t': out += '\\t'; continue; case '\v': out += '\\v'; continue; } if (b < 0x20 || b === 0x7f) out += '\\x' + hex2(b); else out += c; continue; } // Multi-byte: decode one UTF-8 sequence strictly const cp = decodeUtf8At(bytes, i); if (!cp) { out += '\\x' + hex2(b); i++; continue; } const ch = String.fromCodePoint(cp.codePoint); if (PRINTABLE.test(ch)) out += ch; else if (cp.codePoint < 0x10000) out += '\\u' + cp.codePoint.toString(16).padStart(4, '0'); else out += '\\U' + cp.codePoint.toString(16).padStart(8, '0'); i += cp.length; } return out + '"'; } function hex2(b: number): string { return b.toString(16).padStart(2, '0'); } /** Strict UTF-8 decode of the sequence at i (rejects overlongs, surrogates, > U+10FFFF). */ function decodeUtf8At(buf: Buffer, i: number): { codePoint: number; length: number } | undefined { const b0 = buf[i]; let len: number; let cp: number; let min: number; if (b0 >= 0xc2 && b0 <= 0xdf) { len = 2; cp = b0 & 0x1f; min = 0x80; } else if (b0 >= 0xe0 && b0 <= 0xef) { len = 3; cp = b0 & 0x0f; min = 0x800; } else if (b0 >= 0xf0 && b0 <= 0xf4) { len = 4; cp = b0 & 0x07; min = 0x10000; } else return undefined; if (i + len > buf.length) return undefined; for (let k = 1; k < len; k++) { const b = buf[i + k]; if ((b & 0xc0) !== 0x80) return undefined; cp = (cp << 6) | (b & 0x3f); } if (cp < min || cp > 0x10ffff || (cp >= 0xd800 && cp <= 0xdfff)) return undefined; return { codePoint: cp, length: len }; } /** * Go strconv.Unquote for double-quoted strings. Returns the raw bytes (descriptions may * contain invalid UTF-8). Throws on malformed input. */ export function goUnquoteBytes(quoted: string): Buffer { if (quoted.length < 2 || quoted[0] !== '"' || quoted[quoted.length - 1] !== '"') { throw new Error(`not a Go double-quoted string: ${JSON.stringify(quoted)}`); } const s = quoted.slice(1, -1); const out: number[] = []; const pushStr = (str: string) => out.push(...Buffer.from(str, 'utf8')); for (let i = 0; i < s.length; ) { const c = s[i]; if (c === '"') throw new Error(`unescaped quote in Go string: ${JSON.stringify(quoted)}`); if (c !== '\\') { const cp = s.codePointAt(i)!; const ch = String.fromCodePoint(cp); pushStr(ch); i += ch.length; continue; } const e = s[i + 1]; const simple: Record = { a: 7, b: 8, f: 12, n: 10, r: 13, t: 9, v: 11, '\\': 92, '"': 34, "'": 39 }; if (e in simple) { out.push(simple[e]); i += 2; } else if (e === 'x') { out.push(parseHex(s.slice(i + 2, i + 4), quoted)); i += 4; } else if (e === 'u') { pushStr(String.fromCodePoint(parseHex(s.slice(i + 2, i + 6), quoted))); i += 6; } else if (e === 'U') { pushStr(String.fromCodePoint(parseHex(s.slice(i + 2, i + 10), quoted))); i += 10; } else if (e >= '0' && e <= '7') { const oct = s.slice(i + 1, i + 4); if (!/^[0-7]{3}$/.test(oct)) throw new Error(`bad octal escape in ${JSON.stringify(quoted)}`); out.push(parseInt(oct, 8)); i += 4; } else { throw new Error(`bad escape \\${e} in Go string ${JSON.stringify(quoted)}`); } } return Buffer.from(out); } /** goUnquoteBytes decoded as UTF-8 (invalid sequences become U+FFFD). */ export function goUnquote(quoted: string): string { return goUnquoteBytes(quoted).toString('utf8'); } function parseHex(h: string, ctx: string): number { if (!/^[0-9a-fA-F]+$/.test(h)) throw new Error(`bad hex escape in ${JSON.stringify(ctx)}`); return parseInt(h, 16); } /** POSIX sh single-quoting: the result is always one shell word with the exact bytes of s. */ export function shellQuote(s: string): string { return `'${s.split("'").join(`'\\''`)}'`; }