baja/obj/Blob.js

/*
 * @copyright 2022 Tridium, Inc. All Rights Reserved.
 * @author Sai Komaravolu
 */

/*eslint-env browser */

/**
 * Defines {@link baja.Blob}
 * @module baja/obj/Blob
 */
define([
  'bajaScript/sys',
  'bajaScript/baja/obj/Simple',
  'bajaScript/baja/obj/byteUtil',
  'lex!' ], function (
  baja,
  Simple,
  byteUtil,
  lexjs) {

  'use strict';

  /**
   * A `baja:Blob` of Bajascript is a wrapper class for raw byte array objects.
   *
   * @since Niagara 4.14
   * @class
   * @alias baja.Blob
   * @extends baja.Simple
   */
  class Blob extends Simple {

    /**
     * When creating a `Blob` use make() instead of new Object.
     * Constructor should be considered private.
     *
     * @param {Uint8Array} byteArr array of bytes
     */
    constructor(byteArr = new Uint8Array()) {
      super();

      if (!(byteArr && byteArr instanceof Uint8Array)) {
        throw new Error(lexjs.$getSync({ module: 'baja', key: 'blob.arrayInput.message', def: 'array input is required.' }));
      }
      this.$byteArr = byteArr;
    }

    /**
     * Returns the Blob as an encoded string.
     *
     * @returns {String} encoded string
     */
    encodeToString() {
      return byteUtil.byteArrayToBase64String(this.$byteArr);
    }

    /**
     * Converts the input string to a Blob.
     *
     * @param {string} base64String
     * @returns {baja.Blob}
     */
    decodeFromString(base64String = '') {
      if (base64String.length === 0) {
        return Blob.DEFAULT;
      }
      return Blob.make(byteUtil.base64StringToByteArray(base64String));
    }

    /**
     * Makes a `Blob`.
     *
     * @param {Uint8Array} byteArr array of bytes
     * @returns {baja.Blob}
     * @example
     *
     * // blob can be created with a byte array.
     * let blob = baja.Blob.make(new Uint8Array([ 1, 2, 3, 4, 5, 6 ]));
     *
     * // DEFAULT blob can be created with no parameters.
     * let blob = baja.Blob.make();
     *
     * // blob can be created as a DEFAULT.
     * let blob = baja.Blob.DEFAULT;
     */
    make(byteArr = new Uint8Array()) {
      return Blob.make(byteArr);
    }

    /**
     * @returns {Number} Byte length of the Blob
     */
    length() {
      return this.$byteArr.length;
    }

    /**
     * @param {Number} index position of the bytes in the Blob
     * @returns {Number|undefined} Number if a byte is available at given index else undefined.
     */
    byteAt(index) {
      return this.$byteArr.at(index);
    }

    /**
     * @returns {Uint8Array} array of the Blob bytes
     */
    getBytes() {
      //Return a copy of the byte array to avoid instance modification.
      return this.$byteArr.slice();
    }

    /**
     * Returns the blob's byte array with the index position as the pivot.
     * In case of an empty array, bytes are copied into it and index position is not considered.
     *
     * @param {Array} arrayToCopy
     * @param index
     * @returns {Uint8Array}
     */
    copyBytes(arrayToCopy, index) {
      arrayToCopy.splice(index, 0, ...Array.from(this.$byteArr));
      return new Uint8Array(arrayToCopy);
    }

    /**
     *  Checks for equality of bytes in the Blob and input.
     *
     * @param {Array} byteArr
     * @returns {boolean}
     */
    bytesEqual(byteArr) {
      if (byteArr.length !== this.$byteArr.length) {
        return false;
      }

      for (let i = 0; i < this.$byteArr.length - 1; i++) {
        if (byteArr[i] !== this.$byteArr[i]) {
          return false;
        }
      }
      return true;
    }

    /**
     * @param {Uint8Array} byteArr
     * @returns {baja.Blob}
     */
    static make(byteArr = new Uint8Array()) {
      return new Blob(byteArr);
    }

    /**
     * Makes a Blob from an input hex string.
     *
     * @param {String} hexString
     * @returns {baja.Blob}
     * @example
     *
     * let blob = baja.Blob.make('00123a');
     */
    static makeBlobFromHexString(hexString) {
      // If user enters an empty string, return a default Blob without invoking the Blob.DEFAULT.decodeFromString()
      if (hexString.trim().length === 0) {
        return Blob.DEFAULT;
      }
      // Check if input string is a Hex and return Blob from decoded Hex String.
      if (isHex(hexString)) {
        let base64String = hexStringToBase64String(hexString);
        return Blob.DEFAULT.decodeFromString(base64String);
      }
      // Throw an error if input is not a Hex String.
      throw new Error(lexjs.$getSync({ module: 'baja', key: 'blob.notAHexString.message', def: '{0} is not a hexadecimal.', args: [ hexString ] }));
    }

    /**
     * @returns {baja.Blob}
     */
    static get DEFAULT() {
      return DEFAULT;
    }

  }


  /**
   * Converts Hex string to a Base64 array.
   *
   * @private
   * @param {String} hexString
   * @returns {String}
   */
  const hexStringToBase64String = (hexString) => {
    // hex string cannot have odd length, prepending a '0' if it's odd.
    hexString = (hexString.length % 2 === 0 ? hexString : '0' + hexString);

    return btoa(String.fromCharCode.apply(null,
      hexString.replace(/([A-Fa-f\d]{2})/g, "0x$1;") // 00123a => [ 0x00;0x12;0x3a]
        .replace(/;$/, "").split(";")) // removes end ; and splits them
    );
  };

  /**
   * Validates if input string is Hexadecimal.
   *
   * @private
   * @param {String} str
   * @returns {boolean}
   */
  const isHex = (str) => {
    return !str.match(/[^A-Fa-f\d]/);
  };

  const DEFAULT = Blob.make();

  return Blob;
});