Source: binary-record-builder.js

/**
 * @author Toru Nagashima
 * @copyright 2016 Toru Nagashima. All rights reserved.
 * See LICENSE file in root directory for full license.
 */
"use strict"

//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------

const accessors = require("./accessors")
const assert = require("./assert")
const BinaryRecord = require("./binary-record").BinaryRecord
const getArrayRecord = require("./array-record").getArrayRecord
const getObjectRecord = require("./object-record").getObjectRecord

//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------

const NUMBER_ACCESSOR_TYPE = /^(?:u?int(?:8|16|32)|bit[1-7]|float(?:32|64)?|double)$/
const STRING_TYPE = /^([a-zA-Z0-9_-]+)\(([1-9][0-9]*)\)$/
const ARRAY_BRACKET = /^([a-zA-Z0-9_-]+(?:\[[1-9][0-9]*\])*)\[([1-9][0-9]*)\]$/

/**
 * Parse bracket notations in the given type name.
 *
 * e.g.
 * - `"int8[4]" → ["int8", 4]`
 * - `"int32[3][4]" → ["int32", 3, 4]`
 * - `"string(16)[4]" → ["string(16)", 4]`
 *
 * @param {string} type - The type name to parse.
 * @returns {any[]} Parsed information. This is an array.
 *      1st element is originated type name.
 *      2nd and later elements are array lengths.
 * @private
 */
function parseArrayBrackets(type) {
    if (!(typeof type === "string")) {
        return type
    }

    const m = ARRAY_BRACKET.exec(type)
    if (m == null) {
        return type
    }

    const length = parseInt(m[2], 10)
    const elementType = parseArrayBrackets(m[1])
    if (Array.isArray(elementType)) {
        elementType.push(length)
        return elementType
    }

    return [elementType, length]
}

/**
 * Gets the accessor implementation of the given type name.
 *
 * If the accessor implementation of the type had not found, this will throw an
 * error.
 *
 * @param {string} type - The type name to get.
 * @returns {bre/accessors~Accessor} Found accessor.
 * @private
 */
function getAccessor(type) {
    const strippedType = parseArrayBrackets(type)

    // Address arrays.
    if (Array.isArray(strippedType)) {
        let accessor = getAccessor(strippedType[0])
        for (let i = strippedType.length - 1; i >= 1; --i) {
            const length = strippedType[i]

            accessor = accessors.record(getArrayRecord(accessor, length))
        }

        return accessor
    }

    // Embedded types and array shorthands.
    if (typeof strippedType === "string") {
        // Number types.
        if (NUMBER_ACCESSOR_TYPE.test(strippedType)) {
            return accessors[strippedType]
        }

        // String types.
        let m = null
        if ((m = STRING_TYPE.exec(strippedType)) != null) {
            const byteLength = parseInt(m[2], 10)
            const encoding = (m[1] === "string") ? "utf8" : m[1]

            return accessors.string(encoding, byteLength)
        }
    }

    // Record types.
    if (typeof strippedType === "function" &&
        strippedType.prototype instanceof BinaryRecord
    ) {
        return accessors.record(strippedType)
    }

    throw new Error(`invalid type: ${strippedType}`)
}

//------------------------------------------------------------------------------
// Exports
//------------------------------------------------------------------------------

/**
 * Defines an object record.
 *
 * Object records are similar to C `struct`.
 * It has several properties and it can map to binary blocks.
 *
 *     const ExampleRecord = bre.defineObjectRecord("ExampleRecord", [
 *         {type: "uint8", name: "a"},
 *         {type: "uint8", name: "b"},
 *     ])
 *
 *     const buffer = Buffer.from([0x01, 0x02])
 *     const record = ExampleRecord.view(buffer)
 *
 *     console.log("a =", record.a)           // => "a = 1"
 *     console.log("buffer[0] =", buffer[0])  // => "buffer[0] = 1"
 *     record.a = 7
 *     console.log("a =", record.a)           // => "a = 7"
 *     console.log("buffer[0] =", buffer[0])  // => "buffer[0] = 7"
 *
 * @param {string} className - The class name of new record.
 *
 * - This requires [a valid identifier of ECMAScript].
 * - This requires an upper case character at the head of the name.
 *
 * [a valid identifier of ECMAScript]: http://www.ecma-international.org/ecma-262/7.0/#sec-identifiers
 *
 * @method module:bre.defineObjectRecord
 * @param {Array<module:bre.FieldDefinition|module:bre.SkipDefinition>} fields
 *  The list of field definition.
 * @returns {class} The defined record class.
 */
exports.defineObjectRecord = function defineObjectRecord(className, fields) {
    assert.instanceOf(fields, Array, "fields")

    const definition = fields.reduce(
        (d, f, i) => {
            if ("skip" in f) {
                assert.integer(f.skip, `fields[${i}].skip`)
                assert.gte(f.skip, 1, `fields[${i}].skip`)

                d.bitLength += f.skip
            }
            else {
                const name = f.name
                const accessor = getAccessor(f.type)
                const offset = d.bitLength

                d.fields.push({name, accessor, offset})
                d.bitLength += accessor.bits
            }

            return d
        },
        {className, fields: [], bitLength: 0}
    )

    return getObjectRecord(definition)
}

