import { LLSDTokenType } from './LLSDTokenType'; import type { LLSDType } from './LLSDType'; import type { LLSDTokenSpec } from './LLSDTokenSpec'; import type { LLSDToken } from './LLSDToken'; import type { LLSDTokenGenerator } from './LLSDTokenGenerator'; import { LLSDMap } from './LLSDMap'; import { UUID } from '../UUID'; import { LLSDArray } from './LLSDArray'; import { LLSDInteger } from "./LLSDInteger"; import { LLSDReal } from "./LLSDReal"; import { LLSDURI } from "./LLSDURI"; export class LLSDNotation { private static readonly tokenSpecs: LLSDTokenSpec[] = [ {regex: /^\s+/, type: LLSDTokenType.Whitespace}, {regex: /^!/, type: LLSDTokenType.Null}, {regex: /^\{/, type: LLSDTokenType.MapStart}, {regex: /^}/, type: LLSDTokenType.MapEnd}, {regex: /^:/, type: LLSDTokenType.Colon}, {regex: /^,/, type: LLSDTokenType.Comma}, {regex: /^\[/, type: LLSDTokenType.ArrayStart}, {regex: /^]/, type: LLSDTokenType.ArrayEnd}, {regex: /^(?:true|false|TRUE|FALSE|1|0|T|F|t|f)/, type: LLSDTokenType.Boolean}, {regex: /^i(-?[0-9]+)/, type: LLSDTokenType.Integer}, {regex: /^r(-?[0-9.]+(?:[eE]-?[0-9]+)?)/, type: LLSDTokenType.Real}, {regex: /^rNaN/, type: LLSDTokenType.Real}, {regex: /^rInfinity/, type: LLSDTokenType.Real}, {regex: /^r-Infinity/, type: LLSDTokenType.Real}, {regex: /^u([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/, type: LLSDTokenType.UUID}, {regex: /^'([^'\\]*(?:\\.[^'\\\n]*)*)'/, type: LLSDTokenType.StringFixedSingle}, {regex: /^"([^"\\]*(?:\\.[^"\\\n]*)*)"/, type: LLSDTokenType.StringFixedDouble}, {regex: /^s\(([0-9]+)\)"/, type: LLSDTokenType.StringDynamicStart}, {regex: /^l"([^"]*?)"/, type: LLSDTokenType.URI}, {regex: /^d"([0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]+Z)"/, type: LLSDTokenType.Date}, {regex: /^b[0-9]{2}"[0-9a-zA-Z+/=]*?"/, type: LLSDTokenType.BinaryStatic}, {regex: /^b\(([0-9]+)\)"/, type: LLSDTokenType.BinaryDynamicStart} ]; public static parseValueToken(gen: LLSDTokenGenerator, initialToken?: LLSDToken): LLSDType { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition while (true) { let t: LLSDToken | undefined = undefined; if (initialToken !== undefined) { t = initialToken; initialToken = undefined; } else { t = gen(); if (t === undefined) { throw new Error('Unexpected end of input'); } } switch (t.type) { case LLSDTokenType.Unknown: { throw new Error('Unexpected token: ' + t.value); } case LLSDTokenType.Null: { return null; } case LLSDTokenType.Boolean: { return t.value === 'true' || t.value === 'TRUE' || t.value === 'T' || t.value === 't' || t.value === '1'; } case LLSDTokenType.Integer: { return new LLSDInteger(parseInt(t.value, 10)); } case LLSDTokenType.Real: { return new LLSDReal(t.value); } case LLSDTokenType.UUID: { return new UUID(t.value); } case LLSDTokenType.StringFixedSingle: { return this.unescapeStringSimple(t.value, '\''); } case LLSDTokenType.StringFixedDouble: { return this.unescapeStringSimple(t.value, '"'); } case LLSDTokenType.URI: { return new LLSDURI(t.value); } case LLSDTokenType.Date: { return new Date(t.value); } case LLSDTokenType.BinaryStatic: { const b = /^b([0-9]{2})"([0-9a-zA-Z+/=]*?)"/.exec(t.value); if (b === null || b.length < 3) { throw new Error('Invalid BINARY_STATIC'); } const base = parseInt(b[1], 10); if (base !== 16 && base !== 64) { throw new Error('Unsupported base ' + String(base)); } return Buffer.from(b[2], base === 64 ? 'base64' : 'hex'); } case LLSDTokenType.StringDynamicStart: { const length = parseInt(t.value, 10); const s = t.dataContainer.input.slice(t.dataContainer.index, t.dataContainer.index + length); t.dataContainer.index += length; if (t.dataContainer.input[t.dataContainer.index] !== '"') { throw new Error('Expected " at end of dynamic string') } t.dataContainer.index += 1; return s; } case LLSDTokenType.BinaryDynamicStart: { const length = parseInt(t.value, 10); const s = t.dataContainer.input.slice(t.dataContainer.index, t.dataContainer.index + length); t.dataContainer.index += length; if (t.dataContainer.input[t.dataContainer.index] !== '"') { throw new Error('Expected " at end of dynamic binary string') } t.dataContainer.index += 1; return Buffer.from(s, 'binary'); } case LLSDTokenType.MapStart: { return LLSDMap.parseNotation(gen); } case LLSDTokenType.ArrayStart: { return LLSDArray.parseNotation(gen); } case LLSDTokenType.MapEnd: case LLSDTokenType.Colon: case LLSDTokenType.Comma: case LLSDTokenType.ArrayEnd: case LLSDTokenType.Whitespace: break; } } } public static* tokenize(input: string): Generator { const dataContainer = { input: input, index: 0 } while (dataContainer.index < dataContainer.input.length) { const currentInput = dataContainer.input.slice(dataContainer.index); if (currentInput.length === 0) { return; // End of input } let matched = false; for (const {regex, type} of this.tokenSpecs) { const tokenMatch = currentInput.match(regex); if (tokenMatch) { matched = true; let [value] = tokenMatch; if (tokenMatch.length > 1) { value = tokenMatch[tokenMatch.length - 1]; } dataContainer.index += tokenMatch[0].length; // Move past this token yield {type, value, rawValue: tokenMatch[0], dataContainer}; break; } } if (!matched) { dataContainer.index++; yield { type: LLSDTokenType.Unknown, value: dataContainer.input[dataContainer.index - 1], rawValue: dataContainer.input[dataContainer.index - 1], dataContainer }; } } } public static encodeValue(value: LLSDType): string { if (value instanceof LLSDMap) { return value.toNotation(); } else if (value instanceof LLSDInteger) { return 'i' + value.valueOf(); } else if (value instanceof LLSDReal) { const v = value.valueOf(); if (isNaN(v)) { return 'rNaN'; } else if (v === -Infinity) { return 'r-Infinity'; } else if (v === Infinity) { return 'rInfinity'; } else if (Object.is(v, -0)) { return 'r-0'; } return 'r' + v; } else if (value instanceof UUID) { return 'u' + value.toString(); } else if (value instanceof LLSDURI) { return 'l"' + this.escapeStringSimple(value.toString(), '"') + '"'; } else if (value instanceof Buffer) { return 'b64"' + value.toString('base64') + '"'; } else if (value instanceof Date) { return 'd"' + value.toISOString() + '"'; } else if (value === null) { return '!'; } else if (value === true) { return '1'; } else if (value === false) { return '0'; } else if (typeof value === 'string') { return '"' + this.escapeStringSimple(value, '"') + '"'; } else if (Array.isArray(value)) { return LLSDArray.toNotation(value); } else { throw new Error('Unknown type: ' + String(value)); } } public static escapeStringSimple(input: string, quote: string): string { if (quote.length !== 1) { throw new Error('Quote must be a single character'); } const escapeRegex = new RegExp(`[${quote}\x00-\x1F\x7F\\\\]`, 'g'); return input.replace(escapeRegex, (char) => { if (char === "\\") { return "\\\\"; } else if (char === quote) { return '\\' + quote; } const hex = char.charCodeAt(0).toString(16).padStart(2, "0"); return `\\x${hex}`; }); } public static unescapeStringSimple(input: string, quote: string): string { if (quote.length !== 1) { throw new Error("Quote parameter must be a single character."); } // Create a regex that matches \\, \quote, or \xHH const unescapeRegex = new RegExp(`\\\\(\\\\|${quote}|x[0-9A-Fa-f]{2})`, 'g'); return input.replace(unescapeRegex, (match: string, p1: string) => { if (p1 === "\\") { return "\\"; } if (p1 === quote) { return quote; } if (p1.startsWith('x')) { const hex = p1.slice(1); const charCode = parseInt(hex, 16); if (isNaN(charCode)) { return match; } return String.fromCharCode(charCode); } return match; }); } }