Source: dataContainer.js

/**
 * @file This file contains the definition of the class `DataContainer` which represents
 * messages exchanged between the client and the server.
 * @author Thorsten Kummerow <thorsten@textcontrol.com>
 */
'use strict';

const HeaderSize = 16;  // Four 32 bit integers

/**
 * Data container for messages 
 */
class DataContainer {

   /**
    * Creates an instance of this class from a byte array.
    * @param {Uint8Array} data The message's raw data as a byte array.
    */
   constructor(data = null) {
      // Command ID and parameters are integers:

      /** @type {number} */
      this.commandID = 0;
      /** @type {number} */
      this.param1 = 0;
      /** @type {number} */
      this.param2 = 0;
      /** @type {number} */
      this.param3 = 0;

      /**
       * Property 'data' contains the (optional) data content of the message 
       * @type {Uint8Array} 
       */
      this.data = null;

      // Parse the raw data
      if (data) this.fromByteArray(data);
   }

   /**
    * Initializes this instance from a byte array.
    * @param {Uint8Array} data The message's raw data as a byte array.
    */
   fromByteArray(data) {
      if (data.length < HeaderSize) throw new Error(`Buffer is too small to contain a command header. Bailing out. (Buffer size: ${data.length})`);
      var view = new DataView(data.buffer);
      this.commandID = view.getInt32(0, true);
      this.param1 = view.getInt32(4, true);
      this.param2 = view.getInt32(8, true);
      this.param3 = view.getInt32(12, true);
      if (data.length > HeaderSize) {
         this.data = new Uint8Array(data.length - HeaderSize);
         this.data.set(data.subarray(HeaderSize));
      }
   }

   /**
    * Converts this instance to a byte array.
    * @returns {Uint8Array} A byte array representing this data container.
    */
   toByteArray() {
      let bufSize = HeaderSize;
      if (this.data !== null) bufSize += this.data.length;
      let result = new Uint8Array(bufSize);
      let viewResult = new DataView(result.buffer);
      viewResult.setInt32(0, this.commandID, true);
      viewResult.setUint32(4, this.param1, true);
      viewResult.setUint32(8, this.param2, true);
      viewResult.setUint32(12, this.param3, true);
      if (this.data !== null) result.set(this.data, 16);
      return result;
   }

   /**
    * Serializes a server message to JSON. The content of the "data" property is 
    * @returns {string} The message serialized to JSON
    */
   serialize() {
      let command = {
         CommandID: this.commandID,
         Param1: this.param1,
         Param2: this.param2,
         Param3: this.param3,
         Data: this.dataAsObject
      };
      return JSON.stringify(command);
   }

   /**
    * The data part of this message as an object.
    * @readonly
    * @type {Object}
    */
   get dataAsObject() {
      let strData = null;
      try {
         strData = this.dataAsString;
      }
      catch (err) {
         // In case data does not contain a valid utf-8 string it is most likely 
         // raw binary data. Return that as base64 in those cases.
         return Buffer.from(this.data).toString('base64');
      }
      if (strData === null) return null;
      if (!strData.length) return {};
      try {
         return JSON.parse(strData);
      }
      catch (err) { 
         // In case the data does not contain a JSON string it is most likely 
         // raw binary data. Return that as base64 in those cases.
         return Buffer.from(this.data).toString('base64');
      }
   }

   /**
    * The data part of this message as a string. Doesn't catch exceptions which arise 
    * when calling Buffer.from().
    * @readonly
    * @type {String}
    */
   get dataAsString() {
      if (this.data === null) return null;
      if (!this.data.length) return '';
      return Buffer.from(this.data, 'utf-8').toString();
   }
}

/**
 * Creates a DataContainer instance from a byte array.
 * @param {Uint8Array} data The message's raw data as a byte array.
 */
function fromByteArray(data) {
   return new DataContainer(data);
}

/**
 * Creates a DataContainer instance from a JSON string.
 * @param {String} strJSON The message as a JSON string.
 */
function fromJSON(strJSON) {
   // Assume strJSON to be a JSON string. Caller is responsible for catching
   // possible exceptions.
   let obj = JSON.parse(strJSON);
   if ((obj === null) || !obj.hasOwnProperty('CommandID')) {
      console.error('Command string does not contain a command ID.');
      return null;
   }

   let result = new DataContainer();
   result.commandID = obj.CommandID;
   try {
      if (obj.hasOwnProperty('Param1')) result.param1 = obj.Param1;
      if (obj.hasOwnProperty('Param2')) result.param2 = obj.Param2;
      if (obj.hasOwnProperty('Param3')) result.param3 = obj.Param3;
      if (obj.hasOwnProperty('Data')) {
         let data = obj.Data;
         if (typeof data === 'string') {
            result.data = new Buffer.from(data, 'utf-8');
         }
         else {
            // Just revert to JSON and see to it later
            let strData = JSON.stringify(data);
            result.data = new Buffer.from(strData, 'utf-8');
         }
      }
   }
   catch (err) { console.error(`Error in DataContainer.fromJSON: '${err.message}'`); }
   return result;
}

module.exports.fromByteArray = fromByteArray;
module.exports.fromJSON = fromJSON;
module.exports.DataContainer = DataContainer;