diff --git a/CHANGELOG.md b/CHANGELOG.md index 94d83c0..b469ac5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Change Log +## [1.2.0] - 2018-03-14 + +* **API** Add [UTF8][doc:UTF8] to encode UTF strings in a possibly + bounded buffer, resolving [issue #21][issue#21]. +* **API** Allow the layout parameter of + a [VariantLayout][doc:VariantLayout] to be omitted in cases where no + data beyond the discriminator is required, + resolving [issue #20][issue#20]. + ## [1.1.0] - 2018-01-06 * **API** Add a third parameter to Structure specifying it should decode @@ -122,6 +131,7 @@ * Initial release. +[1.2.0]: https://github.com/pabigot/buffer-layout/compare/v1.1.0...v1.2.0 [1.1.0]: https://github.com/pabigot/buffer-layout/compare/v1.0.0...v1.1.0 [1.0.0]: https://github.com/pabigot/buffer-layout/compare/v0.13.0...v1.0.0 [0.13.0]: https://github.com/pabigot/buffer-layout/compare/v0.12.0...v0.13.0 @@ -154,14 +164,16 @@ [doc:NearInt64]: http://pabigot.github.io/buffer-layout/module-Layout-NearInt64.html [doc:OffsetLayout]: http://pabigot.github.io/buffer-layout/module-Layout-OffsetLayout.html [doc:patchIssue3992]: http://pabigot.github.io/buffer-layout/module-patchIssue3992.html -[doc:Union]: http://pabigot.github.io/buffer-layout/module-Layout-Union.html -[doc:Union.getSourceVariant]: http://pabigot.github.io/buffer-layout/module-Layout-Union.html#getSourceVariant -[doc:UnionDiscriminator]: http://pabigot.github.io/buffer-layout/module-Layout-UnionDiscriminator.html [doc:Sequence]: http://pabigot.github.io/buffer-layout/module-Layout-Sequence.html [doc:Sequence.count]: http://pabigot.github.io/buffer-layout/module-Layout-Sequence.html#count [doc:Structure]: http://pabigot.github.io/buffer-layout/module-Layout-Structure.html [doc:Structure.layoutFor]: http://pabigot.github.io/buffer-layout/module-Layout-Structure.html#layoutFor [doc:Structure.offsetOf]: http://pabigot.github.io/buffer-layout/module-Layout-Structure.html#offsetOf +[doc:Union]: http://pabigot.github.io/buffer-layout/module-Layout-Union.html +[doc:Union.getSourceVariant]: http://pabigot.github.io/buffer-layout/module-Layout-Union.html#getSourceVariant +[doc:UnionDiscriminator]: http://pabigot.github.io/buffer-layout/module-Layout-UnionDiscriminator.html +[doc:UTF8]: http://pabigot.github.io/buffer-layout/module-Layout-UTF8.html +[doc:VariantLayout]: http://pabigot.github.io/buffer-layout/module-Layout-VariantLayout.html [issue#1]: https://github.com/pabigot/buffer-layout/issues/1 [issue#2]: https://github.com/pabigot/buffer-layout/issues/2 [issue#3]: https://github.com/pabigot/buffer-layout/issues/3 @@ -179,6 +191,8 @@ [issue#15]: https://github.com/pabigot/buffer-layout/issues/15 [issue#17]: https://github.com/pabigot/buffer-layout/issues/17 [issue#19]: https://github.com/pabigot/buffer-layout/issues/19 +[issue#20]: https://github.com/pabigot/buffer-layout/issues/20 +[issue#21]: https://github.com/pabigot/buffer-layout/issues/21 [ci:travis]: https://travis-ci.org/pabigot/buffer-layout [ci:coveralls]: https://coveralls.io/github/pabigot/buffer-layout [node:issue#3992]: https://github.com/nodejs/node/issues/3992 diff --git a/README.md b/README.md index de05017..02e3667 100644 --- a/README.md +++ b/README.md @@ -132,10 +132,14 @@ The buffer-layout way: const t = lo.u8('t'); const un = lo.union(t, lo.seq(lo.u8(), 4, 'u8')); + const nul = un.addVariant('n'.charCodeAt(0), 'nul'); const u32 = un.addVariant('w'.charCodeAt(0), lo.u32(), 'u32'); const s16 = un.addVariant('h'.charCodeAt(0), lo.seq(lo.s16(), 2), 's16'); const f32 = un.addVariant('f'.charCodeAt(0), lo.f32(), 'f32'); const b = Buffer.alloc(un.span); + assert.deepEqual(un.decode(b), {t: 0, u8: [0, 0, 0, 0]}); + assert.deepEqual(un.decode(Buffer.from('6e01020304', 'hex')), + {nul: true}); assert.deepEqual(un.decode(Buffer.from('7778563412', 'hex')), {u32: 0x12345678}); assert.deepEqual(un.decode(Buffer.from('660000bd41', 'hex')), @@ -156,6 +160,11 @@ representing the union and the variants: lo.bindConstructorLayout(Union, lo.union(lo.u8('t'), lo.seq(lo.u8(), 4, 'u8'))); + function Vn() {} + util.inherits(Vn, Union); + lo.bindConstructorLayout(Vn, + Union.layout_.addVariant('n'.charCodeAt(0), 'nul')); + function Vu32(v) { this.u32 = v; } util.inherits(Vu32, Union); lo.bindConstructorLayout(Vu32, @@ -186,6 +195,14 @@ representing the union and the variants: v.encode(b); assert.equal(Buffer.from('660000bd41', 'hex').compare(b), 0); + b.fill(0xFF); + v = new Vn(); + v.encode(b); + assert.equal(Buffer.from('6effffffff', 'hex').compare(b), 0); + +Note that one variant (`'n'`) carries no data, leaving the remainder of +the buffer unchanged when stored. + See [Layout.makeDestinationObject()](http://pabigot.github.io/buffer-layout/module-Layout-Layout.html#makeDestinationObject) and diff --git a/jsdoc/custom/local.js b/jsdoc/custom/local.js index 1ac1591..70e460b 100644 --- a/jsdoc/custom/local.js +++ b/jsdoc/custom/local.js @@ -30,9 +30,20 @@ function buildRE(prefix, tag) { exports.handlers = { jsdocCommentFound: function(e) { if (thisModule) for (var local in registry) { - var sv = '$1'+thisModule+'~'+'$2'; - e.comment = e.comment.replace(buildRE('{', local), sv); - e.comment = e.comment.replace(buildRE('{@link\\s*\\*?\\s*', local), sv); + /* Handle {@link local} => {@link module~local|local} (across EOL) */ + var re = new RegExp('({@link\\s*\\*?\\s*)\\b(' + local + '\\b[^|}]*)}', 'g'); + e.comment = e.comment.replace(re, + '$1' + thisModule + '~$2\|$2}'); + + /* Handle {local} => {thisModule~local}. Brace reference + * doesn't support providing alternative text. */ + e.comment = e.comment.replace(buildRE('{', local), + '$1' + thisModule + '~$2'); + + /* Handle `@cmd local` => `@cmd thisModule~local` for + * certain commands (across EOL) */ + e.comment = e.comment.replace(buildRE('@(event|link|memberof|name)\\s*\\*?\\s*', local), + '$1' + thisModule + '~$3'); } }, diff --git a/lib/Layout.js b/lib/Layout.js index 907f883..b6cf096 100644 --- a/lib/Layout.js +++ b/lib/Layout.js @@ -123,6 +123,7 @@ * @local Blob * @local CString * @local Constant + * @local bindConstructorLayout * @module Layout * @license MIT * @author Peter A. Bigot @@ -147,7 +148,6 @@ const assert = require('assert'); * @param {string} [property] - Initializer for {@link * Layout#property|property}. * - * @constructor * @abstract */ class Layout { @@ -160,9 +160,8 @@ class Layout { * * Positive values are generally expected. * - * Zero will only appear in {@link Constant|Constant}s and in - * {@link Sequence|Sequence}s where the {@link - * Sequence#count|count} is zero. + * 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}. */ @@ -184,15 +183,15 @@ class Layout { * * Used only for layouts that {@link Layout#decode|decode} to Object * instances, which means: - * * {@link Structure|Structure} - * * {@link Union|Union} - * * {@link VariantLayout|VariantLayout} - * * {@link BitStructure|BitStructure} + * * {@link Structure} + * * {@link Union} + * * {@link VariantLayout} + * * {@link BitStructure} * * If left undefined the JavaScript representation of these layouts * will be Object instances. * - * See {@link module:Layout.bindConstructorLayout|bindConstructorLayout()}. + * See {@link bindConstructorLayout}. */ makeDestinationObject() { return {}; @@ -219,7 +218,7 @@ class Layout { * * @param {(Number|Array|Object)} src - the value to be encoded into * the buffer. The type accepted depends on the (sub-)type of {@link - * Layout|Layout}. + * Layout}. * * @param {Buffer} b - the buffer into which encoded data will be * written. @@ -267,7 +266,7 @@ class Layout { * 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|Layout} instances are + * with a different name since all {@link Layout} instances are * immutable. * * **NOTE** This is a shallow copy. All fields except {@link @@ -290,12 +289,12 @@ class Layout { * 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|Structure}, which includes + * 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|Union}s. If you want this feature for a - * union you must use {@link Union.getVariant|getVariant} to select - * the desired layout. + * 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} @@ -349,8 +348,8 @@ exports.nameWithProperty = nameWithProperty; * @param {class} Class - a JavaScript class with a nullary * constructor. * - * @param {Layout} layout - the {@link Layout|Layout} instance used to - * encode instances of `Class`. + * @param {Layout} layout - the {@link Layout} instance used to encode + * instances of `Class`. */ function bindConstructorLayout(Class, layout) { if ('function' !== typeof Class) { @@ -393,8 +392,7 @@ exports.bindConstructorLayout = bindConstructorLayout; * * **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|Layout} - * functions. + * 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. @@ -423,7 +421,7 @@ class ExternalLayout extends Layout { } /** - * An {@link ExternalLayout|ExternalLayout} that determines its {@link + * An {@link ExternalLayout} that determines its {@link * Layout#decode|value} based on offset into and length of the buffer * on which it is invoked. * @@ -435,7 +433,6 @@ class ExternalLayout extends Layout { * @param {string} [property] - initializer for {@link * Layout#property|property}. * - * @constructor * @augments {ExternalLayout} */ class GreedyCount extends ExternalLayout { @@ -475,10 +472,9 @@ class GreedyCount extends ExternalLayout { } /** - * An {@link ExternalLayout|ExternalLayout} that supports accessing a - * {@link Layout|Layout} at a fixed offset from the start of another - * Layout. The offset may be before, within, or after the base - * layout. + * 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} * @@ -493,7 +489,6 @@ class GreedyCount extends ExternalLayout { * OffsetLayout#layout|layout}. If not provided the `layout` is used * unchanged. * - * @constructor * @augments {Layout} */ class OffsetLayout extends ExternalLayout { @@ -559,7 +554,6 @@ class OffsetLayout extends ExternalLayout { * @param {string} [property] - initializer for {@link * Layout#property|property}. * - * @constructor * @augments {Layout} */ class UInt extends Layout { @@ -602,7 +596,6 @@ class UInt extends Layout { * @param {string} [property] - initializer for {@link * Layout#property|property}. * - * @constructor * @augments {Layout} */ class UIntBE extends Layout { @@ -645,7 +638,6 @@ class UIntBE extends Layout { * @param {string} [property] - initializer for {@link * Layout#property|property}. * - * @constructor * @augments {Layout} */ class Int extends Layout { @@ -688,7 +680,6 @@ class Int extends Layout { * @param {string} [property] - initializer for {@link * Layout#property|property}. * - * @constructor * @augments {Layout} */ class IntBE extends Layout { @@ -742,7 +733,6 @@ function roundedInt64(hi32, lo32) { * **NOTE** Values with magnitude greater than 2^52 may not decode to * the exact value of the encoded representation. * - * @constructor * @augments {Layout} */ class NearUInt64 extends Layout { @@ -781,7 +771,6 @@ class NearUInt64 extends Layout { * **NOTE** Values with magnitude greater than 2^52 may not decode to * the exact value of the encoded representation. * - * @constructor * @augments {Layout} */ class NearUInt64BE extends Layout { @@ -820,7 +809,6 @@ class NearUInt64BE extends Layout { * **NOTE** Values with magnitude greater than 2^52 may not decode to * the exact value of the encoded representation. * - * @constructor * @augments {Layout} */ class NearInt64 extends Layout { @@ -859,7 +847,6 @@ class NearInt64 extends Layout { * **NOTE** Values with magnitude greater than 2^52 may not decode to * the exact value of the encoded representation. * - * @constructor * @augments {Layout} */ class NearInt64BE extends Layout { @@ -897,7 +884,6 @@ class NearInt64BE extends Layout { * @param {string} [property] - initializer for {@link * Layout#property|property}. * - * @constructor * @augments {Layout} */ class Float extends Layout { @@ -931,7 +917,6 @@ class Float extends Layout { * @param {string} [property] - initializer for {@link * Layout#property|property}. * - * @constructor * @augments {Layout} */ class FloatBE extends Layout { @@ -965,7 +950,6 @@ class FloatBE extends Layout { * @param {string} [property] - initializer for {@link * Layout#property|property}. * - * @constructor * @augments {Layout} */ class Double extends Layout { @@ -999,7 +983,6 @@ class Double extends Layout { * @param {string} [property] - initializer for {@link * Layout#property|property}. * - * @constructor * @augments {Layout} */ class DoubleBE extends Layout { @@ -1040,7 +1023,6 @@ class DoubleBE extends Layout { * @param {string} [property] - initializer for {@link * Layout#property|property}. * - * @constructor * @augments {Layout} */ class Sequence extends Layout { @@ -1067,7 +1049,7 @@ class Sequence extends Layout { /** The number of elements in the sequence. * * This will be either a non-negative integer or an instance of - * {@link ExternalLayout|ExternalLayout} for which {@link + * {@link ExternalLayout} for which {@link * ExternalLayout#isCount|isCount()} is `true`. */ this.count = count; } @@ -1116,7 +1098,7 @@ class Sequence extends Layout { return rv; } - /** Implement {@link Layout#encode|encode} for {@link Sequence|Sequence}. + /** 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 @@ -1124,16 +1106,15 @@ class Sequence extends Layout { * ignored. * * **NOTE** If {@link Layout#count|count} is an instance of {@link - * ExternalLayout|ExternalLayout} then the length of `src` will be - * encoded as the count after `src` is encoded. */ + * 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) => { - elo.encode(v, b, offset + span); - return span + elo.getSpan(b, offset + span); + return span + elo.encode(v, b, offset + span); }, 0); if (this.count instanceof ExternalLayout) { this.count.encode(src.length, b, offset); @@ -1150,7 +1131,7 @@ class Sequence extends Layout { * * **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 + * 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 @@ -1172,7 +1153,6 @@ class Sequence extends Layout { * @throws {Error} - if `fields` contains an unnamed variable-length * layout. * - * @constructor * @augments {Layout} */ class Structure extends Layout { @@ -1202,7 +1182,7 @@ class Structure extends Layout { } super(span, property); - /** The sequence of {@link Layout|Layout} values that comprise the + /** The sequence of {@link Layout} values that comprise the * structure. * * The individual elements need not be the same type, and may be @@ -1266,7 +1246,7 @@ class Structure extends Layout { return dest; } - /** Implement {@link Layout#encode|encode} for {@link Structure|Structure}. + /** 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 @@ -1369,7 +1349,7 @@ class Structure extends Layout { /** * An object that can provide a {@link - * Union#discriminator|discriminator} API for {@link * Union|Union}. + * 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 @@ -1379,7 +1359,6 @@ class Structure extends Layout { * @param {string} [property] - Default for {@link * UnionDiscriminator#property|property}. * - * @constructor * @abstract */ class UnionDiscriminator { @@ -1410,9 +1389,9 @@ class UnionDiscriminator { /** * An object that can provide a {@link - * UnionDiscriminator|discriminator API} for {@link Union|Union} using - * an unsigned integral {@link Layout|Layout} instance located either - * inside or outside the union. + * 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 @@ -1423,7 +1402,6 @@ class UnionDiscriminator { * from `layout`, but defaulting to `variant` if neither `property` * nor layout provide a property name. * - * @constructor * @augments {UnionDiscriminator} */ class UnionLayoutDiscriminator extends UnionDiscriminator { @@ -1435,8 +1413,8 @@ class UnionLayoutDiscriminator extends UnionDiscriminator { super(property || layout.property || 'variant'); - /** The {@link ExternalLayout|ExternalLayout} used to access the - * discriminator value. */ + /** The {@link ExternalLayout} used to access the discriminator + * value. */ this.layout = layout; } @@ -1480,36 +1458,34 @@ class UnionLayoutDiscriminator extends UnionDiscriminator { * 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|VariantLayout} instance. + * 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|UnionDiscriminator}, an {@link - * ExternalLayout|ExternalLayout} that satisfies {@link - * ExternalLayout#isCount|isCount()}, or {@link UInt|UInt} (or {@link - * UIntBE|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|UnionDiscriminator} - * instance is recorded as {@link Union#discriminator|discriminator}. + * 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|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}. 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}. * - * @constructor * @augments {Layout} */ class Union extends Layout { @@ -1556,12 +1532,12 @@ class Union extends Layout { /** The interface for the discriminator value in isolation. * - * This a {@link UnionDiscriminator|UnionDiscriminator} either - * passed to the constructor or synthesized from the `discr` - * constructor argument. {@link + * 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|Layout} instance. */ + * Layout} instance. */ this.discriminator = discr; /** `true` if the {@link Union#discriminator|discriminator} is the @@ -1584,9 +1560,8 @@ class Union extends Layout { * * The keys are unsigned integers which should be compatible with * {@link Union.discriminator|discriminator}. The property value - * is the corresponding {@link VariantLayout|VariantLayout} - * instances assigned to this union by {@link - * Union#addVariant|addVariant}. + * 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 @@ -1652,32 +1627,56 @@ class Union extends Layout { /** * Method to infer a registered Union variant compatible with `src`. * - * @param {Object} src - an object presumed to be compatible with the content of the Union. + * 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 the constraints of the variant it identifies); + * * If `src` does not have a property matching the Union + * discriminator, but does have a property matching a registered + * variant, then the variant is returned (because the source + * matches a variant without an explicit conflict); + * * An error is thrown (because we either can't identify a variant, + * or we were explicitly told the variant but can't satisfy it). + * + * @param {Object} src - an object presumed to be compatible with + * the content of the Union. * - * @return {(undefined|VariantLayout)} - If `src` has properties - * matching the Union discriminator and default layout properties this - * returns `undefined`. Otherwise, if `src` has a property matching a - * {@link VariantLayout#property|variant property} that variant is - * returned. Otherwise an error is thrown. + * @return {(undefined|VariantLayout)} - as described above. * * @throws {Error} - if `src` cannot be associated with a default or * registered variant. */ defaultGetSourceVariant(src) { - if (src.hasOwnProperty(this.discriminator.property) - && src.hasOwnProperty(this.defaultLayout.property)) { - return undefined; - } - for (const k in this.registry) { - const lo = this.registry[k]; - if (src.hasOwnProperty(lo.property)) { - return lo; + if (src.hasOwnProperty(this.discriminator.property)) { + if (this.defaultLayout + && src.hasOwnProperty(this.defaultLayout.property)) { + return undefined; + } + const vlo = this.registry[src[this.discriminator.property]]; + if (vlo + && ((!vlo.layout) + || src.hasOwnProperty(vlo.property))) { + return vlo; + } + } else { + for (const tag in this.registry) { + const vlo = this.registry[tag]; + if (src.hasOwnProperty(vlo.property)) { + return vlo; + } } } throw new Error('unable to infer src variant'); } - /** Implement {@link Layout#decode|decode} for {@link Union|Union}. + /** Implement {@link Layout#decode|decode} for {@link Union}. * * If the variant is {@link Union#addVariant|registered} the return * value is an instance of that variant, with no explicit @@ -1706,7 +1705,7 @@ class Union extends Layout { return dest; } - /** Implement {@link Layout#encode|encode} for {@link Union|Union}. + /** Implement {@link Layout#encode|encode} for {@link Union}. * * This API assumes the `src` object is consistent with the union's * {@link Union#defaultLayout|default layout}. To encode variants @@ -1793,15 +1792,16 @@ class Union extends Layout { * @param {Number} variant - initializer for {@link * VariantLayout#variant|variant}. * - * @param {Layout} layout - initializer for {@link - * VariantLayout#layout|layout}. + * @param {Layout} [layout] - initializer for {@link + * VariantLayout#layout|layout}. If absent the variant carries no + * data. * - * @param {String} property - initializer for {@link + * @param {String} [property] - initializer for {@link * Layout#property|property}. Unlike many other layouts, variant - * layouts must include a property so they can be identified within - * their containing @{link Union|Union}. + * layouts normally include a property name so they can be identified + * within their containing {@link Union}. The property identifier may + * be absent only if `layout` is is absent. * - * @constructor * @augments {Layout} */ class VariantLayout extends Layout { @@ -1812,27 +1812,34 @@ class VariantLayout extends Layout { if ((!Number.isInteger(variant)) || (0 > variant)) { throw new TypeError('variant must be a (non-negative) integer'); } - if (!(layout instanceof Layout)) { - throw new TypeError('layout must be a Layout'); - } - if ((null !== union.defaultLayout) - && (0 <= layout.span) - && (layout.span > union.defaultLayout.span)) { - throw new Error('variant span exceeds span of containing union'); + if (('string' === typeof layout) + && (undefined === property)) { + property = layout; + layout = null; } - if ('string' !== typeof property) { - throw new TypeError('variant must have a String property'); + if (layout) { + if (!(layout instanceof Layout)) { + throw new TypeError('layout must be a Layout'); + } + if ((null !== union.defaultLayout) + && (0 <= layout.span) + && (layout.span > union.defaultLayout.span)) { + throw new Error('variant span exceeds span of containing union'); + } + if ('string' !== typeof property) { + throw new TypeError('variant must have a String property'); + } } let span = union.span; if (0 > union.span) { - span = layout.span; + span = layout ? layout.span : 0; if ((0 <= span) && union.usesPrefixDiscriminator) { span += union.discriminator.layout.span; } } super(span, property); - /** The {@link Union|Union} to which this variant belongs. */ + /** The {@link Union} to which this variant belongs. */ this.union = union; /** The unsigned integral value identifying this variant within @@ -1840,10 +1847,11 @@ class VariantLayout extends Layout { * union. */ this.variant = variant; - /** The {@link Layout|Layout} to be used when reading/writing the + /** The {@link Layout} to be used when reading/writing the * non-discriminator part of the {@link - * VariantLayout#union|union}. */ - this.layout = layout; + * VariantLayout#union|union}. If `null` the variant carries no + * data. */ + this.layout = layout || null; } /** @override */ @@ -1877,7 +1885,13 @@ class VariantLayout extends Layout { if (this.union.usesPrefixDiscriminator) { contentOffset = this.union.discriminator.layout.span; } - dest[this.property] = this.layout.decode(b, offset + contentOffset); + if (this.layout) { + dest[this.property] = this.layout.decode(b, offset + contentOffset); + } else if (this.property) { + dest[this.property] = true; + } else if (this.union.usesPrefixDiscriminator) { + dest[this.union.discriminator.property] = this.variant; + } return dest; } @@ -1890,15 +1904,19 @@ class VariantLayout extends Layout { if (this.union.usesPrefixDiscriminator) { contentOffset = this.union.discriminator.layout.span; } - if (!src.hasOwnProperty(this.property)) { + if (this.layout + && (!src.hasOwnProperty(this.property))) { throw new TypeError('variant lacks property ' + this.property); } this.union.discriminator.encode(this.variant, b, offset); - this.layout.encode(src[this.property], b, offset + contentOffset); - const span = contentOffset + this.layout.getSpan(b, offset + contentOffset); - if ((0 <= this.union.span) - && (span > this.union.span)) { - throw new Error('encoded variant overruns containing union'); + let span = contentOffset; + if (this.layout) { + this.layout.encode(src[this.property], b, offset + contentOffset); + span += this.layout.getSpan(b, offset + contentOffset); + if ((0 <= this.union.span) + && (span > this.union.span)) { + throw new Error('encoded variant overruns containing union'); + } } return span; } @@ -1906,7 +1924,9 @@ class VariantLayout extends Layout { /** Delegate {@link Layout#fromArray|fromArray} to {@link * VariantLayout#layout|layout}. */ fromArray(values) { - return this.layout.fromArray(values); + if (this.layout) { + return this.layout.fromArray(values); + } } } @@ -1928,20 +1948,19 @@ function fixBitwiseResult(v) { * * *Factory*: {@link module:Layout.bits|bits} * - * This is a container element; within it there are {@link - * BitField|BitField} instances that provide the extracted properties. - * The container simply defines the aggregate representation and its - * bit ordering. The representation is an object containing - * properties with numeric or {@link Boolean|boolean} values. + * This is a container element; within it there are {@link BitField} + * instances that provide the extracted properties. The container + * simply defines the aggregate representation and its bit ordering. + * The representation is an object containing properties with numeric + * or {@link Boolean} values. * - * {@link BitField|BitField}s are added with the {@link + * {@link BitField}s are added with the {@link * BitStructure#addField|addField} and {@link * BitStructure#addBoolean|addBoolean} methods. * @param {Layout} word - initializer for {@link * BitStructure#word|word}. The parameter must be an instance of - * {@link UInt|UInt} (or {@link UIntBE|UIntBE}) that is no more than 4 - * bytes wide. + * {@link UInt} (or {@link UIntBE}) that is no more than 4 bytes wide. * * @param {bool} [msb] - `true` if the bit numbering starts at the * most significant bit of the containing word; `false` (default) if @@ -1953,7 +1972,6 @@ function fixBitwiseResult(v) { * @param {string} [property] - initializer for {@link * Layout#property|property}. * - * @constructor * @augments {Layout} */ class BitStructure extends Layout { @@ -1972,9 +1990,9 @@ class BitStructure extends Layout { } super(word.span, property); - /** The layout used for the packed value. {@link - * BitField|BitField} instances are packed sequentially depending - * on {@link BitStructure#msb|msb}. */ + /** The layout used for the packed value. {@link BitField} + * instances are packed sequentially depending on {@link + * BitStructure#msb|msb}. */ this.word = word; /** Whether the bit sequences are packed starting at the most @@ -1982,12 +2000,12 @@ class BitStructure extends Layout { * bit growing up (`false`). * * **NOTE** Regardless of this value, the least significant bit of - * any {@link BitField|BitField} value is the least significant - * bit of the corresponding section of the packed value. */ + * any {@link BitField} value is the least significant bit of the + * corresponding section of the packed value. */ this.msb = !!msb; - /** The sequence of {@link BitField|BitField} layouts that - * comprise the packed structure. + /** The sequence of {@link BitField} layouts that comprise the + * packed structure. * * **NOTE** The array remains mutable to allow fields to be {@link * BitStructure#addField|added} after construction. Users should @@ -2023,7 +2041,7 @@ class BitStructure extends Layout { return dest; } - /** Implement {@link Layout#encode|encode} for {@link BitStructure|BitStructure}. + /** Implement {@link Layout#encode|encode} for {@link BitStructure}. * * If `src` is missing a property for a member with a defined {@link * Layout#property|property} the corresponding region of the packed @@ -2096,8 +2114,7 @@ class BitStructure extends Layout { } /** - * Represent a sequence of bits within a {@link - * BitStructure|BitStructure}. + * Represent a sequence of bits within a {@link BitStructure}. * * All bit field values are represented as unsigned integers. * @@ -2105,8 +2122,8 @@ class BitStructure extends Layout { * Use the container {@link BitStructure#addField|addField} helper * method. * - * **NOTE** BitField instances are not instances of {@link - * Layout|Layout} since {@link Layout#span|span} measures 8-bit units. + * **NOTE** BitField instances are not instances of {@link Layout} + * since {@link Layout#span|span} measures 8-bit units. * * @param {BitStructure} container - initializer for {@link * BitField#container|container}. @@ -2115,8 +2132,6 @@ class BitStructure extends Layout { * * @param {string} [property] - initializer for {@link * Layout#property|property}. - * - * @constructor */ class BitField { constructor(container, bits, property) { @@ -2134,8 +2149,8 @@ class BitField { + totalBits + ' remain)'); } - /** The {@link BitStructure|BitStructure} instance to which this - * bit field belongs. */ + /** The {@link BitStructure} instance to which this bit field + * belongs. */ this.container = container; /** The span of this value in bits. */ @@ -2204,8 +2219,8 @@ class BitField { } /** - * Represent a single bit within a {@link BitStructure|BitStructure} - * as a JavaScript boolean. + * Represent a single bit within a {@link BitStructure} as a + * JavaScript boolean. * * **NOTE** User code should not invoke this constructor directly. * Use the container {@link BitStructure#addBoolean|addBoolean} helper @@ -2217,7 +2232,6 @@ class BitField { * @param {string} [property] - initializer for {@link * Layout#property|property}. * - * @constructor * @augments {BitField} */ /* eslint-disable no-extend-native */ @@ -2256,7 +2270,6 @@ class Boolean extends BitField { * @param {String} [property] - initializer for {@link * Layout#property|property}. * - * @constructor * @augments {Layout} */ class Blob extends Layout { @@ -2276,7 +2289,7 @@ class Blob extends Layout { /** The number of bytes in the blob. * * This may be a non-negative integer, or an instance of {@link - * ExternalLayout|ExternalLayout} that satisfies {@link + * ExternalLayout} that satisfies {@link * ExternalLayout#isCount|isCount()}. */ this.length = length; } @@ -2302,11 +2315,11 @@ class Blob extends Layout { return b.slice(offset, offset + span); } - /** Implement {@link Layout#encode|encode} for {@link Blob|Blob}. + /** Implement {@link Layout#encode|encode} for {@link Blob}. * * **NOTE** If {@link Layout#count|count} is an instance of {@link - * ExternalLayout|ExternalLayout} then the length of `src` will be encoded - * as the count after `src` is encoded. */ + * ExternalLayout} then the length of `src` will be encoded as the + * count after `src` is encoded. */ encode(src, b, offset) { let span = this.length; if (this.length instanceof ExternalLayout) { @@ -2339,7 +2352,6 @@ class Blob extends Layout { * @param {String} [property] - initializer for {@link * Layout#property|property}. * - * @constructor * @augments {Layout} */ class CString extends Layout { @@ -2393,6 +2405,100 @@ class CString extends Layout { } } +/** + * Contain a UTF8 string with implicit length. + * + * *Factory*: {@link module:Layout.utf8|utf8} + * + * **NOTE** Because the length is implicit in the size of the buffer + * this layout should be used only in isolation, or in a situation + * where the length can be expressed by operating on a slice of the + * containing buffer. + * + * @param {Number} [maxSpan] - the maximum length allowed for encoded + * string content. If not provided there is no bound on the allowed + * content. + * + * @param {String} [property] - initializer for {@link + * Layout#property|property}. + * + * @augments {Layout} + */ +class UTF8 extends Layout { + constructor(maxSpan, property) { + if (('string' === typeof maxSpan) + && (undefined === property)) { + property = maxSpan; + maxSpan = undefined; + } + if (undefined === maxSpan) { + maxSpan = -1; + } else if (!Number.isInteger(maxSpan)) { + throw new TypeError('maxSpan must be an integer'); + } + + super(-1, property); + + /** The maximum span of the layout in bytes. + * + * Positive values are generally expected. Zero is abnormal. + * Attempts to encode or decode a value that exceeds this length + * will throw a `RangeError`. + * + * A negative value indicates that there is no bound on the length + * of the content. */ + this.maxSpan = maxSpan; + } + + /** @override */ + getSpan(b, offset) { + if (!(b instanceof Buffer)) { + throw new TypeError('b must be a Buffer'); + } + if (undefined === offset) { + offset = 0; + } + return b.length - offset; + } + + /** @override */ + decode(b, offset, dest) { + if (undefined === offset) { + offset = 0; + } + let span = this.getSpan(b, offset); + if ((0 <= this.maxSpan) + && (this.maxSpan < span)) { + throw new RangeError('text length exceeds maxSpan'); + } + return b.slice(offset, offset + span).toString('utf-8'); + } + + /** @override */ + encode(src, b, offset) { + if (undefined === offset) { + offset = 0; + } + /* Must force this to a string, lest it be a number and the + * "utf8-encoding" below actually allocate a buffer of length + * src */ + if ('string' !== typeof src) { + src = src.toString(); + } + const srcb = new Buffer(src, 'utf8'); + const span = srcb.length; + if ((0 <= this.maxSpan) + && (this.maxSpan < span)) { + throw new RangeError('text length exceeds maxSpan'); + } + if ((offset + span) > b.length) { + throw new RangeError('encoding overruns Buffer'); + } + srcb.copy(b, offset); + return span; + } +} + /** * Contain a constant value. * @@ -2410,7 +2516,6 @@ class CString extends Layout { * @param {String} [property] - initializer for {@link * Layout#property|property}. * - * @constructor * @augments {Layout} */ class Constant extends Layout { @@ -2463,12 +2568,13 @@ exports.BitField = BitField; exports.Boolean = Boolean; exports.Blob = Blob; exports.CString = CString; +exports.UTF8 = UTF8; exports.Constant = Constant; -/** Factory for {@link GreedyCount|GreedyCount}. */ +/** Factory for {@link GreedyCount}. */ exports.greedy = ((elementSpan, property) => new GreedyCount(elementSpan, property)); -/** Factory for {@link OffsetLayout|OffsetLayout}. */ +/** Factory for {@link OffsetLayout}. */ exports.offset = ((layout, offset, property) => new OffsetLayout(layout, offset, property)); /** Factory for {@link UInt|unsigned int layouts} spanning one @@ -2587,26 +2693,29 @@ exports.f64 = (property => new Double(property)); /** Factory for {@link DoubleBE|big-endian 64-bit floating point} values. */ exports.f64be = (property => new DoubleBE(property)); -/** Factory for {@link Structure|Structure} values. */ +/** Factory for {@link Structure} values. */ exports.struct = ((fields, property, decodePrefixes) => new Structure(fields, property, decodePrefixes)); -/** Factory for {@link BitStructure|BitStructure} values. */ +/** Factory for {@link BitStructure} values. */ exports.bits = ((word, msb, property) => new BitStructure(word, msb, property)); -/** Factory for {@link Sequence|Sequence} values. */ +/** Factory for {@link Sequence} values. */ exports.seq = ((elementLayout, count, property) => new Sequence(elementLayout, count, property)); -/** Factory for {@link Union|Union} values. */ +/** Factory for {@link Union} values. */ exports.union = ((discr, defaultLayout, property) => new Union(discr, defaultLayout, property)); -/** Factory for {@link UnionLayoutDiscriminator|UnionLayoutDiscriminator} values. */ +/** Factory for {@link UnionLayoutDiscriminator} values. */ exports.unionLayoutDiscriminator = ((layout, property) => new UnionLayoutDiscriminator(layout, property)); -/** Factory for {@link Blob|Blob} values. */ +/** Factory for {@link Blob} values. */ exports.blob = ((length, property) => new Blob(length, property)); -/** Factory for {@link CString|CString} values. */ +/** Factory for {@link CString} values. */ exports.cstr = (property => new CString(property)); -/** Factory for {@link Constant|Constant} values. */ +/** Factory for {@link UTF8} values. */ +exports.utf8 = ((maxSpan, property) => new UTF8(maxSpan, property)); + +/** Factory for {@link Constant} values. */ exports.const = ((value, property) => new Constant(value, property)); diff --git a/package.json b/package.json index 3f01448..00ba755 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "buffer-layout", - "version": "1.1.0", + "version": "1.2.0", "description": "Translation between JavaScript values and Buffers", "keywords": [ "Buffer", @@ -19,13 +19,13 @@ "main": "./lib/Layout.js", "devDependencies": { "coveralls": "^3.0.0", - "eslint": "~4.13.1", + "eslint": "~4.18.2", "eslint-config-google": "~0.9.1", "eslint-plugin-pabigot": "~1.1.0", "istanbul": "~0.4.5", "jsdoc": "~3.5.5", - "lodash": "~4.17.4", - "mocha": "~4.0.1" + "lodash": "~4.17.5", + "mocha": "~5.0.4" }, "engines": { "node": ">=4.5" diff --git a/test/LayoutTest.js b/test/LayoutTest.js index b959522..5263c60 100644 --- a/test/LayoutTest.js +++ b/test/LayoutTest.js @@ -912,13 +912,12 @@ suite('Layout', function() { assert.throws(() => new lo.VariantLayout(un), TypeError); assert.throws(() => new lo.VariantLayout(un, 1.2), TypeError); assert.throws(() => new lo.VariantLayout(un, 'str'), TypeError); - assert.throws(() => new lo.VariantLayout(un, 1), TypeError); - assert.throws(() => new lo.VariantLayout(un, 1, 'other'), - TypeError); assert.throws(() => new lo.VariantLayout(un, 1, lo.f64()), Error); assert.throws(() => new lo.VariantLayout(un, 1, lo.f32()), TypeError); + assert.throws(() => new lo.VariantLayout(un, 1, 23), + TypeError); }); test('ctor', function() { const un = new lo.Union(lo.u8(), lo.u32()); @@ -930,6 +929,21 @@ suite('Layout', function() { assert.equal(d.variant, 1); assert.equal(d.property, 'd'); }); + test('ctor without layout', function() { + const un = new lo.Union(lo.u8(), lo.u32()); + const v0 = new lo.VariantLayout(un, 0); + assert.strictEqual(v0.union, un); + assert.equal(v0.span, 5); + assert.strictEqual(v0.layout, null); + assert.equal(v0.variant, 0); + assert.equal(v0.property, undefined); + const v1 = new lo.VariantLayout(un, 1, 'nul'); + assert.strictEqual(v1.union, un); + assert.equal(v1.span, 5); + assert.strictEqual(v1.layout, null); + assert.equal(v1.variant, 1); + assert.equal(v1.property, 'nul'); + }); test('span', function() { const un = new lo.Union(lo.u8(), lo.u32()); const d = new lo.VariantLayout(un, 1, lo.cstr(), 's'); @@ -1126,6 +1140,10 @@ suite('Layout', function() { assert.equal(Buffer.from('0305060406', 'hex').compare(b), 0); assert.throws(() => v2.encode(obj, b), TypeError); assert.throws(() => v2.decode(b), Error); + const v0 = un.addVariant(0, 'v0'); + assert.equal(un.discriminator.encode(v0.variant, b), dlo.span); + assert.strictEqual(un.getVariant(b), v0); + assert.deepEqual(un.decode(b), {v0: true}); }); test('custom default', function() { const dlo = lo.u8('number'); @@ -1268,6 +1286,7 @@ suite('Layout', function() { }); test('from src', function() { const un = new lo.Union(lo.u8('v'), lo.u32('u32')); + const v0 = un.addVariant(0, 'nul'); const v1 = un.addVariant(1, lo.f32(), 'f32'); const v2 = un.addVariant(2, lo.seq(lo.u8(), 4), 'u8.4'); const v3 = un.addVariant(3, lo.cstr(), 'str'); @@ -1275,12 +1294,39 @@ suite('Layout', function() { assert.equal(un.span, 5); + // Unregistered variant with default content let src = {v: 5, u32: 0x12345678}; let vlo = un.getSourceVariant(src); assert.strictEqual(vlo, undefined); assert.equal(un.encode(src, b), un.span); assert.equal(Buffer.from('0578563412', 'hex').compare(b), 0); + // Unregistered variant without default content + src = {v: 5}; + assert.throws(() => un.getSourceVariant(src), Error); + + // Registered variant with default content + src = {v: 1, u32: 0x12345678}; + assert.strictEqual(un.getSourceVariant(src), undefined); + + // Registered variant with incompatible content + src = {v: 2, f32: 26.5}; + assert.throws(() => un.getSourceVariant(src), Error); + + // Registered variant with no content + src = {v: 0}; + vlo = un.getSourceVariant(src); + assert.strictEqual(vlo, v0); + b.fill(255); + assert.equal(vlo.encode(src, b), 1); + assert.strictEqual(un.getSpan(b), 5); + assert.equal(Buffer.from('00ffffffff', 'hex').compare(b), 0); + + // Registered variant with compatible content (ignore discriminator) + src = {v: 1, f32: 26.5}; + assert.strictEqual(un.getSourceVariant(src), v1); + + // Inferred variant from content src = {f32: 26.5}; vlo = un.getSourceVariant(src); assert.strictEqual(vlo, v1); @@ -1308,6 +1354,9 @@ suite('Layout', function() { assert.equal(vlo.span, un.span); assert.equal(vlo.layout.getSpan(b, 1), 3); assert.equal(vlo.getSpan(b), un.span); + + src = {v: 6}; + assert.throws(() => un.getSourceVariant(src), Error); }); test('customize src', function() { const un = lo.union(lo.u8('v'), lo.u32('u32')); @@ -1324,15 +1373,38 @@ suite('Layout', function() { }); test('variable span', function() { const un = lo.union(lo.u8('v')); + const v0 = un.addVariant(0, 'nul'); const v1 = un.addVariant(1, lo.u32(), 'u32'); const v2 = un.addVariant(2, lo.f64(), 'f64'); const v3 = un.addVariant(3, lo.cstr(), 'str'); + const v255 = un.addVariant(255); // unnamed contentless const b = Buffer.alloc(16); assert(0 > un.span); - b.fill(0xFF); + b.fill(0xa5); assert.throws(() => un.decode(b), Error); - let obj = {u32: 0x12345678}; + + let obj = {v: v255.variant}; + assert.equal(un.encode(obj, b), 1 + 0); + assert.equal(Buffer.from('ffa5a5', 'hex') + .compare(b.slice(0, 1 + 2)), 0); + assert.strictEqual(v255.layout, null); + assert.deepEqual(un.decode(b), obj); + assert.equal(v0.getSpan(b), 1); + assert.equal(un.getSpan(b), 1); + + obj = {nul: true}; + b.fill(0xff); + assert.equal(un.encode(obj, b), 1 + 0); + assert.equal(Buffer.from('00ffff', 'hex') + .compare(b.slice(0, 1 + 2)), 0); + assert.strictEqual(v0.layout, null); + assert.deepEqual(un.decode(b), obj); + assert.equal(v0.getSpan(b), 1); + assert.equal(un.getSpan(b), 1); + + b.fill(0xFF); + obj = {u32: 0x12345678}; assert.equal(un.encode(obj, b), 1 + 4); assert.equal(v1.getSpan(b), 5); assert.equal(un.getSpan(b), 5); @@ -1376,13 +1448,25 @@ suite('Layout', function() { assert(0 > un.span); assert(!un.usesPrefixDiscriminator); const st = lo.struct([dlo, un], 'st'); + const v0 = un.addVariant(0, 'nul'); const v1 = un.addVariant(1, lo.cstr(), 's'); - const obj = {v: v1.variant, u: {s: 'hi'}}; + const v2 = un.addVariant(2); + let obj = {v: v1.variant, u: {s: 'hi'}}; const b = Buffer.alloc(6); b.fill(0xa5); st.encode(obj, b, 1); assert.equal(Buffer.from('a501686900a5', 'hex').compare(b), 0); assert.deepEqual(st.decode(b, 1), obj); + obj = {v: v0.variant}; + b.fill(0x5a); + st.encode(obj, b, 1); + assert.equal(Buffer.from('5a005a5a5a5a', 'hex').compare(b), 0); + assert.deepEqual(st.decode(b, 1), Object.assign({u: {nul: true}}, obj)); + obj = {v: v2.variant}; + b.fill(0x5a); + st.encode(obj, b, 1); + assert.equal(Buffer.from('5a025a5a5a5a', 'hex').compare(b), 0); + assert.deepEqual(st.decode(b, 1), Object.assign({u: {}}, obj)); }); }); test('fromArray', function() { @@ -1392,9 +1476,11 @@ suite('Layout', function() { assert.deepEqual(st.fromArray([1, 2]), {a: 1, b: 2}); const un = new lo.Union(lo.u8('v'), lo.u32('c')); assert.strictEqual(un.fromArray([1, 2, 3]), undefined); + un.addVariant(0, 'v0'); const v1 = un.addVariant(1, st, 'v1'); un.addVariant(2, lo.f32(), 'v2'); assert(v1 instanceof lo.VariantLayout); + assert.deepEqual(un.getVariant(0).fromArray([1, 2, 3]), undefined); assert.deepEqual(un.getVariant(1).fromArray([1, 2, 3]), {a: 1, b: 2, c: 3}); assert.strictEqual(un.getVariant(2).fromArray([1, 2, 3]), undefined); }); @@ -1837,6 +1923,77 @@ suite('Layout', function() { assert.equal(Buffer.from('68690075006300', 'hex').compare(b), 0); }); }); + suite('UTF8', function() { + test('ctor', function() { + const cst = lo.utf8(); + assert(0 > cst.span); + assert.strictEqual(cst.maxSpan, -1); + }); + test('ctor with maxSpan', function() { + const cst = lo.utf8(5); + assert.strictEqual(cst.maxSpan, 5); + }); + test('ctor with invalid maxSpan', function() { + assert.throws(() => new lo.UTF8(23.1), TypeError); + }); + test('#getSpan', function() { + const cst = new lo.UTF8(); + assert.throws(() => cst.getSpan(), TypeError); + assert.equal(cst.getSpan(Buffer.from('00', 'hex')), 1); + assert.equal(cst.getSpan(Buffer.from('4100', 'hex')), 2); + assert.equal(cst.getSpan(Buffer.from('4100', 'hex'), 1), 1); + assert.equal(cst.getSpan(Buffer.from('4142', 'hex')), 2); + }); + test('#decode', function() { + const cst = new lo.UTF8(3); + assert.equal(cst.decode(Buffer.from('00', 'hex')), '\x00'); + assert.equal(cst.decode(Buffer.from('4100', 'hex')), 'A\x00'); + assert.equal(cst.decode(Buffer.from('4100', 'hex'), 1), '\x00'); + assert.equal(cst.decode(Buffer.from('4142', 'hex')), 'AB'); + assert.throws(() => cst.decode(Buffer.from('four', 'utf8')), + RangeError); + }); + test('#encode', function() { + const cst = new lo.UTF8(); + const b = Buffer.alloc(3); + b.fill(0xFF); + assert.equal(cst.encode('', b), 0); + assert.equal(Buffer.from('ffffff', 'hex').compare(b), 0); + assert.equal(cst.encode('A', b), 1); + assert.equal(Buffer.from('41ffff', 'hex').compare(b), 0); + assert.equal(cst.encode('B', b, 1), 1); + assert.equal(Buffer.from('4142ff', 'hex').compare(b), 0); + assert.equal(cst.encode(5, b), 1); + assert.equal(Buffer.from('3542ff', 'hex').compare(b), 0); + assert.equal(cst.encode('abc', b), 3); + assert.equal(Buffer.from('616263', 'hex').compare(b), 0); + assert.throws(() => cst.encode('four', b), RangeError); + }); + test('#encode with maxSpan', function() { + const cst = new lo.UTF8(2); + const b = Buffer.alloc(3); + b.fill(0xFF); + assert.throws(() => cst.encode('abc', b), RangeError); + }); + test('in struct', function() { + const st = lo.struct([lo.utf8('k'), + lo.utf8('v')]); + const b = Buffer.from('6162323334', 'hex'); + assert.throws(() => st.getSpan(), RangeError); + assert.equal(st.fields[0].getSpan(b), b.length); + assert.equal(st.fields[1].getSpan(b, 2), b.length - 2); + assert.equal(st.getSpan(b), b.length); + assert.deepEqual(st.decode(b), {k: 'ab234', v: ''}); + }); + test('in seq', function() { + const seq = lo.seq(lo.utf8(), 3); + const b = Buffer.from('4162633435', 'hex'); + assert.deepEqual(seq.decode(b), ['Abc45', '', '']); + b.fill(0xFF); + assert.equal(seq.encode(['hi', 'u', 'c'], b), 2 + 1 + 1); + assert.equal(Buffer.from('68697563ff', 'hex').compare(b), 0); + }); + }); suite('Constant', function() { test('ctor', function() { const c = new lo.Constant('value', 'p'); diff --git a/test/examples.js b/test/examples.js index 141e59e..39c9208 100644 --- a/test/examples.js +++ b/test/examples.js @@ -63,10 +63,14 @@ struct { */ const t = lo.u8('t'); const un = lo.union(t, lo.seq(lo.u8(), 4, 'u8')); + const nul = un.addVariant('n'.charCodeAt(0), 'nul'); const u32 = un.addVariant('w'.charCodeAt(0), lo.u32(), 'u32'); const s16 = un.addVariant('h'.charCodeAt(0), lo.seq(lo.s16(), 2), 's16'); const f32 = un.addVariant('f'.charCodeAt(0), lo.f32(), 'f32'); const b = Buffer.alloc(un.span); + assert.deepEqual(un.decode(b), {t: 0, u8: [0, 0, 0, 0]}); + assert.deepEqual(un.decode(Buffer.from('6e01020304', 'hex')), + {nul: true}); assert.deepEqual(un.decode(Buffer.from('7778563412', 'hex')), {u32: 0x12345678}); assert.deepEqual(un.decode(Buffer.from('660000bd41', 'hex')), @@ -81,6 +85,11 @@ struct { lo.bindConstructorLayout(Union, lo.union(lo.u8('t'), lo.seq(lo.u8(), 4, 'u8'))); + function Vn() {} + util.inherits(Vn, Union); + lo.bindConstructorLayout(Vn, + Union.layout_.addVariant('n'.charCodeAt(0), 'nul')); + function Vu32(v) {this.u32 = v;} util.inherits(Vu32, Union); lo.bindConstructorLayout(Vu32, @@ -110,6 +119,11 @@ struct { v = new Vf32(23.625); v.encode(b); assert.equal(Buffer.from('660000bd41', 'hex').compare(b), 0); + + b.fill(0xFF); + v = new Vn(); + v.encode(b); + assert.equal(Buffer.from('6effffffff', 'hex').compare(b), 0); }); test('Bit structures (lsb on little-endian)', function() { /*