Actually, the message format is LLSD notation not python (d'oh)

This commit is contained in:
Casper Warden
2023-11-10 14:25:34 +00:00
parent 72d4eff2d8
commit 3c69b8f05e
26 changed files with 792 additions and 682 deletions

View File

@@ -0,0 +1,53 @@
import { LLSDTokenGenerator } from './LLSDTokenGenerator';
import { LLSDTokenType } from './LLSDTokenType';
import { LLSDNotationParser } from './LLSDNotationParser';
import { LLSDType } from './LLSDType';
export class LLSDArray
{
public static parse(gen: LLSDTokenGenerator): LLSDType[]
{
const arr: LLSDType[] = [];
let value: LLSDType | undefined = undefined;
while (true)
{
const token = gen();
if (token === undefined)
{
throw new Error('Unexpected end of input in array');
}
switch (token.type)
{
case LLSDTokenType.WHITESPACE:
{
continue;
}
case LLSDTokenType.ARRAY_END:
{
if (value !== undefined)
{
arr.push(value);
}
return arr;
}
case LLSDTokenType.COMMA:
{
if (value === undefined)
{
throw new Error('Expected value before comma');
}
arr.push(value);
value = undefined;
continue;
}
}
if (value !== undefined)
{
throw new Error('Comma or end brace expected');
}
value = LLSDNotationParser.parseValueToken(gen, token);
}
}
}

115
lib/classes/llsd/LLSDMap.ts Normal file
View File

@@ -0,0 +1,115 @@
import { LLSDObject } from './LLSDObject';
import { LLSDTokenGenerator } from './LLSDTokenGenerator';
import { LLSDTokenType } from './LLSDTokenType';
import { LLSDNotationParser } from './LLSDNotationParser';
import { LLSDType } from './LLSDType';
export class LLSDMap extends LLSDObject
{
public data: Map<LLSDType, LLSDType> = new Map<LLSDType, LLSDType>();
public static parse(gen: LLSDTokenGenerator): LLSDMap
{
const m = new LLSDMap();
let expectsKey = true
let key: LLSDType | undefined = undefined;
let value: LLSDType | undefined = undefined;
while (true)
{
const token = gen();
if (token === undefined)
{
throw new Error('Unexpected end of input in map');
}
switch (token.type)
{
case LLSDTokenType.WHITESPACE:
{
continue;
}
case LLSDTokenType.MAP_END:
{
if (expectsKey)
{
throw new Error('Unexpected end of map');
}
if (key !== undefined && value !== undefined)
{
m.data.set(key, value);
}
else if (m.data.size > 0)
{
throw new Error('Expected value before end of map');
}
return m;
}
case LLSDTokenType.COLON:
{
if (!expectsKey)
{
throw new Error('Unexpected symbol: :');
}
if (key === undefined)
{
throw new Error('Empty key not allowed');
}
expectsKey = false;
continue;
}
case LLSDTokenType.COMMA:
{
if (expectsKey)
{
throw new Error('Empty map entry not allowed');
}
if (value === undefined)
{
throw new Error('Empty map value not allowed');
}
if (key !== undefined)
{
m.data.set(key, value);
}
key = undefined;
value = undefined;
expectsKey = true;
continue;
}
}
if (expectsKey && key !== undefined)
{
throw new Error('Colon expected');
}
else if (value !== undefined)
{
throw new Error('Comma or end brace expected');
}
const val = LLSDNotationParser.parseValueToken(gen, token);
if (expectsKey)
{
key = val;
}
else
{
value = val;
}
}
}
get length(): number
{
return Object.keys(this.data).length;
}
public get(key: LLSDType): LLSDType | undefined
{
return this.data.get(key);
}
public toJSON(): unknown
{
return Object.fromEntries(this.data);
}
}

View File

@@ -0,0 +1,204 @@
import * as assert from 'assert';
import { LLSDNotationParser } from './LLSDNotationParser';
import { LLSDMap } from './LLSDMap';
import { UUID } from '../UUID';
describe('LLSDNotationParser', () =>
{
describe('parse', () =>
{
it('can parse a complex LLSD Notation document', () =>
{
const notationDoc = `{
'nested_map': {
'nested_again': {
'array': [
i0,
'string',
"string2",
[
"another",
"array",
r4.3,
i22,
!,
{
'isThis': i42
}
]
]
}
},
'undef': !,
'booleans': [
true,
false,
1,
0,
T,
F,
t,
f,
TRUE,
FALSE
],
'integer': i69,
'negInt': i-69,
'real': r3.141,
'realNeg': r-3.141,
'uuid': ufb54f6b1-8120-40c9-9aa3-f9abef1a168f,
'string1': "gday\\"mate",
'string2': 'gday\\'mate',
'string3': s(11)"hello"there",
'uri': l"https://secondlife.com/",
'date': d"2023-11-10T13:32:32.93Z",
'binary': b64"amVsbHlmaXNo",
'binary2': b16"6261636F6E62697473",
'binary3': b(32)"KÚ~¬\béGÀt|ϓ˜h,9µEK¹*;]ÆÁåb/"
}`;
const parsed = LLSDNotationParser.parse(notationDoc);
if (!(parsed instanceof LLSDMap))
{
assert('Parsed document is not a map');
return;
}
const nested = parsed.get('nested_map');
if (nested instanceof LLSDMap)
{
const nestedAgain = nested.get('nested_again');
if (nestedAgain instanceof LLSDMap)
{
const arr = nestedAgain.get('array');
if (!Array.isArray(arr))
{
assert(false, 'Nested array is not an array');
}
else
{
assert.equal(arr.length, 4);
assert.equal(arr[0], 0);
assert.equal(arr[1], 'string');
assert.equal(arr[2], 'string2');
const nestedAgainArr = arr[3];
if (!Array.isArray(nestedAgainArr))
{
assert(false, 'Nested again array is not an array');
}
else
{
assert.equal(nestedAgainArr.length, 6);
assert.equal(nestedAgainArr[0], 'another');
assert.equal(nestedAgainArr[1], 'array');
assert.equal(nestedAgainArr[2], 4.3);
assert.equal(nestedAgainArr[3], 22);
assert.equal(nestedAgainArr[4], null);
const thirdNestedMap = nestedAgainArr[5];
if (thirdNestedMap instanceof LLSDMap)
{
assert.equal(thirdNestedMap.get('isThis'), 42);
}
else
{
assert(false, 'Third nested map is not a map');
}
}
}
}
else
{
assert(false, 'NestedAgain is not a map');
}
}
else
{
assert(false, 'Nested is not a map');
}
assert.equal(parsed.get('undef'), null);
const bools = parsed.get('booleans');
if (!Array.isArray(bools))
{
assert(false, 'Booleans array is not bools');
}
else
{
assert.equal(bools.length, 10);
assert.equal(bools[0], true);
assert.equal(bools[1], false);
assert.equal(bools[2], true);
assert.equal(bools[3], false);
assert.equal(bools[4], true);
assert.equal(bools[5], false);
assert.equal(bools[6], true);
assert.equal(bools[7], false);
assert.equal(bools[8], true);
assert.equal(bools[9], false);
}
/*
'string1': "gday\"mate",
'string2': 'gday\'mate',
'string3': s(11)"hello"there",
'uri': l"https://secondlife.com/",
'date': d"2023-11-10T13:32:32.93Z",
'binary': b64"amVsbHlmaXNo",
'binary2': b16"6261636F6E62697473",
'binary3': b(32)"KÚ~¬éGÀt|ϓ˜h,9µEK¹*;]ÆÁåb/"
*/
assert.equal(parsed.get('integer'), 69);
assert.equal(parsed.get('negInt'), -69);
assert.equal(parsed.get('real'), 3.141);
assert.equal(parsed.get('realNeg'), -3.141);
const u = parsed.get('uuid');
if (u instanceof UUID)
{
assert.equal(u.equals('fb54f6b1-8120-40c9-9aa3-f9abef1a168f'), true);
}
else
{
assert(false, 'UUID is not a UUID');
}
assert.equal(parsed.get('string1'), 'gday"mate');
assert.equal(parsed.get('string2'), 'gday\'mate');
assert.equal(parsed.get('string3'), 'hello"there');
assert.equal(parsed.get('uri'), 'https://secondlife.com/');
const d = parsed.get('date');
if (d instanceof Date)
{
assert.equal(d.getTime(), 1699623152930);
}
else
{
assert(false, 'Date entry is not a date');
}
const buf1 = parsed.get('binary');
if (buf1 instanceof Buffer)
{
assert.equal(buf1.toString('utf-8'), 'jellyfish');
}
else
{
assert(false, 'Buf1 is not a buffer');
}
const buf2 = parsed.get('binary2');
if (buf2 instanceof Buffer)
{
assert.equal(buf2.toString('utf-8'), 'baconbits');
}
else
{
assert(false, 'Buf2 is not a buffer');
}
const buf3 = parsed.get('binary3');
if (buf3 instanceof Buffer)
{
assert.equal(buf3.equals(Buffer.from('KÚ~¬\béGÀt|ϓ˜h,9µEK¹*;]ÆÁåb/', 'binary')), true);
}
else
{
assert(false, 'Buf3 is not a buffer');
}
});
});
});

View File

@@ -0,0 +1,320 @@
import { LLSDTokenType } from './LLSDTokenType';
import { LLSDType } from './LLSDType';
import { LLSDTokenSpec } from './LLSDTokenSpec';
import { LLSDToken } from './LLSDToken';
import { LLSDTokenGenerator } from './LLSDTokenGenerator';
import { LLSDMap } from './LLSDMap';
import { UUID } from '../UUID';
import { LLSDArray } from './LLSDArray';
export class LLSDNotationParser
{
private static tokenSpecs: LLSDTokenSpec[] =
[
{ regex: /^\s+/, type: LLSDTokenType.WHITESPACE },
{ regex: /^!/, type: LLSDTokenType.NULL },
{ regex: /^\{/, type: LLSDTokenType.MAP_START },
{ regex: /^}/, type: LLSDTokenType.MAP_END },
{ regex: /^:/, type: LLSDTokenType.COLON },
{ regex: /^,/, type: LLSDTokenType.COMMA },
{ regex: /^\[/, type: LLSDTokenType.ARRAY_START },
{ regex: /^]/, type: LLSDTokenType.ARRAY_END },
{ 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.]+)/, 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.STRING_FIXED_SINGLE },
{ regex: /^"([^"\\]*(?:\\.[^"\\\n]*)*)"/, type: LLSDTokenType.STRING_FIXED_DOUBLE },
{ regex: /^s\(([0-9]+)\)"/, type: LLSDTokenType.STRING_DYNAMIC_START },
{ 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]{2}Z)"/, type: LLSDTokenType.DATE },
{ regex: /^b[0-9]{2}"[0-9a-zA-Z+\/=]*?"/, type: LLSDTokenType.BINARY_STATIC },
{ regex: /^b\(([0-9]+)\)"/, type: LLSDTokenType.BINARY_DYNAMIC_START }
];
private static* tokenize(inpt: string): Generator<LLSDToken | void>
{
const dataContainer = {
input: inpt,
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[0];
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 parseValueToken(gen: LLSDTokenGenerator, initialToken?: LLSDToken): LLSDType
{
while (true)
{
let t: LLSDToken | 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.WHITESPACE:
{
continue;
}
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 parseInt(t.value, 10);
}
case LLSDTokenType.REAL:
{
return parseFloat(t.value);
}
case LLSDTokenType.UUID:
{
return new UUID(t.value);
}
case LLSDTokenType.STRING_FIXED_SINGLE:
{
return t.value.replace(/\\'/, '\'')
.replace(/\\\\/g, '\\');
}
case LLSDTokenType.STRING_FIXED_DOUBLE:
{
return t.value.replace(/\\"/, '"')
.replace(/\\\\/g, '\\');
}
case LLSDTokenType.URI:
{
return t.value;
}
case LLSDTokenType.DATE:
{
return new Date(t.value);
}
case LLSDTokenType.BINARY_STATIC:
{
const b = t.value.match(/^b([0-9]{2})"([0-9a-zA-Z+\/=]*?)"/);
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.STRING_DYNAMIC_START:
{
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.BINARY_DYNAMIC_START:
{
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.MAP_START:
{
return LLSDMap.parse(gen);
}
case LLSDTokenType.ARRAY_START:
{
return LLSDArray.parse(gen);
}
}
}
}
public static parse(input: string): LLSDType
{
const generator = this.tokenize(input);
const getToken: LLSDTokenGenerator = (): LLSDToken | undefined =>
{
return generator.next().value;
}
const data = this.parseValueToken(getToken);
do
{
const t = getToken();
if (t === undefined)
{
return data;
}
if (t.type !== LLSDTokenType.WHITESPACE)
{
throw new Error('Unexpected token at end of document: ' + t.value);
}
}
while (true);
}
/*
private static readObject(cont: LLSDParserContainer): LLSDType
{
let tokenType: LLSDTokenType | null = null;
const stack: LLSDObject[] = [];
while (cont.pos < cont.str.length)
{
const subString = cont.str.slice(cont.pos);
let value: string | null = null;
for (const { regex, type } of this.tokenSpecs)
{
const tokenMatch = subString.match(regex);
if (tokenMatch)
{
value = tokenMatch[0];
tokenType = type;
if (tokenMatch.length > 1)
{
value = tokenMatch[tokenMatch.length - 1];
}
cont.pos += tokenMatch[0].length; // Move past this token
break;
}
}
if (tokenType === null)
{
tokenType = LLSDTokenType.UNKNOWN;
value = cont.str[cont.pos++];
}
if (stack.length > 0)
{
if (stack[stack.length - 1].acceptToken(tokenType, value))
{
// stack object completed
}
}
switch (tokenType)
{
case LLSDTokenType.WHITESPACE:
{
continue;
}
case LLSDTokenType.UNKNOWN:
{
throw new Error('Unexpected token: ' + value);
}
case LLSDTokenType.NULL:
{
return null;
}
case LLSDTokenType.MAP_START:
{
break;
}
case LLSDTokenType.MAP_END:
break;
case LLSDTokenType.COLON:
break;
case LLSDTokenType.COMMA:
break;
case LLSDTokenType.ARRAY_START:
break;
case LLSDTokenType.ARRAY_END:
break;
case LLSDTokenType.BOOLEAN:
break;
case LLSDTokenType.INTEGER:
break;
case LLSDTokenType.REAL:
break;
case LLSDTokenType.UUID:
break;
case LLSDTokenType.STRING_FIXED:
break;
case LLSDTokenType.STRING_DYNAMIC_START:
break;
case LLSDTokenType.URI:
break;
case LLSDTokenType.DATE:
break;
case LLSDTokenType.BINARY_STATIC:
break;
case LLSDTokenType.BINARY_DYNAMIC_START:
break;
}
}
}
public static parse(input: string): LLSDType
{
const cont: LLSDParserContainer = {
str: input,
pos: 0
};
const token = this.readObject(cont);
if (cont.pos < input.length)
{
throw new Error('Only one root object expected');
}
return token;
}
*/
}

View File

@@ -0,0 +1,9 @@
export abstract class LLSDObject
{
public toString(): string
{
return JSON.stringify(this.toJSON());
}
public abstract toJSON(): unknown;
}

View File

@@ -0,0 +1,12 @@
import { LLSDTokenType } from './LLSDTokenType';
export interface LLSDToken
{
type: LLSDTokenType;
value: string;
rawValue: string;
dataContainer: {
input: string,
index: number
}
}

View File

@@ -0,0 +1,9 @@
import { LLSDToken } from './LLSDToken';
import { LLSDTokenGenerator } from './LLSDTokenGenerator';
export interface LLSDTokenContainer
{
tokens: LLSDToken[];
index: number;
gen: LLSDTokenGenerator;
}

View File

@@ -0,0 +1,3 @@
import { LLSDToken } from './LLSDToken';
export type LLSDTokenGenerator = () => LLSDToken | undefined;

View File

@@ -0,0 +1,7 @@
import { LLSDTokenType } from './LLSDTokenType';
export interface LLSDTokenSpec
{
regex: RegExp;
type: LLSDTokenType;
}

View File

@@ -0,0 +1,23 @@
export enum LLSDTokenType
{
UNKNOWN = 0,
WHITESPACE,
NULL,
MAP_START,
MAP_END,
COLON,
COMMA,
ARRAY_START,
ARRAY_END,
BOOLEAN,
INTEGER,
REAL,
UUID,
STRING_FIXED_SINGLE,
STRING_FIXED_DOUBLE,
STRING_DYNAMIC_START,
URI,
DATE,
BINARY_STATIC,
BINARY_DYNAMIC_START,
}

View File

@@ -0,0 +1,4 @@
import { LLSDObject } from './LLSDObject';
import { UUID } from '../UUID';
export type LLSDType = number | boolean | string | Buffer | LLSDObject | null | UUID | Date | LLSDType[];