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

@@ -38,9 +38,8 @@ import { ObjectResolvedEvent } from '../events/ObjectResolvedEvent';
import { Avatar } from './public/Avatar';
import Timer = NodeJS.Timer;
import { GenericStreamingMessageMessage } from './messages/GenericStreamingMessage';
import { PythonParser } from './python/PythonParser';
import { PythonDict } from './python/PythonDict';
import { PythonList } from './python/PythonList';
import { LLSDNotationParser } from './llsd/LLSDNotationParser';
import { LLSDMap } from './llsd/LLSDMap';
export class ObjectStoreLite implements IObjectStore
{
@@ -85,12 +84,12 @@ export class ObjectStoreLite implements IObjectStore
const genMsg = packet.message as GenericStreamingMessageMessage;
if (genMsg.MethodData.Method === 0x4175)
{
// Whoever decided to use python notation for this is a psychopath
const result = PythonParser.parse(genMsg.DataBlock.Data.toString('utf-8'));
if (result instanceof PythonDict)
// LLSD Notation format
const result = LLSDNotationParser.parse(genMsg.DataBlock.Data.toString('utf-8'));
if (result instanceof LLSDMap)
{
const arr = result.get('te');
if (arr instanceof PythonList)
if (Array.isArray(arr))
{
if (arr.length === 0)
{
@@ -98,7 +97,7 @@ export class ObjectStoreLite implements IObjectStore
}
// TODO: figure out what to do with this..
// console.log(JSON.stringify(result, null, 4));
console.log(JSON.stringify(result, null, 4));
}
}
}

View File

@@ -68,6 +68,9 @@ export class CommunicationsCommands extends CommandsBase
Message: Utils.StringToBuffer(itemOrFolder.name),
BinaryBucket: bucket
};
im.EstateBlock = {
EstateID: 0
};
const sequenceNo = circuit.sendMessage(im, PacketFlags.Reliable);
return await circuit.waitForAck(sequenceNo, 10000);
}
@@ -99,6 +102,9 @@ export class CommunicationsCommands extends CommandsBase
Message: Utils.StringToBuffer(message),
BinaryBucket: Buffer.allocUnsafe(0)
};
im.EstateBlock = {
EstateID: 0
};
const sequenceNo = circuit.sendMessage(im, PacketFlags.Reliable);
return await circuit.waitForAck(sequenceNo, 10000);
}
@@ -223,6 +229,9 @@ export class CommunicationsCommands extends CommandsBase
Message: Utils.StringToBuffer(''),
BinaryBucket: Buffer.allocUnsafe(0)
};
im.EstateBlock = {
EstateID: 0
};
const sequenceNo = circuit.sendMessage(im, PacketFlags.Reliable);
return await circuit.waitForAck(sequenceNo, 10000);
}
@@ -254,6 +263,9 @@ export class CommunicationsCommands extends CommandsBase
Message: Utils.StringToBuffer(''),
BinaryBucket: Buffer.allocUnsafe(0)
};
im.EstateBlock = {
EstateID: 0
};
const sequenceNo = circuit.sendMessage(im, PacketFlags.Reliable);
return await circuit.waitForAck(sequenceNo, 10000);
}
@@ -407,6 +419,9 @@ export class CommunicationsCommands extends CommandsBase
Message: Buffer.allocUnsafe(0),
BinaryBucket: Buffer.allocUnsafe(0)
};
im.EstateBlock = {
EstateID: 0
};
this.agent.deleteChatSession(groupID);
const sequenceNo = this.circuit.sendMessage(im, PacketFlags.Reliable);
return this.circuit.waitForAck(sequenceNo, 10000);
@@ -445,6 +460,9 @@ export class CommunicationsCommands extends CommandsBase
Message: Utils.StringToBuffer(message),
BinaryBucket: Utils.StringToBuffer('')
};
im.EstateBlock = {
EstateID: 0
};
circuit.sendMessage(im, PacketFlags.Reliable);
await Utils.waitOrTimeOut(this.currentRegion.clientEvents.onGroupChatSessionJoin, 10000, (event: GroupChatSessionJoinEvent) =>
{
@@ -514,6 +532,9 @@ export class CommunicationsCommands extends CommandsBase
Message: Utils.StringToBuffer(message),
BinaryBucket: Utils.StringToBuffer('')
};
im.EstateBlock = {
EstateID: 0
};
const sequenceNo = circuit.sendMessage(im, PacketFlags.Reliable);
await this.circuit.waitForAck(sequenceNo, 10000);

View File

@@ -307,6 +307,9 @@ export class FriendCommands extends CommandsBase
Message: Utils.StringToBuffer(message),
BinaryBucket: Utils.StringToBuffer('')
};
im.EstateBlock = {
EstateID: 0
};
const sequenceNo = this.circuit.sendMessage(im, PacketFlags.Reliable);
return await this.circuit.waitForAck(sequenceNo, 10000);
}

