Source: lib/util/data_view_writer.js

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

goog.provide('shaka.util.DataViewWriter');

goog.require('goog.asserts');
goog.require('shaka.util.BufferUtils');
goog.require('shaka.util.StringUtils');
goog.require('shaka.util.Error');


/**
 * @summary DataViewWriter abstracts a growable DataView for binary writing.
 * @export
 */
shaka.util.DataViewWriter = class {
  /**
   * @param {number} initialSize
   * @param {shaka.util.DataViewWriter.Endianness} endianness The endianness.
   */
  constructor(initialSize, endianness) {
    /** @private {!Uint8Array} */
    this.buffer_ = new Uint8Array(initialSize);

    /** @private {!DataView} */
    this.dataView_ = shaka.util.BufferUtils.toDataView(this.buffer_);

    /** @private {boolean} */
    this.littleEndian_ =
        endianness == shaka.util.DataViewWriter.Endianness.LITTLE_ENDIAN;

    /** @private {number} */
    this.position_ = 0;
  }

  /** @return {number} */
  getPosition() {
    return this.position_;
  }

  /** @return {number} */
  getLength() {
    return this.position_;
  }

  /** @return {!Uint8Array} */
  getBytes() {
    return shaka.util.BufferUtils.toUint8(this.buffer_, 0, this.position_);
  }

  /**
   * Resets the position.
   */
  reset() {
    this.position_ = 0;
  }

  /**
   * @param {number} bytes
   * @private
   */
  ensureSpace_(bytes) {
    const required = this.position_ + bytes;
    if (required <= this.buffer_.length) {
      return;
    }

    const newSize = Math.max(this.buffer_.length * 2, required);
    const newBuffer = new Uint8Array(newSize);
    newBuffer.set(this.buffer_);
    this.buffer_ = newBuffer;
    this.dataView_ = shaka.util.BufferUtils.toDataView(this.buffer_);
  }

  /** @param {number} value */
  writeUint8(value) {
    this.ensureSpace_(1);
    this.dataView_.setUint8(this.position_, value & 0xff);
    this.position_ += 1;
  }

  /** @param {number} value */
  writeUint16(value) {
    this.ensureSpace_(2);
    this.dataView_.setUint16(
        this.position_, value & 0xffff, this.littleEndian_);
    this.position_ += 2;
  }

  /** @param {number} value */
  writeUint32(value) {
    this.ensureSpace_(4);
    this.dataView_.setUint32(this.position_, value >>> 0, this.littleEndian_);
    this.position_ += 4;
  }

  /** @param {number} value */
  writeUint64(value) {
    if (value < 0 || value > Number.MAX_SAFE_INTEGER) {
      throw new shaka.util.Error(
          shaka.util.Error.Severity.CRITICAL,
          shaka.util.Error.Category.MEDIA,
          shaka.util.Error.Code.JS_INTEGER_OVERFLOW);
    }

    this.ensureSpace_(8);

    const high = Math.floor(value / 0x100000000);
    const low = value >>> 0;

    if (this.littleEndian_) {
      this.dataView_.setUint32(this.position_, low, true);
      this.dataView_.setUint32(this.position_ + 4, high, true);
    } else {
      this.dataView_.setUint32(this.position_, high, false);
      this.dataView_.setUint32(this.position_ + 4, low, false);
    }

    this.position_ += 8;
  }

  /**
   * @param {!Uint8Array} bytes
   */
  writeBytes(bytes) {
    goog.asserts.assert(bytes, 'Bad call to writeBytes');
    this.ensureSpace_(bytes.byteLength);
    const view = shaka.util.BufferUtils.toUint8(
        this.buffer_, this.position_, bytes.byteLength);
    view.set(bytes);
    this.position_ += bytes.byteLength;
  }

  /**
   * Writes a UTF-8 string prefixed by its length as uint32.
   * @param {string} str
   */
  writeString(str) {
    const bytes = shaka.util.BufferUtils.toUint8(
        shaka.util.StringUtils.toUTF8(str));
    this.writeUint32(bytes.length);
    this.writeBytes(bytes);
  }

  /**
   * Variable-length unsigned integer (up to 53 bits).
   * @param {number} value
   */
  writeVarInt53(value) {
    if (value < 0) {
      throw new Error(`Underflow: ${value}`);
    }

    const MAX_U6 = (1 << 6) - 1;
    const MAX_U14 = (1 << 14) - 1;
    const MAX_U30 = (1 << 30) - 1;
    const MAX_U53 = Number.MAX_SAFE_INTEGER;

    if (value <= MAX_U6) {
      // 1-byte encoding (0xxxxxxx)
      this.writeUint8(value);
    } else if (value <= MAX_U14) {
      // 2-byte encoding (10xxxxxx xxxxxxxx)
      this.writeUint8(((value >> 8) & 0x3f) | 0x40);
      this.writeUint8(value & 0xff);
    } else if (value <= MAX_U30) {
      // 4-byte encoding (110xxxxx xxxxxxxx xxxxxxxx xxxxxxxx)
      this.writeUint8(((value >> 24) & 0x1f) | 0x80);
      this.writeUint8((value >> 16) & 0xff);
      this.writeUint8((value >> 8) & 0xff);
      this.writeUint8(value & 0xff);
    } else if (value <= MAX_U53) {
      // 8-byte encoding (1110xxxx xxxxxxxx xxxxxxxx xxxxxxxx
      //                  xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx)
      const high = Math.floor(value / 0x100000000);
      const low = value % 0x100000000;
      this.writeUint8(((high >> 24) & 0x0f) | 0xc0);
      this.writeUint8((high >> 16) & 0xff);
      this.writeUint8((high >> 8) & 0xff);
      this.writeUint8(high & 0xff);

      this.writeUint8((low >> 24) & 0xff);
      this.writeUint8((low >> 16) & 0xff);
      this.writeUint8((low >> 8) & 0xff);
      this.writeUint8(low & 0xff);
    } else {
      throw new shaka.util.Error(
          shaka.util.Error.Severity.CRITICAL,
          shaka.util.Error.Category.MEDIA,
          shaka.util.Error.Code.JS_INTEGER_OVERFLOW);
    }
  }

  /**
   * Variable-length unsigned integer (up to 62 bits).
   * @param {!number} value
   */
  writeVarInt62(value) {
    if (value < 0) {
      throw new Error(`Underflow: ${value}`);
    }

    if (value <= Number.MAX_SAFE_INTEGER) {
      this.writeVarInt53(value);
      return;
    }

    const v = BigInt(value);

    const maskFF = BigInt(0xff);
    const mask0F = BigInt(0x0f);
    const prefixC0 = BigInt(0xc0);

    this.ensureSpace_(8);

    this.writeUint8(Number(((v >> BigInt(56)) & mask0F) | prefixC0));
    this.writeUint8(Number((v >> BigInt(48)) & maskFF));
    this.writeUint8(Number((v >> BigInt(40)) & maskFF));
    this.writeUint8(Number((v >> BigInt(32)) & maskFF));
    this.writeUint8(Number((v >> BigInt(24)) & maskFF));
    this.writeUint8(Number((v >> BigInt(16)) & maskFF));
    this.writeUint8(Number((v >> BigInt(8)) & maskFF));
    this.writeUint8(Number(v & maskFF));
  }

  /**
   * Writes a UTF-8 string prefixed by its length as a var int (up to 53 bits).
   * @param {string} str
   */
  writeStringVarInt(str) {
    const bytes = shaka.util.BufferUtils.toUint8(
        shaka.util.StringUtils.toUTF8(str));
    this.writeVarInt53(bytes.length);
    this.writeBytes(bytes);
  }

  /**
   * @param {number} position
   */
  seek(position) {
    goog.asserts.assert(position >= 0, 'Bad seek');
    if (position > this.buffer_.length) {
      throw this.outOfBounds_();
    }
    this.position_ = position;
  }

  /**
   * @param {number} bytes
   */
  skip(bytes) {
    goog.asserts.assert(bytes >= 0, 'Bad skip');
    this.ensureSpace_(bytes);
    this.position_ += bytes;
  }

  /**
   * Reserve 2 bytes and return their position for later patching.
   * @return {number}
   */
  reserveUint16() {
    const pos = this.position_;
    this.skip(2);
    return pos;
  }

  /**
   * @param {number} position
   * @param {number} value
   */
  patchUint16(position, value) {
    const cur = this.position_;
    this.position_ = position;
    this.writeUint16(value);
    this.position_ = cur;
  }

  /**
   * @return {!shaka.util.Error}
   * @private
   */
  outOfBounds_() {
    return new shaka.util.Error(
        shaka.util.Error.Severity.CRITICAL,
        shaka.util.Error.Category.MEDIA,
        shaka.util.Error.Code.BUFFER_WRITE_OUT_OF_BOUNDS);
  }
};


/**
 * Endianness.
 * @enum {number}
 * @export
 */
shaka.util.DataViewWriter.Endianness = {
  'BIG_ENDIAN': 0,
  'LITTLE_ENDIAN': 1,
};