Source: lib/msf/msf_classes.js

/*! @license
 * Shaka Player
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

goog.provide('shaka.msf.Reader');
goog.provide('shaka.msf.Writer');

goog.require('shaka.util.BufferUtils');
goog.require('shaka.util.IReleasable');
goog.require('shaka.util.StringUtils');

goog.requireType('shaka.msf.Utils');

/**
 * Reader wraps a stream and provides convenience methods for reading
 * pieces from a stream.
 *
 * @implements {shaka.util.IReleasable}
 */
shaka.msf.Reader = class {
  /**
   * @param {!Uint8Array} buffer
   * @param {!ReadableStream<!Uint8Array>} stream
   */
  constructor(buffer, stream) {
    /** @private {!Uint8Array} */
    this.buffer_ = buffer;
    /** @private {!ReadableStream<!Uint8Array>} */
    this.stream_ = stream;
    /** @private {!ReadableStreamDefaultReader<!Uint8Array>} */
    this.reader_ = /** @type {!ReadableStreamDefaultReader<!Uint8Array>} */ (
      stream.getReader());
  }

  /**
   * @return {number}
   */
  getByteLength() {
    return this.buffer_.byteLength;
  }

  /**
   * @return {!Uint8Array}
   */
  getBuffer() {
    return shaka.util.BufferUtils.toUint8(this.buffer_);
  }

  /**
   * Adds more data to the buffer, returning true if more data was added.
   *
   * @return {!Promise<boolean>}
   * @private
   */
  async fill_() {
    const result = await this.reader_.read();
    if (result.done) {
      return false;
    }

    const buffer = shaka.util.BufferUtils.toUint8(result.value);

    if (this.buffer_.byteLength === 0) {
      this.buffer_ = buffer;
    } else {
      const temp = new Uint8Array(this.buffer_.byteLength + buffer.byteLength);
      temp.set(this.buffer_);
      temp.set(buffer, this.buffer_.byteLength);
      this.buffer_ = temp;
    }

    return true;
  }

  /**
   * Add more data to the buffer until it's at least size bytes.
   *
   * @param {number} size
   * @return {!Promise}
   * @private
   */
  async fillTo_(size) {
    while (this.buffer_.byteLength < size) {
      // eslint-disable-next-line no-await-in-loop
      if (!(await this.fill_())) {
        throw new Error('unexpected end of stream');
      }
    }
  }

  /**
   * Consumes the first size bytes of the buffer.
   *
   * @param {number} size
   * @return {!Uint8Array}
   * @private
   */
  slice_(size) {
    const result = shaka.util.BufferUtils.toUint8(this.buffer_, 0, size);
    this.buffer_ = shaka.util.BufferUtils.toUint8(this.buffer_, size);
    return result;
  }

  /**
   * @param {number} size
   * @return {!Promise<!Uint8Array>}
   */
  async read(size) {
    if (size === 0) {
      return new Uint8Array([]);
    }
    await this.fillTo_(size);
    return this.slice_(size);
  }

  /**
   * @return {!Promise<!Uint8Array>}
   */
  async readAll() {
    // eslint-disable-next-line no-empty,no-await-in-loop
    while (await this.fill_()) {}
    return this.slice_(this.buffer_.byteLength);
  }

  /**
   * @return {!Promise<!Array<string>>}
   */
  async tuple() {
    // Get the count of tuple elements
    const count = await this.u53();

    // Read each tuple element individually
    const tupleElements = [];
    for (let i = 0; i < count; i++) {
      // Each element is a var int length followed by that many bytes
      // eslint-disable-next-line no-await-in-loop
      const length = await this.u53();
      // eslint-disable-next-line no-await-in-loop
      const bytes = await this.read(length);
      const element = shaka.util.StringUtils.fromUTF8(bytes);
      tupleElements.push(element);
    }

    return tupleElements;
  }

  /**
   * @param {(number|undefined)=} maxLength
   * @return {!Promise<string>}
   */
  async string(maxLength) {
    const length = await this.u53();
    if (maxLength !== undefined && length > maxLength) {
      throw new Error(
          `string length ${length} exceeds max length ${maxLength}`);
    }

    const buffer = await this.read(length);
    return shaka.util.StringUtils.fromUTF8(buffer);
  }

  /**
   * @return {!Promise<number>}
   */
  async u8() {
    await this.fillTo_(1);
    return this.slice_(1)[0];
  }

  /**
   * @return {!Promise<boolean>}
   */
  async u8Bool() {
    return (await this.u8()) !== 0;
  }

  /**
   * Returns a Number using 53-bits, the max Javascript can use for integer math
   * @return {!Promise<number>}
   */
  async u53() {
    const result = await this.u53WithSize();
    return result.value;
  }

  /**
   * Returns a Number using 53-bits and tracks the number of bytes read
   * @return {!Promise<{value: number, bytesRead: number}>}
   */
  async u53WithSize() {
    const result = await this.u62WithSize();
    const v = result.value;
    if (v > Number.MAX_SAFE_INTEGER) {
      throw new Error('value larger than 53-bits; use v62 instead');
    }

    return {value: Number(v), bytesRead: result.bytesRead};
  }

  /**
   * If the number is greater than 53 bits, it throws an error.
   *
   * @return {!Promise<number>}
   */
  async u62() {
    const result = await this.u62WithSize();
    return result.value;
  }

  /**
   * Returns a number and tracks the number of bytes read
   * If the number is greater than 53 bits, it throws an error.
   *
   * @return {!Promise<{value: number, bytesRead: number}>}
   */
  async u62WithSize() {
    await this.fillTo_(1);
    const size = (this.buffer_[0] & 0xc0) >> 6;

    let value;
    let bytesRead;

    if (size === 0) {
      bytesRead = 1;
      const first = this.slice_(1)[0];
      value = first & 0x3f; // 6 bits
    } else if (size === 1) {
      bytesRead = 2;
      await this.fillTo_(2);
      const slice = this.slice_(2);
      const view = shaka.util.BufferUtils.toDataView(slice);
      value = view.getInt16(0) & 0x3fff; // 14 bits
    } else if (size === 2) {
      bytesRead = 4;
      await this.fillTo_(4);
      const slice = this.slice_(4);
      const view = shaka.util.BufferUtils.toDataView(slice);
      value = view.getUint32(0) & 0x3fffffff; // 30 bits
    } else if (size === 3) {
      bytesRead = 8;
      await this.fillTo_(8);
      const slice = this.slice_(8);
      const view = shaka.util.BufferUtils.toDataView(slice);
      value = BigInt(view.getBigUint64(0)) & BigInt('0x3fffffffffffffff');
      if (value > BigInt(Number.MAX_SAFE_INTEGER)) {
        throw new Error('Number bigger than 53-bits');
      }
      value = Number(value);
    } else {
      throw new Error(`invalid size: ${size}`);
    }
    return {value, bytesRead};
  }

  /**
   * @return {!Promise<!Array<shaka.msf.Utils.KeyValuePair>>}
   */
  async keyValuePairs() {
    const numPairs = await this.u53();
    const result = [];
    for (let i = 0; i < numPairs; i++) {
      // eslint-disable-next-line no-await-in-loop
      const key = await this.u62();
      if (key % 2 === 0) {
        // eslint-disable-next-line no-await-in-loop
        const value = await this.u62();
        result.push({type: key, value});
      } else {
        // eslint-disable-next-line no-await-in-loop
        const length = await this.u53();
        // eslint-disable-next-line no-await-in-loop
        const value = await this.read(length);
        result.push({type: key, value});
      }
    }
    return result;
  }

  /**
   * @return {!Promise<boolean>}
   */
  async done() {
    if (this.buffer_.byteLength > 0) {
      return false;
    }
    return !(await this.fill_());
  }

  /**
   * @return {!Promise}
   */
  async close() {
    this.reader_.releaseLock();
    await this.stream_.cancel('Reader closed');
  }

  /**
   * @override
   */
  release() {
    this.reader_.releaseLock();
  }
};

/**
 * Writer wraps a stream and writes chunks of data.
 */
shaka.msf.Writer = class {
  /**
   * @param {!WritableStream} stream
   */
  constructor(stream) {
    /** @private {!WritableStream} */
    this.stream_ = stream;
    /** @private {!WritableStreamDefaultWriter} */
    this.writer_ = stream.getWriter();
  }

  /**
   * @param {!Uint8Array} value
   * @return {!Promise}
   */
  async write(value) {
    await this.writer_.write(value);
  }
};