View File

@@ -50,6 +50,9 @@ export class GroupCommands extends CommandsBase
Message: Utils.StringToBuffer(subject + '|' + message),
BinaryBucket: Buffer.allocUnsafe(0)
};
im.EstateBlock = {
EstateID: 0
};
const sequenceNo = circuit.sendMessage(im, PacketFlags.Reliable);
return await circuit.waitForAck(sequenceNo, 10000);
}
@@ -137,6 +140,9 @@ export class GroupCommands extends CommandsBase
Message: Utils.StringToBuffer(''),
BinaryBucket: Buffer.allocUnsafe(0)
};
im.EstateBlock = {
EstateID: 0
};
const sequenceNo = circuit.sendMessage(im, PacketFlags.Reliable);
return await circuit.waitForAck(sequenceNo, 10000);
}
@@ -164,6 +170,9 @@ export class GroupCommands extends CommandsBase
Message: Utils.StringToBuffer(''),
BinaryBucket: Buffer.allocUnsafe(0)
};
im.EstateBlock = {
EstateID: 0
};
const sequenceNo = circuit.sendMessage(im, PacketFlags.Reliable);
return await circuit.waitForAck(sequenceNo, 10000);
}

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

@@ -1,4 +1,4 @@
export abstract class PythonObject
export abstract class LLSDObject
{
public toString(): string
{

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[];

View File

@@ -1,109 +0,0 @@
import { PythonTokenContainer } from './PythonTokenContainer';
import { PythonObject } from './PythonObject';
import { PythonTokenType } from './PythonTokenType';
import { PythonType } from './PythonType';
import { PythonParser } from './PythonParser';
import { PythonTuple } from './PythonTuple';
// Define PythonKey type for dictionary keys
export type PythonKey = string | boolean | PythonType[] | number | PythonTuple;
export class PythonDict extends PythonObject
{
public data: Map<PythonKey, PythonType> = new Map<PythonKey, PythonType>();
public static parse(container: PythonTokenContainer): PythonDict
{
const dict = new PythonDict();
let isKey = true;
let key: PythonKey | null = null;
while (container.index < container.tokens.length)
{
const token = container.tokens[container.index];
switch (token.type)
{
case PythonTokenType.BRACE_END:
{
if (isKey)
{
// The last token is a key, which is invalid
throw new Error('Unexpected end of dictionary: Expected a key-value pair.');
}
container.index++;
return dict;
}
case PythonTokenType.COLON:
{
if (!isKey)
{
throw new Error('Expected a key before the colon in a dictionary.');
}
isKey = false;
container.index++;
break;
}
case PythonTokenType.COMMA:
{
if (isKey)
{
throw new Error('No value provided with dictionary key');
}
isKey = true;
container.index++;
break;
}
default:
{
if (isKey)
{
// Parse the key and check its type
key = PythonParser.parseValueToken(container) as PythonKey;
if (
typeof key !== 'string' &&
typeof key !== 'number' &&
typeof key !== 'boolean' &&
!(key instanceof PythonTuple) && // Check if it's a PythonTuple
typeof key !== 'object' // Allow floats
)
{
throw new Error('Invalid key type in a dictionary.');
}
}
else
{
// Parse the value
if (key === null)
{
throw new Error('Key cannot be null in a dictionary.');
}
const value = PythonParser.parseValueToken(container);
dict.data.set(key, value);
key = null;
}
}
}
}
throw new Error('Expected close brace } in dictionary');
}
get length(): number
{
return Object.keys(this.data).length;
}
public get(key: PythonKey): PythonType | undefined
{
return this.data.get(key);
}
public toJSON(): unknown
{
return Object.fromEntries(this.data);
}
}

View File

@@ -1,69 +0,0 @@
import { PythonTokenContainer } from './PythonTokenContainer';
import { PythonTokenType } from './PythonTokenType';
import { PythonObject } from './PythonObject';
import { PythonType } from './PythonType';
import { PythonParser } from './PythonParser';
export class PythonList extends PythonObject
{
public data: PythonType[] = [];
public static parse(container: PythonTokenContainer): PythonList
{
let expectingComma = false;
const list = new PythonList();
do
{
const token = container.tokens[container.index];
switch (token.type)
{
case PythonTokenType.LIST_END:
{
container.index++;
return list;
}
case PythonTokenType.COMMA:
{
if (!expectingComma)
{
throw new Error('Unexpected comma in list');
}
expectingComma = false;
container.index++;
break;
}
default:
{
if (expectingComma)
{
throw new Error('Unexpected token')
}
list.data.push(PythonParser.parseValueToken(container));
expectingComma = true;
}
}
}
while (container.index < container.tokens.length);
throw new Error('Expected ] end bracket in list')
}
public get(index: number): PythonType | undefined
{
return this.data[index];
}
get length(): number
{
return this.data.length;
}
public toString(): string
{
return '[' + this.data.join(', ') + ']';
}
public toJSON(): unknown
{
return this.data;
}
}

View File

@@ -1,148 +0,0 @@
import { PythonParser } from './PythonParser';
import * as assert from 'assert';
import { PythonDict } from './PythonDict';
import { PythonList } from './PythonList';
import { PythonTuple } from './PythonTuple';
describe('PythonParser', () =>
{
describe('parse', () =>
{
it('can parse a complex python dictionary notation', () =>
{
const notationDoc = `{
"nested_dict": {
"key1": "value1",
"key2": {
"inner_key": "inner_value"
}
},
"list": [1, 2, 3, [4, 5]],
"boolean": True,
"tuple": (1, 2, ("nested_tuple", 3)),
"bytes": b'hello',
"float": 3.14,
'integer': 42,
"hex_number": 0x1A,
"octal_number": 0o52,
"string_single": 'single-quoted\\' string',
"string_double": "double-quoted \\" string",
"string_triple_single": '''triple-quoted\'
single-quoted string''',
"string_triple_double": """triple-quoted\"
double-quoted string""",
"raw_string_single": r'raw single-quoted\\ string',
"raw_string_double": r"raw double-quoted\\ string",
"raw_string_triple_single": r'''raw triple\\''-quoted
single-quoted string''',
"raw_string_triple_double": r"""raw triple\\""-quoted
double-quoted string"""
}`;
const parsed = PythonParser.parse(notationDoc);
if (!(parsed instanceof PythonDict))
{
assert(false);
return;
}
const nested = parsed.get('nested_dict');
assert.ok(nested);
if (!(nested instanceof PythonDict))
{
assert(false);
}
else
{
assert.equal(nested.get('key1'), 'value1');
const key2 = nested.get('key2');
if (!(key2 instanceof PythonDict))
{
assert(false);
}
else
{
assert.equal(key2.get('inner_key'), 'inner_value');
}
}
const list = parsed.get('list');
if (!(list instanceof PythonList))
{
assert(false);
}
else
{
assert.equal(list.length, 4);
assert.equal(list.get(0), 1);
assert.equal(list.get(1), 2);
assert.equal(list.get(2), 3);
const nestedList = list.get(3);
if (!(nestedList instanceof PythonList))
{
assert(false);
}
else
{
assert.equal(nestedList.get(0), 4);
assert.equal(nestedList.get(1), 5);
}
assert.equal(list.get(4), undefined);
}
assert.equal(parsed.get('boolean'), true);
const tuple = parsed.get('tuple');
if (!(tuple instanceof PythonTuple))
{
assert(false);
}
else
{
assert.equal(tuple.get(0), 1);
assert.equal(tuple.get(1), 2);
const nestedTuple = tuple.get(2);
if (!(nestedTuple instanceof PythonTuple))
{
assert(false);
}
else
{
assert.equal(nestedTuple.get(0), 'nested_tuple');
assert.equal(nestedTuple.get(1), 3);
}
assert.equal(tuple.get(3), undefined);
}
const buf = parsed.get('bytes');
if (buf instanceof Buffer)
{
assert.equal(Buffer.from('hello', 'binary').compare(buf), 0);
}
else
{
assert(false);
}
assert.equal(parsed.get('float'), 3.14);
assert.equal(parsed.get('integer'), 42);
assert.equal(parsed.get('hex_number'), 26);
assert.equal(parsed.get('octal_number'), 42);
assert.equal(parsed.get('string_single'), 'single-quoted\\\' string');
assert.equal(parsed.get('string_double'), 'double-quoted \\" string');
assert.equal(parsed.get('string_triple_single'), 'triple-quoted\'\nsingle-quoted string');
assert.equal(parsed.get('string_triple_double'), 'triple-quoted\"\ndouble-quoted string');
/*
raw_string_single": r'raw single-quoted\ string',
"raw_string_double": r"raw double-quoted\ string",
"raw_string_triple_single": r'''raw triple\''-quoted
single-quoted string''',
"raw_string_triple_double": r"""raw triple\''-quoted
double-quoted string"""
*/
assert.equal(parsed.get('raw_string_single'), 'raw single-quoted\\ string');
assert.equal(parsed.get('raw_string_double'), 'raw double-quoted\\ string');
assert.equal(parsed.get('raw_string_triple_single'), 'raw triple\\\'\'-quoted\nsingle-quoted string');
assert.equal(parsed.get('raw_string_triple_double'), 'raw triple\\""-quoted\ndouble-quoted string');
});
});
});

View File

@@ -1,168 +0,0 @@
import { PythonTokenType } from './PythonTokenType';
import { PythonToken } from './PythonToken';
import { PythonSet } from './PythonSet';
import { PythonTokenContainer } from './PythonTokenContainer';
import { PythonType } from './PythonType';
import { PythonList } from './PythonList';
import { PythonTuple } from './PythonTuple';
interface TokenSpec
{
regex: RegExp;
type: PythonTokenType;
}
export class PythonParser
{
private static tokenSpecs: TokenSpec[] =
[
{ regex: /^\s+/, type: PythonTokenType.UNKNOWN }, // WHITESPACE is treated as UNKNOWN
{ regex: /^{/, type: PythonTokenType.BRACE_START },
{ regex: /^}/, type: PythonTokenType.BRACE_END },
{ regex: /^[:]/, type: PythonTokenType.COLON },
{ regex: /^[,]/, type: PythonTokenType.COMMA },
{ regex: /^None\b/, type: PythonTokenType.NONE },
{ regex: /^(True|False)\b/, type: PythonTokenType.BOOLEAN },
{ regex: /^((?:-?[0-9]+\.[0-9]*)|(?:-?[0.9]*\.[0-9]+))/, type: PythonTokenType.FLOAT },
{ regex: /^\d+\b/, type: PythonTokenType.INTEGER },
{ regex: /^0x([0-9a-fA-F]+\b)/, type: PythonTokenType.HEX },
{ regex: /^0o([0-7]+)/, type: PythonTokenType.OCTAL },
{ regex: /^\(/, type: PythonTokenType.TUPLE_START },
{ regex: /^\)/, type: PythonTokenType.TUPLE_END },
{ regex: /^\[/, type: PythonTokenType.LIST_START },
{ regex: /^\]/, type: PythonTokenType.LIST_END },
{ regex: /^"""((?:[^"]*|\n|\\"|")*?)"""/, type: PythonTokenType.STRING }, // triple double quoted string
{ regex: /^'''((?:[^']*|\n|\\'|')*?)'''/, type: PythonTokenType.STRING }, // triple single quoted string
{ regex: /^'([^'\\]*(?:\\.[^'\\\n]*)*)'/, type: PythonTokenType.STRING }, // single quoted string
{ regex: /^"([^"\\]*(?:\\.[^"\\\n]*)*)"/, type: PythonTokenType.STRING }, // double quoted string
{ regex: /^b"""((?:[^"]*|\n|\\"|")*?)"""/, type: PythonTokenType.BINARY_STRING }, // triple double quoted string
{ regex: /^b'''((?:[^']*|\n|\\'|')*?)'''/, type: PythonTokenType.BINARY_STRING }, // triple single quoted string
{ regex: /^b'([^'\\]*(?:\\.[^'\\\n]*)*)'/, type: PythonTokenType.BINARY_STRING }, // single quoted string
{ regex: /^b"([^"\\]*(?:\\.[^"\\\n]*)*)"/, type: PythonTokenType.BINARY_STRING }, // double quoted string
{ regex: /^r"""((?:[^"]*|\n|")*?)"""/, type: PythonTokenType.RAW_STRING }, // triple double quoted string
{ regex: /^r'''((?:[^']*|\n|')*?)'''/, type: PythonTokenType.RAW_STRING }, // triple single quoted string
{ regex: /^r'([^'\n]*?)'/, type: PythonTokenType.RAW_STRING }, // single quoted string
{ regex: /^r"([^"\n]*?)"/, type: PythonTokenType.RAW_STRING }, // double quoted string
{ regex: /^-?\d+\.?\d*[eE][-+]?\d+/, type: PythonTokenType.FLOAT }, // Scientific notation
{ regex: /^\d+(_\d+)*\b/, type: PythonTokenType.INTEGER }, // Integer with underscores, e.g., 1_000_000
{ regex: /^[^\s:{},"'\[\]\(\)]+/, type: PythonTokenType.UNKNOWN } // Catch all for other non-structured sequences
];
private static* tokenize(input: string): Generator<PythonToken, void, undefined>
{
let index = 0;
while (index < input.length)
{
const currentInput = input.slice(index);
if (currentInput.length === 0)
{
return; // End of input
}
let matched = false;
for (const { regex, type } of PythonParser.tokenSpecs)
{
const tokenMatch = currentInput.match(regex);
if (tokenMatch)
{
matched = true;
let value = tokenMatch[0];
if (tokenMatch.length > 1)
{
value = tokenMatch[tokenMatch.length - 1];
}
index += tokenMatch[0].length; // Move past this token
if (type !== PythonTokenType.UNKNOWN) // WHITESPACE is UNKNOWN and not yielded
{
yield { type, value };
}
break;
}
}
if (!matched)
{
throw new Error(`Unexpected token at index ${index}: "${currentInput[0]}"`);
}
}
}
public static parseValueToken(container: PythonTokenContainer): PythonType
{
const t = container.tokens[container.index++];
switch (t.type)
{
case PythonTokenType.BRACE_START:
{
return PythonSet.parse(container);
}
case PythonTokenType.STRING:
{
return t.value;
}
case PythonTokenType.BINARY_STRING:
{
return Buffer.from(t.value, 'binary');
}
case PythonTokenType.RAW_STRING:
{
return t.value;
}
case PythonTokenType.BOOLEAN:
{
return t.value === 'True';
}
case PythonTokenType.LIST_START:
{
return PythonList.parse(container);
}
case PythonTokenType.TUPLE_START:
{
return PythonTuple.parse(container);
}
case PythonTokenType.NONE:
{
return null;
}
case PythonTokenType.HEX:
{
return parseInt(t.value, 16);
}
case PythonTokenType.OCTAL:
{
return parseInt(t.value, 8);
}
case PythonTokenType.INTEGER:
{
return parseInt(t.value, 10);
}
case PythonTokenType.FLOAT:
{
return parseFloat(t.value);
}
default:
throw new Error('Unexpected token: ' + PythonTokenType[t.type]);
}
}
public static parse(input: string): PythonType
{
const cont = new PythonTokenContainer()
for (const token of PythonParser.tokenize(input))
{
cont.tokens.push(token);
}
const parsedToken = this.parseValueToken(cont);
if (cont.index < cont.tokens.length)
{
throw new Error('Only one token expected at root level');
}
return parsedToken;
}
}

View File

@@ -1,70 +0,0 @@
import { PythonTokenContainer } from './PythonTokenContainer';
import { PythonTokenType } from './PythonTokenType';
import { PythonDict } from './PythonDict';
import { PythonObject } from './PythonObject';
import { PythonType } from './PythonType';
import { PythonParser } from './PythonParser';
export class PythonSet extends PythonObject
{
public data = new Set<PythonType>();
public static parse(container: PythonTokenContainer): PythonSet | PythonDict
{
let expectingComma = false;
const startIndex = container.index;
const set = new PythonSet();
do
{
const token = container.tokens[container.index];
switch (token.type)
{
case PythonTokenType.BRACE_END:
{
if (container.index === startIndex)
{
// Empty braces, this is an empty PythonDict
return new PythonDict();
}
else
{
container.index++;
return set;
}
}
case PythonTokenType.COMMA:
{
if (!expectingComma)
{
throw new Error('Unexpected comma in list');
}
expectingComma = false;
container.index++;
break;
}
case PythonTokenType.COLON:
{
// This is a dictionary, not a set, start again..
container.index = startIndex;
return PythonDict.parse(container);
}
default:
{
if (expectingComma)
{
throw new Error('Unexpected token')
}
set.data.add(PythonParser.parseValueToken(container));
expectingComma = true;
}
}
}
while (container.index < container.tokens.length);
throw new Error('Expected } end brace in set')
}
public toJSON(): unknown
{
return Array.from(this.data);
}
}

View File

@@ -1,7 +0,0 @@
import { PythonTokenType } from './PythonTokenType';
export interface PythonToken
{
type: PythonTokenType;
value: string;
}

View File

@@ -1,7 +0,0 @@
import { PythonToken } from './PythonToken';
export class PythonTokenContainer
{
tokens: PythonToken[] = [];
index = 0;
}

View File

@@ -1,21 +0,0 @@
export enum PythonTokenType
{
NONE,
BRACE_START,
BRACE_END,
COLON,
COMMA,
BOOLEAN,
INTEGER,
FLOAT,
STRING,
BINARY_STRING,
RAW_STRING,
LIST_START,
LIST_END,
TUPLE_START,
TUPLE_END,
HEX,
OCTAL,
UNKNOWN // Catch all for other sequences
}

View File

@@ -1,70 +0,0 @@
import { PythonTokenContainer } from './PythonTokenContainer';
import { PythonTokenType } from './PythonTokenType';
import { PythonObject } from './PythonObject';
import { PythonType } from './PythonType';
import { PythonParser } from './PythonParser';
export class PythonTuple extends PythonObject
{
public data: PythonType[] = [];
public static parse(container: PythonTokenContainer): PythonTuple
{
let expectingComma = false;
const tuple = new PythonTuple();
do
{
const token = container.tokens[container.index];
switch (token.type)
{
case PythonTokenType.TUPLE_END:
{
container.index++;
return tuple;
}
case PythonTokenType.COMMA:
{
if (!expectingComma)
{
throw new Error('Unexpected comma in list');
}
expectingComma = false;
container.index++;
break;
}
default:
{
if (expectingComma)
{
throw new Error('Unexpected token')
}
tuple.data.push(PythonParser.parseValueToken(container));
expectingComma = true;
break;
}
}
}
while (container.index < container.tokens.length);
throw new Error('Expected ) end bracket in tuple')
}
public get(index: number): PythonType | undefined
{
return this.data[index];
}
get length(): number
{
return this.data.length;
}
public toString(): string
{
return '(' + this.data.join(', ') + ')';
}
public toJSON(): unknown
{
return this.data;
}
}

View File

@@ -1,3 +0,0 @@
import { PythonObject } from './PythonObject';
export type PythonType = number | boolean | string | Buffer | PythonObject | null;