/**
 * Defines an array record.
 *
 * The class this method defined is array-like.
 * It has the ability to access by indexes and iterators.
 *
 *     const ListRecord = bre.defineArrayRecord("uint8", 3)
 *
 *     const buffer = Buffer.from([0x01, 0x02, 0x03])
 *     const record = ListRecord.view(buffer)
 *
 *     console.log(record.length)  // => 3
 *     console.log(record[0])      // => 1
 *     console.log(record[1])      // => 2
 *     console.log(record[2])      // => 3
 *
 *     for (const value of record) {
 *         console.log(value)      // => 1 2 3
 *     }
 *
 * And it has several methods of `Array.prototype`:
 *
 * - `concat`
 * - `entries`
 * - `every`
 * - `filter`
 * - `find`
 * - `findIndex`
 * - `forEach`
 * - `includes`
 * - `indexOf`
 * - `join`
 * - `keys`
 * - `lastIndexOf`
 * - `map`
 * - `reduce`
 * - `reduceRight`
 * - `slice`
 * - `some`
 * - `values`
 *
 * @method module:bre.defineArrayRecord
 * @param {string|class|Array} type - The type of the elements.
 *  See {@link module:bre.FieldDefinition}'s `type` property for details.
 * @param {number} length - The count of elements.
 * @returns {class} The defined record class.
 */
exports.defineArrayRecord = function defineArrayRecord(type, length) {
    return getArrayRecord(getAccessor(type), length)
}

/**
 * The definition of a field.
 *
 * This type is used to define object records.
 * A field defines a getter/setter pair of the record.
 *
 *     const SubRecord = bre.defineObjectRecord("SubRecord", [
 *         {type: "uint8", name: "a"},
 *         {type: "uint8", name: "b"},
 *     ])
 *
 *     const Record = bre.defineObjectRecord("Record", [
 *         {type: "uint8[4]", name: "list"},
 *         {type: SubRecord, name: "sub"},
 *         {type: [SubRecord, 8], name: "subList"},
 *     ])
 *
 * @memberof module:bre
 * @interface FieldDefinition
 */

/**
 * The name of this field.
 *
 * - This requires [a valid identifier of ECMAScript].
 * - The following names are forbidden:
 *     - `__defineGetter__`
 *     - `__defineSetter__`
 *     - `__lookupGetter__`
 *     - `__lookupSetter__`
 *     - `__proto__`
 *     - `__noSuchMethod__`
 *     - `constructor`
 *     - `hasOwnProperty`
 *     - `isPrototypeOf`
 *     - `propertyIsEnumerable`
 *     - `toJSON`
 *     - `toLocaleString`
 *     - `toString`
 *     - `valueOf`
 *
 * [a valid identifier of ECMAScript]: http://www.ecma-international.org/ecma-262/7.0/#sec-identifiers
 *
 * @name module:bre.FieldDefinition#name
 * @type {string}
 */

/**
 * The type name of this field.
 *
 * - **Signed integer types:**
 *     - `"int8"` - 8 bits signed integer (`-128..127`).
 *     - `"int16"` - 16 bits signed integer (`-32768..32767`).
 *     - `"int32"` - 32 bits signed integer (`-2147483648..2147483647`).
 * - **Unsigned integer types:**
 *     - `"uint8"` - 8 bits unsigned integer (`0..255`).
 *     - `"uint16"` - 16 bits unsigned integer (`0..65535`).
 *     - `"uint32"` - 32 bits unsigned integer (`0..4294967295`).
 * - **Small unsigned integer types:**
 *     - `"bit1"` - 1 bits unsigned integer (`0..1`).
 *     - `"bit2"` - 2 bits unsigned integer (`0..3`).
 *     - `"bit3"` - 3 bits unsigned integer (`0..7`).
 *     - `"bit4"` - 4 bits unsigned integer (`0..15`).
 *     - `"bit5"` - 5 bits unsigned integer (`0..31`).
 *     - `"bit6"` - 6 bits unsigned integer (`0..63`).
 *     - `"bit7"` - 7 bits unsigned integer (`0..127`).
 * - **Floating point number types:**
 *     - `"float32"` - 32 bits floating point number (a.k.a. `float`).
 *     - `"float64"` - 64 bits floating point number (a.k.a. `double`).
 * - **String types:**
 *     - `"string(N)"` - fixed length (N bytes) string (e.g. `"string(32)"`).
 *       The default encoding of this type is `utf8`. It can use a specific
 *       encoding type instead of `"string"`. For examples:
 *         - `"utf8(N)"` - fixed length (N bytes) string.
 *         - `"utf16be(N)"` - fixed length (N bytes) string.
 *         - `"shift_jis(N)"` - fixed length (N bytes) string.
 *     - **Note:** It needs to set
 *       {@link module:bre/lib/text-encoders.TextEncoder text encoder}
 *       before use string types.
 * - **Record types:**
 *     - Another record class (e.g. `ExampleRecord`).
 * - **Array types:**
 *     - `"T[N]"` - fixed length (count N) array. T is another type
 *       (e.g. `"uint8[8]"`). An array type can be T (e.g. `"uint8[2][3]"`).
 *     - `[T, N]` - fixed length (count N) array. T is another type
 *       (e.g. `["uint8", 8]`, this is same as `"uint8[8]"`). This notation
 *       is for record types (e.g. `[ExampleRecord, 8]`).
 *
 * @name module:bre.FieldDefinition#type
 * @type {string|class|Array}
 */

/**
 * The definition of skipping for object records.
 *
 * This type is used to define object records.
 *
 *     const SubRecord = bre.defineObjectRecord("SubRecord", [
 *         {type: "bit4", name: "a"},
 *         {skip: 4},
 *         {type: "uint8", name: "b"},
 *     ])
 *
 * @interface module:bre.SkipDefinition
 */

/**
 * The size in bits to skip.
 *
 * @name module:bre.SkipDefinition#skip
 * @type {number}
 */