import DynBuffer from '@seirdotexe/dynbuffer';
import Markers from '../AMF/markers.js';
import Reference from './reference.js';
/**
* @typedef {import('../AMF/alias.js').default} ClassAlias
*/
/** @module AMF3/Deserializer */
export default class Deserializer {
/**
* The AMF class alias holder
* @private
* @type {ClassAlias}
*/
#classAlias;
/**
* The DynBuffer instance containing AMF3 bytes for this instance
* @private
* @type {DynBuffer}
*/
#dynbuf;
/**
* The AMF0 reference holder
* @private
* @type {Reference}
*/
#reference;
/**
* Creates a new AMF3 deserializer
* @param {ClassAlias} classAlias - The class alias internally coming from the AMF entrypoint class
*/
constructor(classAlias) {
this.#classAlias = classAlias;
this.#dynbuf = new DynBuffer();
this.#reference = new Reference();
}
/**
* Returns the AMF3 reference reset method, needed for serializing and deserializing AMF packets in AMF0 base
* @returns {Function} The 'reset' function coming from the AMF3 Reference class
*/
get reset() {
return this.#reference.reset;
}
/**
* Returns the amount of AMF3 bytes available, needed for deserializing AMF packets where a header has leftover data
* @returns {number} The amount of AMF3 bytes available
*/
bytesAvailable() {
return this.#dynbuf.bytesAvailable;
}
/**
* Reads a variable length unsigned 29-bit integer
* @private
* @returns {number} The decoded integer
*/
#readUint29() {
let byte = this.#dynbuf.readUnsignedByte();
let value = byte & 0x7F;
if (!(byte & 0x80)) return value;
byte = this.#dynbuf.readUnsignedByte();
value = (value << 7) | (byte & 0x7F);
if (!(byte & 0x80)) return value;
byte = this.#dynbuf.readUnsignedByte();
value = (value << 7) | (byte & 0x7F);
if (!(byte & 0x80)) return value;
byte = this.#dynbuf.readUnsignedByte();
return (value << 8) | byte;
}
/**
* Deserializes AMF binary data to an object
* @param {Buffer?} buffer - The buffer containing the AMF binary data, only applicable from base call in AMF entrypoint class
* @returns {any} The deserialized object
*/
deserialize(buffer) {
if (buffer) {
if (this.#dynbuf.length !== 0) this.#dynbuf.clear(); // Compatibility for AVM+ to clear and deserialize again (example: when a packet has the AVM+ marker twice)
this.#dynbuf.writeBytes(buffer); this.#dynbuf.position = 0; // Read and reset to the start so we can start reading AMF binary data
}
const marker = this.#dynbuf.readByte();
switch (marker) {
case Markers.AMF3.NULL: return null;
case Markers.AMF3.UNDEFINED: return undefined;
case Markers.AMF3.INTEGER: case Markers.AMF3.DOUBLE: return this.#deserializeInteger(marker);
case Markers.AMF3.TRUE: case Markers.AMF3.FALSE: return this.#deserializeBoolean(marker);
case Markers.AMF3.STRING: return this.#deserializeString();
case Markers.AMF3.OBJECT: return this.#deserializeObject();
case Markers.AMF3.ARRAY: return this.#deserializeArray();
case Markers.AMF3.DATE: return this.#deserializeDate();
case Markers.AMF3.BYTE_ARRAY: return this.#deserializeByteArray();
case Markers.AMF3.VECTOR_INT: case Markers.AMF3.VECTOR_UINT: case Markers.AMF3.VECTOR_DOUBLE: case Markers.AMF3.VECTOR_OBJECT: return this.#deserializeVector(marker);
case Markers.AMF3.DICTIONARY: return this.#deserializeDictionary();
default: return this.#deserializeUnidentifiedObject(marker);
}
}
/**
* Deserializes a boolean
* @private
* @param {2|3} marker - The marker representing one of the two boolean markers, true or false
* @returns {boolean} The deserialized boolean
*/
#deserializeBoolean(marker) {
return (marker === Markers.AMF3.TRUE);
}
/**
* Deserializes an integer
* @private
* @param {4|5} marker - The marker representing one of the two 'number' markers, integer or double
* @returns {number} The deserialized integer
*/
#deserializeInteger(marker) {
if (marker === Markers.AMF3.INTEGER) {
return (this.#readUint29() << 3 >> 3);
} else {
return this.#dynbuf.readDouble();
}
}
/**
* Deserializes a string
* @private
* @returns {string} The deserialized string
*/
#deserializeString() {
const ref = this.#readUint29();
if ((ref & 1) === 0) return this.#reference.get(ref >> 1, 'strings');
const length = (ref >> 1);
const value = this.#dynbuf.readUTFBytes(length);
if (length > 0) this.#reference.set(value, 'strings');
return value;
}
/**
* Deserializes an object
* @private
* @returns {object} The deserialized object
* @throws {ReferenceError} If an attempt is made to deserialize an unregistered class
*/
#deserializeObject() {
const ref = this.#readUint29();
if ((ref & 1) === 0) return this.#reference.get(ref >> 1, 'objects');
let value = {};
let traits = { className: null, externalizable: null, dynamic: null, keys: [], count: null };
if ((ref & 3) === 1) { // Extract the lowest 2 bits, and check if they're equal to 1 (true)
traits = this.#reference.get(ref >> 2, 'traits'); // Then this trait is referenceable
} else { // Else, we need to read the trait
traits.className = this.#deserializeString();
traits.externalizable = ((ref & 4) === 4); // Bit 2 (0x04)
traits.dynamic = ((ref & 8) === 8); // Bit 3 (0x08)
traits.count = (ref >> 4); // Bits 4 and above
for (let i = 0; i < traits.count; i++) {
traits.keys[i] = this.#deserializeString();
}
this.#reference.set(JSON.stringify(traits), 'traits');
}
const classObj = (traits.externalizable || traits.className !== '') ? this.#classAlias.getClassByAlias(traits.className) : undefined;
// Externalizable/registered classes
if (traits.externalizable || traits.className !== '') {
if (!classObj) throw new ReferenceError(`Tried to deserialize an unregistered class: '${traits.className}'.`);
value = new classObj();
this.#reference.set(value, 'objects'); // Important reference set for typed classes as we need to save a constructed copy
if (traits.externalizable) {
value.readExternal(this.#dynbuf);
return value;
}
}
if (traits.dynamic && traits.className === '') { // Regular objects
for (let key = this.#deserializeString(); key !== ''; key = this.#deserializeString()) { // Read until uint29 terminator
value[key] = this.deserialize();
}
this.#reference.set(value, 'objects');
return value;
} else { // Registered class where we already know the keys
for (let i = 0; i < traits.count; i++) {
value[traits.keys[i]] = this.deserialize();
}
// Seal the class when the 'dynamic' getter is explicitly set to false to disallow adding properties to the class
if (Object.getOwnPropertyDescriptor(Object.getPrototypeOf(value), 'dynamic')?.get && !traits.dynamic) {
Object.seal(value);
}
return value;
}
}
/**
* Deserializes an array
* @private
* @returns {any[]} The deserialized array
*/
#deserializeArray() {
const ref = this.#readUint29();
if ((ref & 1) === 0) return this.#reference.get(ref >> 1, 'objects');
const length = (ref >> 1);
const value = []; value.length = length;
this.#reference.set(value, 'objects');
for (let key = this.#deserializeString(); key !== ''; key = this.#deserializeString()) {
value[key] = this.deserialize();
}
for (let i = 0; i < length; i++) {
value[i] = this.deserialize();
}
return value;
}
/**
* Deserializes a date
* @private
* @returns {Date} The deserialized date
*/
#deserializeDate() {
const ref = this.#readUint29();
if ((ref & 1) === 0) return this.#reference.get(ref >> 1, 'objects');
const value = new Date(this.#dynbuf.readDouble());
this.#reference.set(value, 'objects');
return value;
}
/**
* Deserializes a ByteArray (DynBuffer)
* @private
* @returns {DynBuffer} The deserialized ByteArray
*/
#deserializeByteArray() {
const ref = this.#readUint29();
if ((ref & 1) === 0) return this.#reference.get(ref >> 1, 'objects');
const length = (ref >> 1);
const value = new DynBuffer();
this.#reference.set(value, 'objects');
value.writeBytes(this.#dynbuf, this.#dynbuf.position, length); // Write 'x' in 'length' amount of bytes available to us into the new value
value.position = 0; // Reset the position of the new DynBuffer just like AVM+ does, for convenience
this.#dynbuf.position += length; // Increment our AMF DynBuffer with the amount of read data
return value;
}
/**
* Deserializes a Vector (typed array)
* @private
* @param {0x0D|0x0E|0x0F|0x10} marker - The marker indicating the type of the Vector
* @returns {Int32Array|Uint32Array|Float64Array|any[]} The deserialized Vector
*/
#deserializeVector(marker) {
const ref = this.#readUint29();
if ((ref & 1) === 0) return this.#reference.get(ref >> 1, 'objects');
const isFixed = this.#dynbuf.readBoolean();
const length = (ref >> 1);
const value = marker === Markers.AMF3.VECTOR_INT ? new Int32Array(length) :
marker === Markers.AMF3.VECTOR_UINT ? new Uint32Array(length) :
marker === Markers.AMF3.VECTOR_DOUBLE ? new Float64Array(length) : [];
if (marker === Markers.AMF3.VECTOR_OBJECT) {
const aliasName = this.#deserializeString();
const classObj = this.#classAlias.getClassByAlias(aliasName);
Object.defineProperty(value, 'VectorObject', { value: aliasName });
}
for (let i = 0; i < length; i++) {
value[i] = marker === Markers.AMF3.VECTOR_INT ? this.#dynbuf.readInt() :
marker === Markers.AMF3.VECTOR_UINT ? this.#dynbuf.readUnsignedInt() :
marker === Markers.AMF3.VECTOR_DOUBLE ? this.#dynbuf.readDouble() : this.deserialize();
}
if (isFixed) Object.preventExtensions(value);
this.#reference.set(value, 'objects');
return value;
}
/**
* Deserializes a Dictionary (Map)
* @private
* @returns {Map} The deserialized Dictionary
*/
#deserializeDictionary() {
const ref = this.#readUint29();
if ((ref & 1) === 0) return this.#reference.get(ref >> 1, 'objects');
const isWeak = this.#dynbuf.readBoolean(); // Todo
const length = (ref >> 1);
const value = new Map();
this.#reference.set(value, 'objects');
for (let i = 0; i < length; i++) {
value.set(this.deserialize(), this.deserialize());
}
return value;
}
/**
* Catch an unidentifiable object
* @private
* @param {number} marker - The unknown AMF3 marker
* @throws {ReferenceError} If an unknown AMF3 marker has been found
*/
#deserializeUnidentifiedObject(marker) {
throw new ReferenceError(`Unknown or unsupported AMF3 marker found: '${marker}'.`);
}
}