├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── index.js ├── package-lock.json ├── package.json ├── src ├── Array.js ├── Base.js ├── Bitfield.js ├── Boolean.js ├── Buffer.js ├── DecodeStream.js ├── EncodeStream.js ├── Enum.js ├── LazyArray.js ├── Number.js ├── Optional.js ├── Pointer.js ├── Reserved.js ├── String.js ├── Struct.js ├── VersionedStruct.js └── utils.js └── test ├── Array.js ├── Bitfield.js ├── Boolean.js ├── Buffer.js ├── DecodeStream.js ├── EncodeStream.js ├── Enum.js ├── LazyArray.js ├── Number.js ├── Optional.js ├── Pointer.js ├── Reserved.js ├── String.js ├── Struct.js └── VersionedStruct.js /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: [push, pull_request] 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [14.x, 16.x, 18.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v2 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - run: npm install 24 | - run: npm test 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output/ 2 | coverage.html 3 | coverage/ 4 | node_modules/ 5 | .parcel-cache 6 | dist 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github/ 2 | .parcel-cache/ 3 | node_modules/ 4 | coverage/ 5 | coverage.html 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2015-present Devon Govett 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Restructure 2 | 3 | [![Build Status](https://travis-ci.org/devongovett/restructure.svg?branch=master)](https://travis-ci.org/devongovett/restructure) 4 | [![Coverage Status](https://coveralls.io/repos/devongovett/restructure/badge.png?branch=master)](https://coveralls.io/r/devongovett/restructure?branch=master) 5 | 6 | Restructure allows you to declaratively encode and decode binary data. 7 | It supports a wide variety of types to enable you to express a multitude 8 | of binary formats without writing any parsing code. 9 | 10 | Some of the supported features are C-like structures, versioned structures, 11 | pointers, arrays of any type, strings of a large number of encodings, enums, 12 | bitfields, and more. See the documentation below for more details. 13 | 14 | ## Example 15 | 16 | This is just a small example of what Restructure can do. Check out the API documentation 17 | below for more information. 18 | 19 | ```javascript 20 | import * as r from 'restructure'; 21 | 22 | let Person = new r.Struct({ 23 | name: new r.String(r.uint8, 'utf8'), 24 | age: r.uint8 25 | }); 26 | 27 | // decode a person from a buffer 28 | let value = Person.fromBuffer(new Uint8Array([/* ... */])); // returns an object with the fields defined above 29 | 30 | // encode a person from an object 31 | let buffer = Person.toBuffer({ 32 | name: 'Devon', 33 | age: 21 34 | }); 35 | ``` 36 | 37 | ## API 38 | 39 | All of the following types support three standard methods: 40 | 41 | * `fromBuffer(buffer)` - decodes an instance of the type from the given Uint8Array 42 | * `size(value)` - returns the amount of space the value would take if encoded 43 | * `toBuffer(value)` - encodes the given value into a Uint8Array 44 | 45 | Restructure supports a wide variety of types, but if you need to write your own for 46 | some custom use that cannot be represented by them, you can do so by just implementing 47 | the above methods. Then you can use your type just as you would any other type, in structures 48 | and whatnot. 49 | 50 | ### Number Types 51 | 52 | The following built-in number types are available: 53 | 54 | ```javascript 55 | uint8, uint16, uint24, uint32, int8, int16, int24, int32, float, double, fixed16, fixed32 56 | ``` 57 | 58 | Numbers are big-endian (network order) by default, but little-endian is supported, too: 59 | 60 | ```javascript 61 | uint16le, uint24le, uint32le, int16le, int24le, int32le, floatle, doublele, fixed16le, fixed32le 62 | ``` 63 | 64 | To avoid ambiguity, big-endian may be used explicitly: 65 | 66 | ```javascript 67 | uint16be, uint24be, uint32be, int16be, int24be, int32be, floatbe, doublebe, fixed16be, fixed32be 68 | ``` 69 | 70 | ### Boolean 71 | 72 | Booleans are encoded as `0` or `1` using one of the above number types. 73 | 74 | ```javascript 75 | var bool = new r.Boolean(r.uint32); 76 | ``` 77 | 78 | ### Reserved 79 | 80 | The `Reserved` type simply skips data in a structure, where there are reserved fields. 81 | Encoding produces zeros. 82 | 83 | ```javascript 84 | // 10 reserved uint8s (default is 1) 85 | var reserved = new r.Reserved(r.uint8, 10); 86 | ``` 87 | 88 | ### Optional 89 | 90 | The `Optional` type only encodes or decodes when given condition is truthy. 91 | 92 | ```javascript 93 | // includes field 94 | var optional = new r.Optional(r.uint8, true); 95 | 96 | // excludes field 97 | var optional = new r.Optional(r.uint8, false); 98 | 99 | // determine whether field is to be included at runtime with a function 100 | var optional = new r.Optional(r.uint8, function() { 101 | return this.flags & 0x50; 102 | }); 103 | ``` 104 | 105 | ### Enum 106 | 107 | The `Enum` type maps a number to the value at that index in an array. 108 | 109 | ```javascript 110 | var color = new r.Enum(r.uint8, ['red', 'orange', 'yellow', 'green', 'blue', 'purple']); 111 | ``` 112 | 113 | ### Bitfield 114 | 115 | The `Bitfield` type maps a number to an object with boolean keys mapping to each bit in that number, 116 | as defined in an array. 117 | 118 | ```javascript 119 | var bitfield = new r.Bitfield(r.uint8, ['Jack', 'Kack', 'Lack', 'Mack', 'Nack', 'Oack', 'Pack', 'Quack']); 120 | bitfield.decode(stream); 121 | 122 | var result = { 123 | Jack: true, 124 | Kack: false, 125 | Lack: false, 126 | Mack: true, 127 | Nack: true, 128 | Oack: false, 129 | Pack: true, 130 | Quack: true 131 | }; 132 | 133 | bitfield.encode(stream, result); 134 | ``` 135 | 136 | ### Buffer 137 | 138 | Extracts a slice of the buffer to a `Uint8Array`. The length can be a constant, or taken from 139 | a previous field in the parent structure. 140 | 141 | ```javascript 142 | // fixed length 143 | var buf = new r.Buffer(2); 144 | 145 | // length from parent structure 146 | var struct = new r.Struct({ 147 | bufLen: r.uint8, 148 | buf: new r.Buffer('bufLen') 149 | }); 150 | ``` 151 | 152 | ### String 153 | 154 | A `String` maps a JavaScript string to and from binary encodings. The length, in bytes, can be a constant, 155 | taken from a previous field in the parent structure, encoded using a number type immediately before the 156 | string. 157 | 158 | Fully supported encodings include `'ascii'`, `'utf8'`, `'ucs2'`, `'utf16le'`, `'utf16be'`. Decoding is also possible 159 | with any encoding supported by [TextDecoder](https://developer.mozilla.org/en-US/docs/Web/API/Encoding_API/Encodings), 160 | however encoding these is not supported. 161 | 162 | ```javascript 163 | // fixed length, ascii encoding by default 164 | var str = new r.String(2); 165 | 166 | // length encoded as number before the string, utf8 encoding 167 | var str = new r.String(r.uint8, 'utf8'); 168 | 169 | // length from parent structure 170 | var struct = new r.Struct({ 171 | len: r.uint8, 172 | str: new r.String('len', 'utf16be') 173 | }); 174 | 175 | // null-terminated string (also known as C string) 176 | var str = new r.String(null, 'utf8'); 177 | ``` 178 | 179 | ### Array 180 | 181 | An `Array` maps to and from a JavaScript array containing instances of a sub-type. The length can be a constant, 182 | taken from a previous field in the parent structure, encoded using a number type immediately 183 | before the string, or computed by a function. 184 | 185 | ```javascript 186 | // fixed length, containing numbers 187 | var arr = new r.Array(r.uint16, 2); 188 | 189 | // length encoded as number before the array containing strings 190 | var arr = new r.Array(new r.String(10), r.uint8); 191 | 192 | // length computed by a function 193 | var arr = new r.Array(r.uint8, function() { return 5 }); 194 | 195 | // length from parent structure 196 | var struct = new r.Struct({ 197 | len: r.uint8, 198 | arr: new r.Array(r.uint8, 'len') 199 | }); 200 | 201 | // treat as amount of bytes instead (may be used in all the above scenarios) 202 | var arr = new r.Array(r.uint16, 6, 'bytes'); 203 | ``` 204 | 205 | ### LazyArray 206 | 207 | The `LazyArray` type extends from the `Array` type, and is useful for large arrays that you do not need to access sequentially. 208 | It avoids decoding the entire array upfront, and instead only decodes and caches individual items as needed. It only works when 209 | the elements inside the array have a fixed size. 210 | 211 | Instead of returning a JavaScript array, the `LazyArray` type returns a custom object that can be used to access the elements. 212 | 213 | ```javascript 214 | var arr = new r.LazyArray(r.uint16, 2048); 215 | var res = arr.decode(stream); 216 | 217 | // get a single element 218 | var el = res.get(2); 219 | 220 | // convert to a normal array (decode all elements) 221 | var array = res.toArray(); 222 | ``` 223 | 224 | ### Struct 225 | 226 | A `Struct` maps to and from JavaScript objects, containing keys of various previously discussed types. Sub structures, 227 | arrays of structures, and pointers to other types (discussed below) are supported. 228 | 229 | ```javascript 230 | var Person = new r.Struct({ 231 | name: new r.String(r.uint8, 'utf8'), 232 | age: r.uint8 233 | }); 234 | ``` 235 | 236 | ### VersionedStruct 237 | 238 | A `VersionedStruct` is a `Struct` that has multiple versions. The version is typically encoded at 239 | the beginning of the structure, or as a field in a parent structure. There is an optional `header` 240 | common to all versions, and separate fields listed for each version number. 241 | 242 | ```javascript 243 | // the version is read as a uint8 in this example 244 | // you could also get the version from a key on the parent struct 245 | var Person = new r.VersionedStruct(r.uint8, { 246 | // optional header common to all versions 247 | header: { 248 | name: new r.String(r.uint8, 'utf8') 249 | }, 250 | 0: { 251 | age: r.uint8 252 | }, 253 | 1: { 254 | hairColor: r.Enum(r.uint8, ['black', 'brown', 'blonde']) 255 | } 256 | }); 257 | ``` 258 | 259 | ### Pointer 260 | 261 | Pointers map an address or offset encoded as a number, to a value encoded elsewhere in the buffer. 262 | There are a few options you can use: `type`, `relativeTo`, `allowNull`, and `nullValue`. 263 | The `type` option has these possible values: 264 | 265 | * `local` (default) - the encoded offset is relative to the start of the containing structure 266 | * `immediate` - the encoded offset is relative to the position of the pointer itself 267 | * `parent` - the encoded offset is relative to the parent structure of the immediate container 268 | * `global` - the encoded offset is global to the start of the file 269 | 270 | The `relativeTo` option accepts a function callback that should return the field on the containing structure which the encoded offset is relative to. The callback is called with the context as parameter. 271 | By default, pointers are relative to the start of the containing structure (`local`). 272 | 273 | The `allowNull` option lets you specify whether zero offsets are allowed or should produce `null`. This is 274 | set to `true` by default. The `nullValue` option is related, and lets you override the encoded value that 275 | represents `null`. By default, the `nullValue` is zero. 276 | 277 | The `lazy` option allows lazy decoding of the pointer's value by defining a getter on the parent object. 278 | This only works when the pointer is contained within a Struct, but can be used to speed up decoding 279 | quite a bit when not all of the data is needed right away. 280 | 281 | ```javascript 282 | var Address = new r.Struct({ 283 | street: new r.String(r.uint8), 284 | zip: new r.String(5) 285 | }); 286 | 287 | var Person = new r.Struct({ 288 | name: new r.String(r.uint8, 'utf8'), 289 | age: r.uint8, 290 | ptrStart: r.uint8, 291 | address: new r.Pointer(r.uint8, Address) 292 | }); 293 | ``` 294 | 295 | If the type of a pointer is set to 'void', it is not decoded and the computed address in the buffer 296 | is simply returned. To encode a void pointer, create a `new r.VoidPointer(type, value)`. 297 | 298 | ## License 299 | 300 | MIT 301 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | export {EncodeStream} from './src/EncodeStream.js'; 2 | export {DecodeStream} from './src/DecodeStream.js'; 3 | export {Array} from './src/Array.js'; 4 | export {LazyArray} from './src/LazyArray.js'; 5 | export {Bitfield} from './src/Bitfield.js'; 6 | export {Boolean} from './src/Boolean.js'; 7 | export {Buffer} from './src/Buffer.js'; 8 | export {Enum} from './src/Enum.js'; 9 | export {Optional} from './src/Optional.js'; 10 | export {Reserved} from './src/Reserved.js'; 11 | export {String} from './src/String.js'; 12 | export {Struct} from './src/Struct.js'; 13 | export {VersionedStruct} from './src/VersionedStruct.js'; 14 | 15 | export * from './src/utils.js'; 16 | export * from './src/Number.js'; 17 | export * from './src/Pointer.js'; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "restructure", 3 | "version": "3.0.2", 4 | "description": "Declaratively encode and decode binary data", 5 | "type": "module", 6 | "main": "./dist/main.cjs", 7 | "module": "./index.js", 8 | "source": "./index.js", 9 | "exports": { 10 | "import": "./index.js", 11 | "require": "./dist/main.cjs" 12 | }, 13 | "targets": { 14 | "module": false 15 | }, 16 | "devDependencies": { 17 | "mocha": "^10.0.0", 18 | "parcel": "^2.6.1" 19 | }, 20 | "scripts": { 21 | "test": "mocha", 22 | "build": "parcel build", 23 | "prepublishOnly": "parcel build" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git://github.com/devongovett/restructure.git" 28 | }, 29 | "keywords": [ 30 | "binary", 31 | "struct", 32 | "encode", 33 | "decode" 34 | ], 35 | "author": "Devon Govett ", 36 | "license": "MIT", 37 | "bugs": { 38 | "url": "https://github.com/devongovett/restructure/issues" 39 | }, 40 | "homepage": "https://github.com/devongovett/restructure" 41 | } 42 | -------------------------------------------------------------------------------- /src/Array.js: -------------------------------------------------------------------------------- 1 | import {Base} from './Base.js'; 2 | import {Number as NumberT} from './Number.js'; 3 | import * as utils from './utils.js'; 4 | 5 | class ArrayT extends Base { 6 | constructor(type, length, lengthType = 'count') { 7 | super(); 8 | this.type = type; 9 | this.length = length; 10 | this.lengthType = lengthType; 11 | } 12 | 13 | decode(stream, parent) { 14 | let length; 15 | const { pos } = stream; 16 | 17 | const res = []; 18 | let ctx = parent; 19 | 20 | if (this.length != null) { 21 | length = utils.resolveLength(this.length, stream, parent); 22 | } 23 | 24 | if (this.length instanceof NumberT) { 25 | // define hidden properties 26 | Object.defineProperties(res, { 27 | parent: { value: parent }, 28 | _startOffset: { value: pos }, 29 | _currentOffset: { value: 0, writable: true }, 30 | _length: { value: length } 31 | }); 32 | 33 | ctx = res; 34 | } 35 | 36 | if ((length == null) || (this.lengthType === 'bytes')) { 37 | const target = (length != null) ? 38 | stream.pos + length 39 | : (parent != null ? parent._length : undefined) ? 40 | parent._startOffset + parent._length 41 | : 42 | stream.length; 43 | 44 | while (stream.pos < target) { 45 | res.push(this.type.decode(stream, ctx)); 46 | } 47 | 48 | } else { 49 | for (let i = 0, end = length; i < end; i++) { 50 | res.push(this.type.decode(stream, ctx)); 51 | } 52 | } 53 | 54 | return res; 55 | } 56 | 57 | size(array, ctx, includePointers = true) { 58 | if (!array) { 59 | return this.type.size(null, ctx) * utils.resolveLength(this.length, null, ctx); 60 | } 61 | 62 | let size = 0; 63 | if (this.length instanceof NumberT) { 64 | size += this.length.size(); 65 | ctx = {parent: ctx, pointerSize: 0}; 66 | } 67 | 68 | for (let item of array) { 69 | size += this.type.size(item, ctx); 70 | } 71 | 72 | if (ctx && includePointers && this.length instanceof NumberT) { 73 | size += ctx.pointerSize; 74 | } 75 | 76 | return size; 77 | } 78 | 79 | encode(stream, array, parent) { 80 | let ctx = parent; 81 | if (this.length instanceof NumberT) { 82 | ctx = { 83 | pointers: [], 84 | startOffset: stream.pos, 85 | parent 86 | }; 87 | 88 | ctx.pointerOffset = stream.pos + this.size(array, ctx, false); 89 | this.length.encode(stream, array.length); 90 | } 91 | 92 | for (let item of array) { 93 | this.type.encode(stream, item, ctx); 94 | } 95 | 96 | if (this.length instanceof NumberT) { 97 | let i = 0; 98 | while (i < ctx.pointers.length) { 99 | const ptr = ctx.pointers[i++]; 100 | ptr.type.encode(stream, ptr.val, ptr.parent); 101 | } 102 | } 103 | } 104 | } 105 | 106 | export {ArrayT as Array}; 107 | -------------------------------------------------------------------------------- /src/Base.js: -------------------------------------------------------------------------------- 1 | import {DecodeStream} from './DecodeStream.js'; 2 | import {EncodeStream} from './EncodeStream.js'; 3 | 4 | export class Base { 5 | fromBuffer(buffer) { 6 | let stream = new DecodeStream(buffer); 7 | return this.decode(stream); 8 | } 9 | 10 | toBuffer(value) { 11 | let size = this.size(value); 12 | let buffer = new Uint8Array(size); 13 | let stream = new EncodeStream(buffer); 14 | this.encode(stream, value); 15 | return buffer; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Bitfield.js: -------------------------------------------------------------------------------- 1 | import {Base} from './Base.js'; 2 | 3 | export class Bitfield extends Base { 4 | constructor(type, flags = []) { 5 | super(); 6 | this.type = type; 7 | this.flags = flags; 8 | } 9 | 10 | decode(stream) { 11 | const val = this.type.decode(stream); 12 | 13 | const res = {}; 14 | for (let i = 0; i < this.flags.length; i++) { 15 | const flag = this.flags[i]; 16 | if (flag != null) { 17 | res[flag] = !!(val & (1 << i)); 18 | } 19 | } 20 | 21 | return res; 22 | } 23 | 24 | size() { 25 | return this.type.size(); 26 | } 27 | 28 | encode(stream, keys) { 29 | let val = 0; 30 | for (let i = 0; i < this.flags.length; i++) { 31 | const flag = this.flags[i]; 32 | if (flag != null) { 33 | if (keys[flag]) { val |= (1 << i); } 34 | } 35 | } 36 | 37 | return this.type.encode(stream, val); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Boolean.js: -------------------------------------------------------------------------------- 1 | import {Base} from './Base.js'; 2 | 3 | export class BooleanT extends Base { 4 | constructor(type) { 5 | super(); 6 | this.type = type; 7 | } 8 | 9 | decode(stream, parent) { 10 | return !!this.type.decode(stream, parent); 11 | } 12 | 13 | size(val, parent) { 14 | return this.type.size(val, parent); 15 | } 16 | 17 | encode(stream, val, parent) { 18 | return this.type.encode(stream, +val, parent); 19 | } 20 | } 21 | 22 | export {BooleanT as Boolean}; 23 | -------------------------------------------------------------------------------- /src/Buffer.js: -------------------------------------------------------------------------------- 1 | import {Base} from './Base.js'; 2 | import {Number as NumberT} from './Number.js'; 3 | import * as utils from './utils.js'; 4 | 5 | export class BufferT extends Base { 6 | constructor(length) { 7 | super(); 8 | this.length = length; 9 | } 10 | 11 | decode(stream, parent) { 12 | const length = utils.resolveLength(this.length, stream, parent); 13 | return stream.readBuffer(length); 14 | } 15 | 16 | size(val, parent) { 17 | if (!val) { 18 | return utils.resolveLength(this.length, null, parent); 19 | } 20 | 21 | let len = val.length; 22 | if (this.length instanceof NumberT) { 23 | len += this.length.size(); 24 | } 25 | 26 | return len; 27 | } 28 | 29 | encode(stream, buf, parent) { 30 | if (this.length instanceof NumberT) { 31 | this.length.encode(stream, buf.length); 32 | } 33 | 34 | return stream.writeBuffer(buf); 35 | } 36 | } 37 | 38 | export {BufferT as Buffer}; 39 | -------------------------------------------------------------------------------- /src/DecodeStream.js: -------------------------------------------------------------------------------- 1 | // Node back-compat. 2 | const ENCODING_MAPPING = { 3 | utf16le: 'utf-16le', 4 | ucs2: 'utf-16le', 5 | utf16be: 'utf-16be' 6 | } 7 | 8 | export class DecodeStream { 9 | constructor(buffer) { 10 | this.buffer = buffer; 11 | this.view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength); 12 | this.pos = 0; 13 | this.length = this.buffer.length; 14 | } 15 | 16 | readString(length, encoding = 'ascii') { 17 | encoding = ENCODING_MAPPING[encoding] || encoding; 18 | 19 | let buf = this.readBuffer(length); 20 | try { 21 | let decoder = new TextDecoder(encoding); 22 | return decoder.decode(buf); 23 | } catch (err) { 24 | return buf; 25 | } 26 | } 27 | 28 | readBuffer(length) { 29 | return this.buffer.slice(this.pos, (this.pos += length)); 30 | } 31 | 32 | readUInt24BE() { 33 | return (this.readUInt16BE() << 8) + this.readUInt8(); 34 | } 35 | 36 | readUInt24LE() { 37 | return this.readUInt16LE() + (this.readUInt8() << 16); 38 | } 39 | 40 | readInt24BE() { 41 | return (this.readInt16BE() << 8) + this.readUInt8(); 42 | } 43 | 44 | readInt24LE() { 45 | return this.readUInt16LE() + (this.readInt8() << 16); 46 | } 47 | } 48 | 49 | DecodeStream.TYPES = { 50 | UInt8: 1, 51 | UInt16: 2, 52 | UInt24: 3, 53 | UInt32: 4, 54 | Int8: 1, 55 | Int16: 2, 56 | Int24: 3, 57 | Int32: 4, 58 | Float: 4, 59 | Double: 8 60 | }; 61 | 62 | for (let key of Object.getOwnPropertyNames(DataView.prototype)) { 63 | if (key.slice(0, 3) === 'get') { 64 | let type = key.slice(3).replace('Ui', 'UI'); 65 | if (type === 'Float32') { 66 | type = 'Float'; 67 | } else if (type === 'Float64') { 68 | type = 'Double'; 69 | } 70 | let bytes = DecodeStream.TYPES[type]; 71 | DecodeStream.prototype['read' + type + (bytes === 1 ? '' : 'BE')] = function () { 72 | const ret = this.view[key](this.pos, false); 73 | this.pos += bytes; 74 | return ret; 75 | }; 76 | 77 | if (bytes !== 1) { 78 | DecodeStream.prototype['read' + type + 'LE'] = function () { 79 | const ret = this.view[key](this.pos, true); 80 | this.pos += bytes; 81 | return ret; 82 | }; 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/EncodeStream.js: -------------------------------------------------------------------------------- 1 | import {DecodeStream} from './DecodeStream.js'; 2 | 3 | const textEncoder = new TextEncoder(); 4 | const isBigEndian = new Uint8Array(new Uint16Array([0x1234]).buffer)[0] == 0x12; 5 | 6 | export class EncodeStream { 7 | constructor(buffer) { 8 | this.buffer = buffer; 9 | this.view = new DataView(this.buffer.buffer, this.buffer.byteOffset, this.buffer.byteLength); 10 | this.pos = 0; 11 | } 12 | 13 | writeBuffer(buffer) { 14 | this.buffer.set(buffer, this.pos); 15 | this.pos += buffer.length; 16 | } 17 | 18 | writeString(string, encoding = 'ascii') { 19 | let buf; 20 | switch (encoding) { 21 | case 'utf16le': 22 | case 'utf16-le': 23 | case 'ucs2': // node treats this the same as utf16. 24 | buf = stringToUtf16(string, isBigEndian); 25 | break; 26 | 27 | case 'utf16be': 28 | case 'utf16-be': 29 | buf = stringToUtf16(string, !isBigEndian); 30 | break; 31 | 32 | case 'utf8': 33 | buf = textEncoder.encode(string); 34 | break; 35 | 36 | case 'ascii': 37 | buf = stringToAscii(string); 38 | break; 39 | 40 | default: 41 | throw new Error(`Unsupported encoding: ${encoding}`); 42 | } 43 | 44 | this.writeBuffer(buf); 45 | } 46 | 47 | writeUInt24BE(val) { 48 | this.buffer[this.pos++] = (val >>> 16) & 0xff; 49 | this.buffer[this.pos++] = (val >>> 8) & 0xff; 50 | this.buffer[this.pos++] = val & 0xff; 51 | } 52 | 53 | writeUInt24LE(val) { 54 | this.buffer[this.pos++] = val & 0xff; 55 | this.buffer[this.pos++] = (val >>> 8) & 0xff; 56 | this.buffer[this.pos++] = (val >>> 16) & 0xff; 57 | } 58 | 59 | writeInt24BE(val) { 60 | if (val >= 0) { 61 | this.writeUInt24BE(val); 62 | } else { 63 | this.writeUInt24BE(val + 0xffffff + 1); 64 | } 65 | } 66 | 67 | writeInt24LE(val) { 68 | if (val >= 0) { 69 | this.writeUInt24LE(val); 70 | } else { 71 | this.writeUInt24LE(val + 0xffffff + 1); 72 | } 73 | } 74 | 75 | fill(val, length) { 76 | if (length < this.buffer.length) { 77 | this.buffer.fill(val, this.pos, this.pos + length); 78 | this.pos += length; 79 | } else { 80 | const buf = new Uint8Array(length); 81 | buf.fill(val); 82 | this.writeBuffer(buf); 83 | } 84 | } 85 | } 86 | 87 | function stringToUtf16(string, swap) { 88 | let buf = new Uint16Array(string.length); 89 | for (let i = 0; i < string.length; i++) { 90 | let code = string.charCodeAt(i); 91 | if (swap) { 92 | code = (code >> 8) | ((code & 0xff) << 8); 93 | } 94 | buf[i] = code; 95 | } 96 | return new Uint8Array(buf.buffer); 97 | } 98 | 99 | function stringToAscii(string) { 100 | let buf = new Uint8Array(string.length); 101 | for (let i = 0; i < string.length; i++) { 102 | // Match node.js behavior - encoding allows 8-bit rather than 7-bit. 103 | buf[i] = string.charCodeAt(i); 104 | } 105 | return buf; 106 | } 107 | 108 | for (let key of Object.getOwnPropertyNames(DataView.prototype)) { 109 | if (key.slice(0, 3) === 'set') { 110 | let type = key.slice(3).replace('Ui', 'UI'); 111 | if (type === 'Float32') { 112 | type = 'Float'; 113 | } else if (type === 'Float64') { 114 | type = 'Double'; 115 | } 116 | let bytes = DecodeStream.TYPES[type]; 117 | EncodeStream.prototype['write' + type + (bytes === 1 ? '' : 'BE')] = function (value) { 118 | this.view[key](this.pos, value, false); 119 | this.pos += bytes; 120 | }; 121 | 122 | if (bytes !== 1) { 123 | EncodeStream.prototype['write' + type + 'LE'] = function (value) { 124 | this.view[key](this.pos, value, true); 125 | this.pos += bytes; 126 | }; 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Enum.js: -------------------------------------------------------------------------------- 1 | import {Base} from './Base.js'; 2 | 3 | export class Enum extends Base { 4 | constructor(type, options = []) { 5 | super(); 6 | this.type = type; 7 | this.options = options; 8 | } 9 | 10 | decode(stream) { 11 | const index = this.type.decode(stream); 12 | return this.options[index] || index; 13 | } 14 | 15 | size() { 16 | return this.type.size(); 17 | } 18 | 19 | encode(stream, val) { 20 | const index = this.options.indexOf(val); 21 | if (index === -1) { 22 | throw new Error(`Unknown option in enum: ${val}`); 23 | } 24 | 25 | return this.type.encode(stream, index); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/LazyArray.js: -------------------------------------------------------------------------------- 1 | import {Array as ArrayT} from './Array.js'; 2 | import {Number as NumberT} from './Number.js'; 3 | import * as utils from './utils.js'; 4 | 5 | export class LazyArray extends ArrayT { 6 | decode(stream, parent) { 7 | const { pos } = stream; 8 | const length = utils.resolveLength(this.length, stream, parent); 9 | 10 | if (this.length instanceof NumberT) { 11 | parent = { 12 | parent, 13 | _startOffset: pos, 14 | _currentOffset: 0, 15 | _length: length 16 | }; 17 | } 18 | 19 | const res = new LazyArrayValue(this.type, length, stream, parent); 20 | 21 | stream.pos += length * this.type.size(null, parent); 22 | return res; 23 | } 24 | 25 | size(val, ctx) { 26 | if (val instanceof LazyArrayValue) { 27 | val = val.toArray(); 28 | } 29 | 30 | return super.size(val, ctx); 31 | } 32 | 33 | encode(stream, val, ctx) { 34 | if (val instanceof LazyArrayValue) { 35 | val = val.toArray(); 36 | } 37 | 38 | return super.encode(stream, val, ctx); 39 | } 40 | } 41 | 42 | class LazyArrayValue { 43 | constructor(type, length, stream, ctx) { 44 | this.type = type; 45 | this.length = length; 46 | this.stream = stream; 47 | this.ctx = ctx; 48 | this.base = this.stream.pos; 49 | this.items = []; 50 | } 51 | 52 | get(index) { 53 | if ((index < 0) || (index >= this.length)) { 54 | return undefined; 55 | } 56 | 57 | if (this.items[index] == null) { 58 | const { pos } = this.stream; 59 | this.stream.pos = this.base + (this.type.size(null, this.ctx) * index); 60 | this.items[index] = this.type.decode(this.stream, this.ctx); 61 | this.stream.pos = pos; 62 | } 63 | 64 | return this.items[index]; 65 | } 66 | 67 | toArray() { 68 | const result = []; 69 | for (let i = 0, end = this.length; i < end; i++) { 70 | result.push(this.get(i)); 71 | } 72 | return result; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Number.js: -------------------------------------------------------------------------------- 1 | import {DecodeStream} from './DecodeStream.js'; 2 | import {Base} from './Base.js'; 3 | 4 | class NumberT extends Base { 5 | constructor(type, endian = 'BE') { 6 | super(); 7 | this.type = type; 8 | this.endian = endian; 9 | this.fn = this.type; 10 | if (this.type[this.type.length - 1] !== '8') { 11 | this.fn += this.endian; 12 | } 13 | } 14 | 15 | size() { 16 | return DecodeStream.TYPES[this.type]; 17 | } 18 | 19 | decode(stream) { 20 | return stream[`read${this.fn}`](); 21 | } 22 | 23 | encode(stream, val) { 24 | return stream[`write${this.fn}`](val); 25 | } 26 | } 27 | 28 | export {NumberT as Number}; 29 | 30 | export const uint8 = new NumberT('UInt8'); 31 | export const uint16be = new NumberT('UInt16', 'BE'); 32 | export const uint16 = uint16be; 33 | export const uint16le = new NumberT('UInt16', 'LE'); 34 | export const uint24be = new NumberT('UInt24', 'BE'); 35 | export const uint24 = uint24be; 36 | export const uint24le = new NumberT('UInt24', 'LE'); 37 | export const uint32be = new NumberT('UInt32', 'BE'); 38 | export const uint32 = uint32be; 39 | export const uint32le = new NumberT('UInt32', 'LE'); 40 | export const int8 = new NumberT('Int8'); 41 | export const int16be = new NumberT('Int16', 'BE'); 42 | export const int16 = int16be; 43 | export const int16le = new NumberT('Int16', 'LE'); 44 | export const int24be = new NumberT('Int24', 'BE'); 45 | export const int24 = int24be; 46 | export const int24le = new NumberT('Int24', 'LE'); 47 | export const int32be = new NumberT('Int32', 'BE'); 48 | export const int32 = int32be; 49 | export const int32le = new NumberT('Int32', 'LE'); 50 | export const floatbe = new NumberT('Float', 'BE'); 51 | export const float = floatbe; 52 | export const floatle = new NumberT('Float', 'LE'); 53 | export const doublebe = new NumberT('Double', 'BE'); 54 | export const double = doublebe; 55 | export const doublele = new NumberT('Double', 'LE'); 56 | 57 | export class Fixed extends NumberT { 58 | constructor(size, endian, fracBits = size >> 1) { 59 | super(`Int${size}`, endian); 60 | this._point = 1 << fracBits; 61 | } 62 | 63 | decode(stream) { 64 | return super.decode(stream) / this._point; 65 | } 66 | 67 | encode(stream, val) { 68 | return super.encode(stream, (val * this._point) | 0); 69 | } 70 | } 71 | 72 | export const fixed16be = new Fixed(16, 'BE'); 73 | export const fixed16 = fixed16be; 74 | export const fixed16le = new Fixed(16, 'LE'); 75 | export const fixed32be = new Fixed(32, 'BE'); 76 | export const fixed32 = fixed32be; 77 | export const fixed32le = new Fixed(32, 'LE'); 78 | -------------------------------------------------------------------------------- /src/Optional.js: -------------------------------------------------------------------------------- 1 | import {Base} from './Base.js'; 2 | 3 | export class Optional extends Base { 4 | constructor(type, condition = true) { 5 | super(); 6 | this.type = type; 7 | this.condition = condition; 8 | } 9 | 10 | decode(stream, parent) { 11 | let { condition } = this; 12 | if (typeof condition === 'function') { 13 | condition = condition.call(parent, parent); 14 | } 15 | 16 | if (condition) { 17 | return this.type.decode(stream, parent); 18 | } 19 | } 20 | 21 | size(val, parent) { 22 | let { condition } = this; 23 | if (typeof condition === 'function') { 24 | condition = condition.call(parent, parent); 25 | } 26 | 27 | if (condition) { 28 | return this.type.size(val, parent); 29 | } else { 30 | return 0; 31 | } 32 | } 33 | 34 | encode(stream, val, parent) { 35 | let { condition } = this; 36 | if (typeof condition === 'function') { 37 | condition = condition.call(parent, parent); 38 | } 39 | 40 | if (condition) { 41 | return this.type.encode(stream, val, parent); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Pointer.js: -------------------------------------------------------------------------------- 1 | import * as utils from './utils.js'; 2 | import {Base} from './Base.js'; 3 | 4 | export class Pointer extends Base { 5 | constructor(offsetType, type, options = {}) { 6 | super(); 7 | this.offsetType = offsetType; 8 | this.type = type; 9 | this.options = options; 10 | if (this.type === 'void') { this.type = null; } 11 | if (this.options.type == null) { this.options.type = 'local'; } 12 | if (this.options.allowNull == null) { this.options.allowNull = true; } 13 | if (this.options.nullValue == null) { this.options.nullValue = 0; } 14 | if (this.options.lazy == null) { this.options.lazy = false; } 15 | if (this.options.relativeTo) { 16 | if (typeof this.options.relativeTo !== 'function') { 17 | throw new Error('relativeTo option must be a function'); 18 | } 19 | this.relativeToGetter = options.relativeTo; 20 | } 21 | } 22 | 23 | decode(stream, ctx) { 24 | const offset = this.offsetType.decode(stream, ctx); 25 | 26 | // handle NULL pointers 27 | if ((offset === this.options.nullValue) && this.options.allowNull) { 28 | return null; 29 | } 30 | 31 | let relative; 32 | switch (this.options.type) { 33 | case 'local': relative = ctx._startOffset; break; 34 | case 'immediate': relative = stream.pos - this.offsetType.size(); break; 35 | case 'parent': relative = ctx.parent._startOffset; break; 36 | default: 37 | var c = ctx; 38 | while (c.parent) { 39 | c = c.parent; 40 | } 41 | 42 | relative = c._startOffset || 0; 43 | } 44 | 45 | if (this.options.relativeTo) { 46 | relative += this.relativeToGetter(ctx); 47 | } 48 | 49 | const ptr = offset + relative; 50 | 51 | if (this.type != null) { 52 | let val = null; 53 | const decodeValue = () => { 54 | if (val != null) { return val; } 55 | 56 | const { pos } = stream; 57 | stream.pos = ptr; 58 | val = this.type.decode(stream, ctx); 59 | stream.pos = pos; 60 | return val; 61 | }; 62 | 63 | // If this is a lazy pointer, define a getter to decode only when needed. 64 | // This obviously only works when the pointer is contained by a Struct. 65 | if (this.options.lazy) { 66 | return new utils.PropertyDescriptor({ 67 | get: decodeValue}); 68 | } 69 | 70 | return decodeValue(); 71 | } else { 72 | return ptr; 73 | } 74 | } 75 | 76 | size(val, ctx) { 77 | const parent = ctx; 78 | switch (this.options.type) { 79 | case 'local': case 'immediate': 80 | break; 81 | case 'parent': 82 | ctx = ctx.parent; 83 | break; 84 | default: // global 85 | while (ctx.parent) { 86 | ctx = ctx.parent; 87 | } 88 | } 89 | 90 | let { type } = this; 91 | if (type == null) { 92 | if (!(val instanceof VoidPointer)) { 93 | throw new Error("Must be a VoidPointer"); 94 | } 95 | 96 | ({ type } = val); 97 | val = val.value; 98 | } 99 | 100 | if (val && ctx) { 101 | // Must be written as two separate lines rather than += in case `type.size` mutates ctx.pointerSize. 102 | let size = type.size(val, parent); 103 | ctx.pointerSize += size; 104 | } 105 | 106 | return this.offsetType.size(); 107 | } 108 | 109 | encode(stream, val, ctx) { 110 | let relative; 111 | const parent = ctx; 112 | if ((val == null)) { 113 | this.offsetType.encode(stream, this.options.nullValue); 114 | return; 115 | } 116 | 117 | switch (this.options.type) { 118 | case 'local': 119 | relative = ctx.startOffset; 120 | break; 121 | case 'immediate': 122 | relative = stream.pos + this.offsetType.size(val, parent); 123 | break; 124 | case 'parent': 125 | ctx = ctx.parent; 126 | relative = ctx.startOffset; 127 | break; 128 | default: // global 129 | relative = 0; 130 | while (ctx.parent) { 131 | ctx = ctx.parent; 132 | } 133 | } 134 | 135 | if (this.options.relativeTo) { 136 | relative += this.relativeToGetter(parent.val); 137 | } 138 | 139 | this.offsetType.encode(stream, ctx.pointerOffset - relative); 140 | 141 | let { type } = this; 142 | if (type == null) { 143 | if (!(val instanceof VoidPointer)) { 144 | throw new Error("Must be a VoidPointer"); 145 | } 146 | 147 | ({ type } = val); 148 | val = val.value; 149 | } 150 | 151 | ctx.pointers.push({ 152 | type, 153 | val, 154 | parent 155 | }); 156 | 157 | return ctx.pointerOffset += type.size(val, parent); 158 | } 159 | } 160 | 161 | // A pointer whose type is determined at decode time 162 | export class VoidPointer { 163 | constructor(type, value) { 164 | this.type = type; 165 | this.value = value; 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/Reserved.js: -------------------------------------------------------------------------------- 1 | import {Base} from './Base.js'; 2 | import * as utils from './utils.js'; 3 | 4 | export class Reserved extends Base { 5 | constructor(type, count = 1) { 6 | super(); 7 | this.type = type; 8 | this.count = count; 9 | } 10 | decode(stream, parent) { 11 | stream.pos += this.size(null, parent); 12 | return undefined; 13 | } 14 | 15 | size(data, parent) { 16 | const count = utils.resolveLength(this.count, null, parent); 17 | return this.type.size() * count; 18 | } 19 | 20 | encode(stream, val, parent) { 21 | return stream.fill(0, this.size(val, parent)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/String.js: -------------------------------------------------------------------------------- 1 | import {Base} from './Base.js'; 2 | import {Number as NumberT} from './Number.js'; 3 | import * as utils from './utils.js'; 4 | 5 | class StringT extends Base { 6 | constructor(length, encoding = 'ascii') { 7 | super(); 8 | this.length = length; 9 | this.encoding = encoding; 10 | } 11 | 12 | decode(stream, parent) { 13 | let length, pos; 14 | 15 | let { encoding } = this; 16 | if (typeof encoding === 'function') { 17 | encoding = encoding.call(parent, parent) || 'ascii'; 18 | } 19 | let width = encodingWidth(encoding); 20 | 21 | if (this.length != null) { 22 | length = utils.resolveLength(this.length, stream, parent); 23 | } else { 24 | let buffer; 25 | ({buffer, length, pos} = stream); 26 | 27 | while ((pos < length - width + 1) && 28 | (buffer[pos] !== 0x00 || 29 | (width === 2 && buffer[pos+1] !== 0x00) 30 | )) { 31 | pos += width; 32 | } 33 | 34 | length = pos - stream.pos; 35 | } 36 | 37 | 38 | const string = stream.readString(length, encoding); 39 | 40 | if ((this.length == null) && (stream.pos < stream.length)) { 41 | stream.pos+=width; 42 | } 43 | 44 | return string; 45 | } 46 | 47 | size(val, parent) { 48 | // Use the defined value if no value was given 49 | if (val === undefined || val === null) { 50 | return utils.resolveLength(this.length, null, parent); 51 | } 52 | 53 | let { encoding } = this; 54 | if (typeof encoding === 'function') { 55 | encoding = encoding.call(parent != null ? parent.val : undefined, parent != null ? parent.val : undefined) || 'ascii'; 56 | } 57 | 58 | if (encoding === 'utf16be') { 59 | encoding = 'utf16le'; 60 | } 61 | 62 | let size = byteLength(val, encoding); 63 | if (this.length instanceof NumberT) { 64 | size += this.length.size(); 65 | } 66 | 67 | if ((this.length == null)) { 68 | size += encodingWidth(encoding); 69 | } 70 | 71 | return size; 72 | } 73 | 74 | encode(stream, val, parent) { 75 | let { encoding } = this; 76 | if (typeof encoding === 'function') { 77 | encoding = encoding.call(parent != null ? parent.val : undefined, parent != null ? parent.val : undefined) || 'ascii'; 78 | } 79 | 80 | if (this.length instanceof NumberT) { 81 | this.length.encode(stream, byteLength(val, encoding)); 82 | } 83 | 84 | stream.writeString(val, encoding); 85 | 86 | if ((this.length == null)) { 87 | return encodingWidth(encoding) == 2 ? 88 | stream.writeUInt16LE(0x0000) : 89 | stream.writeUInt8(0x00); 90 | } 91 | } 92 | } 93 | 94 | function encodingWidth(encoding) { 95 | switch(encoding) { 96 | case 'ascii': 97 | case 'utf8': // utf8 is a byte-based encoding for zero-term string 98 | return 1; 99 | case 'utf16le': 100 | case 'utf16-le': 101 | case 'utf-16be': 102 | case 'utf-16le': 103 | case 'utf16be': 104 | case 'utf16-be': 105 | case 'ucs2': 106 | return 2; 107 | default: 108 | //TODO: assume all other encodings are 1-byters 109 | //throw new Error('Unknown encoding ' + encoding); 110 | return 1; 111 | } 112 | } 113 | 114 | function byteLength(string, encoding) { 115 | switch (encoding) { 116 | case 'ascii': 117 | return string.length; 118 | case 'utf8': 119 | let len = 0; 120 | for (let i = 0; i < string.length; i++) { 121 | let c = string.charCodeAt(i); 122 | 123 | if (c >= 0xd800 && c <= 0xdbff && i < string.length - 1) { 124 | let c2 = string.charCodeAt(++i); 125 | if ((c2 & 0xfc00) === 0xdc00) { 126 | c = ((c & 0x3ff) << 10) + (c2 & 0x3ff) + 0x10000; 127 | } else { 128 | // unmatched surrogate. 129 | i--; 130 | } 131 | } 132 | 133 | if ((c & 0xffffff80) === 0) { 134 | len++; 135 | } else if ((c & 0xfffff800) === 0) { 136 | len += 2; 137 | } else if ((c & 0xffff0000) === 0) { 138 | len += 3; 139 | } else if ((c & 0xffe00000) === 0) { 140 | len += 4; 141 | } 142 | } 143 | return len; 144 | case 'utf16le': 145 | case 'utf16-le': 146 | case 'utf16be': 147 | case 'utf16-be': 148 | case 'ucs2': 149 | return string.length * 2; 150 | default: 151 | throw new Error('Unknown encoding ' + encoding); 152 | } 153 | } 154 | 155 | export {StringT as String}; 156 | -------------------------------------------------------------------------------- /src/Struct.js: -------------------------------------------------------------------------------- 1 | import {Base} from './Base.js'; 2 | import * as utils from './utils.js'; 3 | 4 | export class Struct extends Base { 5 | constructor(fields = {}) { 6 | super(); 7 | this.fields = fields; 8 | } 9 | 10 | decode(stream, parent, length = 0) { 11 | const res = this._setup(stream, parent, length); 12 | this._parseFields(stream, res, this.fields); 13 | 14 | if (this.process != null) { 15 | this.process.call(res, stream); 16 | } 17 | return res; 18 | } 19 | 20 | _setup(stream, parent, length) { 21 | const res = {}; 22 | 23 | // define hidden properties 24 | Object.defineProperties(res, { 25 | parent: { value: parent }, 26 | _startOffset: { value: stream.pos }, 27 | _currentOffset: { value: 0, writable: true }, 28 | _length: { value: length } 29 | }); 30 | 31 | return res; 32 | } 33 | 34 | _parseFields(stream, res, fields) { 35 | for (let key in fields) { 36 | var val; 37 | const type = fields[key]; 38 | if (typeof type === 'function') { 39 | val = type.call(res, res); 40 | } else { 41 | val = type.decode(stream, res); 42 | } 43 | 44 | if (val !== undefined) { 45 | if (val instanceof utils.PropertyDescriptor) { 46 | Object.defineProperty(res, key, val); 47 | } else { 48 | res[key] = val; 49 | } 50 | } 51 | 52 | res._currentOffset = stream.pos - res._startOffset; 53 | } 54 | 55 | } 56 | 57 | size(val, parent, includePointers = true) { 58 | if (val == null) { val = {}; } 59 | const ctx = { 60 | parent, 61 | val, 62 | pointerSize: 0 63 | }; 64 | 65 | if (this.preEncode != null) { 66 | this.preEncode.call(val); 67 | } 68 | 69 | let size = 0; 70 | for (let key in this.fields) { 71 | const type = this.fields[key]; 72 | if (type.size != null) { 73 | size += type.size(val[key], ctx); 74 | } 75 | } 76 | 77 | if (includePointers) { 78 | size += ctx.pointerSize; 79 | } 80 | 81 | return size; 82 | } 83 | 84 | encode(stream, val, parent) { 85 | let type; 86 | if (this.preEncode != null) { 87 | this.preEncode.call(val, stream); 88 | } 89 | 90 | const ctx = { 91 | pointers: [], 92 | startOffset: stream.pos, 93 | parent, 94 | val, 95 | pointerSize: 0 96 | }; 97 | 98 | ctx.pointerOffset = stream.pos + this.size(val, ctx, false); 99 | 100 | for (let key in this.fields) { 101 | type = this.fields[key]; 102 | if (type.encode != null) { 103 | type.encode(stream, val[key], ctx); 104 | } 105 | } 106 | 107 | let i = 0; 108 | while (i < ctx.pointers.length) { 109 | const ptr = ctx.pointers[i++]; 110 | ptr.type.encode(stream, ptr.val, ptr.parent); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/VersionedStruct.js: -------------------------------------------------------------------------------- 1 | import {Struct} from './Struct.js'; 2 | 3 | const getPath = (object, pathArray) => { 4 | return pathArray.reduce((prevObj, key) => prevObj && prevObj[key], object); 5 | }; 6 | 7 | export class VersionedStruct extends Struct { 8 | constructor(type, versions = {}) { 9 | super(); 10 | this.type = type; 11 | this.versions = versions; 12 | if (typeof type === 'string') { 13 | this.versionPath = type.split('.'); 14 | } 15 | } 16 | 17 | decode(stream, parent, length = 0) { 18 | const res = this._setup(stream, parent, length); 19 | 20 | if (typeof this.type === 'string') { 21 | res.version = getPath(parent, this.versionPath); 22 | } else { 23 | res.version = this.type.decode(stream); 24 | } 25 | 26 | if (this.versions.header) { 27 | this._parseFields(stream, res, this.versions.header); 28 | } 29 | 30 | const fields = this.versions[res.version]; 31 | if ((fields == null)) { 32 | throw new Error(`Unknown version ${res.version}`); 33 | } 34 | 35 | if (fields instanceof VersionedStruct) { 36 | return fields.decode(stream, parent); 37 | } 38 | 39 | this._parseFields(stream, res, fields); 40 | 41 | if (this.process != null) { 42 | this.process.call(res, stream); 43 | } 44 | return res; 45 | } 46 | 47 | size(val, parent, includePointers = true) { 48 | let key, type; 49 | if (!val) { 50 | throw new Error('Not a fixed size'); 51 | } 52 | 53 | if (this.preEncode != null) { 54 | this.preEncode.call(val); 55 | } 56 | 57 | const ctx = { 58 | parent, 59 | val, 60 | pointerSize: 0 61 | }; 62 | 63 | let size = 0; 64 | if (typeof this.type !== 'string') { 65 | size += this.type.size(val.version, ctx); 66 | } 67 | 68 | if (this.versions.header) { 69 | for (key in this.versions.header) { 70 | type = this.versions.header[key]; 71 | if (type.size != null) { 72 | size += type.size(val[key], ctx); 73 | } 74 | } 75 | } 76 | 77 | const fields = this.versions[val.version]; 78 | if ((fields == null)) { 79 | throw new Error(`Unknown version ${val.version}`); 80 | } 81 | 82 | for (key in fields) { 83 | type = fields[key]; 84 | if (type.size != null) { 85 | size += type.size(val[key], ctx); 86 | } 87 | } 88 | 89 | if (includePointers) { 90 | size += ctx.pointerSize; 91 | } 92 | 93 | return size; 94 | } 95 | 96 | encode(stream, val, parent) { 97 | let key, type; 98 | if (this.preEncode != null) { 99 | this.preEncode.call(val, stream); 100 | } 101 | 102 | const ctx = { 103 | pointers: [], 104 | startOffset: stream.pos, 105 | parent, 106 | val, 107 | pointerSize: 0 108 | }; 109 | 110 | ctx.pointerOffset = stream.pos + this.size(val, ctx, false); 111 | 112 | if (typeof this.type !== 'string') { 113 | this.type.encode(stream, val.version); 114 | } 115 | 116 | if (this.versions.header) { 117 | for (key in this.versions.header) { 118 | type = this.versions.header[key]; 119 | if (type.encode != null) { 120 | type.encode(stream, val[key], ctx); 121 | } 122 | } 123 | } 124 | 125 | const fields = this.versions[val.version]; 126 | for (key in fields) { 127 | type = fields[key]; 128 | if (type.encode != null) { 129 | type.encode(stream, val[key], ctx); 130 | } 131 | } 132 | 133 | let i = 0; 134 | while (i < ctx.pointers.length) { 135 | const ptr = ctx.pointers[i++]; 136 | ptr.type.encode(stream, ptr.val, ptr.parent); 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import {Number as NumberT} from './Number.js'; 2 | 3 | export function resolveLength(length, stream, parent) { 4 | let res; 5 | if (typeof length === 'number') { 6 | res = length; 7 | 8 | } else if (typeof length === 'function') { 9 | res = length.call(parent, parent); 10 | 11 | } else if (parent && (typeof length === 'string')) { 12 | res = parent[length]; 13 | 14 | } else if (stream && length instanceof NumberT) { 15 | res = length.decode(stream); 16 | } 17 | 18 | if (isNaN(res)) { 19 | throw new Error('Not a fixed size'); 20 | } 21 | 22 | return res; 23 | }; 24 | 25 | export class PropertyDescriptor { 26 | constructor(opts = {}) { 27 | this.enumerable = true; 28 | this.configurable = true; 29 | 30 | for (let key in opts) { 31 | const val = opts[key]; 32 | this[key] = val; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/Array.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import {Array as ArrayT, Pointer, uint8, uint16, DecodeStream, EncodeStream} from 'restructure'; 3 | 4 | describe('Array', function() { 5 | describe('decode', function() { 6 | it('should decode fixed length', function() { 7 | const buffer = new Uint8Array([1, 2, 3, 4, 5]); 8 | const array = new ArrayT(uint8, 4); 9 | assert.deepEqual(array.fromBuffer(buffer), [1, 2, 3, 4]); 10 | }); 11 | 12 | it('should decode fixed amount of bytes', function() { 13 | const buffer = new Uint8Array([1, 2, 3, 4, 5]); 14 | const array = new ArrayT(uint16, 4, 'bytes'); 15 | assert.deepEqual(array.fromBuffer(buffer), [258, 772]); 16 | }); 17 | 18 | it('should decode length from parent key', function() { 19 | const stream = new DecodeStream(new Uint8Array([1, 2, 3, 4, 5])); 20 | const array = new ArrayT(uint8, 'len'); 21 | assert.deepEqual(array.decode(stream, {len: 4}), [1, 2, 3, 4]); 22 | }); 23 | 24 | it('should decode amount of bytes from parent key', function() { 25 | const stream = new DecodeStream(new Uint8Array([1, 2, 3, 4, 5])); 26 | const array = new ArrayT(uint16, 'len', 'bytes'); 27 | assert.deepEqual(array.decode(stream, {len: 4}), [258, 772]); 28 | }); 29 | 30 | it('should decode length as number before array', function() { 31 | const buffer = new Uint8Array([4, 1, 2, 3, 4, 5]); 32 | const array = new ArrayT(uint8, uint8); 33 | assert.deepEqual(array.fromBuffer(buffer), [1, 2, 3, 4]); 34 | }); 35 | 36 | it('should decode amount of bytes as number before array', function() { 37 | const buffer = new Uint8Array([4, 1, 2, 3, 4, 5]); 38 | const array = new ArrayT(uint16, uint8, 'bytes'); 39 | assert.deepEqual(array.fromBuffer(buffer), [258, 772]); 40 | }); 41 | 42 | it('should decode length from function', function() { 43 | const buffer = new Uint8Array([1, 2, 3, 4, 5]); 44 | const array = new ArrayT(uint8, function() { return 4; }); 45 | assert.deepEqual(array.fromBuffer(buffer), [1, 2, 3, 4]); 46 | }); 47 | 48 | it('should decode amount of bytes from function', function() { 49 | const buffer = new Uint8Array([1, 2, 3, 4, 5]); 50 | const array = new ArrayT(uint16, (function() { return 4; }), 'bytes'); 51 | assert.deepEqual(array.fromBuffer(buffer), [258, 772]); 52 | }); 53 | 54 | it('should decode to the end of the parent if no length is given', function() { 55 | const stream = new DecodeStream(new Uint8Array([1, 2, 3, 4, 5])); 56 | const array = new ArrayT(uint8); 57 | assert.deepEqual(array.decode(stream, {_length: 4, _startOffset: 0}), [1, 2, 3, 4]); 58 | }); 59 | 60 | it('should decode to the end of the stream if no parent and length is given', function() { 61 | const buffer = new Uint8Array([1, 2, 3, 4]); 62 | const array = new ArrayT(uint8); 63 | assert.deepEqual(array.fromBuffer(buffer), [1, 2, 3, 4]); 64 | }); 65 | }); 66 | 67 | describe('size', function() { 68 | it('should use array length', function() { 69 | const array = new ArrayT(uint8, 10); 70 | assert.equal(array.size([1, 2, 3, 4]), 4); 71 | }); 72 | 73 | it('should add size of length field before string', function() { 74 | const array = new ArrayT(uint8, uint8); 75 | assert.equal(array.size([1, 2, 3, 4]), 5); 76 | }); 77 | 78 | it('should use defined length if no value given', function() { 79 | const array = new ArrayT(uint8, 10); 80 | assert.equal(array.size(), 10); 81 | }); 82 | }); 83 | 84 | describe('encode', function() { 85 | it('should encode using array length', function() { 86 | const array = new ArrayT(uint8, 10); 87 | const buffer = array.toBuffer([1, 2, 3, 4]); 88 | assert.deepEqual(buffer, new Uint8Array([1, 2, 3, 4])); 89 | }); 90 | 91 | it('should encode length as number before array', function() { 92 | const array = new ArrayT(uint8, uint8); 93 | const buffer = array.toBuffer([1, 2, 3, 4]); 94 | assert.deepEqual(buffer, new Uint8Array([4, 1, 2, 3, 4])); 95 | }); 96 | 97 | it('should add pointers after array if length is encoded at start', function() { 98 | const array = new ArrayT(new Pointer(uint8, uint8), uint8); 99 | const buffer = array.toBuffer([1, 2, 3, 4]); 100 | assert.deepEqual(buffer, new Uint8Array([4, 5, 6, 7, 8, 1, 2, 3, 4])); 101 | }); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /test/Bitfield.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import {Bitfield, uint8, DecodeStream, EncodeStream} from 'restructure'; 3 | 4 | describe('Bitfield', function() { 5 | const bitfield = new Bitfield(uint8, ['Jack', 'Kack', 'Lack', 'Mack', 'Nack', 'Oack', 'Pack', 'Quack']); 6 | const JACK = 1 << 0; 7 | const KACK = 1 << 1; 8 | const LACK = 1 << 2; 9 | const MACK = 1 << 3; 10 | const NACK = 1 << 4; 11 | const OACK = 1 << 5; 12 | const PACK = 1 << 6; 13 | const QUACK = 1 << 7; 14 | 15 | it('should have the right size', () => assert.equal(bitfield.size(), 1)); 16 | 17 | it('should decode', function() { 18 | const buffer = new Uint8Array([JACK | MACK | PACK | NACK | QUACK]); 19 | assert.deepEqual( 20 | bitfield.fromBuffer(buffer), 21 | {Jack: true, Kack: false, Lack: false, Mack: true, Nack: true, Oack: false, Pack: true, Quack: true} 22 | ); 23 | }); 24 | 25 | it('should encode', function() { 26 | let buffer = bitfield.toBuffer({Jack: true, Kack: false, Lack: false, Mack: true, Nack: true, Oack: false, Pack: true, Quack: true}); 27 | assert.deepEqual(buffer, Buffer.from([JACK | MACK | PACK | NACK | QUACK])); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /test/Boolean.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import {Boolean, uint8, DecodeStream, EncodeStream} from 'restructure'; 3 | 4 | describe('Boolean', function() { 5 | describe('decode', function() { 6 | it('should decode 0 as false', function() { 7 | const buffer = new Uint8Array([0]); 8 | const boolean = new Boolean(uint8); 9 | assert.deepEqual(boolean.fromBuffer(buffer), false); 10 | }); 11 | 12 | it('should decode 1 as true', function() { 13 | const buffer = new Uint8Array([1]); 14 | const boolean = new Boolean(uint8); 15 | assert.deepEqual(boolean.fromBuffer(buffer), true); 16 | }); 17 | }); 18 | 19 | describe('size', () => 20 | it('should return given type size', function() { 21 | const boolean = new Boolean(uint8); 22 | assert.deepEqual(boolean.size(), 1); 23 | }) 24 | ); 25 | 26 | describe('encode', function() { 27 | it('should encode false as 0', function() { 28 | const boolean = new Boolean(uint8); 29 | const buffer = boolean.toBuffer(false); 30 | assert.deepEqual(buffer, Buffer.from([0])); 31 | }); 32 | 33 | it('should encode true as 1', function() { 34 | const boolean = new Boolean(uint8); 35 | const buffer = boolean.toBuffer(true); 36 | assert.deepEqual(buffer, Buffer.from([1])); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /test/Buffer.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import {Buffer as BufferT, uint8, DecodeStream, EncodeStream} from 'restructure'; 3 | 4 | describe('Buffer', function() { 5 | describe('decode', function() { 6 | it('should decode', function() { 7 | const buffer = new Uint8Array([0xab, 0xff]); 8 | const buf = new BufferT(2); 9 | assert.deepEqual(buf.fromBuffer(buffer), new Uint8Array([0xab, 0xff])); 10 | }); 11 | 12 | it('should decode with parent key length', function() { 13 | const stream = new DecodeStream(new Uint8Array([0xab, 0xff, 0x1f, 0xb6])); 14 | const buf = new BufferT('len'); 15 | assert.deepEqual(buf.decode(stream, {len: 3}), new Uint8Array([0xab, 0xff, 0x1f])); 16 | assert.deepEqual(buf.decode(stream, {len: 1}), new Uint8Array([0xb6])); 17 | }); 18 | }); 19 | 20 | describe('size', function() { 21 | it('should return size', function() { 22 | const buf = new BufferT(2); 23 | assert.equal(buf.size(new Uint8Array([0xab, 0xff])), 2); 24 | }); 25 | 26 | it('should use defined length if no value given', function() { 27 | const array = new BufferT(10); 28 | assert.equal(array.size(), 10); 29 | }); 30 | }); 31 | 32 | describe('encode', function() { 33 | it('should encode', function() { 34 | const buf = new BufferT(2); 35 | const buffer = buf.toBuffer(new Uint8Array([0xab, 0xff])); 36 | assert.deepEqual(buffer, new Uint8Array([0xab, 0xff])); 37 | }); 38 | 39 | it('should encode length before buffer', function() { 40 | const buf = new BufferT(uint8); 41 | const buffer = buf.toBuffer(new Uint8Array([0xab, 0xff])); 42 | assert.deepEqual(buffer, new Uint8Array([2, 0xab, 0xff])); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/DecodeStream.js: -------------------------------------------------------------------------------- 1 | import {DecodeStream} from 'restructure'; 2 | import assert from 'assert'; 3 | 4 | describe('DecodeStream', function() { 5 | it('should read a buffer', function() { 6 | const buf = new Uint8Array([1,2,3]); 7 | const stream = new DecodeStream(buf); 8 | assert.deepEqual(stream.readBuffer(buf.length), new Uint8Array([1,2,3])); 9 | }); 10 | 11 | it('should readUInt16BE', function() { 12 | const buf = new Uint8Array([0xab, 0xcd]); 13 | const stream = new DecodeStream(buf); 14 | assert.deepEqual(stream.readUInt16BE(), 0xabcd); 15 | }); 16 | 17 | it('should readUInt16LE', function() { 18 | const buf = new Uint8Array([0xab, 0xcd]); 19 | const stream = new DecodeStream(buf); 20 | assert.deepEqual(stream.readUInt16LE(), 0xcdab); 21 | }); 22 | 23 | it('should readUInt24BE', function() { 24 | const buf = new Uint8Array([0xab, 0xcd, 0xef]); 25 | const stream = new DecodeStream(buf); 26 | assert.deepEqual(stream.readUInt24BE(), 0xabcdef); 27 | }); 28 | 29 | it('should readUInt24LE', function() { 30 | const buf = new Uint8Array([0xab, 0xcd, 0xef]); 31 | const stream = new DecodeStream(buf); 32 | assert.deepEqual(stream.readUInt24LE(), 0xefcdab); 33 | }); 34 | 35 | it('should readInt24BE', function() { 36 | const buf = new Uint8Array([0xff, 0xab, 0x24]); 37 | const stream = new DecodeStream(buf); 38 | assert.deepEqual(stream.readInt24BE(), -21724); 39 | }); 40 | 41 | it('should readInt24LE', function() { 42 | const buf = new Uint8Array([0x24, 0xab, 0xff]); 43 | const stream = new DecodeStream(buf); 44 | assert.deepEqual(stream.readInt24LE(), -21724); 45 | }); 46 | 47 | describe('readString', function() { 48 | it('should decode ascii by default', function() { 49 | const buf = Buffer.from('some text', 'ascii'); 50 | const stream = new DecodeStream(buf); 51 | assert.equal(stream.readString(buf.length), 'some text'); 52 | }); 53 | 54 | it('should decode ascii', function() { 55 | const buf = Buffer.from('some text', 'ascii'); 56 | const stream = new DecodeStream(buf); 57 | assert.equal(stream.readString(buf.length, 'ascii'), 'some text'); 58 | }); 59 | 60 | it('should decode utf8', function() { 61 | const buf = Buffer.from('unicode! 👍', 'utf8'); 62 | const stream = new DecodeStream(buf); 63 | assert.equal(stream.readString(buf.length, 'utf8'), 'unicode! 👍'); 64 | }); 65 | 66 | it('should decode utf16le', function() { 67 | const buf = Buffer.from('unicode! 👍', 'utf16le'); 68 | const stream = new DecodeStream(buf); 69 | assert.equal(stream.readString(buf.length, 'utf16le'), 'unicode! 👍'); 70 | }); 71 | 72 | it('should decode ucs2', function() { 73 | const buf = Buffer.from('unicode! 👍', 'ucs2'); 74 | const stream = new DecodeStream(buf); 75 | assert.equal(stream.readString(buf.length, 'ucs2'), 'unicode! 👍'); 76 | }); 77 | 78 | it('should decode utf16be', function() { 79 | const buf = Buffer.from('unicode! 👍', 'utf16le'); 80 | for (let i = 0, end = buf.length - 1; i < end; i += 2) { 81 | const byte = buf[i]; 82 | buf[i] = buf[i + 1]; 83 | buf[i + 1] = byte; 84 | } 85 | 86 | const stream = new DecodeStream(buf); 87 | assert.equal(stream.readString(buf.length, 'utf16be'), 'unicode! 👍'); 88 | }); 89 | 90 | it('should decode macroman', function() { 91 | const buf = new Uint8Array([0x8a, 0x63, 0x63, 0x65, 0x6e, 0x74, 0x65, 0x64, 0x20, 0x63, 0x68, 0x87, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x73]); 92 | const stream = new DecodeStream(buf); 93 | assert.equal(stream.readString(buf.length, 'mac'), 'äccented cháracters'); 94 | }); 95 | 96 | it('should return a buffer for unsupported encodings', function() { 97 | const stream = new DecodeStream(new Uint8Array([1, 2, 3])); 98 | assert.deepEqual(stream.readString(3, 'unsupported'), new Uint8Array([1, 2, 3])); 99 | }); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /test/EncodeStream.js: -------------------------------------------------------------------------------- 1 | import {EncodeStream} from 'restructure'; 2 | import assert from 'assert'; 3 | 4 | describe('EncodeStream', function() { 5 | it('should write a buffer', function() { 6 | const stream = new EncodeStream(new Uint8Array(3)); 7 | stream.writeBuffer(new Uint8Array([1,2,3])); 8 | assert.deepEqual(stream.buffer, new Uint8Array([1,2,3])); 9 | }); 10 | 11 | it('should writeUInt16BE', function() { 12 | const stream = new EncodeStream(new Uint8Array(2)); 13 | stream.writeUInt16BE(0xabcd); 14 | assert.deepEqual(stream.buffer, new Uint8Array([0xab, 0xcd])); 15 | }); 16 | 17 | it('should writeUInt16LE', function() { 18 | const stream = new EncodeStream(new Uint8Array(2)); 19 | stream.writeUInt16LE(0xcdab); 20 | assert.deepEqual(stream.buffer, new Uint8Array([0xab, 0xcd])); 21 | }); 22 | 23 | it('should writeUInt24BE', function() { 24 | const stream = new EncodeStream(new Uint8Array(3)); 25 | stream.writeUInt24BE(0xabcdef); 26 | assert.deepEqual(stream.buffer, new Uint8Array([0xab, 0xcd, 0xef])); 27 | }); 28 | 29 | it('should writeUInt24LE', function() { 30 | const stream = new EncodeStream(new Uint8Array(3)); 31 | stream.writeUInt24LE(0xabcdef); 32 | assert.deepEqual(stream.buffer, new Uint8Array([0xef, 0xcd, 0xab])); 33 | }); 34 | 35 | it('should writeInt24BE', function() { 36 | const stream = new EncodeStream(new Uint8Array(6)); 37 | stream.writeInt24BE(-21724); 38 | stream.writeInt24BE(0xabcdef); 39 | assert.deepEqual(stream.buffer, new Uint8Array([0xff, 0xab, 0x24, 0xab, 0xcd, 0xef])); 40 | }); 41 | 42 | it('should writeInt24LE', function() { 43 | const stream = new EncodeStream(new Uint8Array(6)); 44 | stream.writeInt24LE(-21724); 45 | stream.writeInt24LE(0xabcdef); 46 | assert.deepEqual(stream.buffer, new Uint8Array([0x24, 0xab, 0xff, 0xef, 0xcd, 0xab])); 47 | }); 48 | 49 | it('should fill', function() { 50 | const stream = new EncodeStream(new Uint8Array(5)); 51 | stream.fill(10, 5); 52 | assert.deepEqual(stream.buffer, new Uint8Array([10, 10, 10, 10, 10])); 53 | }); 54 | 55 | describe('writeString', function() { 56 | it('should encode ascii by default', function() { 57 | const expected = Buffer.from('some text', 'ascii'); 58 | const stream = new EncodeStream(new Uint8Array(expected.length)); 59 | stream.writeString('some text'); 60 | assert.deepEqual(stream.buffer, expected); 61 | }); 62 | 63 | it('should encode ascii', function() { 64 | const expected = Buffer.from('some text', 'ascii'); 65 | const stream = new EncodeStream(new Uint8Array(expected.length)); 66 | stream.writeString('some text', 'ascii'); 67 | assert.deepEqual(stream.buffer, expected); 68 | }); 69 | 70 | it('should encode utf8', function() { 71 | const expected = Buffer.from('unicode! 👍', 'utf8'); 72 | const stream = new EncodeStream(new Uint8Array(expected.length)); 73 | stream.writeString('unicode! 👍', 'utf8'); 74 | assert.deepEqual(stream.buffer, expected); 75 | }); 76 | 77 | it('should encode utf16le', function() { 78 | const expected = Buffer.from('unicode! 👍', 'utf16le'); 79 | const stream = new EncodeStream(new Uint8Array(expected.length)); 80 | stream.writeString('unicode! 👍', 'utf16le'); 81 | assert.deepEqual(stream.buffer, expected); 82 | }); 83 | 84 | it('should encode ucs2', function() { 85 | const expected = Buffer.from('unicode! 👍', 'ucs2'); 86 | const stream = new EncodeStream(new Uint8Array(expected.length)); 87 | stream.writeString('unicode! 👍', 'ucs2'); 88 | assert.deepEqual(stream.buffer, expected); 89 | }); 90 | 91 | it('should encode utf16be', function() { 92 | const expected = Buffer.from('unicode! 👍', 'utf16le'); 93 | for (let i = 0, end = expected.length - 1; i < end; i += 2) { 94 | const byte = expected[i]; 95 | expected[i] = expected[i + 1]; 96 | expected[i + 1] = byte; 97 | } 98 | 99 | const stream = new EncodeStream(new Uint8Array(expected.length)); 100 | stream.writeString('unicode! 👍', 'utf16be'); 101 | assert.deepEqual(stream.buffer, expected); 102 | }); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /test/Enum.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import {Enum, uint8, DecodeStream, EncodeStream} from 'restructure'; 3 | 4 | describe('Enum', function() { 5 | const e = new Enum(uint8, ['foo', 'bar', 'baz']); 6 | it('should have the right size', () => assert.equal(e.size(), 1)); 7 | 8 | it('should decode', function() { 9 | const stream = new DecodeStream(new Uint8Array([1, 2, 0])); 10 | assert.equal(e.decode(stream), 'bar'); 11 | assert.equal(e.decode(stream), 'baz'); 12 | assert.equal(e.decode(stream), 'foo'); 13 | }); 14 | 15 | it('should encode', function() { 16 | assert.deepEqual(e.toBuffer('bar'), new Uint8Array([1])); 17 | assert.deepEqual(e.toBuffer('baz'), new Uint8Array([2])); 18 | assert.deepEqual(e.toBuffer('foo'), new Uint8Array([0])); 19 | }); 20 | 21 | it('should throw on unknown option', function() { 22 | return assert.throws(() => e.toBuffer('unknown')); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/LazyArray.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import {LazyArray, Pointer, uint8, uint16, DecodeStream, EncodeStream} from 'restructure'; 3 | 4 | describe('LazyArray', function() { 5 | describe('decode', function() { 6 | it('should decode items lazily', function() { 7 | const stream = new DecodeStream(new Uint8Array([1, 2, 3, 4, 5])); 8 | const array = new LazyArray(uint8, 4); 9 | 10 | const arr = array.decode(stream); 11 | assert(!(arr instanceof Array)); 12 | assert.equal(arr.length, 4); 13 | assert.equal(stream.pos, 4); 14 | 15 | assert.equal(arr.get(0), 1); 16 | assert.equal(arr.get(1), 2); 17 | assert.equal(arr.get(2), 3); 18 | assert.equal(arr.get(3), 4); 19 | 20 | assert.equal(arr.get(-1), null); 21 | assert.equal(arr.get(5), null); 22 | }); 23 | 24 | it('should be able to convert to an array', function() { 25 | const stream = new DecodeStream(new Uint8Array([1, 2, 3, 4, 5])); 26 | const array = new LazyArray(uint8, 4); 27 | 28 | const arr = array.decode(stream); 29 | assert.deepEqual(arr.toArray(), [1, 2, 3, 4]); 30 | }); 31 | 32 | it('should decode length as number before array', function() { 33 | const stream = new DecodeStream(new Uint8Array([4, 1, 2, 3, 4, 5])); 34 | const array = new LazyArray(uint8, uint8); 35 | const arr = array.decode(stream); 36 | 37 | assert.deepEqual(arr.toArray(), [1, 2, 3, 4]); 38 | }); 39 | }); 40 | 41 | describe('size', () => 42 | it('should work with LazyArrays', function() { 43 | const stream = new DecodeStream(new Uint8Array([1, 2, 3, 4, 5])); 44 | const array = new LazyArray(uint8, 4); 45 | const arr = array.decode(stream); 46 | 47 | assert.equal(array.size(arr), 4); 48 | }) 49 | ); 50 | 51 | describe('encode', () => 52 | it('should work with LazyArrays', function() { 53 | const array = new LazyArray(uint8, 4); 54 | const arr = array.fromBuffer(new Uint8Array([1, 2, 3, 4, 5])); 55 | const buffer = array.toBuffer(arr); 56 | assert.deepEqual(buffer, new Uint8Array([1, 2, 3, 4])); 57 | }) 58 | ); 59 | }); 60 | -------------------------------------------------------------------------------- /test/Number.js: -------------------------------------------------------------------------------- 1 | import { 2 | uint8, 3 | uint16, uint16be, uint16le, 4 | uint24, uint24be, uint24le, 5 | uint32, uint32be, uint32le, 6 | int8, 7 | int16, int16be, int16le, 8 | int24, int24be, int24le, 9 | int32, int32be, int32le, 10 | float, floatbe, floatle, 11 | double, doublebe, doublele, 12 | fixed16, fixed16be, fixed16le, 13 | fixed32, fixed32be, fixed32le, 14 | DecodeStream, EncodeStream 15 | } from 'restructure'; 16 | import assert from 'assert'; 17 | 18 | describe('Number', function() { 19 | describe('uint8', function() { 20 | it('should decode', function() { 21 | const stream = new DecodeStream(new Uint8Array([0xab, 0xff])); 22 | assert.equal(uint8.decode(stream), 0xab); 23 | assert.equal(uint8.decode(stream), 0xff); 24 | }); 25 | 26 | it('should have a size', () => assert.equal(uint8.size(), 1)); 27 | 28 | it('should encode', function() { 29 | assert.deepEqual(uint8.toBuffer(0xab), new Uint8Array([0xab])); 30 | assert.deepEqual(uint8.toBuffer(0xff), new Uint8Array([0xff])); 31 | }); 32 | }); 33 | 34 | describe('uint16', () => 35 | it('is an alias for uint16be', () => assert.deepEqual(uint16, uint16be)) 36 | ); 37 | 38 | describe('uint16be', function() { 39 | it('should decode', function() { 40 | const stream = new DecodeStream(new Uint8Array([0xab, 0xff])); 41 | assert.equal(uint16be.decode(stream), 0xabff); 42 | }); 43 | 44 | it('should have a size', () => assert.equal(uint16be.size(), 2)); 45 | 46 | it('should encode', function() { 47 | assert.deepEqual(uint16be.toBuffer(0xabff), new Uint8Array([0xab, 0xff])); 48 | }); 49 | }); 50 | 51 | describe('uint16le', function() { 52 | it('should decode', function() { 53 | const stream = new DecodeStream(new Uint8Array([0xff, 0xab])); 54 | assert.equal(uint16le.decode(stream), 0xabff); 55 | }); 56 | 57 | it('should have a size', () => assert.equal(uint16le.size(), 2)); 58 | 59 | it('should encode', function() { 60 | assert.deepEqual(uint16le.toBuffer(0xabff), new Uint8Array([0xff, 0xab])); 61 | }); 62 | }); 63 | 64 | describe('uint24', () => 65 | it('is an alias for uint24be', () => assert.deepEqual(uint24, uint24be)) 66 | ); 67 | 68 | describe('uint24be', function() { 69 | it('should decode', function() { 70 | const stream = new DecodeStream(new Uint8Array([0xff, 0xab, 0x24])); 71 | assert.equal(uint24be.decode(stream), 0xffab24); 72 | }); 73 | 74 | it('should have a size', () => assert.equal(uint24be.size(), 3)); 75 | 76 | it('should encode', function() { 77 | assert.deepEqual(uint24be.toBuffer(0xffab24), new Uint8Array([0xff, 0xab, 0x24])); 78 | }); 79 | }); 80 | 81 | describe('uint24le', function() { 82 | it('should decode', function() { 83 | const stream = new DecodeStream(new Uint8Array([0x24, 0xab, 0xff])); 84 | assert.equal(uint24le.decode(stream), 0xffab24); 85 | }); 86 | 87 | it('should have a size', () => assert.equal(uint24le.size(), 3)); 88 | 89 | it('should encode', function() { 90 | assert.deepEqual(uint24le.toBuffer(0xffab24), new Uint8Array([0x24, 0xab, 0xff])); 91 | }); 92 | }); 93 | 94 | describe('uint32', () => 95 | it('is an alias for uint32be', () => assert.deepEqual(uint32, uint32be)) 96 | ); 97 | 98 | describe('uint32be', function() { 99 | it('should decode', function() { 100 | const stream = new DecodeStream(new Uint8Array([0xff, 0xab, 0x24, 0xbf])); 101 | assert.equal(uint32be.decode(stream), 0xffab24bf); 102 | }); 103 | 104 | it('should have a size', () => assert.equal(uint32be.size(), 4)); 105 | 106 | it('should encode', function() { 107 | assert.deepEqual(uint32be.toBuffer(0xffab24bf), new Uint8Array([0xff, 0xab, 0x24, 0xbf])); 108 | }); 109 | }); 110 | 111 | describe('uint32le', function() { 112 | it('should decode', function() { 113 | const stream = new DecodeStream(new Uint8Array([0xbf, 0x24, 0xab, 0xff])); 114 | assert.equal(uint32le.decode(stream), 0xffab24bf); 115 | }); 116 | 117 | it('should have a size', () => assert.equal(uint32le.size(), 4)); 118 | 119 | it('should encode', function() { 120 | assert.deepEqual(uint32le.toBuffer(0xffab24bf), new Uint8Array([0xbf, 0x24, 0xab, 0xff])); 121 | }); 122 | }); 123 | 124 | describe('int8', function() { 125 | it('should decode', function() { 126 | const stream = new DecodeStream(new Uint8Array([0x7f, 0xff])); 127 | assert.equal(int8.decode(stream), 127); 128 | assert.equal(int8.decode(stream), -1); 129 | }); 130 | 131 | it('should have a size', () => assert.equal(int8.size(), 1)); 132 | 133 | it('should encode', function() { 134 | assert.deepEqual(uint8.toBuffer(127), new Uint8Array([0x7f])); 135 | assert.deepEqual(uint8.toBuffer(-1), new Uint8Array([0xff])); 136 | }); 137 | }); 138 | 139 | describe('int16', () => 140 | it('is an alias for int16be', () => assert.deepEqual(int16, int16be)) 141 | ); 142 | 143 | describe('int16be', function() { 144 | it('should decode', function() { 145 | const stream = new DecodeStream(new Uint8Array([0xff, 0xab])); 146 | assert.equal(int16be.decode(stream), -85); 147 | }); 148 | 149 | it('should have a size', () => assert.equal(int16be.size(), 2)); 150 | 151 | it('should encode', function() { 152 | assert.deepEqual(int16be.toBuffer(-85), new Uint8Array([0xff, 0xab])); 153 | }); 154 | }); 155 | 156 | describe('int16le', function() { 157 | it('should decode', function() { 158 | const stream = new DecodeStream(new Uint8Array([0xab, 0xff])); 159 | assert.equal(int16le.decode(stream), -85); 160 | }); 161 | 162 | it('should have a size', () => assert.equal(int16le.size(), 2)); 163 | 164 | it('should encode', function() { 165 | assert.deepEqual(int16le.toBuffer(-85), new Uint8Array([0xab, 0xff])); 166 | }); 167 | }); 168 | 169 | describe('int24', () => 170 | it('is an alias for int24be', () => assert.deepEqual(int24, int24be)) 171 | ); 172 | 173 | describe('int24be', function() { 174 | it('should decode', function() { 175 | const stream = new DecodeStream(new Uint8Array([0xff, 0xab, 0x24])); 176 | assert.equal(int24be.decode(stream), -21724); 177 | }); 178 | 179 | it('should have a size', () => assert.equal(int24be.size(), 3)); 180 | 181 | it('should encode', function() { 182 | assert.deepEqual(int24be.toBuffer(-21724), new Uint8Array([0xff, 0xab, 0x24])); 183 | }); 184 | }); 185 | 186 | describe('int24le', function() { 187 | it('should decode', function() { 188 | const stream = new DecodeStream(new Uint8Array([0x24, 0xab, 0xff])); 189 | assert.equal(int24le.decode(stream), -21724); 190 | }); 191 | 192 | it('should have a size', () => assert.equal(int24le.size(), 3)); 193 | 194 | it('should encode', function() { 195 | assert.deepEqual(int24le.toBuffer(-21724), new Uint8Array([0x24, 0xab, 0xff])); 196 | }); 197 | }); 198 | 199 | describe('int32', () => 200 | it('is an alias for int32be', () => assert.deepEqual(int32, int32be)) 201 | ); 202 | 203 | describe('int32be', function() { 204 | it('should decode', function() { 205 | const stream = new DecodeStream(new Uint8Array([0xff, 0xab, 0x24, 0xbf])); 206 | assert.equal(int32be.decode(stream), -5561153); 207 | }); 208 | 209 | it('should have a size', () => assert.equal(int32be.size(), 4)); 210 | 211 | it('should encode', function() { 212 | assert.deepEqual(int32be.toBuffer(-5561153), new Uint8Array([0xff, 0xab, 0x24, 0xbf])); 213 | }); 214 | }); 215 | 216 | describe('int32le', function() { 217 | it('should decode', function() { 218 | const stream = new DecodeStream(new Uint8Array([0xbf, 0x24, 0xab, 0xff])); 219 | assert.equal(int32le.decode(stream), -5561153); 220 | }); 221 | 222 | it('should have a size', () => assert.equal(int32le.size(), 4)); 223 | 224 | it('should encode', function() { 225 | assert.deepEqual(int32le.toBuffer(-5561153), new Uint8Array([0xbf, 0x24, 0xab, 0xff])); 226 | }); 227 | }); 228 | 229 | describe('float', () => 230 | it('is an alias for floatbe', () => assert.deepEqual(float, floatbe)) 231 | ); 232 | 233 | describe('floatbe', function() { 234 | it('should decode', function() { 235 | const value = floatbe.fromBuffer(new Uint8Array([0x43, 0x7a, 0x8c, 0xcd])); 236 | assert(value >= 250.55 - 0.005); 237 | assert(value <= 250.55 + 0.005); 238 | }); 239 | 240 | it('should have a size', () => assert.equal(floatbe.size(), 4)); 241 | 242 | it('should encode', function() { 243 | assert.deepEqual(floatbe.toBuffer(250.55), new Uint8Array([0x43, 0x7a, 0x8c, 0xcd])); 244 | }); 245 | }); 246 | 247 | describe('floatle', function() { 248 | it('should decode', function() { 249 | const value = floatle.fromBuffer(new Uint8Array([0xcd, 0x8c, 0x7a, 0x43])); 250 | assert(value >= 250.55 - 0.005); 251 | assert(value <= 250.55 + 0.005); 252 | }); 253 | 254 | it('should have a size', () => assert.equal(floatle.size(), 4)); 255 | 256 | it('should encode', function() { 257 | assert.deepEqual(floatle.toBuffer(250.55), new Uint8Array([0xcd, 0x8c, 0x7a, 0x43])); 258 | }); 259 | }); 260 | 261 | describe('double', () => 262 | it('is an alias for doublebe', () => assert.deepEqual(double, doublebe)) 263 | ); 264 | 265 | describe('doublebe', function() { 266 | it('should decode', function() { 267 | const value = doublebe.fromBuffer(new Uint8Array([0x40, 0x93, 0x4a, 0x3d, 0x70, 0xa3, 0xd7, 0x0a])); 268 | assert(value >= 1234.56 - 0.005); 269 | assert(value <= 1234.56 + 0.005); 270 | }); 271 | 272 | it('should have a size', () => assert.equal(doublebe.size(), 8)); 273 | 274 | it('should encode', function() { 275 | assert.deepEqual(doublebe.toBuffer(1234.56), new Uint8Array([0x40, 0x93, 0x4a, 0x3d, 0x70, 0xa3, 0xd7, 0x0a])); 276 | }); 277 | }); 278 | 279 | describe('doublele', function() { 280 | it('should decode', function() { 281 | const value = doublele.fromBuffer(new Uint8Array([0x0a, 0xd7, 0xa3, 0x70, 0x3d, 0x4a, 0x93, 0x40])); 282 | assert(value >= 1234.56 - 0.005); 283 | assert(value <= 1234.56 + 0.005); 284 | }); 285 | 286 | it('should have a size', () => assert.equal(doublele.size(), 8)); 287 | 288 | it('should encode', function() { 289 | assert.deepEqual(doublele.toBuffer(1234.56), new Uint8Array([0x0a, 0xd7, 0xa3, 0x70, 0x3d, 0x4a, 0x93, 0x40])); 290 | }); 291 | }); 292 | 293 | describe('fixed16', () => 294 | it('is an alias for fixed16be', () => assert.deepEqual(fixed16, fixed16be)) 295 | ); 296 | 297 | describe('fixed16be', function() { 298 | it('should decode', function() { 299 | const value = fixed16be.fromBuffer(new Uint8Array([0x19, 0x57])); 300 | assert(value >= 25.34 - 0.005); 301 | assert(value <= 25.34 + 0.005); 302 | }); 303 | 304 | it('should have a size', () => assert.equal(fixed16be.size(), 2)); 305 | 306 | it('should encode', function() { 307 | assert.deepEqual(fixed16be.toBuffer(25.34), new Uint8Array([0x19, 0x57])); 308 | }); 309 | }); 310 | 311 | describe('fixed16le', function() { 312 | it('should decode', function() { 313 | const value = fixed16le.fromBuffer(new Uint8Array([0x57, 0x19])); 314 | assert(value >= 25.34 - 0.005); 315 | assert(value <= 25.34 + 0.005); 316 | }); 317 | 318 | it('should have a size', () => assert.equal(fixed16le.size(), 2)); 319 | 320 | it('should encode', function() { 321 | assert.deepEqual(fixed16le.toBuffer(25.34), new Uint8Array([0x57, 0x19])); 322 | }); 323 | }); 324 | 325 | describe('fixed32', () => 326 | it('is an alias for fixed32be', () => assert.deepEqual(fixed32, fixed32be)) 327 | ); 328 | 329 | describe('fixed32be', function() { 330 | it('should decode', function() { 331 | const value = fixed32be.fromBuffer(new Uint8Array([0x00, 0xfa, 0x8c, 0xcc])); 332 | assert(value >= 250.55 - 0.005); 333 | assert(value <= 250.55 + 0.005); 334 | }); 335 | 336 | it('should have a size', () => assert.equal(fixed32be.size(), 4)); 337 | 338 | it('should encode', function() { 339 | assert.deepEqual(fixed32be.toBuffer(250.55), new Uint8Array([0x00, 0xfa, 0x8c, 0xcc])); 340 | }); 341 | }); 342 | 343 | describe('fixed32le', function() { 344 | it('should decode', function() { 345 | const value = fixed32le.fromBuffer(new Uint8Array([0xcc, 0x8c, 0xfa, 0x00])); 346 | assert(value >= 250.55 - 0.005); 347 | assert(value <= 250.55 + 0.005); 348 | }); 349 | 350 | it('should have a size', () => assert.equal(fixed32le.size(), 4)); 351 | 352 | it('should encode', function() { 353 | assert.deepEqual(fixed32le.toBuffer(250.55), new Uint8Array([0xcc, 0x8c, 0xfa, 0x00])); 354 | }); 355 | }); 356 | }); 357 | -------------------------------------------------------------------------------- /test/Optional.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import {Optional, uint8, DecodeStream, EncodeStream} from 'restructure'; 3 | 4 | describe('Optional', function() { 5 | describe('decode', function() { 6 | it('should not decode when condition is falsy', function() { 7 | const stream = new DecodeStream(new Uint8Array([0])); 8 | const optional = new Optional(uint8, false); 9 | assert.equal(optional.decode(stream), null); 10 | assert.equal(stream.pos, 0); 11 | }); 12 | 13 | it('should not decode when condition is a function and falsy', function() { 14 | const stream = new DecodeStream(new Uint8Array([0])); 15 | const optional = new Optional(uint8, function() { return false; }); 16 | assert.equal(optional.decode(stream), null); 17 | assert.equal(stream.pos, 0); 18 | }); 19 | 20 | it('should decode when condition is omitted', function() { 21 | const stream = new DecodeStream(new Uint8Array([0])); 22 | const optional = new Optional(uint8); 23 | assert(optional.decode(stream) != null); 24 | assert.equal(stream.pos, 1); 25 | }); 26 | 27 | it('should decode when condition is truthy', function() { 28 | const stream = new DecodeStream(new Uint8Array([0])); 29 | const optional = new Optional(uint8, true); 30 | assert(optional.decode(stream) != null); 31 | assert.equal(stream.pos, 1); 32 | }); 33 | 34 | it('should decode when condition is a function and truthy', function() { 35 | const stream = new DecodeStream(new Uint8Array([0])); 36 | const optional = new Optional(uint8, function() { return true; }); 37 | assert(optional.decode(stream) != null); 38 | assert.equal(stream.pos, 1); 39 | }); 40 | }); 41 | 42 | describe('size', function() { 43 | it('should return 0 when condition is falsy', function() { 44 | const stream = new DecodeStream(new Uint8Array([0])); 45 | const optional = new Optional(uint8, false); 46 | assert.equal(optional.size(), 0); 47 | }); 48 | 49 | it('should return 0 when condition is a function and falsy', function() { 50 | const stream = new DecodeStream(new Uint8Array([0])); 51 | const optional = new Optional(uint8, function() { return false; }); 52 | assert.equal(optional.size(), 0); 53 | }); 54 | 55 | it('should return given type size when condition is omitted', function() { 56 | const stream = new DecodeStream(new Uint8Array([0])); 57 | const optional = new Optional(uint8); 58 | assert.equal(optional.size(), 1); 59 | }); 60 | 61 | it('should return given type size when condition is truthy', function() { 62 | const stream = new DecodeStream(new Uint8Array([0])); 63 | const optional = new Optional(uint8, true); 64 | assert.equal(optional.size(), 1); 65 | }); 66 | 67 | it('should return given type size when condition is a function and truthy', function() { 68 | const stream = new DecodeStream(new Uint8Array([0])); 69 | const optional = new Optional(uint8, function() { return true; }); 70 | assert.equal(optional.size(), 1); 71 | }); 72 | }); 73 | 74 | describe('encode', function() { 75 | it('should not encode when condition is falsy', function() { 76 | const optional = new Optional(uint8, false); 77 | assert.deepEqual(optional.toBuffer(128), new Uint8Array(0)); 78 | }); 79 | 80 | it('should not encode when condition is a function and falsy', function() { 81 | const optional = new Optional(uint8, function() { return false; }); 82 | assert.deepEqual(optional.toBuffer(128), new Uint8Array(0)); 83 | }); 84 | 85 | it('should encode when condition is omitted', function() { 86 | const optional = new Optional(uint8); 87 | assert.deepEqual(optional.toBuffer(128), new Uint8Array([128])); 88 | }); 89 | 90 | it('should encode when condition is truthy', function() { 91 | const optional = new Optional(uint8, true); 92 | assert.deepEqual(optional.toBuffer(128), new Uint8Array([128])); 93 | }); 94 | 95 | it('should encode when condition is a function and truthy', function() { 96 | const optional = new Optional(uint8, function() { return true; }); 97 | assert.deepEqual(optional.toBuffer(128), new Uint8Array([128])); 98 | }); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /test/Pointer.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import {Pointer, VoidPointer, uint8, DecodeStream, EncodeStream, Struct} from 'restructure'; 3 | 4 | describe('Pointer', function() { 5 | describe('decode', function() { 6 | it('should handle null pointers', function() { 7 | const stream = new DecodeStream(new Uint8Array([0])); 8 | const pointer = new Pointer(uint8, uint8); 9 | return assert.equal(pointer.decode(stream, {_startOffset: 50}), null); 10 | }); 11 | 12 | it('should use local offsets from start of parent by default', function() { 13 | const stream = new DecodeStream(new Uint8Array([1, 53])); 14 | const pointer = new Pointer(uint8, uint8); 15 | assert.equal(pointer.decode(stream, {_startOffset: 0}), 53); 16 | }); 17 | 18 | it('should support immediate offsets', function() { 19 | const stream = new DecodeStream(new Uint8Array([1, 53])); 20 | const pointer = new Pointer(uint8, uint8, {type: 'immediate'}); 21 | assert.equal(pointer.decode(stream), 53); 22 | }); 23 | 24 | it('should support offsets relative to the parent', function() { 25 | const stream = new DecodeStream(new Uint8Array([0, 0, 1, 53])); 26 | stream.pos = 2; 27 | const pointer = new Pointer(uint8, uint8, {type: 'parent'}); 28 | assert.equal(pointer.decode(stream, {parent: {_startOffset: 2}}), 53); 29 | }); 30 | 31 | it('should support global offsets', function() { 32 | const stream = new DecodeStream(new Uint8Array([1, 2, 4, 0, 0, 0, 53])); 33 | const pointer = new Pointer(uint8, uint8, {type: 'global'}); 34 | stream.pos = 2; 35 | assert.equal(pointer.decode(stream, {parent: {parent: {_startOffset: 2}}}), 53); 36 | }); 37 | 38 | it('should support offsets relative to a property on the parent', function() { 39 | const stream = new DecodeStream(new Uint8Array([1, 0, 0, 0, 0, 53])); 40 | const pointer = new Pointer(uint8, uint8, {relativeTo: ctx => ctx.parent.ptr}); 41 | assert.equal(pointer.decode(stream, {_startOffset: 0, parent: {ptr: 4}}), 53); 42 | }); 43 | 44 | it('should throw when passing a non function relativeTo option', function() { 45 | return assert.throws(() => new Pointer(uint8, uint8, {relativeTo: 'parent.ptr'})); 46 | }); 47 | 48 | it('should support returning pointer if there is no decode type', function() { 49 | const stream = new DecodeStream(new Uint8Array([4])); 50 | const pointer = new Pointer(uint8, 'void'); 51 | assert.equal(pointer.decode(stream, {_startOffset: 0}), 4); 52 | }); 53 | 54 | it('should support decoding pointers lazily', function() { 55 | const stream = new DecodeStream(new Uint8Array([1, 53])); 56 | const struct = new Struct({ 57 | ptr: new Pointer(uint8, uint8, {lazy: true})}); 58 | 59 | const res = struct.decode(stream); 60 | assert.equal(typeof Object.getOwnPropertyDescriptor(res, 'ptr').get, 'function'); 61 | assert.equal(Object.getOwnPropertyDescriptor(res, 'ptr').enumerable, true); 62 | assert.equal(res.ptr, 53); 63 | }); 64 | }); 65 | 66 | describe('size', function() { 67 | it('should add to local pointerSize', function() { 68 | const pointer = new Pointer(uint8, uint8); 69 | const ctx = {pointerSize: 0}; 70 | assert.equal(pointer.size(10, ctx), 1); 71 | assert.equal(ctx.pointerSize, 1); 72 | }); 73 | 74 | it('should add to immediate pointerSize', function() { 75 | const pointer = new Pointer(uint8, uint8, {type: 'immediate'}); 76 | const ctx = {pointerSize: 0}; 77 | assert.equal(pointer.size(10, ctx), 1); 78 | assert.equal(ctx.pointerSize, 1); 79 | }); 80 | 81 | it('should add to parent pointerSize', function() { 82 | const pointer = new Pointer(uint8, uint8, {type: 'parent'}); 83 | const ctx = {parent: {pointerSize: 0}}; 84 | assert.equal(pointer.size(10, ctx), 1); 85 | assert.equal(ctx.parent.pointerSize, 1); 86 | }); 87 | 88 | it('should add to global pointerSize', function() { 89 | const pointer = new Pointer(uint8, uint8, {type: 'global'}); 90 | const ctx = {parent: {parent: {parent: {pointerSize: 0}}}}; 91 | assert.equal(pointer.size(10, ctx), 1); 92 | assert.equal(ctx.parent.parent.parent.pointerSize, 1); 93 | }); 94 | 95 | it('should handle void pointers', function() { 96 | const pointer = new Pointer(uint8, 'void'); 97 | const ctx = {pointerSize: 0}; 98 | assert.equal(pointer.size(new VoidPointer(uint8, 50), ctx), 1); 99 | assert.equal(ctx.pointerSize, 1); 100 | }); 101 | 102 | it('should throw if no type and not a void pointer', function() { 103 | const pointer = new Pointer(uint8, 'void'); 104 | const ctx = {pointerSize: 0}; 105 | assert.throws(() => pointer.size(30, ctx)); 106 | }); 107 | 108 | it('should return a fixed size without a value', function() { 109 | const pointer = new Pointer(uint8, uint8); 110 | assert.equal(pointer.size(), 1); 111 | }); 112 | }); 113 | 114 | describe('encode', function() { 115 | it('should handle null pointers', function() { 116 | const ptr = new Pointer(uint8, uint8); 117 | const ctx = { 118 | pointerSize: 0, 119 | startOffset: 0, 120 | pointerOffset: 0, 121 | pointers: [] 122 | }; 123 | 124 | const stream = new EncodeStream(new Uint8Array(ptr.size(null))); 125 | ptr.encode(stream, null, ctx); 126 | assert.equal(ctx.pointerSize, 0); 127 | 128 | assert.deepEqual(stream.buffer, new Uint8Array([0])); 129 | }); 130 | 131 | it('should handle local offsets', function() { 132 | const ptr = new Pointer(uint8, uint8); 133 | const ctx = { 134 | pointerSize: 0, 135 | startOffset: 0, 136 | pointerOffset: 1, 137 | pointers: [] 138 | }; 139 | 140 | const stream = new EncodeStream(new Uint8Array(ptr.size(10))); 141 | ptr.encode(stream, 10, ctx); 142 | assert.equal(ctx.pointerOffset, 2); 143 | assert.deepEqual(ctx.pointers, [ 144 | { type: uint8, val: 10, parent: ctx } 145 | ]); 146 | 147 | assert.deepEqual(stream.buffer, new Uint8Array([1])); 148 | }); 149 | 150 | it('should handle immediate offsets', function() { 151 | const ptr = new Pointer(uint8, uint8, {type: 'immediate'}); 152 | const ctx = { 153 | pointerSize: 0, 154 | startOffset: 0, 155 | pointerOffset: 1, 156 | pointers: [] 157 | }; 158 | 159 | const stream = new EncodeStream(new Uint8Array(ptr.size(10))); 160 | ptr.encode(stream, 10, ctx); 161 | assert.equal(ctx.pointerOffset, 2); 162 | assert.deepEqual(ctx.pointers, [ 163 | { type: uint8, val: 10, parent: ctx } 164 | ]); 165 | 166 | assert.deepEqual(stream.buffer, new Uint8Array([0])); 167 | }); 168 | 169 | it('should handle immediate offsets', function() { 170 | const ptr = new Pointer(uint8, uint8, {type: 'immediate'}); 171 | const ctx = { 172 | pointerSize: 0, 173 | startOffset: 0, 174 | pointerOffset: 1, 175 | pointers: [] 176 | }; 177 | 178 | const stream = new EncodeStream(new Uint8Array(ptr.size(10))); 179 | ptr.encode(stream, 10, ctx); 180 | assert.equal(ctx.pointerOffset, 2); 181 | assert.deepEqual(ctx.pointers, [ 182 | { type: uint8, val: 10, parent: ctx } 183 | ]); 184 | 185 | assert.deepEqual(stream.buffer, new Uint8Array([0])); 186 | }); 187 | 188 | it('should handle offsets relative to parent', function() { 189 | const ptr = new Pointer(uint8, uint8, {type: 'parent'}); 190 | const ctx = { 191 | parent: { 192 | pointerSize: 0, 193 | startOffset: 3, 194 | pointerOffset: 5, 195 | pointers: [] 196 | } 197 | }; 198 | 199 | const stream = new EncodeStream(new Uint8Array(ptr.size(10, {parent: {...ctx.parent}}))); 200 | ptr.encode(stream, 10, ctx); 201 | assert.equal(ctx.parent.pointerOffset, 6); 202 | assert.deepEqual(ctx.parent.pointers, [ 203 | { type: uint8, val: 10, parent: ctx } 204 | ]); 205 | 206 | assert.deepEqual(stream.buffer, new Uint8Array([2])); 207 | }); 208 | 209 | it('should handle global offsets', function() { 210 | const ptr = new Pointer(uint8, uint8, {type: 'global'}); 211 | const ctx = { 212 | parent: { 213 | parent: { 214 | parent: { 215 | pointerSize: 0, 216 | startOffset: 3, 217 | pointerOffset: 5, 218 | pointers: [] 219 | } 220 | } 221 | } 222 | }; 223 | 224 | const stream = new EncodeStream(new Uint8Array(ptr.size(10, JSON.parse(JSON.stringify(ctx))))); 225 | ptr.encode(stream, 10, ctx); 226 | assert.equal(ctx.parent.parent.parent.pointerOffset, 6); 227 | assert.deepEqual(ctx.parent.parent.parent.pointers, [ 228 | { type: uint8, val: 10, parent: ctx } 229 | ]); 230 | 231 | assert.deepEqual(stream.buffer, new Uint8Array([5])); 232 | }); 233 | 234 | it('should support offsets relative to a property on the parent', function() { 235 | const ptr = new Pointer(uint8, uint8, {relativeTo: ctx => ctx.ptr}); 236 | const ctx = { 237 | pointerSize: 0, 238 | startOffset: 0, 239 | pointerOffset: 10, 240 | pointers: [], 241 | val: { 242 | ptr: 4 243 | } 244 | }; 245 | 246 | const stream = new EncodeStream(new Uint8Array(ptr.size(10, {...ctx}))); 247 | ptr.encode(stream, 10, ctx); 248 | assert.equal(ctx.pointerOffset, 11); 249 | assert.deepEqual(ctx.pointers, [ 250 | { type: uint8, val: 10, parent: ctx } 251 | ]); 252 | 253 | assert.deepEqual(stream.buffer, new Uint8Array([6])); 254 | }); 255 | 256 | it('should support void pointers', function() { 257 | const ptr = new Pointer(uint8, 'void'); 258 | const ctx = { 259 | pointerSize: 0, 260 | startOffset: 0, 261 | pointerOffset: 1, 262 | pointers: [] 263 | }; 264 | 265 | const val = new VoidPointer(uint8, 55); 266 | const stream = new EncodeStream(new Uint8Array(ptr.size(val, {...ctx}))); 267 | ptr.encode(stream, val, ctx); 268 | assert.equal(ctx.pointerOffset, 2); 269 | assert.deepEqual(ctx.pointers, [ 270 | { type: uint8, val: 55, parent: ctx } 271 | ]); 272 | 273 | assert.deepEqual(stream.buffer, new Uint8Array([1])); 274 | }); 275 | 276 | it('should throw if not a void pointer instance', function() { 277 | const ptr = new Pointer(uint8, 'void'); 278 | const ctx = { 279 | pointerSize: 0, 280 | startOffset: 0, 281 | pointerOffset: 1, 282 | pointers: [] 283 | }; 284 | 285 | const stream = new EncodeStream(new Uint8Array(0)); 286 | assert.throws(() => ptr.encode(stream, 44, ctx)); 287 | }); 288 | }); 289 | }); 290 | -------------------------------------------------------------------------------- /test/Reserved.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import {Reserved, uint8, uint16, DecodeStream, EncodeStream} from 'restructure'; 3 | 4 | describe('Reserved', function() { 5 | it('should have a default count of 1', function() { 6 | const reserved = new Reserved(uint8); 7 | assert.equal(reserved.size(), 1); 8 | }); 9 | 10 | it('should allow custom counts and types', function() { 11 | const reserved = new Reserved(uint16, 10); 12 | assert.equal(reserved.size(), 20); 13 | }); 14 | 15 | it('should decode', function() { 16 | const stream = new DecodeStream(new Uint8Array([0, 0])); 17 | const reserved = new Reserved(uint16); 18 | assert.equal(reserved.decode(stream), null); 19 | assert.equal(stream.pos, 2); 20 | }); 21 | 22 | it('should encode', function() { 23 | const reserved = new Reserved(uint16); 24 | assert.deepEqual(reserved.toBuffer(), new Uint8Array([0, 0])); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /test/String.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import {String as StringT, uint16le, uint8, DecodeStream, Struct} from 'restructure'; 3 | 4 | describe('String', function() { 5 | describe('decode', function() { 6 | it('should decode fixed length', function() { 7 | const string = new StringT(7); 8 | assert.equal(string.fromBuffer(Buffer.from('testing')), 'testing'); 9 | }); 10 | 11 | it('should decode length from parent key', function() { 12 | const stream = new DecodeStream(Buffer.from('testing')); 13 | const string = new StringT('len'); 14 | assert.equal(string.decode(stream, {len: 7}), 'testing'); 15 | }); 16 | 17 | it('should decode length as number before string', function() { 18 | const string = new StringT(uint8); 19 | assert.equal(string.fromBuffer(Buffer.from('\x07testing')), 'testing'); 20 | }); 21 | 22 | it('should decode utf8', function() { 23 | const string = new StringT(4, 'utf8'); 24 | assert.equal(string.fromBuffer(Buffer.from('🍻')), '🍻'); 25 | }); 26 | 27 | it('should decode encoding computed from function', function() { 28 | const string = new StringT(4, function() { return 'utf8'; }); 29 | assert.equal(string.fromBuffer(Buffer.from('🍻')), '🍻'); 30 | }); 31 | 32 | it('should decode null-terminated string and read past terminator', function() { 33 | const stream = new DecodeStream(Buffer.from('🍻\x00')); 34 | const string = new StringT(null, 'utf8'); 35 | assert.equal(string.decode(stream), '🍻'); 36 | assert.equal(stream.pos, 5); 37 | }); 38 | 39 | it('should decode remainder of buffer when null-byte missing', function() { 40 | const string = new StringT(null, 'utf8'); 41 | assert.equal(string.fromBuffer(Buffer.from('🍻')), '🍻'); 42 | }); 43 | 44 | it('should decode two-byte null-terminated string for utf16le', function() { 45 | const stream = new DecodeStream(Buffer.from('🍻\x00', 'utf16le')); 46 | const string = new StringT(null, 'utf16le'); 47 | assert.equal(string.decode(stream), '🍻'); 48 | assert.equal(stream.pos, 6); 49 | }); 50 | 51 | it('should decode remainder of buffer when null-byte missing, utf16le', function() { 52 | const string = new StringT(null, 'utf16le'); 53 | assert.equal(string.fromBuffer(Buffer.from('🍻', 'utf16le')), '🍻'); 54 | }); 55 | 56 | it('should decode x-mac-roman', function() { 57 | const string = new StringT(null, 'x-mac-roman'); 58 | const buf = new Uint8Array([0x8a, 0x63, 0x63, 0x65, 0x6e, 0x74, 0x65, 0x64, 0x20, 0x63, 0x68, 0x87, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x73]); 59 | assert.equal(string.fromBuffer(buf), 'äccented cháracters'); 60 | }) 61 | }); 62 | 63 | describe('size', function() { 64 | it('should use string length', function() { 65 | const string = new StringT(7); 66 | assert.equal(string.size('testing'), 7); 67 | }); 68 | 69 | it('should use correct encoding', function() { 70 | const string = new StringT(10, 'utf8'); 71 | assert.equal(string.size('🍻'), 4); 72 | }); 73 | 74 | it('should use encoding from function', function() { 75 | const string = new StringT(10, function() { return 'utf8'; }); 76 | assert.equal(string.size('🍻'), 4); 77 | }); 78 | 79 | it('should add size of length field before string', function() { 80 | const string = new StringT(uint8, 'utf8'); 81 | assert.equal(string.size('🍻'), 5); 82 | }); 83 | 84 | it('should work with utf16be encoding', function() { 85 | const string = new StringT(10, 'utf16be'); 86 | assert.equal(string.size('🍻'), 4); 87 | }); 88 | 89 | it('should take null-byte into account', function() { 90 | const string = new StringT(null, 'utf8'); 91 | assert.equal(string.size('🍻'), 5); 92 | }); 93 | 94 | it('should take null-byte into account, utf16le', function() { 95 | const string = new StringT(null, 'utf16le'); 96 | assert.equal(string.size('🍻'), 6); 97 | }); 98 | 99 | it('should use defined length if no value given', function() { 100 | const array = new StringT(10); 101 | assert.equal(array.size(), 10); 102 | }); 103 | }); 104 | 105 | describe('encode', function() { 106 | it('should encode using string length', function() { 107 | const string = new StringT(7); 108 | assert.deepEqual(string.toBuffer('testing'), Buffer.from('testing')); 109 | }); 110 | 111 | it('should encode length as number before string', function() { 112 | const string = new StringT(uint8); 113 | assert.deepEqual(string.toBuffer('testing'), Buffer.from('\x07testing')); 114 | }); 115 | 116 | it('should encode length as number before string utf8', function() { 117 | const string = new StringT(uint8, 'utf8'); 118 | assert.deepEqual(string.toBuffer('testing 😜'), Buffer.from('\x0ctesting 😜', 'utf8')); 119 | }); 120 | 121 | it('should encode utf8', function() { 122 | const string = new StringT(4, 'utf8'); 123 | assert.deepEqual(string.toBuffer('🍻'), Buffer.from('🍻')); 124 | }); 125 | 126 | it('should encode encoding computed from function', function() { 127 | const string = new StringT(4, function() { return 'utf8'; }); 128 | assert.deepEqual(string.toBuffer('🍻'), Buffer.from('🍻')); 129 | }); 130 | 131 | it('should encode null-terminated string', function() { 132 | const string = new StringT(null, 'utf8'); 133 | assert.deepEqual(string.toBuffer('🍻'), Buffer.from('🍻\x00')); 134 | }); 135 | 136 | it('should encode using string length, utf16le', function() { 137 | const string = new StringT(16, 'utf16le'); 138 | assert.deepEqual(string.toBuffer('testing'), Buffer.from('testing', 'utf16le')); 139 | }); 140 | 141 | it('should encode length as number before string utf16le', function() { 142 | const string = new StringT(uint16le, 'utf16le'); 143 | assert.deepEqual(string.toBuffer('testing 😜'), Buffer.from('\u0014testing 😜', 'utf16le')); 144 | }); 145 | 146 | it('should encode two-byte null-terminated string for UTF-16', function() { 147 | const string = new StringT(null, 'utf16le'); 148 | assert.deepEqual(string.toBuffer('🍻'), Buffer.from('🍻\x00', 'utf16le')); 149 | }); 150 | }); 151 | }); 152 | -------------------------------------------------------------------------------- /test/Struct.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import {Struct, String as StringT, Pointer, uint8, DecodeStream, EncodeStream} from 'restructure'; 3 | 4 | describe('Struct', function() { 5 | describe('decode', function() { 6 | it('should decode into an object', function() { 7 | const struct = new Struct({ 8 | name: new StringT(uint8), 9 | age: uint8 10 | }); 11 | 12 | assert.deepEqual(struct.fromBuffer(Buffer.from('\x05devon\x15')), { 13 | name: 'devon', 14 | age: 21 15 | }); 16 | }); 17 | 18 | it('should support process hook', function() { 19 | const struct = new Struct({ 20 | name: new StringT(uint8), 21 | age: uint8 22 | }); 23 | 24 | struct.process = function() { 25 | return this.canDrink = this.age >= 21; 26 | }; 27 | 28 | assert.deepEqual(struct.fromBuffer(Buffer.from('\x05devon\x20')), { 29 | name: 'devon', 30 | age: 32, 31 | canDrink: true 32 | }); 33 | }); 34 | 35 | it('should support function keys', function() { 36 | const struct = new Struct({ 37 | name: new StringT(uint8), 38 | age: uint8, 39 | canDrink() { return this.age >= 21; } 40 | }); 41 | 42 | assert.deepEqual(struct.fromBuffer(Buffer.from('\x05devon\x20')), { 43 | name: 'devon', 44 | age: 32, 45 | canDrink: true 46 | }); 47 | }); 48 | }); 49 | 50 | describe('size', function() { 51 | it('should compute the correct size', function() { 52 | const struct = new Struct({ 53 | name: new StringT(uint8), 54 | age: uint8 55 | }); 56 | 57 | assert.equal(struct.size({name: 'devon', age: 21}), 7); 58 | }); 59 | 60 | it('should compute the correct size with pointers', function() { 61 | const struct = new Struct({ 62 | name: new StringT(uint8), 63 | age: uint8, 64 | ptr: new Pointer(uint8, new StringT(uint8)) 65 | }); 66 | 67 | const size = struct.size({ 68 | name: 'devon', 69 | age: 21, 70 | ptr: 'hello' 71 | }); 72 | 73 | assert.equal(size, 14); 74 | }); 75 | 76 | it('should get the correct size when no value is given', function() { 77 | const struct = new Struct({ 78 | name: new StringT(4), 79 | age: uint8 80 | }); 81 | 82 | assert.equal(struct.size(), 5); 83 | }); 84 | 85 | it('should throw when getting non-fixed length size and no value is given', function() { 86 | const struct = new Struct({ 87 | name: new StringT(uint8), 88 | age: uint8 89 | }); 90 | 91 | assert.throws(() => struct.size(), /not a fixed size/i); 92 | }); 93 | }); 94 | 95 | describe('encode', function() { 96 | it('should encode objects to buffers', function() { 97 | const struct = new Struct({ 98 | name: new StringT(uint8), 99 | age: uint8 100 | }); 101 | 102 | const buf = struct.toBuffer({ 103 | name: 'devon', 104 | age: 21 105 | }); 106 | 107 | assert.deepEqual(buf, Buffer.from('\x05devon\x15')); 108 | }); 109 | 110 | it('should support preEncode hook', function() { 111 | const struct = new Struct({ 112 | nameLength: uint8, 113 | name: new StringT('nameLength'), 114 | age: uint8 115 | }); 116 | 117 | struct.preEncode = function() { 118 | return this.nameLength = this.name.length; 119 | }; 120 | 121 | const buf = struct.toBuffer({ 122 | name: 'devon', 123 | age: 21 124 | }); 125 | 126 | assert.deepEqual(buf, Buffer.from('\x05devon\x15')); 127 | }); 128 | 129 | it('should encode pointer data after structure', function() { 130 | const struct = new Struct({ 131 | name: new StringT(uint8), 132 | age: uint8, 133 | ptr: new Pointer(uint8, new StringT(uint8)) 134 | }); 135 | 136 | const buf = struct.toBuffer({ 137 | name: 'devon', 138 | age: 21, 139 | ptr: 'hello' 140 | }); 141 | 142 | assert.deepEqual(buf, Buffer.from('\x05devon\x15\x08\x05hello')); 143 | }); 144 | }); 145 | }); 146 | 147 | -------------------------------------------------------------------------------- /test/VersionedStruct.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import {VersionedStruct, String as StringT, Pointer, uint8, DecodeStream, EncodeStream} from 'restructure'; 3 | 4 | describe('VersionedStruct', function() { 5 | describe('decode', function() { 6 | it('should get version from number type', function() { 7 | const struct = new VersionedStruct(uint8, { 8 | 0: { 9 | name: new StringT(uint8, 'ascii'), 10 | age: uint8 11 | }, 12 | 1: { 13 | name: new StringT(uint8, 'utf8'), 14 | age: uint8, 15 | gender: uint8 16 | } 17 | } 18 | ); 19 | 20 | let stream = new DecodeStream(Buffer.from('\x00\x05devon\x15')); 21 | assert.deepEqual(struct.decode(stream), { 22 | version: 0, 23 | name: 'devon', 24 | age: 21 25 | }); 26 | 27 | stream = new DecodeStream(Buffer.from('\x01\x0adevon 👍\x15\x00', 'utf8')); 28 | assert.deepEqual(struct.decode(stream), { 29 | version: 1, 30 | name: 'devon 👍', 31 | age: 21, 32 | gender: 0 33 | }); 34 | }); 35 | 36 | it('should throw for unknown version', function() { 37 | const struct = new VersionedStruct(uint8, { 38 | 0: { 39 | name: new StringT(uint8, 'ascii'), 40 | age: uint8 41 | }, 42 | 1: { 43 | name: new StringT(uint8, 'utf8'), 44 | age: uint8, 45 | gender: uint8 46 | } 47 | } 48 | ); 49 | 50 | const stream = new DecodeStream(Buffer.from('\x05\x05devon\x15')); 51 | return assert.throws(() => struct.decode(stream)); 52 | }); 53 | 54 | it('should support common header block', function() { 55 | const struct = new VersionedStruct(uint8, { 56 | header: { 57 | age: uint8, 58 | alive: uint8 59 | }, 60 | 0: { 61 | name: new StringT(uint8, 'ascii') 62 | }, 63 | 1: { 64 | name: new StringT(uint8, 'utf8'), 65 | gender: uint8 66 | } 67 | } 68 | ); 69 | 70 | let stream = new DecodeStream(Buffer.from('\x00\x15\x01\x05devon')); 71 | assert.deepEqual(struct.decode(stream), { 72 | version: 0, 73 | age: 21, 74 | alive: 1, 75 | name: 'devon' 76 | }); 77 | 78 | stream = new DecodeStream(Buffer.from('\x01\x15\x01\x0adevon 👍\x00', 'utf8')); 79 | assert.deepEqual(struct.decode(stream), { 80 | version: 1, 81 | age: 21, 82 | alive: 1, 83 | name: 'devon 👍', 84 | gender: 0 85 | }); 86 | }); 87 | 88 | it('should support parent version key', function() { 89 | const struct = new VersionedStruct('version', { 90 | 0: { 91 | name: new StringT(uint8, 'ascii'), 92 | age: uint8 93 | }, 94 | 1: { 95 | name: new StringT(uint8, 'utf8'), 96 | age: uint8, 97 | gender: uint8 98 | } 99 | } 100 | ); 101 | 102 | let stream = new DecodeStream(Buffer.from('\x05devon\x15')); 103 | assert.deepEqual(struct.decode(stream, {version: 0}), { 104 | version: 0, 105 | name: 'devon', 106 | age: 21 107 | }); 108 | 109 | stream = new DecodeStream(Buffer.from('\x0adevon 👍\x15\x00', 'utf8')); 110 | assert.deepEqual(struct.decode(stream, {version: 1}), { 111 | version: 1, 112 | name: 'devon 👍', 113 | age: 21, 114 | gender: 0 115 | }); 116 | }); 117 | 118 | it('should support parent version nested key', function() { 119 | const struct = new VersionedStruct('obj.version', { 120 | 0: { 121 | name: new StringT(uint8, 'ascii'), 122 | age: uint8 123 | }, 124 | 1: { 125 | name: new StringT(uint8, 'utf8'), 126 | age: uint8, 127 | gender: uint8 128 | } 129 | } 130 | ); 131 | 132 | let stream = new DecodeStream(Buffer.from('\x05devon\x15')); 133 | assert.deepEqual(struct.decode(stream, {obj: {version: 0}}), { 134 | version: 0, 135 | name: 'devon', 136 | age: 21 137 | }); 138 | 139 | stream = new DecodeStream(Buffer.from('\x0adevon 👍\x15\x00', 'utf8')); 140 | assert.deepEqual(struct.decode(stream, {obj: {version: 1}}), { 141 | version: 1, 142 | name: 'devon 👍', 143 | age: 21, 144 | gender: 0 145 | }); 146 | }); 147 | 148 | it('should support sub versioned structs', function() { 149 | const struct = new VersionedStruct(uint8, { 150 | 0: { 151 | name: new StringT(uint8, 'ascii'), 152 | age: uint8 153 | }, 154 | 1: new VersionedStruct(uint8, { 155 | 0: { 156 | name: new StringT(uint8) 157 | }, 158 | 1: { 159 | name: new StringT(uint8), 160 | isDesert: uint8 161 | } 162 | } 163 | ) 164 | } 165 | ); 166 | 167 | let stream = new DecodeStream(Buffer.from('\x00\x05devon\x15')); 168 | assert.deepEqual(struct.decode(stream, {version: 0}), { 169 | version: 0, 170 | name: 'devon', 171 | age: 21 172 | }); 173 | 174 | stream = new DecodeStream(Buffer.from('\x01\x00\x05pasta')); 175 | assert.deepEqual(struct.decode(stream, {version: 0}), { 176 | version: 0, 177 | name: 'pasta' 178 | }); 179 | 180 | stream = new DecodeStream(Buffer.from('\x01\x01\x09ice cream\x01')); 181 | assert.deepEqual(struct.decode(stream, {version: 0}), { 182 | version: 1, 183 | name: 'ice cream', 184 | isDesert: 1 185 | }); 186 | }); 187 | 188 | it('should support process hook', function() { 189 | const struct = new VersionedStruct(uint8, { 190 | 0: { 191 | name: new StringT(uint8, 'ascii'), 192 | age: uint8 193 | }, 194 | 1: { 195 | name: new StringT(uint8, 'utf8'), 196 | age: uint8, 197 | gender: uint8 198 | } 199 | } 200 | ); 201 | 202 | struct.process = function() { 203 | return this.processed = true; 204 | }; 205 | 206 | const stream = new DecodeStream(Buffer.from('\x00\x05devon\x15')); 207 | assert.deepEqual(struct.decode(stream), { 208 | version: 0, 209 | name: 'devon', 210 | age: 21, 211 | processed: true 212 | }); 213 | }); 214 | }); 215 | 216 | describe('size', function() { 217 | it('should compute the correct size', function() { 218 | const struct = new VersionedStruct(uint8, { 219 | 0: { 220 | name: new StringT(uint8, 'ascii'), 221 | age: uint8 222 | }, 223 | 1: { 224 | name: new StringT(uint8, 'utf8'), 225 | age: uint8, 226 | gender: uint8 227 | } 228 | } 229 | ); 230 | 231 | let size = struct.size({ 232 | version: 0, 233 | name: 'devon', 234 | age: 21 235 | }); 236 | 237 | assert.equal(size, 8); 238 | 239 | size = struct.size({ 240 | version: 1, 241 | name: 'devon 👍', 242 | age: 21, 243 | gender: 0 244 | }); 245 | 246 | assert.equal(size, 14); 247 | }); 248 | 249 | it('should throw for unknown version', function() { 250 | const struct = new VersionedStruct(uint8, { 251 | 0: { 252 | name: new StringT(uint8, 'ascii'), 253 | age: uint8 254 | }, 255 | 1: { 256 | name: new StringT(uint8, 'utf8'), 257 | age: uint8, 258 | gender: uint8 259 | } 260 | } 261 | ); 262 | 263 | assert.throws(() => 264 | struct.size({ 265 | version: 5, 266 | name: 'devon', 267 | age: 21 268 | }) 269 | ); 270 | }); 271 | 272 | it('should support common header block', function() { 273 | const struct = new VersionedStruct(uint8, { 274 | header: { 275 | age: uint8, 276 | alive: uint8 277 | }, 278 | 0: { 279 | name: new StringT(uint8, 'ascii') 280 | }, 281 | 1: { 282 | name: new StringT(uint8, 'utf8'), 283 | gender: uint8 284 | } 285 | } 286 | ); 287 | 288 | let size = struct.size({ 289 | version: 0, 290 | age: 21, 291 | alive: 1, 292 | name: 'devon' 293 | }); 294 | 295 | assert.equal(size, 9); 296 | 297 | size = struct.size({ 298 | version: 1, 299 | age: 21, 300 | alive: 1, 301 | name: 'devon 👍', 302 | gender: 0 303 | }); 304 | 305 | assert.equal(size, 15); 306 | }); 307 | 308 | it('should compute the correct size with pointers', function() { 309 | const struct = new VersionedStruct(uint8, { 310 | 0: { 311 | name: new StringT(uint8, 'ascii'), 312 | age: uint8 313 | }, 314 | 1: { 315 | name: new StringT(uint8, 'utf8'), 316 | age: uint8, 317 | ptr: new Pointer(uint8, new StringT(uint8)) 318 | } 319 | } 320 | ); 321 | 322 | const size = struct.size({ 323 | version: 1, 324 | name: 'devon', 325 | age: 21, 326 | ptr: 'hello' 327 | }); 328 | 329 | assert.equal(size, 15); 330 | }); 331 | 332 | it('should throw if no value is given', function() { 333 | const struct = new VersionedStruct(uint8, { 334 | 0: { 335 | name: new StringT(4, 'ascii'), 336 | age: uint8 337 | }, 338 | 1: { 339 | name: new StringT(4, 'utf8'), 340 | age: uint8, 341 | gender: uint8 342 | } 343 | } 344 | ); 345 | 346 | assert.throws(() => struct.size(), /not a fixed size/i); 347 | }); 348 | }); 349 | 350 | describe('encode', function() { 351 | it('should encode objects to buffers', function() { 352 | const struct = new VersionedStruct(uint8, { 353 | 0: { 354 | name: new StringT(uint8, 'ascii'), 355 | age: uint8 356 | }, 357 | 1: { 358 | name: new StringT(uint8, 'utf8'), 359 | age: uint8, 360 | gender: uint8 361 | } 362 | }); 363 | 364 | const buf1 = struct.toBuffer({ 365 | version: 0, 366 | name: 'devon', 367 | age: 21 368 | }); 369 | 370 | assert.deepEqual(buf1, Buffer.from('\x00\x05devon\x15', 'utf8')); 371 | 372 | const buf2 = struct.toBuffer({ 373 | version: 1, 374 | name: 'devon 👍', 375 | age: 21, 376 | gender: 0 377 | }); 378 | 379 | assert.deepEqual(buf2, Buffer.from('\x01\x0adevon 👍\x15\x00', 'utf8')); 380 | }); 381 | 382 | it('should throw for unknown version', function() { 383 | const struct = new VersionedStruct(uint8, { 384 | 0: { 385 | name: new StringT(uint8, 'ascii'), 386 | age: uint8 387 | }, 388 | 1: { 389 | name: new StringT(uint8, 'utf8'), 390 | age: uint8, 391 | gender: uint8 392 | } 393 | }); 394 | 395 | assert.throws(() => 396 | struct.toBuffer({ 397 | version: 5, 398 | name: 'devon', 399 | age: 21 400 | }) 401 | ); 402 | }); 403 | 404 | it('should support common header block', function() { 405 | const struct = new VersionedStruct(uint8, { 406 | header: { 407 | age: uint8, 408 | alive: uint8 409 | }, 410 | 0: { 411 | name: new StringT(uint8, 'ascii') 412 | }, 413 | 1: { 414 | name: new StringT(uint8, 'utf8'), 415 | gender: uint8 416 | } 417 | }); 418 | 419 | const buf1 = struct.toBuffer({ 420 | version: 0, 421 | age: 21, 422 | alive: 1, 423 | name: 'devon' 424 | }); 425 | 426 | assert.deepEqual(buf1, Buffer.from('\x00\x15\x01\x05devon', 'utf8')); 427 | 428 | const buf2 = struct.toBuffer({ 429 | version: 1, 430 | age: 21, 431 | alive: 1, 432 | name: 'devon 👍', 433 | gender: 0 434 | }); 435 | 436 | assert.deepEqual(buf2, Buffer.from('\x01\x15\x01\x0adevon 👍\x00', 'utf8')); 437 | }); 438 | 439 | it('should encode pointer data after structure', function() { 440 | const struct = new VersionedStruct(uint8, { 441 | 0: { 442 | name: new StringT(uint8, 'ascii'), 443 | age: uint8 444 | }, 445 | 1: { 446 | name: new StringT(uint8, 'utf8'), 447 | age: uint8, 448 | ptr: new Pointer(uint8, new StringT(uint8)) 449 | } 450 | }); 451 | 452 | const buf = struct.toBuffer({ 453 | version: 1, 454 | name: 'devon', 455 | age: 21, 456 | ptr: 'hello' 457 | }); 458 | 459 | assert.deepEqual(buf, Buffer.from('\x01\x05devon\x15\x09\x05hello', 'utf8')); 460 | }); 461 | 462 | it('should support preEncode hook', function() { 463 | const struct = new VersionedStruct(uint8, { 464 | 0: { 465 | name: new StringT(uint8, 'ascii'), 466 | age: uint8 467 | }, 468 | 1: { 469 | name: new StringT(uint8, 'utf8'), 470 | age: uint8, 471 | gender: uint8 472 | } 473 | }); 474 | 475 | struct.preEncode = function() { 476 | return this.version = (this.gender != null) ? 1 : 0; 477 | }; 478 | 479 | const buf1 = struct.toBuffer({ 480 | name: 'devon', 481 | age: 21 482 | }); 483 | 484 | assert.deepEqual(buf1, Buffer.from('\x00\x05devon\x15', 'utf8')); 485 | 486 | const buf2 = struct.toBuffer({ 487 | name: 'devon 👍', 488 | age: 21, 489 | gender: 0 490 | }); 491 | 492 | assert.deepEqual(buf2, Buffer.from('\x01\x0adevon 👍\x15\x00', 'utf8')); 493 | }); 494 | }); 495 | }); 496 | --------------------------------------------------------------------------------