Files
node-metaverse/lib/classes/llsd/LLSDNotation.ts
2025-01-17 23:53:31 +00:00

335 lines
11 KiB
TypeScript

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<LLSDToken>
{
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;
});
}
}