UNPKG

buffer-layout

Version:

Translation between JavaScript values and Buffers

1,631 lines (1,529 loc) 84.4 kB
/* The MIT License (MIT) * * Copyright 2015-2018 Peter A. Bigot * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Support for translating between Buffer instances and JavaScript * native types. * * {@link module:Layout~Layout|Layout} is the basis of a class * hierarchy that associates property names with sequences of encoded * bytes. * * Layouts are supported for these scalar (numeric) types: * * {@link module:Layout~UInt|Unsigned integers in little-endian * format} with {@link module:Layout.u8|8-bit}, {@link * module:Layout.u16|16-bit}, {@link module:Layout.u24|24-bit}, * {@link module:Layout.u32|32-bit}, {@link * module:Layout.u40|40-bit}, and {@link module:Layout.u48|48-bit} * representation ranges; * * {@link module:Layout~UIntBE|Unsigned integers in big-endian * format} with {@link module:Layout.u16be|16-bit}, {@link * module:Layout.u24be|24-bit}, {@link module:Layout.u32be|32-bit}, * {@link module:Layout.u40be|40-bit}, and {@link * module:Layout.u48be|48-bit} representation ranges; * * {@link module:Layout~Int|Signed integers in little-endian * format} with {@link module:Layout.s8|8-bit}, {@link * module:Layout.s16|16-bit}, {@link module:Layout.s24|24-bit}, * {@link module:Layout.s32|32-bit}, {@link * module:Layout.s40|40-bit}, and {@link module:Layout.s48|48-bit} * representation ranges; * * {@link module:Layout~IntBE|Signed integers in big-endian format} * with {@link module:Layout.s16be|16-bit}, {@link * module:Layout.s24be|24-bit}, {@link module:Layout.s32be|32-bit}, * {@link module:Layout.s40be|40-bit}, and {@link * module:Layout.s48be|48-bit} representation ranges; * * 64-bit integral values that decode to an exact (if magnitude is * less than 2^53) or nearby integral Number in {@link * module:Layout.nu64|unsigned little-endian}, {@link * module:Layout.nu64be|unsigned big-endian}, {@link * module:Layout.ns64|signed little-endian}, and {@link * module:Layout.ns64be|unsigned big-endian} encodings; * * 32-bit floating point values with {@link * module:Layout.f32|little-endian} and {@link * module:Layout.f32be|big-endian} representations; * * 64-bit floating point values with {@link * module:Layout.f64|little-endian} and {@link * module:Layout.f64be|big-endian} representations; * * {@link module:Layout.const|Constants} that take no space in the * encoded expression. * * and for these aggregate types: * * {@link module:Layout.seq|Sequence}s of instances of a {@link * module:Layout~Layout|Layout}, with JavaScript representation as * an Array and constant or data-dependent {@link * module:Layout~Sequence#count|length}; * * {@link module:Layout.struct|Structure}s that aggregate a * heterogeneous sequence of {@link module:Layout~Layout|Layout} * instances, with JavaScript representation as an Object; * * {@link module:Layout.union|Union}s that support multiple {@link * module:Layout~VariantLayout|variant layouts} over a fixed * (padded) or variable (not padded) span of bytes, using an * unsigned integer at the start of the data or a separate {@link * module:Layout.unionLayoutDiscriminator|layout element} to * determine which layout to use when interpreting the buffer * contents; * * {@link module:Layout.bits|BitStructure}s that contain a sequence * of individual {@link * module:Layout~BitStructure#addField|BitField}s packed into an 8, * 16, 24, or 32-bit unsigned integer starting at the least- or * most-significant bit; * * {@link module:Layout.cstr|C strings} of varying length; * * {@link module:Layout.blob|Blobs} of fixed- or variable-{@link * module:Layout~Blob#length|length} raw data. * * All {@link module:Layout~Layout|Layout} instances are immutable * after construction, to prevent internal state from becoming * inconsistent. * * @local Layout * @local ExternalLayout * @local GreedyCount * @local OffsetLayout * @local UInt * @local UIntBE * @local Int * @local IntBE * @local NearUInt64 * @local NearUInt64BE * @local NearInt64 * @local NearInt64BE * @local Float * @local FloatBE * @local Double * @local DoubleBE * @local Sequence * @local Structure * @local UnionDiscriminator * @local UnionLayoutDiscriminator * @local Union * @local VariantLayout * @local BitStructure * @local BitField * @local Boolean * @local Blob * @local CString * @local Constant * @local bindConstructorLayout * @module Layout * @license MIT * @author Peter A. Bigot * @see {@link https://github.com/pabigot/buffer-layout|buffer-layout on GitHub} */ 'use strict'; /** * Base class for layout objects. * * **NOTE** This is an abstract base class; you can create instances * if it amuses you, but they won't support the {@link * Layout#encode|encode} or {@link Layout#decode|decode} functions. * * @param {Number} span - Initializer for {@link Layout#span|span}. The * parameter must be an integer; a negative value signifies that the * span is {@link Layout#getSpan|value-specific}. * * @param {string} [property] - Initializer for {@link * Layout#property|property}. * * @abstract */ class Layout { constructor(span, property) { if (!Number.isInteger(span)) { throw new TypeError('span must be an integer'); } /** The span of the layout in bytes. * * Positive values are generally expected. * * Zero will only appear in {@link Constant}s and in {@link * Sequence}s where the {@link Sequence#count|count} is zero. * * A negative value indicates that the span is value-specific, and * must be obtained using {@link Layout#getSpan|getSpan}. */ this.span = span; /** The property name used when this layout is represented in an * Object. * * Used only for layouts that {@link Layout#decode|decode} to Object * instances. If left undefined the span of the unnamed layout will * be treated as padding: it will not be mutated by {@link * Layout#encode|encode} nor represented as a property in the * decoded Object. */ this.property = property; } /** Function to create an Object into which decoded properties will * be written. * * Used only for layouts that {@link Layout#decode|decode} to Object * instances, which means: * * {@link Structure} * * {@link Union} * * {@link VariantLayout} * * {@link BitStructure} * * If left undefined the JavaScript representation of these layouts * will be Object instances. * * See {@link bindConstructorLayout}. */ makeDestinationObject() { return {}; } /** * Decode from a Buffer into an JavaScript value. * * @param {Buffer} b - the buffer from which encoded data is read. * * @param {Number} [offset] - the offset at which the encoded data * starts. If absent a zero offset is inferred. * * @returns {(Number|Array|Object)} - the value of the decoded data. * * @abstract */ decode(b, offset) { throw new Error('Layout is abstract'); } /** * Encode a JavaScript value into a Buffer. * * @param {(Number|Array|Object)} src - the value to be encoded into * the buffer. The type accepted depends on the (sub-)type of {@link * Layout}. * * @param {Buffer} b - the buffer into which encoded data will be * written. * * @param {Number} [offset] - the offset at which the encoded data * starts. If absent a zero offset is inferred. * * @returns {Number} - the number of bytes encoded, including the * space skipped for internal padding, but excluding data such as * {@link Sequence#count|lengths} when stored {@link * ExternalLayout|externally}. This is the adjustment to `offset` * producing the offset where data for the next layout would be * written. * * @abstract */ encode(src, b, offset) { throw new Error('Layout is abstract'); } /** * Calculate the span of a specific instance of a layout. * * @param {Buffer} b - the buffer that contains an encoded instance. * * @param {Number} [offset] - the offset at which the encoded instance * starts. If absent a zero offset is inferred. * * @return {Number} - the number of bytes covered by the layout * instance. If this method is not overridden in a subclass the * definition-time constant {@link Layout#span|span} will be * returned. * * @throws {RangeError} - if the length of the value cannot be * determined. */ getSpan(b, offset) { if (0 > this.span) { throw new RangeError('indeterminate span'); } return this.span; } /** * Replicate the layout using a new property. * * This function must be used to get a structurally-equivalent layout * with a different name since all {@link Layout} instances are * immutable. * * **NOTE** This is a shallow copy. All fields except {@link * Layout#property|property} are strictly equal to the origin layout. * * @param {String} property - the value for {@link * Layout#property|property} in the replica. * * @returns {Layout} - the copy with {@link Layout#property|property} * set to `property`. */ replicate(property) { const rv = Object.create(this.constructor.prototype); Object.assign(rv, this); rv.property = property; return rv; } /** * Create an object from layout properties and an array of values. * * **NOTE** This function returns `undefined` if invoked on a layout * that does not return its value as an Object. Objects are * returned for things that are a {@link Structure}, which includes * {@link VariantLayout|variant layouts} if they are structures, and * excludes {@link Union}s. If you want this feature for a union * you must use {@link Union.getVariant|getVariant} to select the * desired layout. * * @param {Array} values - an array of values that correspond to the * default order for properties. As with {@link Layout#decode|decode} * layout elements that have no property name are skipped when * iterating over the array values. Only the top-level properties are * assigned; arguments are not assigned to properties of contained * layouts. Any unused values are ignored. * * @return {(Object|undefined)} */ fromArray(values) { return undefined; } } exports.Layout = Layout; /* Provide text that carries a name (such as for a function that will * be throwing an error) annotated with the property of a given layout * (such as one for which the value was unacceptable). * * @ignore */ function nameWithProperty(name, lo) { if (lo.property) { return name + '[' + lo.property + ']'; } return name; } exports.nameWithProperty = nameWithProperty; /** * Augment a class so that instances can be encoded/decoded using a * given layout. * * Calling this function couples `Class` with `layout` in several ways: * * * `Class.layout_` becomes a static member property equal to `layout`; * * `layout.boundConstructor_` becomes a static member property equal * to `Class`; * * The {@link Layout#makeDestinationObject|makeDestinationObject()} * property of `layout` is set to a function that returns a `new * Class()`; * * `Class.decode(b, offset)` becomes a static member function that * delegates to {@link Layout#decode|layout.decode}. The * synthesized function may be captured and extended. * * `Class.prototype.encode(b, offset)` provides an instance member * function that delegates to {@link Layout#encode|layout.encode} * with `src` set to `this`. The synthesized function may be * captured and extended, but when the extension is invoked `this` * must be explicitly bound to the instance. * * @param {class} Class - a JavaScript class with a nullary * constructor. * * @param {Layout} layout - the {@link Layout} instance used to encode * instances of `Class`. */ function bindConstructorLayout(Class, layout) { if ('function' !== typeof Class) { throw new TypeError('Class must be constructor'); } if (Class.hasOwnProperty('layout_')) { throw new Error('Class is already bound to a layout'); } if (!(layout && (layout instanceof Layout))) { throw new TypeError('layout must be a Layout'); } if (layout.hasOwnProperty('boundConstructor_')) { throw new Error('layout is already bound to a constructor'); } Class.layout_ = layout; layout.boundConstructor_ = Class; layout.makeDestinationObject = (() => new Class()); Object.defineProperty(Class.prototype, 'encode', { value: function(b, offset) { return layout.encode(this, b, offset); }, writable: true, }); Object.defineProperty(Class, 'decode', { value: function(b, offset) { return layout.decode(b, offset); }, writable: true, }); } exports.bindConstructorLayout = bindConstructorLayout; /** * An object that behaves like a layout but does not consume space * within its containing layout. * * This is primarily used to obtain metadata about a member, such as a * {@link OffsetLayout} that can provide data about a {@link * Layout#getSpan|value-specific span}. * * **NOTE** This is an abstract base class; you can create instances * if it amuses you, but they won't support {@link * ExternalLayout#isCount|isCount} or other {@link Layout} functions. * * @param {Number} span - initializer for {@link Layout#span|span}. * The parameter can range from 1 through 6. * * @param {string} [property] - initializer for {@link * Layout#property|property}. * * @abstract * @augments {Layout} */ class ExternalLayout extends Layout { /** * Return `true` iff the external layout decodes to an unsigned * integer layout. * * In that case it can be used as the source of {@link * Sequence#count|Sequence counts}, {@link Blob#length|Blob lengths}, * or as {@link UnionLayoutDiscriminator#layout|external union * discriminators}. * * @abstract */ isCount() { throw new Error('ExternalLayout is abstract'); } } /** * An {@link ExternalLayout} that determines its {@link * Layout#decode|value} based on offset into and length of the buffer * on which it is invoked. * * *Factory*: {@link module:Layout.greedy|greedy} * * @param {Number} [elementSpan] - initializer for {@link * GreedyCount#elementSpan|elementSpan}. * * @param {string} [property] - initializer for {@link * Layout#property|property}. * * @augments {ExternalLayout} */ class GreedyCount extends ExternalLayout { constructor(elementSpan, property) { if (undefined === elementSpan) { elementSpan = 1; } if ((!Number.isInteger(elementSpan)) || (0 >= elementSpan)) { throw new TypeError('elementSpan must be a (positive) integer'); } super(-1, property); /** The layout for individual elements of the sequence. The value * must be a positive integer. If not provided, the value will be * 1. */ this.elementSpan = elementSpan; } /** @override */ isCount() { return true; } /** @override */ decode(b, offset) { if (undefined === offset) { offset = 0; } const rem = b.length - offset; return Math.floor(rem / this.elementSpan); } /** @override */ encode(src, b, offset) { return 0; } } /** * An {@link ExternalLayout} that supports accessing a {@link Layout} * at a fixed offset from the start of another Layout. The offset may * be before, within, or after the base layout. * * *Factory*: {@link module:Layout.offset|offset} * * @param {Layout} layout - initializer for {@link * OffsetLayout#layout|layout}, modulo `property`. * * @param {Number} [offset] - Initializes {@link * OffsetLayout#offset|offset}. Defaults to zero. * * @param {string} [property] - Optional new property name for a * {@link Layout#replicate| replica} of `layout` to be used as {@link * OffsetLayout#layout|layout}. If not provided the `layout` is used * unchanged. * * @augments {Layout} */ class OffsetLayout extends ExternalLayout { constructor(layout, offset, property) { if (!(layout instanceof Layout)) { throw new TypeError('layout must be a Layout'); } if (undefined === offset) { offset = 0; } else if (!Number.isInteger(offset)) { throw new TypeError('offset must be integer or undefined'); } super(layout.span, property || layout.property); /** The subordinated layout. */ this.layout = layout; /** The location of {@link OffsetLayout#layout} relative to the * start of another layout. * * The value may be positive or negative, but an error will thrown * if at the point of use it goes outside the span of the Buffer * being accessed. */ this.offset = offset; } /** @override */ isCount() { return ((this.layout instanceof UInt) || (this.layout instanceof UIntBE)); } /** @override */ decode(b, offset) { if (undefined === offset) { offset = 0; } return this.layout.decode(b, offset + this.offset); } /** @override */ encode(src, b, offset) { if (undefined === offset) { offset = 0; } return this.layout.encode(src, b, offset + this.offset); } } /** * Represent an unsigned integer in little-endian format. * * *Factory*: {@link module:Layout.u8|u8}, {@link * module:Layout.u16|u16}, {@link module:Layout.u24|u24}, {@link * module:Layout.u32|u32}, {@link module:Layout.u40|u40}, {@link * module:Layout.u48|u48} * * @param {Number} span - initializer for {@link Layout#span|span}. * The parameter can range from 1 through 6. * * @param {string} [property] - initializer for {@link * Layout#property|property}. * * @augments {Layout} */ class UInt extends Layout { constructor(span, property) { super(span, property); if (6 < this.span) { throw new RangeError('span must not exceed 6 bytes'); } } /** @override */ decode(b, offset) { if (undefined === offset) { offset = 0; } return b.readUIntLE(offset, this.span); } /** @override */ encode(src, b, offset) { if (undefined === offset) { offset = 0; } b.writeUIntLE(src, offset, this.span); return this.span; } } /** * Represent an unsigned integer in big-endian format. * * *Factory*: {@link module:Layout.u8be|u8be}, {@link * module:Layout.u16be|u16be}, {@link module:Layout.u24be|u24be}, * {@link module:Layout.u32be|u32be}, {@link * module:Layout.u40be|u40be}, {@link module:Layout.u48be|u48be} * * @param {Number} span - initializer for {@link Layout#span|span}. * The parameter can range from 1 through 6. * * @param {string} [property] - initializer for {@link * Layout#property|property}. * * @augments {Layout} */ class UIntBE extends Layout { constructor(span, property) { super( span, property); if (6 < this.span) { throw new RangeError('span must not exceed 6 bytes'); } } /** @override */ decode(b, offset) { if (undefined === offset) { offset = 0; } return b.readUIntBE(offset, this.span); } /** @override */ encode(src, b, offset) { if (undefined === offset) { offset = 0; } b.writeUIntBE(src, offset, this.span); return this.span; } } /** * Represent a signed integer in little-endian format. * * *Factory*: {@link module:Layout.s8|s8}, {@link * module:Layout.s16|s16}, {@link module:Layout.s24|s24}, {@link * module:Layout.s32|s32}, {@link module:Layout.s40|s40}, {@link * module:Layout.s48|s48} * * @param {Number} span - initializer for {@link Layout#span|span}. * The parameter can range from 1 through 6. * * @param {string} [property] - initializer for {@link * Layout#property|property}. * * @augments {Layout} */ class Int extends Layout { constructor(span, property) { super(span, property); if (6 < this.span) { throw new RangeError('span must not exceed 6 bytes'); } } /** @override */ decode(b, offset) { if (undefined === offset) { offset = 0; } return b.readIntLE(offset, this.span); } /** @override */ encode(src, b, offset) { if (undefined === offset) { offset = 0; } b.writeIntLE(src, offset, this.span); return this.span; } } /** * Represent a signed integer in big-endian format. * * *Factory*: {@link module:Layout.s8be|s8be}, {@link * module:Layout.s16be|s16be}, {@link module:Layout.s24be|s24be}, * {@link module:Layout.s32be|s32be}, {@link * module:Layout.s40be|s40be}, {@link module:Layout.s48be|s48be} * * @param {Number} span - initializer for {@link Layout#span|span}. * The parameter can range from 1 through 6. * * @param {string} [property] - initializer for {@link * Layout#property|property}. * * @augments {Layout} */ class IntBE extends Layout { constructor(span, property) { super(span, property); if (6 < this.span) { throw new RangeError('span must not exceed 6 bytes'); } } /** @override */ decode(b, offset) { if (undefined === offset) { offset = 0; } return b.readIntBE(offset, this.span); } /** @override */ encode(src, b, offset) { if (undefined === offset) { offset = 0; } b.writeIntBE(src, offset, this.span); return this.span; } } const V2E32 = Math.pow(2, 32); /* True modulus high and low 32-bit words, where low word is always * non-negative. */ function divmodInt64(src) { const hi32 = Math.floor(src / V2E32); const lo32 = src - (hi32 * V2E32); return {hi32, lo32}; } /* Reconstruct Number from quotient and non-negative remainder */ function roundedInt64(hi32, lo32) { return hi32 * V2E32 + lo32; } /** * Represent an unsigned 64-bit integer in little-endian format when * encoded and as a near integral JavaScript Number when decoded. * * *Factory*: {@link module:Layout.nu64|nu64} * * **NOTE** Values with magnitude greater than 2^52 may not decode to * the exact value of the encoded representation. * * @augments {Layout} */ class NearUInt64 extends Layout { constructor(property) { super(8, property); } /** @override */ decode(b, offset) { if (undefined === offset) { offset = 0; } const lo32 = b.readUInt32LE(offset); const hi32 = b.readUInt32LE(offset + 4); return roundedInt64(hi32, lo32); } /** @override */ encode(src, b, offset) { if (undefined === offset) { offset = 0; } const split = divmodInt64(src); b.writeUInt32LE(split.lo32, offset); b.writeUInt32LE(split.hi32, offset + 4); return 8; } } /** * Represent an unsigned 64-bit integer in big-endian format when * encoded and as a near integral JavaScript Number when decoded. * * *Factory*: {@link module:Layout.nu64be|nu64be} * * **NOTE** Values with magnitude greater than 2^52 may not decode to * the exact value of the encoded representation. * * @augments {Layout} */ class NearUInt64BE extends Layout { constructor(property) { super(8, property); } /** @override */ decode(b, offset) { if (undefined === offset) { offset = 0; } const hi32 = b.readUInt32BE(offset); const lo32 = b.readUInt32BE(offset + 4); return roundedInt64(hi32, lo32); } /** @override */ encode(src, b, offset) { if (undefined === offset) { offset = 0; } const split = divmodInt64(src); b.writeUInt32BE(split.hi32, offset); b.writeUInt32BE(split.lo32, offset + 4); return 8; } } /** * Represent a signed 64-bit integer in little-endian format when * encoded and as a near integral JavaScript Number when decoded. * * *Factory*: {@link module:Layout.ns64|ns64} * * **NOTE** Values with magnitude greater than 2^52 may not decode to * the exact value of the encoded representation. * * @augments {Layout} */ class NearInt64 extends Layout { constructor(property) { super(8, property); } /** @override */ decode(b, offset) { if (undefined === offset) { offset = 0; } const lo32 = b.readUInt32LE(offset); const hi32 = b.readInt32LE(offset + 4); return roundedInt64(hi32, lo32); } /** @override */ encode(src, b, offset) { if (undefined === offset) { offset = 0; } const split = divmodInt64(src); b.writeUInt32LE(split.lo32, offset); b.writeInt32LE(split.hi32, offset + 4); return 8; } } /** * Represent a signed 64-bit integer in big-endian format when * encoded and as a near integral JavaScript Number when decoded. * * *Factory*: {@link module:Layout.ns64be|ns64be} * * **NOTE** Values with magnitude greater than 2^52 may not decode to * the exact value of the encoded representation. * * @augments {Layout} */ class NearInt64BE extends Layout { constructor(property) { super(8, property); } /** @override */ decode(b, offset) { if (undefined === offset) { offset = 0; } const hi32 = b.readInt32BE(offset); const lo32 = b.readUInt32BE(offset + 4); return roundedInt64(hi32, lo32); } /** @override */ encode(src, b, offset) { if (undefined === offset) { offset = 0; } const split = divmodInt64(src); b.writeInt32BE(split.hi32, offset); b.writeUInt32BE(split.lo32, offset + 4); return 8; } } /** * Represent a 32-bit floating point number in little-endian format. * * *Factory*: {@link module:Layout.f32|f32} * * @param {string} [property] - initializer for {@link * Layout#property|property}. * * @augments {Layout} */ class Float extends Layout { constructor(property) { super(4, property); } /** @override */ decode(b, offset) { if (undefined === offset) { offset = 0; } return b.readFloatLE(offset); } /** @override */ encode(src, b, offset) { if (undefined === offset) { offset = 0; } b.writeFloatLE(src, offset); return 4; } } /** * Represent a 32-bit floating point number in big-endian format. * * *Factory*: {@link module:Layout.f32be|f32be} * * @param {string} [property] - initializer for {@link * Layout#property|property}. * * @augments {Layout} */ class FloatBE extends Layout { constructor(property) { super(4, property); } /** @override */ decode(b, offset) { if (undefined === offset) { offset = 0; } return b.readFloatBE(offset); } /** @override */ encode(src, b, offset) { if (undefined === offset) { offset = 0; } b.writeFloatBE(src, offset); return 4; } } /** * Represent a 64-bit floating point number in little-endian format. * * *Factory*: {@link module:Layout.f64|f64} * * @param {string} [property] - initializer for {@link * Layout#property|property}. * * @augments {Layout} */ class Double extends Layout { constructor(property) { super(8, property); } /** @override */ decode(b, offset) { if (undefined === offset) { offset = 0; } return b.readDoubleLE(offset); } /** @override */ encode(src, b, offset) { if (undefined === offset) { offset = 0; } b.writeDoubleLE(src, offset); return 8; } } /** * Represent a 64-bit floating point number in big-endian format. * * *Factory*: {@link module:Layout.f64be|f64be} * * @param {string} [property] - initializer for {@link * Layout#property|property}. * * @augments {Layout} */ class DoubleBE extends Layout { constructor(property) { super(8, property); } /** @override */ decode(b, offset) { if (undefined === offset) { offset = 0; } return b.readDoubleBE(offset); } /** @override */ encode(src, b, offset) { if (undefined === offset) { offset = 0; } b.writeDoubleBE(src, offset); return 8; } } /** * Represent a contiguous sequence of a specific layout as an Array. * * *Factory*: {@link module:Layout.seq|seq} * * @param {Layout} elementLayout - initializer for {@link * Sequence#elementLayout|elementLayout}. * * @param {(Number|ExternalLayout)} count - initializer for {@link * Sequence#count|count}. The parameter must be either a positive * integer or an instance of {@link ExternalLayout}. * * @param {string} [property] - initializer for {@link * Layout#property|property}. * * @augments {Layout} */ class Sequence extends Layout { constructor(elementLayout, count, property) { if (!(elementLayout instanceof Layout)) { throw new TypeError('elementLayout must be a Layout'); } if (!(((count instanceof ExternalLayout) && count.isCount()) || (Number.isInteger(count) && (0 <= count)))) { throw new TypeError('count must be non-negative integer ' + 'or an unsigned integer ExternalLayout'); } let span = -1; if ((!(count instanceof ExternalLayout)) && (0 < elementLayout.span)) { span = count * elementLayout.span; } super(span, property); /** The layout for individual elements of the sequence. */ this.elementLayout = elementLayout; /** The number of elements in the sequence. * * This will be either a non-negative integer or an instance of * {@link ExternalLayout} for which {@link * ExternalLayout#isCount|isCount()} is `true`. */ this.count = count; } /** @override */ getSpan(b, offset) { if (0 <= this.span) { return this.span; } if (undefined === offset) { offset = 0; } let span = 0; let count = this.count; if (count instanceof ExternalLayout) { count = count.decode(b, offset); } if (0 < this.elementLayout.span) { span = count * this.elementLayout.span; } else { let idx = 0; while (idx < count) { span += this.elementLayout.getSpan(b, offset + span); ++idx; } } return span; } /** @override */ decode(b, offset) { if (undefined === offset) { offset = 0; } const rv = []; let i = 0; let count = this.count; if (count instanceof ExternalLayout) { count = count.decode(b, offset); } while (i < count) { rv.push(this.elementLayout.decode(b, offset)); offset += this.elementLayout.getSpan(b, offset); i += 1; } return rv; } /** Implement {@link Layout#encode|encode} for {@link Sequence}. * * **NOTE** If `src` is shorter than {@link Sequence#count|count} then * the unused space in the buffer is left unchanged. If `src` is * longer than {@link Sequence#count|count} the unneeded elements are * ignored. * * **NOTE** If {@link Layout#count|count} is an instance of {@link * ExternalLayout} then the length of `src` will be encoded as the * count after `src` is encoded. */ encode(src, b, offset) { if (undefined === offset) { offset = 0; } const elo = this.elementLayout; const span = src.reduce((span, v) => { return span + elo.encode(v, b, offset + span); }, 0); if (this.count instanceof ExternalLayout) { this.count.encode(src.length, b, offset); } return span; } } /** * Represent a contiguous sequence of arbitrary layout elements as an * Object. * * *Factory*: {@link module:Layout.struct|struct} * * **NOTE** The {@link Layout#span|span} of the structure is variable * if any layout in {@link Structure#fields|fields} has a variable * span. When {@link Layout#encode|encoding} we must have a value for * all variable-length fields, or we wouldn't be able to figure out * how much space to use for storage. We can only identify the value * for a field when it has a {@link Layout#property|property}. As * such, although a structure may contain both unnamed fields and * variable-length fields, it cannot contain an unnamed * variable-length field. * * @param {Layout[]} fields - initializer for {@link * Structure#fields|fields}. An error is raised if this contains a * variable-length field for which a {@link Layout#property|property} * is not defined. * * @param {string} [property] - initializer for {@link * Layout#property|property}. * * @param {Boolean} [decodePrefixes] - initializer for {@link * Structure#decodePrefixes|property}. * * @throws {Error} - if `fields` contains an unnamed variable-length * layout. * * @augments {Layout} */ class Structure extends Layout { constructor(fields, property, decodePrefixes) { if (!(Array.isArray(fields) && fields.reduce((acc, v) => acc && (v instanceof Layout), true))) { throw new TypeError('fields must be array of Layout instances'); } if (('boolean' === typeof property) && (undefined === decodePrefixes)) { decodePrefixes = property; property = undefined; } /* Verify absence of unnamed variable-length fields. */ for (const fd of fields) { if ((0 > fd.span) && (undefined === fd.property)) { throw new Error('fields cannot contain unnamed variable-length layout'); } } let span = -1; try { span = fields.reduce((span, fd) => span + fd.getSpan(), 0); } catch (e) { } super(span, property); /** The sequence of {@link Layout} values that comprise the * structure. * * The individual elements need not be the same type, and may be * either scalar or aggregate layouts. If a member layout leaves * its {@link Layout#property|property} undefined the * corresponding region of the buffer associated with the element * will not be mutated. * * @type {Layout[]} */ this.fields = fields; /** Control behavior of {@link Layout#decode|decode()} given short * buffers. * * In some situations a structure many be extended with additional * fields over time, with older installations providing only a * prefix of the full structure. If this property is `true` * decoding will accept those buffers and leave subsequent fields * undefined, as long as the buffer ends at a field boundary. * Defaults to `false`. */ this.decodePrefixes = !!decodePrefixes; } /** @override */ getSpan(b, offset) { if (0 <= this.span) { return this.span; } if (undefined === offset) { offset = 0; } let span = 0; try { span = this.fields.reduce((span, fd) => { const fsp = fd.getSpan(b, offset); offset += fsp; return span + fsp; }, 0); } catch (e) { throw new RangeError('indeterminate span'); } return span; } /** @override */ decode(b, offset) { if (undefined === offset) { offset = 0; } const dest = this.makeDestinationObject(); for (const fd of this.fields) { if (undefined !== fd.property) { dest[fd.property] = fd.decode(b, offset); } offset += fd.getSpan(b, offset); if (this.decodePrefixes && (b.length === offset)) { break; } } return dest; } /** Implement {@link Layout#encode|encode} for {@link Structure}. * * If `src` is missing a property for a member with a defined {@link * Layout#property|property} the corresponding region of the buffer is * left unmodified. */ encode(src, b, offset) { if (undefined === offset) { offset = 0; } const firstOffset = offset; let lastOffset = 0; let lastWrote = 0; for (const fd of this.fields) { let span = fd.span; lastWrote = (0 < span) ? span : 0; if (undefined !== fd.property) { const fv = src[fd.property]; if (undefined !== fv) { lastWrote = fd.encode(fv, b, offset); if (0 > span) { /* Read the as-encoded span, which is not necessarily the * same as what we wrote. */ span = fd.getSpan(b, offset); } } } lastOffset = offset; offset += span; } /* Use (lastOffset + lastWrote) instead of offset because the last * item may have had a dynamic length and we don't want to include * the padding between it and the end of the space reserved for * it. */ return (lastOffset + lastWrote) - firstOffset; } /** @override */ fromArray(values) { const dest = this.makeDestinationObject(); for (const fd of this.fields) { if ((undefined !== fd.property) && (0 < values.length)) { dest[fd.property] = values.shift(); } } return dest; } /** * Get access to the layout of a given property. * * @param {String} property - the structure member of interest. * * @return {Layout} - the layout associated with `property`, or * undefined if there is no such property. */ layoutFor(property) { if ('string' !== typeof property) { throw new TypeError('property must be string'); } for (const fd of this.fields) { if (fd.property === property) { return fd; } } } /** * Get the offset of a structure member. * * @param {String} property - the structure member of interest. * * @return {Number} - the offset in bytes to the start of `property` * within the structure, or undefined if `property` is not a field * within the structure. If the property is a member but follows a * variable-length structure member a negative number will be * returned. */ offsetOf(property) { if ('string' !== typeof property) { throw new TypeError('property must be string'); } let offset = 0; for (const fd of this.fields) { if (fd.property === property) { return offset; } if (0 > fd.span) { offset = -1; } else if (0 <= offset) { offset += fd.span; } } } } /** * An object that can provide a {@link * Union#discriminator|discriminator} API for {@link Union}. * * **NOTE** This is an abstract base class; you can create instances * if it amuses you, but they won't support the {@link * UnionDiscriminator#encode|encode} or {@link * UnionDiscriminator#decode|decode} functions. * * @param {string} [property] - Default for {@link * UnionDiscriminator#property|property}. * * @abstract */ class UnionDiscriminator { constructor(property) { /** The {@link Layout#property|property} to be used when the * discriminator is referenced in isolation (generally when {@link * Union#decode|Union decode} cannot delegate to a specific * variant). */ this.property = property; } /** Analog to {@link Layout#decode|Layout decode} for union discriminators. * * The implementation of this method need not reference the buffer if * variant information is available through other means. */ decode() { throw new Error('UnionDiscriminator is abstract'); } /** Analog to {@link Layout#decode|Layout encode} for union discriminators. * * The implementation of this method need not store the value if * variant information is maintained through other means. */ encode() { throw new Error('UnionDiscriminator is abstract'); } } /** * An object that can provide a {@link * UnionDiscriminator|discriminator API} for {@link Union} using an * unsigned integral {@link Layout} instance located either inside or * outside the union. * * @param {ExternalLayout} layout - initializes {@link * UnionLayoutDiscriminator#layout|layout}. Must satisfy {@link * ExternalLayout#isCount|isCount()}. * * @param {string} [property] - Default for {@link * UnionDiscriminator#property|property}, superseding the property * from `layout`, but defaulting to `variant` if neither `property` * nor layout provide a property name. * * @augments {UnionDiscriminator} */ class UnionLayoutDiscriminator extends UnionDiscriminator { constructor(layout, property) { if (!((layout instanceof ExternalLayout) && layout.isCount())) { throw new TypeError('layout must be an unsigned integer ExternalLayout'); } super(property || layout.property || 'variant'); /** The {@link ExternalLayout} used to access the discriminator * value. */ this.layout = layout; } /** Delegate decoding to {@link UnionLayoutDiscriminator#layout|layout}. */ decode(b, offset) { return this.layout.decode(b, offset); } /** Delegate encoding to {@link UnionLayoutDiscriminator#layout|layout}. */ encode(src, b, offset) { return this.layout.encode(src, b, offset); } } /** * Represent any number of span-compatible layouts. * * *Factory*: {@link module:Layout.union|union} * * If the union has a {@link Union#defaultLayout|default layout} that * layout must have a non-negative {@link Layout#span|span}. The span * of a fixed-span union includes its {@link * Union#discriminator|discriminator} if the variant is a {@link * Union#usesPrefixDiscriminator|prefix of the union}, plus the span * of its {@link Union#defaultLayout|default layout}. * * If the union does not have a default layout then the encoded span * of the union depends on the encoded span of its variant (which may * be fixed or variable). * * {@link VariantLayout#layout|Variant layout}s are added through * {@link Union#addVariant|addVariant}. If the union has a default * layout, the span of the {@link VariantLayout#layout|layout * contained by the variant} must not exceed the span of the {@link * Union#defaultLayout|default layout} (minus the span of a {@link * Union#usesPrefixDiscriminator|prefix disriminator}, if used). The * span of the variant will equal the span of the union itself. * * The variant for a buffer can only be identified from the {@link * Union#discriminator|discriminator} {@link * UnionDiscriminator#property|property} (in the case of the {@link * Union#defaultLayout|default layout}), or by using {@link * Union#getVariant|getVariant} and examining the resulting {@link * VariantLayout} instance. * * A variant compatible with a JavaScript object can be identified * using {@link Union#getSourceVariant|getSourceVariant}. * * @param {(UnionDiscriminator|ExternalLayout|Layout)} discr - How to * identify the layout used to interpret the union contents. The * parameter must be an instance of {@link UnionDiscriminator}, an * {@link ExternalLayout} that satisfies {@link * ExternalLayout#isCount|isCount()}, or {@link UInt} (or {@link * UIntBE}). When a non-external layout element is passed the layout * appears at the start of the union. In all cases the (synthesized) * {@link UnionDiscriminator} instance is recorded as {@link * Union#discriminator|discriminator}. * * @param {(Layout|null)} defaultLayout - initializer for {@link * Union#defaultLayout|defaultLayout}. If absent defaults to `null`. * If `null` there is no default layout: the union has data-dependent * length and attempts to decode or encode unrecognized variants will * throw an exception. A {@link Layout} instance must have a * non-negative {@link Layout#span|span}, and if it lacks a {@link * Layout#property|property} the {@link * Union#defaultLayout|defaultLayout} will be a {@link * Layout#replicate|replica} with property `content`. * * @param {string} [property] - initializer for {@link * Layout#property|property}. * * @augments {Layout} */ class Union extends Layout { constructor(discr, defaultLayout, property) { const upv = ((discr instanceof UInt) || (discr instanceof UIntBE)); if (upv) { discr = new UnionLayoutDiscriminator(new OffsetLayout(discr)); } else if ((discr instanceof ExternalLayout) && discr.isCount()) { discr = new UnionLayoutDiscriminator(discr); } else if (!(discr instanceof UnionDiscriminator)) { throw new TypeError('discr must be a UnionDiscriminator ' + 'or an unsigned integer layout'); } if (undefined === defaultLayout) { defaultLayout = null; } if (!((null === defaultLayout) || (defaultLayout instanceof Layout))) { throw new TypeError('defaultLayout must be null or a Layout'); } if (null !== defaultLayout) { if (0 > defaultLayout.span) { throw new Error('defaultLayout must have constant span'); } if (undefined === defaultLayout.property) { defaultLayout = defaultLayout.replicate('content'); } } /* The union span can be estimated only if there's a default * layout. The union spans its default layout, plus any prefix * variant layout. By construction both layouts, if present, have * non-negative span. */ let span = -1; if (defaultLayout) { span = defaultLayout.span; if ((0 <= span) && upv) { span += discr.layout.span; } } super(span, property); /** The interface for the discriminator value in isolation. * * This a {@link UnionDiscriminator} either passed to the * constructor or synthesized from the `discr` constructor * argument. {@link * Union#usesPrefixDiscriminator|usesPrefixDiscriminator} will be * `true` iff the `discr` parameter was a non-offset {@link * Layout} instance. */ this.discriminator = discr; /** `true` if the {@link Union#discriminator|discriminator} is the * first field in the union. * * If `false` the discriminator is obtained from somewhere * else. */ this.usesPrefixDiscriminator = upv; /** The layout for non-discriminator content when the value of the * discriminator is not recognized. * * This is the value passed to the constructor. It is * structurally equivalent to the second component of {@link * Union#layout|layout} but may have a different property * name. */ this.defaultLayout = defaultLayout; /** A registry of allowed variants. * * The keys are unsigned integers which should be compatible with * {@link Union.discriminator|discriminator}. The property value * is the corresponding {@link VariantLayout} instances assigned * to this union by {@link Union#addVariant|addVariant}. * * **NOTE** The registry remains mutable so that variants can be * {@link Union#addVariant|added} at any time. Users should not * manipulate the content of this property. */ this.registry = {}; /* Private variable used when invoking getSourceVariant */ let boundGetSourceVariant = this.defaultGetSourceVariant.bind(this); /** Function to infer the variant selected by a source object. * * Defaults to {@link * Union#defaultGetSourceVariant|defaultGetSourceVariant} but may * be overridden using {@link * Union#configGetSourceVariant|configGetSourceVariant}. * * @param {Object} src - as with {@link * Union#defaultGetSourceVariant|defaultGetSourceVariant}. * * @returns {(undefined|VariantLayout)} The default variant * (`undefined`) or first registered variant that uses a property * available in `src`. */ this.getSourceVariant = function(src) { return boundGetSourceVariant(src); }; /** Function to override the implementation of {@link * Union#getSourceVariant|getSourceVariant}. * * Use this if the desired variant cannot be identified using the * algorithm of {@link * Union#defaultGetSourceVariant|defaultGetSourceVariant}. * * **NOTE** The provided function will be invoked bound to this * Union instance, providing local access to {@link * Union#registry|registry}. * * @param {Function} gsv - a function that follows the API of * {@link Union#defaultGetSourceVariant|defaultGetSourceVariant}. */ this.configGetSourceVariant = function(gsv) { boundGetSourceVariant = gsv.bind(this); }; } /** @override */ getSpan(b, offset) { if (0 <= this.span) { return this.span; } if (undefined === offset) { offset = 0; } /* Default layouts always have non-negative span, so we don't have * one and we have to recognize the variant which will in turn * determine the span. */ const vlo = this.getVariant(b, offset); if (!vlo) { throw new Error('unable to determine span for unrecognized variant'); } return vlo.getSpan(b, offset); } /** * Method to infer a registered Union variant compatible with `src`. * * The first satisified rule in the following sequence defines the * return value: * * If `src` has properties matching the Union discriminator and * the default layout, `undefined` is returned regardless of the * value of the discriminator property (this ensures the default * layout will be used); * * If `src` has a property matching the Union discriminator, the * value of the discriminator identifies a registered variant, and * either (a) the variant has no layout, or (b) `src` has the * variant's property, then the variant is returned (because the * source satisfies