Source: AMF0/deserializer.js

import DynBuffer from '@seirdotexe/dynbuffer';
import Markers from '../AMF/markers.js';
import Packet from '../AMF/remoting/packet.js';
import Reference from './reference.js';

/**
 * @typedef {import('../AMF/alias.js').default} ClassAlias
 */

/** @module AMF0/Deserializer */
export default class Deserializer {
  /**
   * A holder for the 'length' param for each AMF packet's header/message, and important for AVM+
   * @static
   * @private
   * @type {number}
   */
  static #AMF_PACKET_DATA_LENGTH = 0;

  /**
   * The AMF class alias holder
   * @private
   * @type {ClassAlias}
   */
  #classAlias;
  /**
   * The 'deserialize' function holder
   * @private
   * @type {Function}
   */
  #AMF_Deserialize;
  /**
   * The 'bytesAvailable' function holder
   * @private
   * @type {Function}
   */
  #AMF3_DYNBUF_BYTESAVAILABLE;
  /**
   * The DynBuffer instance containing AMF0 bytes for this instance
   * @private
   * @type {DynBuffer}
   */
  #dynbuf;
  /**
   * The AMF0 reference holder
   * @private
   * @type {Reference}
   */
  #reference;

  /**
   * Creates a new AMF0 deserializer
   * @param {ClassAlias} classAlias - The class alias internally coming from the AMF entrypoint class
   * @param {Function} AMF_Deserialize - The 'deserialize' function coming from the AMF entrypoint class
   */
  constructor(classAlias, AMF_Deserialize) {
    this.#classAlias = classAlias;
    this.#AMF_Deserialize = AMF_Deserialize;
    this.#AMF3_DYNBUF_BYTESAVAILABLE = null;
    this.#dynbuf = new DynBuffer();
    this.#reference = new Reference();
  }

  /**
   * 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) {
    // Copy over the buffer to the (empty) DynBuffer instance when passed
    if (buffer && (this.#dynbuf.length === 0)) {
      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.AMF0.NULL: return null;
      case Markers.AMF0.UNDEFINED: return undefined;
      case Markers.AMF0.NUMBER: return this.#deserializeNumber();
      case Markers.AMF0.BOOLEAN: return this.#deserializeBoolean();
      case Markers.AMF0.STRING: return this.#deserializeString();
      case Markers.AMF0.LONG_STRING: return this.#deserializeLongString();
      case Markers.AMF0.REFERENCE: return this.#deserializeReference();
      case Markers.AMF0.OBJECT: return this.#deserializeObject();
      case Markers.AMF0.ECMA_ARRAY: return this.#deserializeArray();
      case Markers.AMF0.DATE: return this.#deserializeDate();
      case Markers.AMF0.TYPED_OBJECT: return this.#deserializeTypedObject();
      case Markers.AMF0.AVMPLUS: return this.#deserializeAVMPlus();
      default: return this.#deserializeUnidentifiedObject(marker);
    }
  }

  /**
   * Deserializes AMF binary data to a packet
   * @param {Buffer} buffer - The buffer containing the AMF binary data
   * @param {Function} AMF3_REFERENCE_RESET - The 'reset' function coming from the AMF3 Reference class
   * @returns {Packet} The deserialized packet
   * @throws {RangeError} If the AMF binary data 'length' isn't the same amount as what we deserialized
   */
  deserializePacket(buffer, AMF3_REFERENCE_RESET, AMF3_DYNBUF_BYTESAVAILABLE) {
    if (!this.#AMF3_DYNBUF_BYTESAVAILABLE) this.#AMF3_DYNBUF_BYTESAVAILABLE = AMF3_DYNBUF_BYTESAVAILABLE
    this.#dynbuf.writeBytes(buffer); this.#dynbuf.position = 0; // Read and reset to the start so we can start reading AMF binary data

    let positionBeforeAMF = 0
    const version = this.#dynbuf.readShort();
    const packet = new Packet(version);

    // Read headers
    for (let i = 0, headerCount = this.#dynbuf.readUnsignedShort(); i < headerCount; i++) {
      // Serializers and deserializers must reset reference indices to 0 each time a new header is processed
      this.#reference.reset(); AMF3_REFERENCE_RESET();
      // Read 'name' and 'mustUnderstand'
      const name = this.#dynbuf.readUTF();
      const mustUnderstand = this.#dynbuf.readBoolean();
      // Read the expected amount of AMF bytes and store the position before reading AMF data
      const length = this.#dynbuf.readUnsignedInt(); positionBeforeAMF = this.#dynbuf.position; Deserializer.#AMF_PACKET_DATA_LENGTH = length;
      // Deserialize AMF data
      const data = this.deserialize();
      // Check for length mismatch in the header payload
      if (this.#dynbuf.position !== (positionBeforeAMF + length)) {
        throw new RangeError(`AMF header '${name}' length mismatch. Expected ${length}, but got ${this.#dynbuf.position - positionBeforeAMF} instead`);
      }

      packet.addHeader(name, mustUnderstand, data);
    }

    // Read messages
    for (let i = 0, messageCount = this.#dynbuf.readUnsignedShort(); i < messageCount; i++) {
      // Serializers and deserializers must reset reference indices to 0 each time a new message is processed
      this.#reference.reset(); AMF3_REFERENCE_RESET();
      // Read 'targetURI' and 'responseURI'
      const targetURI = this.#dynbuf.readUTF();
      const responseURI = this.#dynbuf.readUTF();
      // Read the expected amount of AMF bytes and store the position before reading AMF data
      const length = this.#dynbuf.readUnsignedInt(); positionBeforeAMF = this.#dynbuf.position++; Deserializer.#AMF_PACKET_DATA_LENGTH = length; // Skip STRICT_ARRAY marker (++ on pos)
      // Deserialize AMF data
      const data = this.#deserializeStrictArray();
      // Check for length mismatch in the message payload
      if (this.#dynbuf.position !== (positionBeforeAMF + length)) {
        throw new RangeError(`AMF message '${targetURI}' length mismatch. Expected ${length}, but got ${this.#dynbuf.position - positionBeforeAMF} instead`);
      }

      packet.addMessage(targetURI, responseURI, ...data);
    }

    return packet;
  }

  /**
   * Deserializes a number
   * @private
   * @returns {number} The deserialized number
   */
  #deserializeNumber() {
    return this.#dynbuf.readDouble();
  }

  /**
   * Deserializes a boolean
   * @private
   * @returns {boolean} The deserialized boolean
   */
  #deserializeBoolean() {
    return this.#dynbuf.readBoolean();
  }

  /**
   * Deserializes a string
   * @private
   * @returns {string} The deserialized string
   */
  #deserializeString() {
    return this.#dynbuf.readUTF();
  }

  /**
   * Deserializes a long string
   * @private
   * @returns {string} The deserialized long string
   */
  #deserializeLongString() {
    return this.#dynbuf.readUTFBytes(this.#dynbuf.readUnsignedInt());
  }

  /**
   * Deserializes a referenced object
   * @private
   * @returns {object} The deserialized referenced object
   */
  #deserializeReference() {
    return this.#reference.get(this.#dynbuf.readUnsignedShort());
  }

  /**
   * Deserializes an object
   * @private
   * @returns {object} The deserialized object
   */
  #deserializeObject() {
    const value = {};

    this.#reference.set(value);

    for (let key = this.#dynbuf.readUTF(); !!key || (this.#dynbuf.readByte() !== Markers.AMF0.OBJECT_END); key = this.#dynbuf.readUTF()) {
      value[key] = this.deserialize();
    }

    return value;
  }

  /**
   * Deserializes an array
   * @private
   * @returns {any[]} The deserialized array
   */
  #deserializeArray() {
    const value = []; value.length = this.#dynbuf.readUnsignedInt();

    this.#reference.set(value);

    for (let key = this.#dynbuf.readUTF(); !!key || (this.#dynbuf.readByte() !== Markers.AMF0.OBJECT_END); key = this.#dynbuf.readUTF()) {
      value[key] = this.deserialize();

      //! Undocumented behavior - Turn invalid values into empty values
      if ((value[key] === null) || (value[key] === undefined)) {
        delete value[key];
      }
    }

    return value;
  }

  /**
   * Deserializes a date
   * @private
   * @returns {Date} The deserialized date
   */
  #deserializeDate() {
    const time = this.#dynbuf.readDouble();
    const timezoneOffset = this.#dynbuf.readShort(); //! Unused and reserved
    const value = new Date(time);

    this.#reference.set(value);

    return value;
  }

  /**
   * Deserializes a strict array
   * @private
   * @returns {any[]} The deserialized strict array
   */
  #deserializeStrictArray() {
    const value = []; value.length = this.#dynbuf.readUnsignedInt();

    this.#reference.set(value);

    for (let i = 0; i < value.length; i++) {
      value[i] = this.deserialize();
    }

    return value;
  }

  /**
   * Deserializes a typed object
   * @private
   * @return {any} The deserialized typed object
   */
  #deserializeTypedObject() {
    const aliasName = this.#dynbuf.readUTF();
    const classObj = this.#classAlias.getClassByAlias(aliasName);
    const value = new classObj();

    this.#reference.set(value);

    for (let key = this.#dynbuf.readUTF(); !!key || (this.#dynbuf.readByte() !== Markers.AMF0.OBJECT_END); key = this.#dynbuf.readUTF()) {
      value[key] = this.deserialize();
    }

    return value;
  }

  /**
   * Deserializes an AVMPlus marker to switch to AMF3
   * @private
   */
  #deserializeAVMPlus() {
    // Parse the AMF3 binary data by using the current position in the buffer and the amount of AMF bytes we should read, coming from 'length' param
    // When reading a header's data, and it is AVM+, it'll include a zero at the end of the buffer, but it doesn't seem to matter...
    const AMF3Data = this.#dynbuf.stream.subarray(this.#dynbuf.position, this.#dynbuf.position + Deserializer.#AMF_PACKET_DATA_LENGTH);
    const deserialized = this.#AMF_Deserialize(AMF3Data);
    const leftOverBytes = this.#AMF3_DYNBUF_BYTESAVAILABLE();

    this.#dynbuf.position += AMF3Data.length;

    // In an AMF packet, an added message can contain multiple entries of data
    // If below is true, we're certainly dealing with another entry of data, so we must decrement the current position with the amount of AMF3 bytes available to continue deserializing
    if (leftOverBytes !== 0) this.#dynbuf.position -= leftOverBytes;

    return deserialized;
  }

  /**
   * Catch an unidentifiable object
   * @private
   * @param {number} marker - The unknown AMF0 marker
   * @throws {ReferenceError} If an unknown AMF0 marker has been found
   */
  #deserializeUnidentifiedObject(marker) {
    if (marker !== Markers.AMF0.UNSUPPORTED) {
      throw new ReferenceError(`Unknown or unsupported AMF0 marker found: '${marker}'.`);
    }
  }
}