├── .editorconfig ├── .github └── workflows │ ├── nodejs.yml │ └── npmpublish.yml ├── .gitignore ├── .prettierrc ├── LICENSE.md ├── README.md ├── example ├── Hello.class ├── Hello.java ├── bmp.js ├── classfile.js ├── elf32.js ├── hello ├── ip.js ├── jpg.js ├── mbr.js ├── raspbian.img ├── tar.js ├── tcp.js ├── test.bmp ├── test.jpg └── test.tar ├── lib ├── binary_parser.ts └── context.ts ├── package-lock.json ├── package.json ├── test ├── browser.html ├── composite_parser.js ├── primitive_parser.js ├── yy_primitive_encoder.js ├── zz_composite_encoder.js └── zz_encoder_bugs.js └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | 9 | [*.{ts,js,json}] 10 | indent_style = space 11 | indent_size = 2 12 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.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: build 5 | 6 | on: 7 | push: 8 | branches: [ encoder] 9 | pull_request: 10 | branches: [ encoder ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [10.x, 12.x, 14.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - run: npm ci 28 | - run: npm run build --if-present 29 | - run: npm test 30 | env: 31 | CI: true 32 | -------------------------------------------------------------------------------- /.github/workflows/npmpublish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: publish 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-node@v1 16 | with: 17 | node-version: 12 18 | - run: npm ci 19 | - run: npm test 20 | 21 | publish-npm: 22 | needs: build 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v2 26 | - uses: actions/setup-node@v1 27 | with: 28 | node-version: 12 29 | registry-url: https://registry.npmjs.org/ 30 | - run: npm ci 31 | - run: npm publish 32 | env: 33 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Node ### 2 | lib-cov 3 | *.seed 4 | *.log 5 | *.csv 6 | *.dat 7 | *.out 8 | *.pid 9 | *.gz 10 | 11 | pids 12 | logs 13 | results 14 | 15 | npm-debug.log 16 | node_modules 17 | 18 | coverage/ 19 | .nyc_output/ 20 | dist/ 21 | .vscode/ 22 | .cache/ 23 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "typescript", 3 | "trailingComma": "es5", 4 | "tabWidth": 2, 5 | "semi": true, 6 | "singleQuote": true 7 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2019 Keichi Takahashi 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Binary-parser-encoder 2 | 3 | Note: This is a fork of [binary-parser](https://github.com/keichi/binary-parser) 4 | library. It is currently being proposed as a Pull-Request in that project. 5 | 6 | Until the *encoding* feature is merged in baseline of original project, 7 | this branch is published under the name: **binary-parser-encoder** in [npm](https://npmjs.org/). 8 | 9 | [![build](https://github.com/Ericbla/binary-parser/workflows/build/badge.svg)](https://github.com/Ericbla/binary-parser/actions?query=workflow%3Abuild) 10 | [![npm](https://img.shields.io/npm/v/binary-parser-encoder)](https://www.npmjs.com/package/binary-parser-encoder) 11 | 12 | Binary-parser is a parser/encoder builder for JavaScript that enables you to write 13 | efficient binary parsers/encoders in a simple and declarative manner. 14 | 15 | It supports all common data types required to analyze a structured binary 16 | data. Binary-parser dynamically generates and compiles the parser and encoder code 17 | on-the-fly, which runs as fast as a hand-written parser/encoder (which takes much more 18 | time and effort to write). Supported data types are: 19 | 20 | - [Integers](#uint8-16-32-64le-bename-options) (8, 16, 32 and 64 bit signed 21 | and unsigned integers) 22 | - [Floating point numbers](#float-doublele-bename-options) (32 and 64 bit 23 | floating point values) 24 | - [Bit fields](#bit1-32name-options) (bit fields with length from 1 to 32 25 | bits) 26 | - [Strings](#stringname-options) (fixed-length, variable-length and zero 27 | terminated strings with various encodings) 28 | - [Arrays](#arrayname-options) (fixed-length and variable-length arrays of 29 | builtin or user-defined element types) 30 | - [Choices](#choicename-options) (supports integer keys) 31 | - [Pointers](#pointername-options) 32 | - User defined types (arbitrary combination of builtin types) 33 | 34 | Binary-parser was inspired by [BinData](https://github.com/dmendel/bindata) 35 | and [binary](https://github.com/substack/node-binary). 36 | 37 | ## Quick Start 38 | 39 | 1. Create an empty Parser object with `new Parser()` or `Parser.start()`. 40 | 2. Chain methods to build your desired parser and/or encoder. (See 41 | [API](https://github.com/keichi/binary-parser#api) for detailed document of 42 | each method) 43 | 3. Call `Parser.prototype.parse` with a `Buffer`/`Uint8Array` object passed as 44 | an argument. 45 | 4. The parsed result will be returned as an object. 46 | 5. Or call `Parser.prototype.encode` with an object passed as argument. 47 | 6. Encoded result will be returned as a `Buffer` object. 48 | 49 | ```javascript 50 | // Module import 51 | var Parser = require("binary-parser").Parser; 52 | 53 | // Build an IP packet header Parser 54 | var ipHeader = new Parser() 55 | .endianess("big") 56 | .bit4("version") 57 | .bit4("headerLength") 58 | .uint8("tos") 59 | .uint16("packetLength") 60 | .uint16("id") 61 | .bit3("offset") 62 | .bit13("fragOffset") 63 | .uint8("ttl") 64 | .uint8("protocol") 65 | .uint16("checksum") 66 | .array("src", { 67 | type: "uint8", 68 | length: 4 69 | }) 70 | .array("dst", { 71 | type: "uint8", 72 | length: 4 73 | }); 74 | 75 | // Prepare buffer to parse. 76 | var buf = Buffer.from("450002c5939900002c06ef98adc24f6c850186d1", "hex"); 77 | 78 | // Parse buffer and show result 79 | console.log(ipHeader.parse(buf)); 80 | 81 | var anIpHeader = { 82 | version: 4, 83 | headerLength: 5, 84 | tos: 0, 85 | packetLength: 709, 86 | id: 37785, 87 | offset: 0, 88 | fragOffset: 0, 89 | ttl: 44, 90 | protocol: 6, 91 | checksum: 61336, 92 | src: [ 173, 194, 79, 108 ], 93 | dst: [ 133, 1, 134, 209 ] }; 94 | 95 | // Encode an IP header object and show result as hex string 96 | console.log(ipHeader.encode(anIpHeader).toString("hex")); 97 | ``` 98 | 99 | ## API 100 | 101 | ### new Parser([options]) 102 | Create an empty parser object that parses nothing. 103 | `options` is an optional object to pass options to this declarative 104 | parser. 105 | - `smartBufferSize` The chunk size of the encoding (smart)buffer (when encoding is used) (default is 256 bytes). 106 | 107 | ### parse(buffer) 108 | Parse a `Buffer`/`Uint8Array` object `buffer` with this parser and return the 109 | resulting object. When `parse(buffer)` is called for the first time, the 110 | associated parser code is compiled on-the-fly and internally cached. 111 | 112 | ### encode(obj) 113 | Encode an `Object` object `obj` with this parser and return the resulting 114 | `Buffer`. When `encode(obj)` is called for the first time, encoder code is 115 | compiled on-the-fly and internally cached. 116 | 117 | ### create(constructorFunction) 118 | Set the constructor function that should be called to create the object 119 | returned from the `parse` method. 120 | 121 | ### [u]int{8, 16, 32, 64}{le, be}(name[, options]) 122 | Parse bytes as an integer and store it in a variable named `name`. `name` 123 | should consist only of alphanumeric characters and start with an alphabet. 124 | Number of bits can be chosen from 8, 16, 32 and 64. Byte-ordering can be either 125 | `l` for little endian or `b` for big endian. With no prefix, it parses as a 126 | signed number, with `u` prefixed as an unsigned number. The runtime type 127 | returned by the 8, 16, 32 bit methods is `number` while the type 128 | returned by the 64 bit is `bigint`. 129 | 130 | **Note:** [u]int64{be,le} methods only work if your runtime is node v12.0.0 or 131 | greater. Lower version will throw a runtime error. 132 | 133 | ```javascript 134 | var parser = new Parser() 135 | // Signed 32-bit integer (little endian) 136 | .int32le("a") 137 | // Unsigned 8-bit integer 138 | .uint8("b") 139 | // Signed 16-bit integer (big endian) 140 | .int16be("c"); 141 | // signed 64-bit integer (big endian) 142 | .int64be("d") 143 | ``` 144 | 145 | ### bit\[1-32\](name[, options]) 146 | Parse bytes as a bit field and store it in variable `name`. There are 32 147 | methods from `bit1` to `bit32` each corresponding to 1-bit-length to 148 | 32-bits-length bit field. 149 | 150 | ### {float, double}{le, be}(name[, options]) 151 | Parse bytes as a floating-point value and stores it to a variable named 152 | `name`. 153 | 154 | ```javascript 155 | var parser = new Parser() 156 | // 32-bit floating value (big endian) 157 | .floatbe("a") 158 | // 64-bit floating value (little endian) 159 | .doublele("b"); 160 | ``` 161 | 162 | ### string(name[, options]) 163 | Parse bytes as a string. `name` should consist only of alpha numeric 164 | characters and start with an alphabet. `options` is an object which can have 165 | the following keys: 166 | 167 | - `encoding` - (Optional, defaults to `utf8`) Specify which encoding to use. 168 | Supported encodings include `"hex"` and all encodings supported by 169 | [`TextDecoder`](https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder/encoding). 170 | - `length ` - (Optional) Length of the string. Can be a number, string or a 171 | function. Use number for statically sized arrays, string to reference 172 | another variable and function to do some calculation. 173 | Note: When encoding the string is padded with a `padd` charecter to fit the length requirement. 174 | - `zeroTerminated` - (Optional, defaults to `false`) If true, then this parser 175 | reads until it reaches zero (or the specified `length`). When encoding, a *null* character is inserted at end of 176 | the string (if the optional `length` allows it). 177 | - `greedy` - (Optional, defaults to `false`) If true, then this parser reads 178 | until it reaches the end of the buffer. Will consume zero-bytes. (Note: has 179 | no effect on encoding function) 180 | - `stripNull` - (Optional, must be used with `length`) If true, then strip 181 | null characters from end of the string. (Note: When encoding, this will also set the **default** `padd` character 182 | to null instead of space) 183 | - `trim` - (Optional, default to `false`) If true, then trim() (remove leading and trailing spaces) 184 | the parsed string. 185 | - `padding` - (Optional, Only used for encoding, default to `right`) If `left` then the string 186 | will be right aligned (padding left with `padd` char or space) depending of the `length` option 187 | - `padd` - (Optional, Only used for encoding with `length` specified) A string from which first character (1 Byte) 188 | is used as a padding char if necessary (provided string length is less than `length` option). Note: Only 'ascii' 189 | or utf8 < 0x80 are alowed. Note: The default padd character is *space* (or *null* when `stripNull` is used). 190 | 191 | ### buffer(name[, options]) 192 | Parse bytes as a buffer. Its type will be the same as the input to 193 | `parse(buffer)`. `name` should consist only of alpha numeric characters and 194 | start with an alphabet. `options` is an object which can have the following 195 | keys: 196 | 197 | - `clone` - (Optional, defaults to `false`) By default, 198 | `buffer(name [,options])` returns a new buffer which references the same 199 | memory as the parser input, but offset and cropped by a certain range. If 200 | this option is true, input buffer will be cloned and a new buffer 201 | referencing a new memory region is returned. 202 | - `length ` - (either `length` or `readUntil` is required) Length of the 203 | buffer. Can be a number, string or a function. Use number for statically 204 | sized buffers, string to reference another variable and function to do some 205 | calculation. 206 | - `readUntil` - (either `length` or `readUntil` is required) If `"eof"`, then 207 | this parser will read till it reaches the end of the `Buffer`/`Uint8Array` 208 | object. (Note: has no effect on encoding.) 209 | If it is a function, this parser will read the buffer until the 210 | function returns true. 211 | 212 | ### array(name, options) 213 | Parse bytes as an array. `options` is an object which can have the following 214 | keys: 215 | 216 | - `type` - (Required) Type of the array element. Can be a string or an user 217 | defined Parser object. If it's a string, you have to choose from [u]int{8, 218 | 16, 32}{le, be}. 219 | - `length` - (either `length`, `lengthInBytes`, or `readUntil` is required) 220 | Length of the array. Can be a number, string or a function. Use number for 221 | statically sized arrays. 222 | - `lengthInBytes` - (either `length`, `lengthInBytes`, or `readUntil` is 223 | required) Length of the array expressed in bytes. Can be a number, string or 224 | a function. Use number for statically sized arrays. 225 | - `readUntil` - (either `length`, `lengthInBytes`, or `readUntil` is required) 226 | If `"eof"`, then this parser reads until the end of the `Buffer`/`Uint8Array` 227 | object. If function it reads until the function returns true. 228 | **Note**: When encoding, 229 | the `buffer` second parameter of `readUntil` function is the buffer already encoded 230 | before this array. So no *read-ahead* is possible. 231 | - `encodeUntil` - a function (item, object), only used when encoding, that replaces 232 | the `readUntil` function when present and allow limit the number of encoded items 233 | by returning true based on *item* values or other *object* properies. 234 | 235 | ```javascript 236 | var parser = new Parser() 237 | // Statically sized array 238 | .array("data", { 239 | type: "int32", 240 | length: 8 241 | }) 242 | 243 | // Dynamically sized array (references another variable) 244 | .uint8("dataLength") 245 | .array("data2", { 246 | type: "int32", 247 | length: "dataLength" 248 | }) 249 | 250 | // Dynamically sized array (with some calculation) 251 | .array("data3", { 252 | type: "int32", 253 | length: function() { 254 | return this.dataLength - 1; 255 | } // other fields are available through `this` 256 | }) 257 | 258 | // Statically sized array 259 | .array("data4", { 260 | type: "int32", 261 | lengthInBytes: 16 262 | }) 263 | 264 | // Dynamically sized array (references another variable) 265 | .uint8("dataLengthInBytes") 266 | .array("data5", { 267 | type: "int32", 268 | lengthInBytes: "dataLengthInBytes" 269 | }) 270 | 271 | // Dynamically sized array (with some calculation) 272 | .array("data6", { 273 | type: "int32", 274 | lengthInBytes: function() { 275 | return this.dataLengthInBytes - 4; 276 | } // other fields are available through `this` 277 | }) 278 | 279 | // Dynamically sized array (with stop-check on parsed item) 280 | .array("data7", { 281 | type: "int32", 282 | readUntil: function(item, buffer) { 283 | return item === 42; 284 | } // stop when specific item is parsed. buffer can be used to perform a read-ahead. 285 | }) 286 | 287 | // Use user defined parser object 288 | .array("data8", { 289 | type: userDefinedParser, 290 | length: "dataLength" 291 | }); 292 | ``` 293 | 294 | ### choice([name,] options) 295 | Choose one parser from multiple parsers according to a field value and store 296 | its parsed result to key `name`. If `name` is null or omitted, the result of 297 | the chosen parser is directly embedded into the current object. `options` is 298 | an object which can have the following keys: 299 | 300 | - `tag` - (Required) The value used to determine which parser to use from the 301 | `choices` Can be a string pointing to another field or a function. 302 | - `choices` - (Required) An object which key is an integer and value is the 303 | parser which is executed when `tag` equals the key value. 304 | - `defaultChoice` - (Optional) In case if the tag value doesn't match any of 305 | `choices`, this parser is used. 306 | 307 | ```javascript 308 | var parser1 = ...; 309 | var parser2 = ...; 310 | var parser3 = ...; 311 | 312 | var parser = new Parser().uint8("tagValue").choice("data", { 313 | tag: "tagValue", 314 | choices: { 315 | 1: parser1, // if tagValue == 1, execute parser1 316 | 4: parser2, // if tagValue == 4, execute parser2 317 | 5: parser3 // if tagValue == 5, execute parser3 318 | } 319 | }); 320 | ``` 321 | 322 | Combining `choice` with `array` is an idiom to parse 323 | [TLV](http://en.wikipedia.org/wiki/Type-length-value)-based binary formats. 324 | 325 | ### nest([name,] options) 326 | Execute an inner parser and store its result to key `name`. If `name` is null 327 | or omitted, the result of the inner parser is directly embedded into the 328 | current object. `options` is an object which can have the following keys: 329 | 330 | - `type` - (Required) A `Parser` object. 331 | 332 | ### pointer(name [,options]) 333 | Jump to `offset`, execute parser for `type` and rewind to previous offset. 334 | Useful for parsing binary formats such as ELF where the offset of a field is 335 | pointed by another field. 336 | 337 | - `type` - (Required) Can be a string `[u]int{8, 16, 32, 64}{le, be}` 338 | or an user defined Parser object. 339 | - `offset` - (Required) Indicates absolute offset from the beginning of the 340 | input buffer. Can be a number, string or a function. 341 | 342 | ### saveOffset(name [,options]) 343 | Save the current buffer offset as key `name`. This function is only useful 344 | when called after another function which would advance the internal buffer 345 | offset. 346 | 347 | ```javascript 348 | var parser = new Parser() 349 | // this call advances the buffer offset by 350 | // a variable (i.e. unknown to us) number of bytes 351 | .string('name', { 352 | zeroTerminated: true 353 | }) 354 | // this variable points to an absolute position 355 | // in the buffer 356 | .uint32('seekOffset') 357 | // now, save the "current" offset in the stream 358 | // as the variable "currentOffset" 359 | .saveOffset('currentOffset') 360 | // finally, use the saved offset to figure out 361 | // how many bytes we need to skip 362 | .seek(function() { 363 | return this.seekOffset - this.currentOffset; 364 | }) 365 | ... // the parser would continue here 366 | ``` 367 | 368 | ### seek(relOffset) 369 | Move the buffer offset for `relOffset` bytes from the current position. Use a 370 | negative `relOffset` value to rewind the offset. This method was previously 371 | named `skip(length)`. (Note: when encoding, the skipped bytes will be filled with zeros) 372 | 373 | ### endianess(endianess) 374 | Define what endianess to use in this parser. `endianess` can be either 375 | `"little"` or `"big"`. The default endianess of `Parser` is set to big-endian. 376 | 377 | ```javascript 378 | var parser = new Parser() 379 | .endianess("little") 380 | // You can specify endianess explicitly 381 | .uint16be("a") 382 | .uint32le("a") 383 | // Or you can omit endianess (in this case, little-endian is used) 384 | .uint16("b") 385 | .int32("c"); 386 | ``` 387 | 388 | ### encoderSetOptions(opts) 389 | Set specific options for encoding. 390 | Current supported `opts` object may contain: 391 | - bitEndianess: true|false (default false) When true, tell the encoder to respect endianess BITs order, so that 392 | encoding is exactly the reverse of the parsing process for bits fields. 393 | 394 | ```javascript 395 | var parser = new Parser() 396 | .endianess("little") 397 | .encoderSetOptions({bitEndianess: true}) // Use BITs endianess for bits fields 398 | .bit4("a") 399 | .bit4("b") 400 | .uint16("c"); 401 | ``` 402 | 403 | ### namely(alias) 404 | Set an alias to this parser, so there will be an opportunity to refer to it by 405 | name in methods like `.array`, `.nest` and `.choice`, instead of requirement 406 | to have an instance of it. 407 | 408 | Especially, the parser may reference itself: 409 | 410 | ```javascript 411 | var stop = new Parser(); 412 | 413 | var parser = new Parser() 414 | .namely("self") // use 'self' to refer to the parser itself 415 | .uint8("type") 416 | .choice("data", { 417 | tag: "type", 418 | choices: { 419 | 0: stop, 420 | 1: "self", 421 | 2: Parser.start() 422 | .nest("left", { type: "self" }) 423 | .nest("right", { type: "self" }), 424 | 3: Parser.start() 425 | .nest("one", { type: "self" }) 426 | .nest("two", { type: "self" }) 427 | .nest("three", { type: "self" }) 428 | } 429 | }); 430 | 431 | // 2 432 | // / \ 433 | // 3 1 434 | // / | \ \ 435 | // 1 0 2 0 436 | // / / \ 437 | // 0 1 0 438 | // / 439 | // 0 440 | 441 | var buffer = Buffer.from([ 442 | 2, 443 | /* left -> */ 3, 444 | /* one -> */ 1, /* -> */ 0, 445 | /* two -> */ 0, 446 | /* three -> */ 2, 447 | /* left -> */ 1, /* -> */ 0, 448 | /* right -> */ 0, 449 | /* right -> */ 1, /* -> */ 0 450 | ]); 451 | 452 | parser.parse(buffer); 453 | ``` 454 | 455 | For most of the cases there is almost no difference to the instance-way of 456 | referencing, but this method provides the way to parse recursive trees, where 457 | each node could reference the node of the same type from the inside. 458 | 459 | Also, when you reference a parser using its instance twice, the generated code 460 | will contain two similar parts of the code included, while with the named 461 | approach, it will include a function with a name, and will just call this 462 | function for every case of usage. 463 | 464 | **Note**: This style could lead to circular references and infinite recursion, 465 | to avoid this, ensure that every possible path has its end. Also, this 466 | recursion is not tail-optimized, so could lead to memory leaks when it goes 467 | too deep. 468 | 469 | An example of referencing other patches: 470 | 471 | ```javascript 472 | // the line below registers the name 'self', so we will be able to use it in 473 | // `twoCells` as a reference 474 | var parser = Parser.start().namely("self"); 475 | 476 | var stop = Parser.start().namely("stop"); 477 | 478 | var twoCells = Parser.start() 479 | .namely("twoCells") 480 | .nest("left", { type: "self" }) 481 | .nest("right", { type: "stop" }); 482 | 483 | parser.uint8("type").choice("data", { 484 | tag: "type", 485 | choices: { 486 | 0: "stop", 487 | 1: "self", 488 | 2: "twoCells" 489 | } 490 | }); 491 | 492 | var buffer = Buffer.from([2, /* left */ 1, 1, 0, /* right */ 0]); 493 | 494 | parser.parse(buffer); 495 | ``` 496 | 497 | ### compile() and compileEncode() 498 | Compile this parser/encoder on-the-fly and cache its result. Usually, there is no need 499 | to call this method directly, since it's called when `parse(buffer)` or `encode(obj)` is 500 | executed for the first time. 501 | 502 | ### getCode() and getCodeEncode() 503 | Dynamically generates the code for this parser/encoder and returns it as a string. 504 | Useful for debugging the generated code. 505 | 506 | ### Common options 507 | These options can be used in all parsers. 508 | 509 | - `formatter` - Function that transforms the parsed value into a more desired 510 | form. *formatter*(value, obj, buffer, offset) → *new value* \ 511 | where `value` is the value to be formatted, `obj` is the current object being generated, `buffer` is the buffer currently beeing parsed and `offset` is the current offset in that buffer. 512 | ```javascript 513 | var parser = new Parser().array("ipv4", { 514 | type: uint8, 515 | length: "4", 516 | formatter: function(arr, obj, buffer, offset) { 517 | return arr.join("."); 518 | } 519 | }); 520 | ``` 521 | 522 | - `encoder` - Function that transforms an object property into a more desired 523 | form for encoding. This is the opposite of the above `formatter` function. \ 524 | *encoder*(value) → *new value* \ 525 | where `value` is the value to be encoded (de-formatted) and `obj` is the object currently being encoded. 526 | ```javascript 527 | var parser = new Parser().array("ipv4", { 528 | type: uint8, 529 | length: "4", 530 | formatter: function(arr, obj, buffer, offset) { 531 | return arr.join("."); 532 | }, 533 | encoder: function(str, obj) { 534 | return str.split("."); 535 | } 536 | }); 537 | ``` 538 | 539 | - `assert` - Do assertion on the parsed result (useful for checking magic 540 | numbers and so on). If `assert` is a `string` or `number`, the actual parsed 541 | result will be compared with it with `===` (strict equality check), and an 542 | exception is thrown if they mismatch. On the other hand, if `assert` is a 543 | function, that function is executed with one argument (parsed result) and if 544 | it returns false, an exception is thrown. 545 | 546 | ```javascript 547 | // simple maginc number validation 548 | var ClassFile = Parser.start() 549 | .endianess("big") 550 | .uint32("magic", { assert: 0xcafebabe }); 551 | 552 | // Doing more complex assertion with a predicate function 553 | var parser = new Parser() 554 | .int16le("a") 555 | .int16le("b") 556 | .int16le("c", { 557 | assert: function(x) { 558 | return this.a + this.b === x; 559 | } 560 | }); 561 | ``` 562 | 563 | ## Examples 564 | See `example/` for real-world examples. 565 | 566 | ## Support 567 | Please report issues to the 568 | [issue tracker](https://github.com/keichi/binary-parser/issues) if you have 569 | any difficulties using this module, found a bug, or request a new feature. 570 | Pull requests are welcomed. 571 | -------------------------------------------------------------------------------- /example/Hello.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ericbla/binary-parser/295c6ec0337e86f9517c17949156d3984b83be03/example/Hello.class -------------------------------------------------------------------------------- /example/Hello.java: -------------------------------------------------------------------------------- 1 | public class Hello { 2 | public static void main(String[] args) { 3 | System.out.println("Hello, world!"); 4 | } 5 | } -------------------------------------------------------------------------------- /example/bmp.js: -------------------------------------------------------------------------------- 1 | var Parser = require('../dist/binary_parser').Parser; 2 | 3 | // C structure BITMAPFILEHEADER 4 | // typedef struct tagBITMAPFILEHEADER { 5 | // WORD bfType; 6 | // DWORD bfSize; 7 | // WORD bfReserved1; 8 | // WORD bfReserved2; 9 | // DWORD bfOffBits; 10 | // } BITMAPFILEHEADER, *PBITMAPFILEHEADER; 11 | var bmpFileHeader = new Parser() 12 | .endianess('little') 13 | .string('type', { 14 | length: 2, 15 | assert: 'BM', 16 | }) 17 | .uint32('size') 18 | .uint16('reserved1') 19 | .uint16('reserved2') 20 | .uint32('offBits'); 21 | 22 | // C structure BITMAPINFOHEADER definition 23 | // typedef struct tagBITMAPINFOHEADER { 24 | // DWORD biSize; 25 | // LONG biWidth; 26 | // LONG biHeight; 27 | // WORD biPlanes; 28 | // WORD biBitCount; 29 | // DWORD biCompression; 30 | // DWORD biSizeImage; 31 | // LONG biXPelsPerMeter; 32 | // LONG biYPelsPerMeter; 33 | // DWORD biClrUsed; 34 | // DWORD biClrImportant; 35 | // } BITMAPINFOHEADER; 36 | var bmpInfoHeader = new Parser() 37 | .endianess('little') 38 | .uint32('size') 39 | .int32('width') 40 | .int32('height') 41 | .uint16('planes') 42 | .uint16('bitCount') 43 | .uint32('compression') 44 | .uint32('sizeImage') 45 | .int32('xPelsPerMeter') 46 | .int32('yPelsPerMeter') 47 | .uint32('clrUsed') 48 | .uint32('clrImportant'); 49 | 50 | var bmpFile = new Parser() 51 | .nest('fileHeader', { 52 | type: bmpFileHeader, 53 | }) 54 | .nest('infoHeader', { 55 | type: bmpInfoHeader, 56 | }); 57 | 58 | require('fs').readFile('test.bmp', function (err, data) { 59 | console.log(bmpFile.parse(data)); 60 | }); 61 | -------------------------------------------------------------------------------- /example/classfile.js: -------------------------------------------------------------------------------- 1 | var Parser = require('../dist/binary_parser').Parser; 2 | 3 | var ConstantClassInfo = Parser.start().uint16be('name_index'); 4 | 5 | var ConstantFieldrefInfo = Parser.start() 6 | .uint16be('class_index') 7 | .uint16be('name_and_type_index'); 8 | 9 | var ConstantMethodrefInfo = Parser.start() 10 | .uint16be('class_index') 11 | .uint16be('name_and_type_index'); 12 | 13 | var ConstantInterfaceMethodrefInfo = Parser.start() 14 | .uint16be('class_index') 15 | .uint16be('name_and_type_index'); 16 | 17 | var ConstantStringInfo = Parser.start().uint16be('string_index'); 18 | 19 | var ConstantIntegerInfo = Parser.start().uint32be('bytes'); 20 | 21 | var ConstantFloatInfo = Parser.start().uint32be('bytes'); 22 | 23 | var ConstantLongInfo = Parser.start() 24 | .uint32be('high_bytes') 25 | .uint32be('low_bytes'); 26 | 27 | var ConstantDoubleInfo = Parser.start() 28 | .uint32be('high_bytes') 29 | .uint32be('low_bytes'); 30 | 31 | var ConstantNameAndTypeInfo = Parser.start() 32 | .uint16be('name_index') 33 | .uint16be('descriptor_index'); 34 | 35 | var ConstantUtf8Info = Parser.start() 36 | .uint16be('len') 37 | .string('bytes', { length: 'len' }); 38 | 39 | var ConstantMethodHandleInfo = Parser.start() 40 | .uint8('reference_kind') 41 | .uint16be('reference_index'); 42 | 43 | var ConstantMethodTypeInfo = Parser.start().uint16be('descriptor_index'); 44 | 45 | var ConstantInvokeDynamicInfo = Parser.start() 46 | .uint16be('bootstrap_method_attr_index') 47 | .uint16be('name_and_type_index'); 48 | 49 | var CpInfo = Parser.start() 50 | .uint8('tag') 51 | .choice('info', { 52 | tag: 'tag', 53 | choices: { 54 | 7: ConstantClassInfo, 55 | 9: ConstantFieldrefInfo, 56 | 10: ConstantMethodrefInfo, 57 | 11: ConstantInterfaceMethodrefInfo, 58 | 8: ConstantStringInfo, 59 | 3: ConstantIntegerInfo, 60 | 4: ConstantFloatInfo, 61 | 5: ConstantLongInfo, 62 | 6: ConstantDoubleInfo, 63 | 12: ConstantNameAndTypeInfo, 64 | 1: ConstantUtf8Info, 65 | 16: ConstantMethodTypeInfo, 66 | 18: ConstantInvokeDynamicInfo, 67 | }, 68 | }); 69 | 70 | var ClassFile = Parser.start() 71 | .endianess('big') 72 | .uint32('magic', { assert: 0xcafebabe }) 73 | .uint16('minor_version') 74 | .uint16('major_version') 75 | .uint16('constant_pool_count') 76 | .array('cp_info', { 77 | type: CpInfo, 78 | length: function () { 79 | return this.constant_pool_count - 1; 80 | }, 81 | }); 82 | 83 | require('fs').readFile('Hello.class', function (err, data) { 84 | console.log(require('util').inspect(ClassFile.parse(data), { depth: null })); 85 | }); 86 | -------------------------------------------------------------------------------- /example/elf32.js: -------------------------------------------------------------------------------- 1 | var Parser = require('../dist/binary_parser').Parser; 2 | 3 | var ELF32ProgramHeader = new Parser() 4 | .endianess('little') 5 | .uint32('type') 6 | .uint32('offset') 7 | .uint32('vaddr') 8 | .uint32('paddr') 9 | .uint32('filesz') 10 | .uint32('memsz') 11 | .uint32('flags') 12 | .uint32('align'); 13 | 14 | var ELF32ProgramHeaderTable = new Parser().array('items', { 15 | type: ELF32ProgramHeader, 16 | length: function (vars) { 17 | return vars.phnum; 18 | }, 19 | }); 20 | 21 | var ELF32SectionHeader = new Parser() 22 | .endianess('little') 23 | .uint32('name') 24 | .uint32('type') 25 | .uint32('flags') 26 | .uint32('address') 27 | .uint32('offset') 28 | .uint32('size') 29 | .uint32('link') 30 | .uint32('info') 31 | .uint32('addralign') 32 | .uint32('entsize'); 33 | 34 | var ELF32SectionHeaderTable = new Parser().array('items', { 35 | type: ELF32SectionHeader, 36 | length: function (vars) { 37 | return vars.shnum; 38 | }, 39 | }); 40 | 41 | var ELF32SectionHeaderStringTable = new Parser().seek(1).array('items', { 42 | type: new Parser().string('name', { zeroTerminated: true }), 43 | lengthInBytes: function (vars) { 44 | var shstr = vars.section_headers.items[vars.shstrndx]; 45 | return shstr.size - 1; 46 | }, 47 | }); 48 | 49 | var ELF32Header = new Parser() 50 | .endianess('little') 51 | .buffer('ident', { length: 16 }) 52 | .uint16('type') 53 | .uint16('machine') 54 | .uint32('version') 55 | .uint32('entry') 56 | .uint32('phoff') 57 | .uint32('shoff') 58 | .uint32('flags') 59 | .uint16('ehsize') 60 | .uint16('phentsize') 61 | .uint16('phnum') 62 | .uint16('shentsize') 63 | .uint16('shnum') 64 | .uint16('shstrndx') 65 | .pointer('program_headers', { 66 | type: ELF32ProgramHeaderTable, 67 | offset: 'phoff', 68 | }) 69 | .pointer('section_headers', { 70 | type: ELF32SectionHeaderTable, 71 | offset: 'shoff', 72 | }) 73 | .pointer('strings', { 74 | type: ELF32SectionHeaderStringTable, 75 | offset: function () { 76 | var shstr = vars.section_headers.items[vars.shstrndx]; 77 | return shstr.offset; 78 | }, 79 | }); 80 | 81 | require('fs').readFile('hello', function (err, data) { 82 | var result = ELF32Header.parse(data); 83 | console.log(require('util').inspect(result, { depth: null })); 84 | }); 85 | -------------------------------------------------------------------------------- /example/hello: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ericbla/binary-parser/295c6ec0337e86f9517c17949156d3984b83be03/example/hello -------------------------------------------------------------------------------- /example/ip.js: -------------------------------------------------------------------------------- 1 | var Parser = require('../dist/binary_parser').Parser; 2 | 3 | var ipHeader = new Parser() 4 | .endianess('big') 5 | .bit4('version') 6 | .bit4('headerLength') 7 | .uint8('tos') 8 | .uint16('packetLength') 9 | .uint16('id') 10 | .bit3('offset') 11 | .bit13('fragOffset') 12 | .uint8('ttl') 13 | .uint8('protocol') 14 | .uint16('checksum') 15 | .array('src', { 16 | type: 'uint8', 17 | length: 4, 18 | }) 19 | .array('dst', { 20 | type: 'uint8', 21 | length: 4, 22 | }); 23 | 24 | var buf = Buffer.from('450002c5939900002c06ef98adc24f6c850186d1', 'hex'); 25 | 26 | console.log(ipHeader.parse(buf)); 27 | -------------------------------------------------------------------------------- /example/jpg.js: -------------------------------------------------------------------------------- 1 | var Parser = require('../dist/binary_parser').Parser; 2 | 3 | var SOI = Parser.start(); 4 | 5 | var EOI = Parser.start(); 6 | 7 | var APP0 = Parser.start() 8 | .endianess('big') 9 | .uint16('length') 10 | .string('id', { 11 | encoding: 'ascii', 12 | zeroTerminated: true, 13 | assert: 'JFIF', 14 | }) 15 | .uint16('version') 16 | .uint8('unit') 17 | .uint16('xDensity') 18 | .uint16('yDensity') 19 | .uint8('thumbWidth') 20 | .uint8('thumbHeight') 21 | .array('thumbData', { 22 | type: 'uint8', 23 | length: function () { 24 | return this.Xt * this.Yt * 3; 25 | }, 26 | }); 27 | 28 | var COM = Parser.start() 29 | .endianess('big') 30 | .uint16('length') 31 | .string('comment', { 32 | encoding: 'ascii', 33 | length: function () { 34 | return this.length - 2; 35 | }, 36 | }); 37 | 38 | var SOS = Parser.start() 39 | .endianess('big') 40 | .uint16('length') 41 | .uint8('componentCount') 42 | .array('components', { 43 | type: Parser.start().uint8('id').uint8('dht'), 44 | length: 'componentCount', 45 | }) 46 | .uint8('spectrumStart') 47 | .uint8('spectrumEnd') 48 | .uint8('spectrumSelect'); 49 | 50 | var DQT = Parser.start() 51 | .endianess('big') 52 | .uint16('length') 53 | .array('tables', { 54 | type: Parser.start().uint8('precisionAndTableId').array('table', { 55 | type: 'uint8', 56 | length: 64, 57 | }), 58 | length: function () { 59 | return (this.length - 2) / 65; 60 | }, 61 | }); 62 | 63 | var SOF0 = Parser.start() 64 | .endianess('big') 65 | .uint16('length') 66 | .uint8('precision') 67 | .uint16('width') 68 | .uint16('height') 69 | .uint8('componentCount') 70 | .array('components', { 71 | type: Parser.start() 72 | .uint8('id') 73 | .uint8('samplingFactor') 74 | .uint8('quantizationTableId'), 75 | length: 'componentCount', 76 | }); 77 | 78 | var Ignore = Parser.start() 79 | .endianess('big') 80 | .uint16('length') 81 | .seek(function () { 82 | return this.length - 2; 83 | }); 84 | 85 | var Segment = Parser.start() 86 | .endianess('big') 87 | .uint16('marker') 88 | .choice('segment', { 89 | tag: 'marker', 90 | choices: { 91 | 0xffd8: SOI, 92 | 0xffd9: EOI, 93 | 0xffe0: APP0, 94 | 0xffda: SOS, 95 | 0xffdb: DQT, 96 | 0xffc0: SOF0, 97 | }, 98 | defaultChoice: Ignore, 99 | }); 100 | 101 | var JPEG = Parser.start().array('segments', { 102 | type: Segment, 103 | readUntil: 'eof', 104 | }); 105 | 106 | require('fs').readFile('test.jpg', function (err, data) { 107 | console.log(require('util').inspect(JPEG.parse(data), { depth: null })); 108 | }); 109 | -------------------------------------------------------------------------------- /example/mbr.js: -------------------------------------------------------------------------------- 1 | var Parser = require('../dist/binary_parser').Parser; 2 | var fs = require('fs'); 3 | 4 | var chs = new Parser({ 5 | formatter: function (val) { 6 | val.cylinder |= val.cylinderHigh << 8; 7 | delete val.cylinderHigh; 8 | return val; 9 | }, 10 | }) 11 | .uint8('head') 12 | .bit2('cylinderHigh') 13 | .bit6('sector') 14 | .uint8('cylinder'); 15 | 16 | var partitionTable = new Parser() 17 | .uint8('bootFlag') 18 | .nest('startCHS', { 19 | type: chs, 20 | }) 21 | .uint8('type') 22 | .nest('endCHS', { 23 | type: chs, 24 | }) 25 | .uint32le('startLBA') 26 | .uint32le('endLBA'); 27 | 28 | var mbrParser = new Parser() 29 | .seek(446) 30 | .array('partitionTables', { 31 | type: partitionTable, 32 | length: 4, 33 | }) 34 | .int16be('signature', { 35 | assert: 0x55aa, 36 | }); 37 | 38 | fs.readFile('raspbian.img', function (err, data) { 39 | console.dir(mbrParser.parse(data), { depth: null, colors: true }); 40 | }); 41 | -------------------------------------------------------------------------------- /example/raspbian.img: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ericbla/binary-parser/295c6ec0337e86f9517c17949156d3984b83be03/example/raspbian.img -------------------------------------------------------------------------------- /example/tar.js: -------------------------------------------------------------------------------- 1 | var Parser = require('../dist/binary_parser').Parser; 2 | var fs = require('fs'); 3 | 4 | var oct2int = function (s) { 5 | return parseInt(s, 8); 6 | }; 7 | 8 | var tarHeader = new Parser() 9 | .string('name', { length: 100, stripNull: true }) 10 | .string('mode', { length: 8, stripNull: true, formatter: oct2int }) 11 | .string('uid', { length: 8, stripNull: true, formatter: oct2int }) 12 | .string('gid', { length: 8, stripNull: true, formatter: oct2int }) 13 | .string('size', { length: 12, stripNull: true, formatter: oct2int }) 14 | .string('mtime', { length: 12, stripNull: true, formatter: oct2int }) 15 | .string('chksum', { length: 8, stripNull: true, formatter: oct2int }) 16 | .string('typeflag', { length: 1, stripNull: true, formatter: oct2int }) 17 | .string('linkname', { length: 100, stripNull: true }) 18 | .string('magic', { length: 6, stripNull: true }) 19 | .string('version', { length: 2, stripNull: true, formatter: oct2int }) 20 | .string('uname', { length: 32, stripNull: true }) 21 | .string('gname', { length: 32, stripNull: true }) 22 | .string('devmajor', { length: 8, stripNull: true, formatter: oct2int }) 23 | .string('devminor', { length: 8, stripNull: true, formatter: oct2int }) 24 | .string('prefix', { length: 155, stripNull: true }) 25 | .seek(12); 26 | 27 | var tarItem = new Parser() 28 | .nest({ 29 | type: tarHeader, 30 | }) 31 | .seek(function () { 32 | return Math.ceil(this.size / 512) * 512; 33 | }); 34 | 35 | var tarArchive = new Parser().array('files', { 36 | type: tarItem, 37 | readUntil: 'eof', 38 | }); 39 | 40 | fs.readFile('test.tar', function (err, data) { 41 | console.dir(tarArchive.parse(data), { depth: null, colors: true }); 42 | }); 43 | -------------------------------------------------------------------------------- /example/tcp.js: -------------------------------------------------------------------------------- 1 | var Parser = require('../dist/binary_parser').Parser; 2 | 3 | var tcpHeader = new Parser() 4 | .endianess('big') 5 | .uint16('srcPort') 6 | .uint16('dstPort') 7 | .uint32('seq') 8 | .uint32('ack') 9 | .bit4('dataOffset') 10 | .bit6('reserved') 11 | .nest('flags', { 12 | type: new Parser() 13 | .bit1('urg') 14 | .bit1('ack') 15 | .bit1('psh') 16 | .bit1('rst') 17 | .bit1('syn') 18 | .bit1('fin'), 19 | }) 20 | .uint16('windowSize') 21 | .uint16('checksum') 22 | .uint16('urgentPointer'); 23 | 24 | var buf = Buffer.from( 25 | 'e8a203e108e177e13d20756b801829d3004100000101080a2ea486ba793310bc', 26 | 'hex' 27 | ); 28 | 29 | console.log(tcpHeader.parse(buf)); 30 | -------------------------------------------------------------------------------- /example/test.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ericbla/binary-parser/295c6ec0337e86f9517c17949156d3984b83be03/example/test.bmp -------------------------------------------------------------------------------- /example/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ericbla/binary-parser/295c6ec0337e86f9517c17949156d3984b83be03/example/test.jpg -------------------------------------------------------------------------------- /example/test.tar: -------------------------------------------------------------------------------- 1 | a/000755 000765 000024 00000000000 13225560100 011667 5ustar00keichistaff000000 000000 a/h000644 000765 000024 00000000054 13225560100 012040 0ustar00keichistaff000000 000000 hhhhhhhhhh 2 | hhhhhhhhhh 3 | hhhhhhhhhh 4 | hhhhhhhhhh 5 | a/c/000755 000765 000024 00000000000 13225560070 012117 5ustar00keichistaff000000 000000 a/b/000755 000765 000024 00000000000 13225560050 012114 5ustar00keichistaff000000 000000 a/b/e000644 000765 000024 00000000013 13225560050 012255 0ustar00keichistaff000000 000000 eeeeeeeeee 6 | a/c/g000644 000765 000024 00000000041 13225560070 012263 0ustar00keichistaff000000 000000 gggggggggg 7 | gggggggggg 8 | gggggggggg 9 | a/c/d/000755 000765 000024 00000000000 13225560061 012342 5ustar00keichistaff000000 000000 a/c/d/f000644 000765 000024 00000000026 13225560061 012510 0ustar00keichistaff000000 000000 ffffffffff 10 | ffffffffff 11 | -------------------------------------------------------------------------------- /lib/context.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from './binary_parser'; 2 | 3 | export class Context { 4 | code = ''; 5 | scopes = [['vars']]; 6 | bitFields: Parser[] = []; 7 | tmpVariableCount = 0; 8 | references: { [key: string]: { resolved: boolean; requested: boolean } } = {}; 9 | importPath: string; 10 | imports: any[] = []; 11 | reverseImports = new Map(); 12 | 13 | constructor(importPath?: string) { 14 | this.importPath = importPath; 15 | } 16 | 17 | generateVariable(name?: string) { 18 | const arr = []; 19 | 20 | const scopes = this.scopes[this.scopes.length - 1]; 21 | arr.push(...scopes); 22 | if (name) { 23 | arr.push(name); 24 | } 25 | 26 | return arr.join('.'); 27 | } 28 | 29 | generateOption(val: number | string | Function) { 30 | switch (typeof val) { 31 | case 'number': 32 | return val.toString(); 33 | case 'string': 34 | return this.generateVariable(val); 35 | case 'function': 36 | return `${this.addImport(val)}.call(${this.generateVariable()}, vars)`; 37 | } 38 | } 39 | 40 | generateError(err: string) { 41 | this.pushCode('throw new Error(' + err + ');'); 42 | } 43 | 44 | generateTmpVariable() { 45 | return '$tmp' + this.tmpVariableCount++; 46 | } 47 | 48 | pushCode(code: string) { 49 | this.code += code + '\n'; 50 | } 51 | 52 | pushPath(name: string) { 53 | if (name) { 54 | this.scopes[this.scopes.length - 1].push(name); 55 | } 56 | } 57 | 58 | popPath(name: string) { 59 | if (name) { 60 | this.scopes[this.scopes.length - 1].pop(); 61 | } 62 | } 63 | 64 | pushScope(name: string) { 65 | this.scopes.push([name]); 66 | } 67 | 68 | popScope() { 69 | this.scopes.pop(); 70 | } 71 | 72 | addImport(im: any) { 73 | if (!this.importPath) return `(${im})`; 74 | let id = this.reverseImports.get(im); 75 | if (!id) { 76 | id = this.imports.push(im) - 1; 77 | this.reverseImports.set(im, id); 78 | } 79 | return `${this.importPath}[${id}]`; 80 | } 81 | 82 | addReference(alias: string) { 83 | if (this.references[alias]) return; 84 | this.references[alias] = { resolved: false, requested: false }; 85 | } 86 | 87 | markResolved(alias: string) { 88 | this.references[alias].resolved = true; 89 | } 90 | 91 | markRequested(aliasList: string[]) { 92 | aliasList.forEach((alias) => { 93 | this.references[alias].requested = true; 94 | }); 95 | } 96 | 97 | getUnresolvedReferences() { 98 | const references = this.references; 99 | return Object.keys(this.references).filter( 100 | (alias) => !references[alias].resolved && !references[alias].requested 101 | ); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "binary-parser-encoder", 3 | "version": "1.5.3", 4 | "description": "Blazing-fast binary parser builder", 5 | "main": "dist/binary_parser.js", 6 | "types": "dist/binary_parser.d.ts", 7 | "devDependencies": { 8 | "mocha": "^8.2.0", 9 | "nyc": "^15.1.0", 10 | "prettier": "^2.1.2", 11 | "@types/node": "^14.14.0", 12 | "typescript": "^4.0.3" 13 | }, 14 | "scripts": { 15 | "build": "tsc", 16 | "fmt": "prettier --write \"{lib,example,test}/**/*.{ts,js}\"", 17 | "check-fmt": "prettier --list-different \"{lib,example,test}/**/*.{ts,js}\"", 18 | "test": "mocha", 19 | "test-browser": "parcel test/browser.html --open", 20 | "cover": "nyc --reporter html mocha", 21 | "prepare": "npm run build" 22 | }, 23 | "files": [ 24 | "dist/*.js", 25 | "dist/binary_parser.d.ts" 26 | ], 27 | "keywords": [ 28 | "binary", 29 | "parser", 30 | "decode", 31 | "encoder", 32 | "encode", 33 | "unpack", 34 | "struct", 35 | "buffer", 36 | "bit" 37 | ], 38 | "author": { 39 | "name": "Keichi Takahashi", 40 | "email": "keichi.t@me.com", 41 | "url": "https://keichi.net/" 42 | }, 43 | "contributors": [ 44 | { 45 | "name": "Eric Blanchard", 46 | "email": "ericbla@gmail.com" 47 | } 48 | ], 49 | "license": "MIT", 50 | "repository": { 51 | "type": "git", 52 | "url": "http://github.com/Ericbla/binary-parser.git" 53 | }, 54 | "bugs": "http://github.com/Ericbla/binary-parser/issues", 55 | "dependencies": { 56 | "smart-buffer": "^4.1.0" 57 | }, 58 | "engines": { 59 | "node": ">=8.9.0" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /test/browser.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Mocha Tests 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 18 | 19 | 20 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /test/composite_parser.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var TextEncoder = 3 | typeof TextEncoder === 'undefined' 4 | ? require('util').TextEncoder 5 | : TextEncoder; 6 | var Parser = require('../dist/binary_parser').Parser; 7 | 8 | const suite = (Buffer) => 9 | describe(`Composite parser (${Buffer.name})`, function () { 10 | function hexToBuf(hex) { 11 | return Buffer.from( 12 | hex.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)) 13 | ); 14 | } 15 | describe('Array parser', function () { 16 | it('should parse array of primitive types', function () { 17 | var parser = Parser.start().uint8('length').array('message', { 18 | length: 'length', 19 | type: 'uint8', 20 | }); 21 | 22 | var buffer = Buffer.from([12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); 23 | assert.deepEqual(parser.parse(buffer), { 24 | length: 12, 25 | message: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], 26 | }); 27 | }); 28 | it('should parse array of primitive types with lengthInBytes', function () { 29 | var parser = Parser.start().uint8('length').array('message', { 30 | lengthInBytes: 'length', 31 | type: 'uint8', 32 | }); 33 | 34 | var buffer = Buffer.from([12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); 35 | assert.deepEqual(parser.parse(buffer), { 36 | length: 12, 37 | message: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], 38 | }); 39 | }); 40 | it('should parse array of user defined types', function () { 41 | var elementParser = new Parser().uint8('key').int16le('value'); 42 | 43 | var parser = Parser.start().uint16le('length').array('message', { 44 | length: 'length', 45 | type: elementParser, 46 | }); 47 | 48 | var buffer = Buffer.from([ 49 | 0x02, 50 | 0x00, 51 | 0xca, 52 | 0xd2, 53 | 0x04, 54 | 0xbe, 55 | 0xd3, 56 | 0x04, 57 | ]); 58 | assert.deepEqual(parser.parse(buffer), { 59 | length: 0x02, 60 | message: [ 61 | { key: 0xca, value: 1234 }, 62 | { key: 0xbe, value: 1235 }, 63 | ], 64 | }); 65 | }); 66 | it('should parse array of user defined types with lengthInBytes', function () { 67 | var elementParser = new Parser().uint8('key').int16le('value'); 68 | 69 | var parser = Parser.start().uint16le('length').array('message', { 70 | lengthInBytes: 'length', 71 | type: elementParser, 72 | }); 73 | 74 | var buffer = Buffer.from([ 75 | 0x06, 76 | 0x00, 77 | 0xca, 78 | 0xd2, 79 | 0x04, 80 | 0xbe, 81 | 0xd3, 82 | 0x04, 83 | ]); 84 | assert.deepEqual(parser.parse(buffer), { 85 | length: 0x06, 86 | message: [ 87 | { key: 0xca, value: 1234 }, 88 | { key: 0xbe, value: 1235 }, 89 | ], 90 | }); 91 | }); 92 | it('should parse array of user defined types with lengthInBytes literal', function () { 93 | var elementParser = new Parser().uint8('key').int16le('value'); 94 | 95 | var parser = Parser.start().array('message', { 96 | lengthInBytes: 0x06, 97 | type: elementParser, 98 | }); 99 | 100 | var buffer = Buffer.from([0xca, 0xd2, 0x04, 0xbe, 0xd3, 0x04]); 101 | assert.deepEqual(parser.parse(buffer), { 102 | message: [ 103 | { key: 0xca, value: 1234 }, 104 | { key: 0xbe, value: 1235 }, 105 | ], 106 | }); 107 | }); 108 | it('should parse array of user defined types with lengthInBytes function', function () { 109 | var elementParser = new Parser().uint8('key').int16le('value'); 110 | 111 | var parser = Parser.start() 112 | .uint16le('length') 113 | .array('message', { 114 | lengthInBytes: function () { 115 | return this.length; 116 | }, 117 | type: elementParser, 118 | }); 119 | 120 | var buffer = Buffer.from([ 121 | 0x06, 122 | 0x00, 123 | 0xca, 124 | 0xd2, 125 | 0x04, 126 | 0xbe, 127 | 0xd3, 128 | 0x04, 129 | ]); 130 | assert.deepEqual(parser.parse(buffer), { 131 | length: 0x06, 132 | message: [ 133 | { key: 0xca, value: 1234 }, 134 | { key: 0xbe, value: 1235 }, 135 | ], 136 | }); 137 | }); 138 | it('should parse array of arrays', function () { 139 | var rowParser = Parser.start().uint8('length').array('cols', { 140 | length: 'length', 141 | type: 'int32le', 142 | }); 143 | 144 | var parser = Parser.start().uint8('length').array('rows', { 145 | length: 'length', 146 | type: rowParser, 147 | }); 148 | 149 | var size = 1 + 10 * (1 + 5 * 4); 150 | var buffer = Buffer.alloc ? Buffer.alloc(size) : new Buffer(size); 151 | var dataView = new DataView(buffer.buffer); 152 | var i, j; 153 | 154 | var iterator = 0; 155 | buffer[iterator] = 10; 156 | iterator += 1; 157 | for (i = 0; i < 10; i++) { 158 | buffer[iterator] = 5; 159 | iterator += 1; 160 | for (j = 0; j < 5; j++) { 161 | dataView.setInt32(iterator, i * j, true); 162 | iterator += 4; 163 | } 164 | } 165 | 166 | assert.deepEqual(parser.parse(buffer), { 167 | length: 10, 168 | rows: [ 169 | { length: 5, cols: [0, 0, 0, 0, 0] }, 170 | { length: 5, cols: [0, 1, 2, 3, 4] }, 171 | { length: 5, cols: [0, 2, 4, 6, 8] }, 172 | { length: 5, cols: [0, 3, 6, 9, 12] }, 173 | { length: 5, cols: [0, 4, 8, 12, 16] }, 174 | { length: 5, cols: [0, 5, 10, 15, 20] }, 175 | { length: 5, cols: [0, 6, 12, 18, 24] }, 176 | { length: 5, cols: [0, 7, 14, 21, 28] }, 177 | { length: 5, cols: [0, 8, 16, 24, 32] }, 178 | { length: 5, cols: [0, 9, 18, 27, 36] }, 179 | ], 180 | }); 181 | }); 182 | it('should parse until eof when readUntil is specified', function () { 183 | var parser = Parser.start().array('data', { 184 | readUntil: 'eof', 185 | type: 'uint8', 186 | }); 187 | 188 | var buffer = Buffer.from([ 189 | 0xff, 190 | 0xff, 191 | 0xff, 192 | 0xff, 193 | 0xff, 194 | 0xff, 195 | 0xff, 196 | 0xff, 197 | 0xff, 198 | 0xff, 199 | ]); 200 | assert.deepEqual(parser.parse(buffer), { 201 | data: [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff], 202 | }); 203 | }); 204 | it('should parse until function returns true when readUntil is function', function () { 205 | var parser = Parser.start().array('data', { 206 | readUntil: function (item, buf) { 207 | return item === 0; 208 | }, 209 | type: 'uint8', 210 | }); 211 | 212 | var buffer = Buffer.from([ 213 | 0xff, 214 | 0xff, 215 | 0xff, 216 | 0x01, 217 | 0x00, 218 | 0xff, 219 | 0xff, 220 | 0xff, 221 | 0xff, 222 | 0xff, 223 | ]); 224 | assert.deepEqual(parser.parse(buffer), { 225 | data: [0xff, 0xff, 0xff, 0x01, 0x00], 226 | }); 227 | }); 228 | it('should parse until function returns true when readUntil is function (using read-ahead)', function () { 229 | var parser = Parser.start().array('data', { 230 | readUntil: function (item, buf) { 231 | return buf.length > 0 && buf[0] === 0; 232 | }, 233 | type: 'uint8', 234 | }); 235 | 236 | var buffer = Buffer.from([ 237 | 0xff, 238 | 0xff, 239 | 0xff, 240 | 0x01, 241 | 0x00, 242 | 0xff, 243 | 0xff, 244 | 0xff, 245 | 0xff, 246 | 0xff, 247 | ]); 248 | assert.deepEqual(parser.parse(buffer), { 249 | data: [0xff, 0xff, 0xff, 0x01], 250 | }); 251 | }); 252 | it('should parse associative arrays', function () { 253 | var parser = Parser.start() 254 | .int8('numlumps') 255 | .array('lumps', { 256 | type: Parser.start() 257 | .int32le('filepos') 258 | .int32le('size') 259 | .string('name', { length: 8, encoding: 'utf8' }), 260 | length: 'numlumps', 261 | key: 'name', 262 | }); 263 | 264 | var buffer = Buffer.from([ 265 | 0x02, 266 | 0xd2, 267 | 0x04, 268 | 0x00, 269 | 0x00, 270 | 0x2e, 271 | 0x16, 272 | 0x00, 273 | 0x00, 274 | 0x41, 275 | 0x41, 276 | 0x41, 277 | 0x41, 278 | 0x41, 279 | 0x41, 280 | 0x41, 281 | 0x41, 282 | 0x2e, 283 | 0x16, 284 | 0x00, 285 | 0x00, 286 | 0xd2, 287 | 0x04, 288 | 0x00, 289 | 0x00, 290 | 0x62, 291 | 0x62, 292 | 0x62, 293 | 0x62, 294 | 0x62, 295 | 0x62, 296 | 0x62, 297 | 0x62, 298 | ]); 299 | assert.deepEqual(parser.parse(buffer), { 300 | numlumps: 2, 301 | lumps: { 302 | AAAAAAAA: { 303 | filepos: 1234, 304 | size: 5678, 305 | name: 'AAAAAAAA', 306 | }, 307 | bbbbbbbb: { 308 | filepos: 5678, 309 | size: 1234, 310 | name: 'bbbbbbbb', 311 | }, 312 | }, 313 | }); 314 | }); 315 | it('should use formatter to transform parsed array', function () { 316 | var parser = Parser.start().array('data', { 317 | type: 'uint8', 318 | length: 4, 319 | formatter: function (arr) { 320 | return arr.join('.'); 321 | }, 322 | }); 323 | 324 | var buffer = Buffer.from([0x0a, 0x0a, 0x01, 0x6e]); 325 | assert.deepEqual(parser.parse(buffer), { 326 | data: '10.10.1.110', 327 | }); 328 | }); 329 | it('should be able to go into recursion', function () { 330 | var parser = Parser.start() 331 | .namely('self') 332 | .uint8('length') 333 | .array('data', { 334 | type: 'self', 335 | length: 'length', 336 | }); 337 | 338 | var buffer = Buffer.from([1, 1, 1, 0]); 339 | assert.deepEqual(parser.parse(buffer), { 340 | length: 1, 341 | data: [ 342 | { 343 | length: 1, 344 | data: [ 345 | { 346 | length: 1, 347 | data: [{ length: 0, data: [] }], 348 | }, 349 | ], 350 | }, 351 | ], 352 | }); 353 | }); 354 | it('should be able to go into even deeper recursion', function () { 355 | var parser = Parser.start() 356 | .namely('self') 357 | .uint8('length') 358 | .array('data', { 359 | type: 'self', 360 | length: 'length', 361 | }); 362 | 363 | // 2 364 | // / \ 365 | // 3 1 366 | // / | \ \ 367 | // 1 0 2 0 368 | // / / \ 369 | // 0 1 0 370 | // / 371 | // 0 372 | 373 | var buffer = Buffer.from([ 374 | 2, 375 | /* 0 */ 3, 376 | /* 0 */ 1, 377 | /* 0 */ 0, 378 | /* 1 */ 0, 379 | /* 2 */ 2, 380 | /* 0 */ 1, 381 | /* 0 */ 0, 382 | /* 1 */ 0, 383 | /* 1 */ 1, 384 | /* 0 */ 0, 385 | ]); 386 | assert.deepEqual(parser.parse(buffer), { 387 | length: 2, 388 | data: [ 389 | { 390 | length: 3, 391 | data: [ 392 | { length: 1, data: [{ length: 0, data: [] }] }, 393 | { length: 0, data: [] }, 394 | { 395 | length: 2, 396 | data: [ 397 | { length: 1, data: [{ length: 0, data: [] }] }, 398 | { length: 0, data: [] }, 399 | ], 400 | }, 401 | ], 402 | }, 403 | { 404 | length: 1, 405 | data: [{ length: 0, data: [] }], 406 | }, 407 | ], 408 | }); 409 | }); 410 | 411 | it('should allow parent parser attributes as choice key', function () { 412 | var ChildParser = Parser.start().choice('data', { 413 | tag: function (vars) { 414 | return vars.version; 415 | }, 416 | choices: { 417 | 1: Parser.start().uint8('v1'), 418 | 2: Parser.start().uint16('v2'), 419 | }, 420 | }); 421 | 422 | var ParentParser = Parser.start() 423 | .uint8('version') 424 | .nest('child', { type: ChildParser }); 425 | 426 | var buffer = Buffer.from([0x1, 0x2]); 427 | assert.deepEqual(ParentParser.parse(buffer), { 428 | version: 1, 429 | child: { data: { v1: 2 } }, 430 | }); 431 | 432 | buffer = Buffer.from([0x2, 0x3, 0x4]); 433 | assert.deepEqual(ParentParser.parse(buffer), { 434 | version: 2, 435 | child: { data: { v2: 0x0304 } }, 436 | }); 437 | }); 438 | }); 439 | 440 | describe('Choice parser', function () { 441 | it('should parse choices of primitive types', function () { 442 | var parser = Parser.start() 443 | .uint8('tag1') 444 | .choice('data1', { 445 | tag: 'tag1', 446 | choices: { 447 | 0: 'int32le', 448 | 1: 'int16le', 449 | }, 450 | }) 451 | .uint8('tag2') 452 | .choice('data2', { 453 | tag: 'tag2', 454 | choices: { 455 | 0: 'int32le', 456 | 1: 'int16le', 457 | }, 458 | }); 459 | 460 | var buffer = Buffer.from([ 461 | 0x0, 462 | 0x4e, 463 | 0x61, 464 | 0xbc, 465 | 0x00, 466 | 0x01, 467 | 0xd2, 468 | 0x04, 469 | ]); 470 | assert.deepEqual(parser.parse(buffer), { 471 | tag1: 0, 472 | data1: 12345678, 473 | tag2: 1, 474 | data2: 1234, 475 | }); 476 | }); 477 | it('should parse default choice', function () { 478 | var parser = Parser.start() 479 | .uint8('tag') 480 | .choice('data', { 481 | tag: 'tag', 482 | choices: { 483 | 0: 'int32le', 484 | 1: 'int16le', 485 | }, 486 | defaultChoice: 'uint8', 487 | }) 488 | .int32le('test'); 489 | 490 | var buffer = Buffer.from([0x03, 0xff, 0x2f, 0xcb, 0x04, 0x0]); 491 | assert.deepEqual(parser.parse(buffer), { 492 | tag: 3, 493 | data: 0xff, 494 | test: 314159, 495 | }); 496 | }); 497 | it('should parse choices of user defied types', function () { 498 | var parser = Parser.start() 499 | .uint8('tag') 500 | .choice('data', { 501 | tag: 'tag', 502 | choices: { 503 | 1: Parser.start() 504 | .uint8('length') 505 | .string('message', { length: 'length' }), 506 | 3: Parser.start().int32le('number'), 507 | }, 508 | }); 509 | 510 | var buffer = Buffer.from([ 511 | 0x1, 512 | 0xc, 513 | 0x68, 514 | 0x65, 515 | 0x6c, 516 | 0x6c, 517 | 0x6f, 518 | 0x2c, 519 | 0x20, 520 | 0x77, 521 | 0x6f, 522 | 0x72, 523 | 0x6c, 524 | 0x64, 525 | ]); 526 | assert.deepEqual(parser.parse(buffer), { 527 | tag: 1, 528 | data: { 529 | length: 12, 530 | message: 'hello, world', 531 | }, 532 | }); 533 | buffer = Buffer.from([0x03, 0x4e, 0x61, 0xbc, 0x00]); 534 | assert.deepEqual(parser.parse(buffer), { 535 | tag: 3, 536 | data: { 537 | number: 12345678, 538 | }, 539 | }); 540 | }); 541 | it('should be able to go into recursion', function () { 542 | var stop = Parser.start(); 543 | 544 | var parser = Parser.start() 545 | .namely('self') 546 | .uint8('type') 547 | .choice('data', { 548 | tag: 'type', 549 | choices: { 550 | 0: stop, 551 | 1: 'self', 552 | }, 553 | }); 554 | 555 | var buffer = Buffer.from([1, 1, 1, 0]); 556 | assert.deepEqual(parser.parse(buffer), { 557 | type: 1, 558 | data: { 559 | type: 1, 560 | data: { 561 | type: 1, 562 | data: { type: 0, data: {} }, 563 | }, 564 | }, 565 | }); 566 | }); 567 | it('should be able to go into recursion with simple nesting', function () { 568 | var stop = Parser.start(); 569 | 570 | var parser = Parser.start() 571 | .namely('self') 572 | .uint8('type') 573 | .choice('data', { 574 | tag: 'type', 575 | choices: { 576 | 0: stop, 577 | 1: 'self', 578 | 2: Parser.start() 579 | .nest('left', { type: 'self' }) 580 | .nest('right', { type: stop }), 581 | }, 582 | }); 583 | 584 | var buffer = Buffer.from([2, /* left */ 1, 1, 0, /* right */ 0]); 585 | assert.deepEqual(parser.parse(buffer), { 586 | type: 2, 587 | data: { 588 | left: { 589 | type: 1, 590 | data: { type: 1, data: { type: 0, data: {} } }, 591 | }, 592 | right: {}, 593 | }, 594 | }); 595 | }); 596 | it('should be able to refer to other parsers by name', function () { 597 | var parser = Parser.start().namely('self'); 598 | 599 | var stop = Parser.start().namely('stop'); 600 | 601 | var twoCells = Parser.start() 602 | .namely('twoCells') 603 | .nest('left', { type: 'self' }) 604 | .nest('right', { type: 'stop' }); 605 | 606 | parser.uint8('type').choice('data', { 607 | tag: 'type', 608 | choices: { 609 | 0: 'stop', 610 | 1: 'self', 611 | 2: 'twoCells', 612 | }, 613 | }); 614 | 615 | var buffer = Buffer.from([2, /* left */ 1, 1, 0, /* right */ 0]); 616 | assert.deepEqual(parser.parse(buffer), { 617 | type: 2, 618 | data: { 619 | left: { 620 | type: 1, 621 | data: { type: 1, data: { type: 0, data: {} } }, 622 | }, 623 | right: {}, 624 | }, 625 | }); 626 | }); 627 | it('should be able to refer to other parsers both directly and by name', function () { 628 | var parser = Parser.start().namely('self'); 629 | 630 | var stop = Parser.start(); 631 | 632 | var twoCells = Parser.start() 633 | .nest('left', { type: 'self' }) 634 | .nest('right', { type: stop }); 635 | 636 | parser.uint8('type').choice('data', { 637 | tag: 'type', 638 | choices: { 639 | 0: stop, 640 | 1: 'self', 641 | 2: twoCells, 642 | }, 643 | }); 644 | 645 | var buffer = Buffer.from([2, /* left */ 1, 1, 0, /* right */ 0]); 646 | assert.deepEqual(parser.parse(buffer), { 647 | type: 2, 648 | data: { 649 | left: { 650 | type: 1, 651 | data: { type: 1, data: { type: 0, data: {} } }, 652 | }, 653 | right: {}, 654 | }, 655 | }); 656 | }); 657 | it('should be able to go into recursion with complex nesting', function () { 658 | var stop = Parser.start(); 659 | 660 | var parser = Parser.start() 661 | .namely('self') 662 | .uint8('type') 663 | .choice('data', { 664 | tag: 'type', 665 | choices: { 666 | 0: stop, 667 | 1: 'self', 668 | 2: Parser.start() 669 | .nest('left', { type: 'self' }) 670 | .nest('right', { type: 'self' }), 671 | 3: Parser.start() 672 | .nest('one', { type: 'self' }) 673 | .nest('two', { type: 'self' }) 674 | .nest('three', { type: 'self' }), 675 | }, 676 | }); 677 | 678 | // 2 679 | // / \ 680 | // 3 1 681 | // / | \ \ 682 | // 1 0 2 0 683 | // / / \ 684 | // 0 1 0 685 | // / 686 | // 0 687 | 688 | var buffer = Buffer.from([ 689 | 2, 690 | /* left -> */ 3, 691 | /* one -> */ 1, 692 | /* -> */ 0, 693 | /* two -> */ 0, 694 | /* three -> */ 2, 695 | /* left -> */ 1, 696 | /* -> */ 0, 697 | /* right -> */ 0, 698 | /* right -> */ 1, 699 | /* -> */ 0, 700 | ]); 701 | assert.deepEqual(parser.parse(buffer), { 702 | type: 2, 703 | data: { 704 | left: { 705 | type: 3, 706 | data: { 707 | one: { type: 1, data: { type: 0, data: {} } }, 708 | two: { type: 0, data: {} }, 709 | three: { 710 | type: 2, 711 | data: { 712 | left: { type: 1, data: { type: 0, data: {} } }, 713 | right: { type: 0, data: {} }, 714 | }, 715 | }, 716 | }, 717 | }, 718 | right: { 719 | type: 1, 720 | data: { type: 0, data: {} }, 721 | }, 722 | }, 723 | }); 724 | }); 725 | it("should be able to 'flatten' choices when using null varName", function () { 726 | var parser = Parser.start() 727 | .uint8('tag') 728 | .choice(null, { 729 | tag: 'tag', 730 | choices: { 731 | 1: Parser.start() 732 | .uint8('length') 733 | .string('message', { length: 'length' }), 734 | 3: Parser.start().int32le('number'), 735 | }, 736 | }); 737 | 738 | var buffer = Buffer.from([ 739 | 0x1, 740 | 0xc, 741 | 0x68, 742 | 0x65, 743 | 0x6c, 744 | 0x6c, 745 | 0x6f, 746 | 0x2c, 747 | 0x20, 748 | 0x77, 749 | 0x6f, 750 | 0x72, 751 | 0x6c, 752 | 0x64, 753 | ]); 754 | assert.deepEqual(parser.parse(buffer), { 755 | tag: 1, 756 | length: 12, 757 | message: 'hello, world', 758 | }); 759 | buffer = Buffer.from([0x03, 0x4e, 0x61, 0xbc, 0x00]); 760 | assert.deepEqual(parser.parse(buffer), { 761 | tag: 3, 762 | number: 12345678, 763 | }); 764 | }); 765 | it("should be able to 'flatten' choices when omitting varName paramater", function () { 766 | var parser = Parser.start() 767 | .uint8('tag') 768 | .choice({ 769 | tag: 'tag', 770 | choices: { 771 | 1: Parser.start() 772 | .uint8('length') 773 | .string('message', { length: 'length' }), 774 | 3: Parser.start().int32le('number'), 775 | }, 776 | }); 777 | 778 | var buffer = Buffer.from([ 779 | 0x1, 780 | 0xc, 781 | 0x68, 782 | 0x65, 783 | 0x6c, 784 | 0x6c, 785 | 0x6f, 786 | 0x2c, 787 | 0x20, 788 | 0x77, 789 | 0x6f, 790 | 0x72, 791 | 0x6c, 792 | 0x64, 793 | ]); 794 | assert.deepEqual(parser.parse(buffer), { 795 | tag: 1, 796 | length: 12, 797 | message: 'hello, world', 798 | }); 799 | buffer = Buffer.from([0x03, 0x4e, 0x61, 0xbc, 0x00]); 800 | assert.deepEqual(parser.parse(buffer), { 801 | tag: 3, 802 | number: 12345678, 803 | }); 804 | }); 805 | it('should be able to use function as the choice selector', function () { 806 | var parser = Parser.start() 807 | .string('selector', { length: 4 }) 808 | .choice(null, { 809 | tag: function () { 810 | return parseInt(this.selector, 2); // string base 2 to integer decimal 811 | }, 812 | choices: { 813 | 2: Parser.start() 814 | .uint8('length') 815 | .string('message', { length: 'length' }), 816 | 7: Parser.start().int32le('number'), 817 | }, 818 | }); 819 | 820 | var buffer = Buffer.from([ 821 | 48, 822 | 48, 823 | 49, 824 | 48, 825 | 0xc, 826 | 0x68, 827 | 0x65, 828 | 0x6c, 829 | 0x6c, 830 | 0x6f, 831 | 0x2c, 832 | 0x20, 833 | 0x77, 834 | 0x6f, 835 | 0x72, 836 | 0x6c, 837 | 0x64, 838 | ]); 839 | assert.deepEqual(parser.parse(buffer), { 840 | selector: '0010', // -> choice 2 841 | length: 12, 842 | message: 'hello, world', 843 | }); 844 | buffer = Buffer.from([48, 49, 49, 49, 0x4e, 0x61, 0xbc, 0x00]); 845 | assert.deepEqual(parser.parse(buffer), { 846 | selector: '0111', // -> choice 7 847 | number: 12345678, 848 | }); 849 | }); 850 | }); 851 | 852 | describe('Nest parser', function () { 853 | it('should parse nested parsers', function () { 854 | var nameParser = new Parser() 855 | .string('firstName', { 856 | zeroTerminated: true, 857 | }) 858 | .string('lastName', { 859 | zeroTerminated: true, 860 | }); 861 | var infoParser = new Parser().uint8('age'); 862 | var personParser = new Parser() 863 | .nest('name', { 864 | type: nameParser, 865 | }) 866 | .nest('info', { 867 | type: infoParser, 868 | }); 869 | 870 | var buffer = Buffer.from([ 871 | ...Buffer.from(new TextEncoder().encode('John\0Doe\0')), 872 | ...Buffer.from([0x20]), 873 | ]); 874 | assert.deepEqual(personParser.parse(buffer), { 875 | name: { 876 | firstName: 'John', 877 | lastName: 'Doe', 878 | }, 879 | info: { 880 | age: 0x20, 881 | }, 882 | }); 883 | }); 884 | 885 | it('should format parsed nested parser', function () { 886 | var nameParser = new Parser() 887 | .string('firstName', { 888 | zeroTerminated: true, 889 | }) 890 | .string('lastName', { 891 | zeroTerminated: true, 892 | }); 893 | var personParser = new Parser().nest('name', { 894 | type: nameParser, 895 | formatter: function (name) { 896 | return name.firstName + ' ' + name.lastName; 897 | }, 898 | }); 899 | 900 | var buffer = Buffer.from(new TextEncoder().encode('John\0Doe\0')); 901 | assert.deepEqual(personParser.parse(buffer), { 902 | name: 'John Doe', 903 | }); 904 | }); 905 | 906 | it("should 'flatten' output when using null varName", function () { 907 | var parser = new Parser() 908 | .string('s1', { zeroTerminated: true }) 909 | .nest(null, { 910 | type: new Parser().string('s2', { zeroTerminated: true }), 911 | }); 912 | 913 | var buf = Buffer.from(new TextEncoder().encode('foo\0bar\0')); 914 | 915 | assert.deepEqual(parser.parse(buf), { s1: 'foo', s2: 'bar' }); 916 | }); 917 | 918 | it("should 'flatten' output when omitting varName", function () { 919 | var parser = new Parser().string('s1', { zeroTerminated: true }).nest({ 920 | type: new Parser().string('s2', { zeroTerminated: true }), 921 | }); 922 | 923 | var buf = Buffer.from(new TextEncoder().encode('foo\0bar\0')); 924 | 925 | assert.deepEqual(parser.parse(buf), { s1: 'foo', s2: 'bar' }); 926 | }); 927 | }); 928 | 929 | describe('Constructors', function () { 930 | it('should create a custom object type', function () { 931 | function Person() { 932 | this.name = ''; 933 | } 934 | Person.prototype.toString = function () { 935 | return '[object Person]'; 936 | }; 937 | var parser = Parser.start().create(Person).string('name', { 938 | zeroTerminated: true, 939 | }); 940 | 941 | var buffer = Buffer.from(new TextEncoder().encode('John Doe\0')); 942 | var person = parser.parse(buffer); 943 | assert.ok(person instanceof Person); 944 | assert.equal(person.name, 'John Doe'); 945 | }); 946 | }); 947 | 948 | describe('Pointer parser', function () { 949 | it('should move pointer to specified offset', function () { 950 | var parser = Parser.start().pointer('x', { type: 'uint8', offset: 2 }); 951 | var buf = Buffer.from([0x1, 0x2, 0x3, 0x4, 0x5]); 952 | 953 | assert.deepEqual(parser.parse(buf), { x: 3 }); 954 | }); 955 | 956 | it('should restore pointer to original position', function () { 957 | var parser = Parser.start() 958 | .pointer('x', { type: 'uint8', offset: 2 }) 959 | .uint16be('y'); 960 | var buf = Buffer.from([0x1, 0x2, 0x3, 0x4, 0x5]); 961 | 962 | assert.deepEqual(parser.parse(buf), { x: 0x3, y: 0x0102 }); 963 | }); 964 | 965 | it('should work with child parser', function () { 966 | var parser = Parser.start() 967 | .uint32le('x') 968 | .pointer('y', { 969 | type: Parser.start().string('s', { zeroTerminated: true }), 970 | offset: 4, 971 | }); 972 | var buf = Buffer.from(new TextEncoder().encode('\1\2\3\4hello\0\6')); 973 | 974 | assert.deepEqual(parser.parse(buf), { 975 | x: 0x04030201, 976 | y: { s: 'hello' }, 977 | }); 978 | }); 979 | 980 | it('should pass variable context to child parser', function () {}); 981 | var parser = Parser.start() 982 | .uint16be('len') 983 | .pointer('child', { 984 | offset: 4, 985 | type: Parser.start().array('a', { 986 | type: 'uint8', 987 | length: function (vars) { 988 | return vars.len; 989 | }, 990 | }), 991 | }); 992 | var buf = Buffer.from(new TextEncoder().encode('\0\6\0\0\1\2\3\4\5\6')); 993 | 994 | assert.deepEqual(parser.parse(buf), { 995 | len: 6, 996 | child: { a: [1, 2, 3, 4, 5, 6] }, 997 | }); 998 | }); 999 | 1000 | describe('SaveOffset', () => { 1001 | it('should save the offset', () => { 1002 | const buff = Buffer.from([0x01, 0x00, 0x02]); 1003 | const parser = Parser.start() 1004 | .int8('a') 1005 | .int16('b') 1006 | .saveOffset('bytesRead'); 1007 | 1008 | assert.deepEqual(parser.parse(buff), { 1009 | a: 1, 1010 | b: 2, 1011 | bytesRead: 3, 1012 | }); 1013 | }); 1014 | 1015 | it('should save the offset if not at end', () => { 1016 | const buff = Buffer.from([0x01, 0x00, 0x02]); 1017 | const parser = Parser.start() 1018 | .int8('a') 1019 | .saveOffset('bytesRead') 1020 | .int16('b'); 1021 | 1022 | assert.deepEqual(parser.parse(buff), { 1023 | a: 1, 1024 | b: 2, 1025 | bytesRead: 1, 1026 | }); 1027 | }); 1028 | 1029 | it('should save the offset with a dynamic parser', () => { 1030 | const buff = Buffer.from([0x74, 0x65, 0x73, 0x74, 0x00]); 1031 | const parser = Parser.start() 1032 | .string('name', { zeroTerminated: true }) 1033 | .saveOffset('bytesRead'); 1034 | 1035 | assert.deepEqual(parser.parse(buff), { 1036 | name: 'test', 1037 | bytesRead: 5, 1038 | }); 1039 | }); 1040 | }); 1041 | 1042 | describe('Utilities', function () { 1043 | it('should count size for fixed size structs', function () { 1044 | var parser = Parser.start() 1045 | .int8('a') 1046 | .int32le('b') 1047 | .string('msg', { length: 10 }) 1048 | .seek(2) 1049 | .array('data', { 1050 | length: 3, 1051 | type: 'int8', 1052 | }) 1053 | .buffer('raw', { length: 8 }); 1054 | 1055 | assert.equal(parser.sizeOf(), 1 + 4 + 10 + 2 + 3 + 8); 1056 | }); 1057 | it('should assert parsed values', function () { 1058 | var parser = Parser.start().string('msg', { 1059 | encoding: 'utf8', 1060 | zeroTerminated: true, 1061 | assert: 'hello, world', 1062 | }); 1063 | var buffer = hexToBuf('68656c6c6f2c20776f726c6400'); 1064 | assert.doesNotThrow(function () { 1065 | parser.parse(buffer); 1066 | }); 1067 | 1068 | buffer = hexToBuf('68656c6c6f2c206a7300'); 1069 | assert.throws(function () { 1070 | parser.parse(buffer); 1071 | }); 1072 | 1073 | parser = new Parser() 1074 | .int16le('a') 1075 | .int16le('b') 1076 | .int16le('c', { 1077 | assert: function (x) { 1078 | return this.a + this.b === x; 1079 | }, 1080 | }); 1081 | 1082 | buffer = hexToBuf('d2042e16001b'); 1083 | assert.doesNotThrow(function () { 1084 | parser.parse(buffer); 1085 | }); 1086 | buffer = hexToBuf('2e16001bd204'); 1087 | assert.throws(function () { 1088 | parser.parse(buffer); 1089 | }); 1090 | }); 1091 | }); 1092 | 1093 | describe('Parse other fields after bit', function () { 1094 | it('Parse uint8', function () { 1095 | var buffer = Buffer.from([0, 1, 0, 4]); 1096 | for (var i = 17; i <= 24; i++) { 1097 | var parser = Parser.start()['bit' + i]('a').uint8('b'); 1098 | 1099 | assert.deepEqual(parser.parse(buffer), { 1100 | a: 1 << (i - 16), 1101 | b: 4, 1102 | }); 1103 | } 1104 | }); 1105 | }); 1106 | }); 1107 | 1108 | suite(Buffer); 1109 | suite(Uint8Array); 1110 | -------------------------------------------------------------------------------- /test/primitive_parser.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var TextEncoder = 3 | typeof TextEncoder === 'undefined' 4 | ? require('util').TextEncoder 5 | : TextEncoder; 6 | var Parser = require('../dist/binary_parser').Parser; 7 | 8 | const suite = (Buffer) => 9 | describe(`Primitive parser (${Buffer.name})`, function () { 10 | function hexToBuf(hex) { 11 | return Buffer.from( 12 | hex.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)) 13 | ); 14 | } 15 | describe('Primitive parsers', function () { 16 | it('should nothing', function () { 17 | var parser = Parser.start(); 18 | 19 | var buffer = Buffer.from([0xa, 0x14, 0x1e, 0x28, 0x32]); 20 | assert.deepEqual(parser.parse(buffer), {}); 21 | }); 22 | it('should parse integer types', function () { 23 | var parser = Parser.start().uint8('a').int16le('b').uint32be('c'); 24 | 25 | var buffer = Buffer.from([0x00, 0xd2, 0x04, 0x00, 0xbc, 0x61, 0x4e]); 26 | assert.deepEqual(parser.parse(buffer), { a: 0, b: 1234, c: 12345678 }); 27 | }); 28 | describe('BigInt64 parsers', () => { 29 | const [major] = process.version.replace('v', '').split('.'); 30 | if (Number(major) >= 12) { 31 | it('should parse biguints64', () => { 32 | const parser = Parser.start().uint64be('a').uint64le('b'); 33 | // from https://nodejs.org/api/buffer.html#buffer_buf_readbiguint64le_offset 34 | const buf = Buffer.from([ 35 | 0x00, 36 | 0x00, 37 | 0x00, 38 | 0x00, 39 | 0xff, 40 | 0xff, 41 | 0xff, 42 | 0xff, 43 | 0x00, 44 | 0x00, 45 | 0x00, 46 | 0x00, 47 | 0xff, 48 | 0xff, 49 | 0xff, 50 | 0xff, 51 | ]); 52 | assert.deepEqual(parser.parse(buf), { 53 | a: BigInt('4294967295'), 54 | b: BigInt('18446744069414584320'), 55 | }); 56 | }); 57 | 58 | it('should parse bigints64', () => { 59 | const parser = Parser.start() 60 | .int64be('a') 61 | .int64le('b') 62 | .int64be('c') 63 | .int64le('d'); 64 | // from https://nodejs.org/api/buffer.html#buffer_buf_readbiguint64le_offset 65 | const buf = Buffer.from([ 66 | 0x00, 67 | 0x00, 68 | 0x00, 69 | 0x00, 70 | 0xff, 71 | 0xff, 72 | 0xff, 73 | 0xff, 74 | 0x01, 75 | 0x00, 76 | 0x00, 77 | 0x00, 78 | 0xff, 79 | 0xff, 80 | 0xff, 81 | 0xff, 82 | 0x00, 83 | 0x00, 84 | 0x00, 85 | 0x00, 86 | 0xff, 87 | 0xff, 88 | 0xff, 89 | 0xff, 90 | 0x01, 91 | 0x00, 92 | 0x00, 93 | 0x00, 94 | 0xff, 95 | 0xff, 96 | 0xff, 97 | 0xff, 98 | ]); 99 | assert.deepEqual(parser.parse(buf), { 100 | a: BigInt('4294967295'), 101 | b: BigInt('-4294967295'), 102 | c: BigInt('4294967295'), 103 | d: BigInt('-4294967295'), 104 | }); 105 | }); 106 | } else { 107 | it('should throw when run under not v12', () => { 108 | assert.throws(() => Parser.start().bigint64('a')); 109 | }); 110 | } 111 | }); 112 | it('should use formatter to transform parsed integer', function () { 113 | var parser = Parser.start() 114 | .uint8('a', { 115 | formatter: function (val) { 116 | return val * 2; 117 | }, 118 | }) 119 | .int16le('b', { 120 | formatter: function (val) { 121 | return 'test' + String(val); 122 | }, 123 | }); 124 | 125 | var buffer = Buffer.from([0x01, 0xd2, 0x04]); 126 | assert.deepEqual(parser.parse(buffer), { a: 2, b: 'test1234' }); 127 | }); 128 | it('should parse floating point types', function () { 129 | var parser = Parser.start().floatbe('a').doublele('b'); 130 | 131 | var FLT_EPSILON = 0.00001; 132 | var buffer = Buffer.from([ 133 | 0x41, 134 | 0x45, 135 | 0x85, 136 | 0x1f, 137 | 0x7a, 138 | 0x36, 139 | 0xab, 140 | 0x3e, 141 | 0x57, 142 | 0x5b, 143 | 0xb1, 144 | 0xbf, 145 | ]); 146 | var result = parser.parse(buffer); 147 | 148 | assert(Math.abs(result.a - 12.345) < FLT_EPSILON); 149 | assert(Math.abs(result.b - -0.0678) < FLT_EPSILON); 150 | }); 151 | it('should handle endianess', function () { 152 | var parser = Parser.start().int32le('little').int32be('big'); 153 | 154 | var buffer = Buffer.from([ 155 | 0x4e, 156 | 0x61, 157 | 0xbc, 158 | 0x00, 159 | 0x00, 160 | 0xbc, 161 | 0x61, 162 | 0x4e, 163 | ]); 164 | assert.deepEqual(parser.parse(buffer), { 165 | little: 12345678, 166 | big: 12345678, 167 | }); 168 | }); 169 | it('should seek offset', function () { 170 | var parser = Parser.start() 171 | .uint8('a') 172 | .seek(3) 173 | .uint16le('b') 174 | .uint32be('c'); 175 | 176 | var buffer = Buffer.from([ 177 | 0x00, 178 | 0xff, 179 | 0xff, 180 | 0xfe, 181 | 0xd2, 182 | 0x04, 183 | 0x00, 184 | 0xbc, 185 | 0x61, 186 | 0x4e, 187 | ]); 188 | assert.deepEqual(parser.parse(buffer), { a: 0, b: 1234, c: 12345678 }); 189 | }); 190 | }); 191 | 192 | describe('Bit field parsers', function () { 193 | var binaryLiteral = function (s) { 194 | var i; 195 | var bytes = []; 196 | 197 | s = s.replace(/\s/g, ''); 198 | for (i = 0; i < s.length; i += 8) { 199 | bytes.push(parseInt(s.slice(i, i + 8), 2)); 200 | } 201 | 202 | return Buffer.from(bytes); 203 | }; 204 | 205 | it('binary literal helper should work', function () { 206 | assert.deepEqual(binaryLiteral('11110000'), Buffer.from([0xf0])); 207 | assert.deepEqual( 208 | binaryLiteral('11110000 10100101'), 209 | Buffer.from([0xf0, 0xa5]) 210 | ); 211 | }); 212 | 213 | it('should parse 1-byte-length bit field sequence', function () { 214 | var parser = new Parser().bit1('a').bit2('b').bit4('c').bit1('d'); 215 | 216 | var buf = binaryLiteral('1 10 1010 0'); 217 | assert.deepEqual(parser.parse(buf), { 218 | a: 1, 219 | b: 2, 220 | c: 10, 221 | d: 0, 222 | }); 223 | 224 | parser = new Parser() 225 | .endianess('little') 226 | .bit1('a') 227 | .bit2('b') 228 | .bit4('c') 229 | .bit1('d'); 230 | 231 | assert.deepEqual(parser.parse(buf), { 232 | a: 0, 233 | b: 2, 234 | c: 10, 235 | d: 1, 236 | }); 237 | }); 238 | it('should parse 2-byte-length bit field sequence', function () { 239 | var parser = new Parser().bit3('a').bit9('b').bit4('c'); 240 | 241 | var buf = binaryLiteral('101 111000111 0111'); 242 | assert.deepEqual(parser.parse(buf), { 243 | a: 5, 244 | b: 455, 245 | c: 7, 246 | }); 247 | 248 | parser = new Parser().endianess('little').bit3('a').bit9('b').bit4('c'); 249 | assert.deepEqual(parser.parse(buf), { 250 | a: 7, 251 | b: 398, 252 | c: 11, 253 | }); 254 | }); 255 | it('should parse 4-byte-length bit field sequence', function () { 256 | var parser = new Parser() 257 | .bit1('a') 258 | .bit24('b') 259 | .bit4('c') 260 | .bit2('d') 261 | .bit1('e'); 262 | var buf = binaryLiteral('1 101010101010101010101010 1111 01 1'); 263 | assert.deepEqual(parser.parse(buf), { 264 | a: 1, 265 | b: 11184810, 266 | c: 15, 267 | d: 1, 268 | e: 1, 269 | }); 270 | 271 | parser = new Parser() 272 | .endianess('little') 273 | .bit1('a') 274 | .bit24('b') 275 | .bit4('c') 276 | .bit2('d') 277 | .bit1('e'); 278 | assert.deepEqual(parser.parse(buf), { 279 | a: 1, 280 | b: 11184829, 281 | c: 10, 282 | d: 2, 283 | e: 1, 284 | }); 285 | }); 286 | it('should parse nested bit fields', function () { 287 | var parser = new Parser().bit1('a').nest('x', { 288 | type: new Parser().bit2('b').bit4('c').bit1('d'), 289 | }); 290 | 291 | var buf = binaryLiteral('11010100'); 292 | 293 | assert.deepEqual(parser.parse(buf), { 294 | a: 1, 295 | x: { 296 | b: 2, 297 | c: 10, 298 | d: 0, 299 | }, 300 | }); 301 | }); 302 | }); 303 | 304 | describe('String parser', function () { 305 | it('should parse UTF8 encoded string (ASCII only)', function () { 306 | var text = 'hello, world'; 307 | var buffer = Buffer.from(new TextEncoder().encode(text)); 308 | var parser = Parser.start().string('msg', { 309 | length: buffer.length, 310 | encoding: 'utf8', 311 | }); 312 | 313 | assert.equal(parser.parse(buffer).msg, text); 314 | }); 315 | it('should parse UTF8 encoded string', function () { 316 | var text = 'こんにちは、せかい。'; 317 | var buffer = Buffer.from(new TextEncoder().encode(text)); 318 | var parser = Parser.start().string('msg', { 319 | length: buffer.length, 320 | encoding: 'utf8', 321 | }); 322 | 323 | assert.equal(parser.parse(buffer).msg, text); 324 | }); 325 | it('should parse HEX encoded string', function () { 326 | var text = 'cafebabe'; 327 | var buffer = hexToBuf(text); 328 | var parser = Parser.start().string('msg', { 329 | length: buffer.length, 330 | encoding: 'hex', 331 | }); 332 | 333 | assert.equal(parser.parse(buffer).msg, text); 334 | }); 335 | it('should parse variable length string', function () { 336 | var buffer = hexToBuf('0c68656c6c6f2c20776f726c64'); 337 | var parser = Parser.start() 338 | .uint8('length') 339 | .string('msg', { length: 'length', encoding: 'utf8' }); 340 | 341 | assert.equal(parser.parse(buffer).msg, 'hello, world'); 342 | }); 343 | it('should parse zero terminated string', function () { 344 | var buffer = hexToBuf('68656c6c6f2c20776f726c6400'); 345 | var parser = Parser.start().string('msg', { 346 | zeroTerminated: true, 347 | encoding: 'utf8', 348 | }); 349 | 350 | assert.deepEqual(parser.parse(buffer), { msg: 'hello, world' }); 351 | }); 352 | it('should parser zero terminated fixed-length string', function () { 353 | var buffer = Buffer.from( 354 | new TextEncoder().encode('abc\u0000defghij\u0000') 355 | ); 356 | var parser = Parser.start() 357 | .string('a', { length: 5, zeroTerminated: true }) 358 | .string('b', { length: 5, zeroTerminated: true }) 359 | .string('c', { length: 5, zeroTerminated: true }); 360 | 361 | assert.deepEqual(parser.parse(buffer), { 362 | a: 'abc', 363 | b: 'defgh', 364 | c: 'ij', 365 | }); 366 | }); 367 | it('should strip trailing null characters', function () { 368 | var buffer = hexToBuf('746573740000'); 369 | var parser1 = Parser.start().string('str', { 370 | length: 7, 371 | stripNull: false, 372 | }); 373 | var parser2 = Parser.start().string('str', { 374 | length: 7, 375 | stripNull: true, 376 | }); 377 | 378 | assert.equal(parser1.parse(buffer).str, 'test\u0000\u0000'); 379 | assert.equal(parser2.parse(buffer).str, 'test'); 380 | }); 381 | it('should parse string greedily with zero-bytes internally', function () { 382 | var buffer = Buffer.from( 383 | new TextEncoder().encode('abc\u0000defghij\u0000') 384 | ); 385 | var parser = Parser.start().string('a', { greedy: true }); 386 | 387 | assert.deepEqual(parser.parse(buffer), { 388 | a: 'abc\u0000defghij\u0000', 389 | }); 390 | }); 391 | }); 392 | 393 | describe('Buffer parser', function () { 394 | it('should parse as buffer', function () { 395 | var parser = new Parser().uint8('len').buffer('raw', { 396 | length: 'len', 397 | }); 398 | 399 | var buf = hexToBuf('deadbeefdeadbeef'); 400 | var result = parser.parse(Buffer.from([...Buffer.from([8]), ...buf])); 401 | 402 | assert.deepEqual(result.raw, buf); 403 | }); 404 | 405 | it('should clone buffer if options.clone is true', function () { 406 | var parser = new Parser().buffer('raw', { 407 | length: 8, 408 | clone: true, 409 | }); 410 | 411 | var buf = hexToBuf('deadbeefdeadbeef'); 412 | var result = parser.parse(buf); 413 | assert.deepEqual(result.raw, buf); 414 | result.raw[0] = 0xff; 415 | assert.notDeepEqual(result.raw, buf); 416 | }); 417 | 418 | it('should parse until function returns true when readUntil is function', function () { 419 | var parser = new Parser() 420 | .endianess('big') 421 | .uint8('cmd') 422 | .buffer('data', { 423 | readUntil: function (item) { 424 | return item === 2; 425 | }, 426 | }); 427 | 428 | var result = parser.parse(hexToBuf('aa')); 429 | assert.deepEqual(result, { cmd: 0xaa, data: Buffer.from([]) }); 430 | 431 | var result = parser.parse(hexToBuf('aabbcc')); 432 | assert.deepEqual(result, { cmd: 0xaa, data: hexToBuf('bbcc') }); 433 | 434 | var result = parser.parse(hexToBuf('aa02bbcc')); 435 | assert.deepEqual(result, { cmd: 0xaa, data: Buffer.from([]) }); 436 | 437 | var result = parser.parse(hexToBuf('aabbcc02')); 438 | assert.deepEqual(result, { cmd: 0xaa, data: hexToBuf('bbcc') }); 439 | 440 | var result = parser.parse(hexToBuf('aabbcc02dd')); 441 | assert.deepEqual(result, { cmd: 0xaa, data: hexToBuf('bbcc') }); 442 | }); 443 | 444 | // this is a test for testing a fix of a bug, that removed the last byte 445 | // of the buffer parser 446 | it('should return a buffer with same size', function () { 447 | var bufferParser = new Parser().buffer('buf', { 448 | readUntil: 'eof', 449 | formatter: function (buffer) { 450 | return buffer; 451 | }, 452 | }); 453 | 454 | var buffer = Buffer.from('John\0Doe\0'); 455 | assert.deepEqual(bufferParser.parse(buffer), { buf: buffer }); 456 | }); 457 | }); 458 | }); 459 | 460 | suite(Buffer); 461 | suite(Uint8Array); 462 | -------------------------------------------------------------------------------- /test/yy_primitive_encoder.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var util = require('util'); 3 | var Parser = require('../dist/binary_parser').Parser; 4 | 5 | describe('Primitive encoder', function () { 6 | describe('Primitive encoders', function () { 7 | it('should nothing', function () { 8 | var parser = Parser.start(); 9 | 10 | var buffer = parser.encode({ a: 0, b: 1 }); 11 | assert.deepEqual(buffer.length, 0); 12 | }); 13 | it('should encode integer types', function () { 14 | var parser = Parser.start().uint8('a').int16le('b').uint32be('c'); 15 | 16 | var buffer = Buffer.from([0x00, 0xd2, 0x04, 0x00, 0xbc, 0x61, 0x4e]); 17 | var parsed = parser.parse(buffer); 18 | var encoded = parser.encode(parsed); 19 | assert.deepEqual(parsed, { a: 0, b: 1234, c: 12345678 }); 20 | assert.deepEqual(encoded, buffer); 21 | }); 22 | describe('BigInt64 encoders', () => { 23 | const [major] = process.version.replace('v', '').split('.'); 24 | if (Number(major) >= 12) { 25 | it('should encode biguints64', () => { 26 | const parser = Parser.start().uint64be('a').uint64le('b'); 27 | // from https://nodejs.org/api/buffer.html#buffer_buf_readbiguint64le_offset 28 | const buf = Buffer.from([ 29 | 0x00, 30 | 0x00, 31 | 0x00, 32 | 0x00, 33 | 0xff, 34 | 0xff, 35 | 0xff, 36 | 0xff, 37 | 0x00, 38 | 0x00, 39 | 0x00, 40 | 0x00, 41 | 0xff, 42 | 0xff, 43 | 0xff, 44 | 0xff, 45 | ]); 46 | let parsed = parser.parse(buf); 47 | assert.deepEqual(parsed, { 48 | a: BigInt('4294967295'), 49 | b: BigInt('18446744069414584320'), 50 | }); 51 | let encoded = parser.encode(parsed); 52 | assert.deepEqual(encoded, buf); 53 | }); 54 | 55 | it('should encode bigints64', () => { 56 | const parser = Parser.start() 57 | .int64be('a') 58 | .int64le('b') 59 | .int64be('c') 60 | .int64le('d'); 61 | // from https://nodejs.org/api/buffer.html#buffer_buf_readbiguint64le_offset 62 | const buf = Buffer.from([ 63 | 0x00, 64 | 0x00, 65 | 0x00, 66 | 0x00, 67 | 0xff, 68 | 0xff, 69 | 0xff, 70 | 0xff, 71 | 0x01, 72 | 0x00, 73 | 0x00, 74 | 0x00, 75 | 0xff, 76 | 0xff, 77 | 0xff, 78 | 0xff, 79 | 0x00, 80 | 0x00, 81 | 0x00, 82 | 0x00, 83 | 0xff, 84 | 0xff, 85 | 0xff, 86 | 0xff, 87 | 0x01, 88 | 0x00, 89 | 0x00, 90 | 0x00, 91 | 0xff, 92 | 0xff, 93 | 0xff, 94 | 0xff, 95 | ]); 96 | let parsed = parser.parse(buf); 97 | assert.deepEqual(parsed, { 98 | a: BigInt('4294967295'), 99 | b: BigInt('-4294967295'), 100 | c: BigInt('4294967295'), 101 | d: BigInt('-4294967295'), 102 | }); 103 | let encoded = parser.encode(parsed); 104 | assert.deepEqual(encoded, buf); 105 | }); 106 | } else { 107 | it('should throw when run under not v12', () => { 108 | assert.throws(() => Parser.start().bigint64('a')); 109 | }); 110 | } 111 | }); 112 | it('should use encoder to transform to integer', function () { 113 | var parser = Parser.start() 114 | .uint8('a', { 115 | formatter: function (val) { 116 | return val * 2; 117 | }, 118 | encoder: function (val) { 119 | return val / 2; 120 | }, 121 | }) 122 | .int16le('b', { 123 | formatter: function (val) { 124 | return 'test' + String(val); 125 | }, 126 | encoder: function (val) { 127 | return parseInt(val.substr('test'.length)); 128 | }, 129 | }); 130 | 131 | var buffer = Buffer.from([0x01, 0xd2, 0x04]); 132 | var parsed = parser.parse(buffer); 133 | var parsedClone = Object.assign({}, parsed); 134 | var encoded = parser.encode(parsedClone); 135 | assert.deepEqual(parsed, { a: 2, b: 'test1234' }); 136 | assert.deepEqual(encoded, buffer); 137 | }); 138 | it('should encode floating point types', function () { 139 | var parser = Parser.start().floatbe('a').doublele('b'); 140 | 141 | var FLT_EPSILON = 0.00001; 142 | var buffer = Buffer.from([ 143 | 0x41, 144 | 0x45, 145 | 0x85, 146 | 0x1f, 147 | 0x7a, 148 | 0x36, 149 | 0xab, 150 | 0x3e, 151 | 0x57, 152 | 0x5b, 153 | 0xb1, 154 | 0xbf, 155 | ]); 156 | var result = parser.parse(buffer); 157 | 158 | assert(Math.abs(result.a - 12.345) < FLT_EPSILON); 159 | assert(Math.abs(result.b - -0.0678) < FLT_EPSILON); 160 | var encoded = parser.encode(result); 161 | assert.deepEqual(encoded, buffer); 162 | }); 163 | it('should handle endianess', function () { 164 | var parser = Parser.start().int32le('little').int32be('big'); 165 | 166 | var buffer = Buffer.from([ 167 | 0x4e, 168 | 0x61, 169 | 0xbc, 170 | 0x00, 171 | 0x00, 172 | 0xbc, 173 | 0x61, 174 | 0x4e, 175 | ]); 176 | var parsed = parser.parse(buffer); 177 | assert.deepEqual(parsed, { 178 | little: 12345678, 179 | big: 12345678, 180 | }); 181 | var encoded = parser.encode(parsed); 182 | assert.deepEqual(encoded, buffer); 183 | }); 184 | it('should skip when specified', function () { 185 | var parser = Parser.start() 186 | .uint8('a') 187 | .skip(3) 188 | .uint16le('b') 189 | .uint32be('c'); 190 | 191 | var buffer = Buffer.from([ 192 | 0x00, 193 | 0x00, // Skipped will be encoded as Null 194 | 0x00, // Skipped will be encoded as Null 195 | 0x00, // Skipped will be encoded as Null 196 | 0xd2, 197 | 0x04, 198 | 0x00, 199 | 0xbc, 200 | 0x61, 201 | 0x4e, 202 | ]); 203 | var parsed = parser.parse(buffer); 204 | assert.deepEqual(parsed, { a: 0, b: 1234, c: 12345678 }); 205 | var encoded = parser.encode(parsed); 206 | assert.deepEqual(encoded, buffer); 207 | }); 208 | }); 209 | 210 | describe('Bit field encoders', function () { 211 | var binaryLiteral = function (s) { 212 | var i; 213 | var bytes = []; 214 | 215 | s = s.replace(/\s/g, ''); 216 | for (i = 0; i < s.length; i += 8) { 217 | bytes.push(parseInt(s.slice(i, i + 8), 2)); 218 | } 219 | 220 | return Buffer.from(bytes); 221 | }; 222 | 223 | it('binary literal helper should work', function () { 224 | assert.deepEqual(binaryLiteral('11110000'), Buffer.from([0xf0])); 225 | assert.deepEqual( 226 | binaryLiteral('11110000 10100101'), 227 | Buffer.from([0xf0, 0xa5]) 228 | ); 229 | }); 230 | 231 | it('should encode 1-byte-length 8 bit field', function () { 232 | var parser = new Parser().bit8('a'); 233 | 234 | var buf = binaryLiteral('11111111'); 235 | 236 | assert.deepEqual(parser.parse(buf), { a: 255 }); 237 | assert.deepEqual(parser.encode({ a: 255 }), buf); 238 | }); 239 | 240 | it('should encode 1-byte-length 2x 4 bit fields', function () { 241 | var parser = new Parser().bit4('a').bit4('b'); 242 | 243 | var buf = binaryLiteral('1111 1111'); 244 | 245 | assert.deepEqual(parser.parse(buf), { a: 15, b: 15 }); 246 | assert.deepEqual(parser.encode({ a: 15, b: 15 }), buf); 247 | }); 248 | 249 | it('should encode 1-byte-length bit field sequence', function () { 250 | var parser = new Parser().bit1('a').bit2('b').bit4('c').bit1('d'); 251 | 252 | var buf = binaryLiteral('1 10 1010 0'); 253 | var decoded = parser.parse(buf); 254 | assert.deepEqual(decoded, { 255 | a: 1, 256 | b: 2, 257 | c: 10, 258 | d: 0, 259 | }); 260 | 261 | var encoded = parser.encode(decoded); 262 | assert.deepEqual(encoded, buf); 263 | 264 | // Endianess will change nothing you still specify bits for left to right 265 | parser = new Parser() 266 | .endianess('little') 267 | .bit1('a') 268 | .bit2('b') 269 | .bit4('c') 270 | .bit1('d'); 271 | 272 | encoded = parser.encode({ 273 | a: 1, 274 | b: 2, 275 | c: 10, 276 | d: 0, 277 | }); 278 | assert.deepEqual(encoded, buf); 279 | }); 280 | it('should parse 2-byte-length bit field sequence', function () { 281 | var parser = new Parser().bit3('a').bit9('b').bit4('c'); 282 | 283 | var buf = binaryLiteral('101 111000111 0111'); 284 | var decoded = parser.parse(buf); 285 | assert.deepEqual(decoded, { 286 | a: 5, 287 | b: 455, 288 | c: 7, 289 | }); 290 | var encoded = parser.encode(decoded); 291 | assert.deepEqual(encoded, buf); 292 | }); 293 | it('should parse 4-byte-length bit field sequence', function () { 294 | var parser = new Parser() 295 | .bit1('a') 296 | .bit24('b') 297 | .bit4('c') 298 | .bit2('d') 299 | .bit1('e'); 300 | var buf = binaryLiteral('1 101010101010101010101010 1111 01 1'); 301 | var decoded = parser.parse(buf); 302 | assert.deepEqual(decoded, { 303 | a: 1, 304 | b: 11184810, 305 | c: 15, 306 | d: 1, 307 | e: 1, 308 | }); 309 | var encoded = parser.encode(decoded); 310 | assert.deepEqual(encoded, buf); 311 | }); 312 | it('should parse nested bit fields', function () { 313 | var parser = new Parser().bit1('a').nest('x', { 314 | type: new Parser().bit2('b').bit4('c').bit1('d'), 315 | }); 316 | 317 | var buf = binaryLiteral('1 10 1010 0'); 318 | var decoded = parser.parse(buf); 319 | assert.deepEqual(decoded, { 320 | a: 1, 321 | x: { 322 | b: 2, 323 | c: 10, 324 | d: 0, 325 | }, 326 | }); 327 | var encoded = parser.encode(decoded); 328 | assert.deepEqual(encoded, buf); 329 | }); 330 | }); 331 | 332 | describe('String encoder', function () { 333 | it('should encode ASCII encoded string', function () { 334 | var text = 'hello, world'; 335 | var buffer = Buffer.from(text, 'utf8'); 336 | var parser = Parser.start().string('msg', { 337 | length: buffer.length, 338 | encoding: 'utf8', 339 | }); 340 | 341 | var decoded = parser.parse(buffer); 342 | assert.equal(decoded.msg, text); 343 | var encoded = parser.encode(decoded); 344 | assert.deepEqual(encoded, buffer); 345 | }); 346 | it('should encode UTF8 encoded string', function () { 347 | var text = 'こんにちは、せかい。'; 348 | var buffer = Buffer.from(text, 'utf8'); 349 | var parser = Parser.start().string('msg', { 350 | length: buffer.length, 351 | encoding: 'utf8', 352 | }); 353 | 354 | var decoded = parser.parse(buffer); 355 | assert.equal(decoded.msg, text); 356 | var encoded = parser.encode(decoded); 357 | assert.deepEqual(encoded, buffer); 358 | }); 359 | it('should encode HEX encoded string', function () { 360 | var text = 'cafebabe'; 361 | var buffer = Buffer.from(text, 'hex'); 362 | var parser = Parser.start().string('msg', { 363 | length: buffer.length, 364 | encoding: 'hex', 365 | }); 366 | 367 | var decoded = parser.parse(buffer); 368 | assert.equal(decoded.msg, text); 369 | var encoded = parser.encode(decoded); 370 | assert.deepEqual(encoded, buffer); 371 | }); 372 | it('should encode variable length string', function () { 373 | var buffer = Buffer.from('0c68656c6c6f2c20776f726c64', 'hex'); 374 | var parser = Parser.start() 375 | .uint8('length') 376 | .string('msg', { length: 'length', encoding: 'utf8' }); 377 | 378 | var decoded = parser.parse(buffer); 379 | assert.equal(decoded.msg, 'hello, world'); 380 | var encoded = parser.encode(decoded); 381 | assert.deepEqual(encoded, buffer); 382 | }); 383 | it('should encode zero terminated string', function () { 384 | var buffer = Buffer.from('68656c6c6f2c20776f726c6400', 'hex'); 385 | var parser = Parser.start().string('msg', { 386 | zeroTerminated: true, 387 | encoding: 'utf8', 388 | }); 389 | 390 | var decoded = parser.parse(buffer); 391 | assert.deepEqual(decoded, { msg: 'hello, world' }); 392 | var encoded = parser.encode(decoded); 393 | assert.deepEqual(encoded, buffer); 394 | }); 395 | it('should encode zero terminated fixed-length string', function () { 396 | var buffer = Buffer.from('abc\u0000defghij\u0000'); 397 | var parser = Parser.start() 398 | .string('a', { length: 5, zeroTerminated: true }) 399 | .string('b', { length: 5, zeroTerminated: true }) 400 | .string('c', { length: 5, zeroTerminated: true }); 401 | 402 | var decoded = parser.parse(buffer); 403 | assert.deepEqual(decoded, { 404 | a: 'abc', 405 | b: 'defgh', 406 | c: 'ij', 407 | }); 408 | let encoded = parser.encode(decoded); 409 | assert.deepEqual(encoded, buffer); 410 | 411 | encoded = parser.encode({ 412 | a: 'a234', 413 | b: 'b2345', 414 | c: 'c2345678', 415 | }); 416 | assert.deepEqual(encoded, Buffer.from('a234\u0000b2345c2345')); 417 | }); 418 | it('should strip trailing null characters', function () { 419 | var buffer = Buffer.from('746573740000', 'hex'); 420 | var parser1 = Parser.start().string('str', { 421 | length: 6, 422 | stripNull: false, 423 | }); 424 | var parser2 = Parser.start().string('str', { 425 | length: 6, 426 | stripNull: true, 427 | }); 428 | 429 | var decoded1 = parser1.parse(buffer); 430 | assert.equal(decoded1.str, 'test\u0000\u0000'); 431 | var encoded1 = parser1.encode(decoded1); 432 | assert.deepEqual(encoded1, buffer); 433 | 434 | var decoded2 = parser2.parse(buffer); 435 | assert.equal(decoded2.str, 'test'); 436 | var encoded2 = parser2.encode(decoded2); 437 | assert.deepEqual(encoded2, buffer); 438 | }); 439 | it('should encode string with zero-bytes internally', function () { 440 | var buffer = Buffer.from('abc\u0000defghij\u0000'); 441 | var parser = Parser.start().string('a', { greedy: true }); 442 | 443 | var decoded = parser.parse(buffer); 444 | assert.deepEqual(decoded, { 445 | a: 'abc\u0000defghij\u0000', 446 | }); 447 | var encoded = parser.encode(decoded); 448 | assert.deepEqual(encoded, buffer); 449 | }); 450 | it('should encode string with default right padding', function () { 451 | var parser = Parser.start().string('a', { length: 6 }); 452 | var encoded = parser.encode({ a: 'abcd' }); 453 | assert.deepEqual(encoded, Buffer.from('abcd ')); 454 | encoded = parser.encode({ a: 'abcdefgh' }); 455 | assert.deepEqual(encoded, Buffer.from('abcdef')); 456 | }); 457 | it('should encode string with left padding', function () { 458 | var parser = Parser.start().string('a', { length: 6, padding: 'left' }); 459 | var encoded = parser.encode({ a: 'abcd' }); 460 | assert.deepEqual(encoded, Buffer.from(' abcd')); 461 | encoded = parser.encode({ a: 'abcdefgh' }); 462 | assert.deepEqual(encoded, Buffer.from('abcdef')); 463 | }); 464 | it('should encode string with right padding and provided padding char', function () { 465 | var parser = Parser.start().string('a', { length: 6, padd: 'x' }); 466 | var encoded = parser.encode({ a: 'abcd' }); 467 | assert.deepEqual(encoded, Buffer.from('abcdxx')); 468 | encoded = parser.encode({ a: 'abcdefgh' }); 469 | assert.deepEqual(encoded, Buffer.from('abcdef')); 470 | }); 471 | it('should encode string with left padding and provided padding char', function () { 472 | var parser = Parser.start().string('a', { 473 | length: 6, 474 | padding: 'left', 475 | padd: '.', 476 | }); 477 | var encoded = parser.encode({ a: 'abcd' }); 478 | assert.deepEqual(encoded, Buffer.from('..abcd')); 479 | encoded = parser.encode({ a: 'abcdefgh' }); 480 | assert.deepEqual(encoded, Buffer.from('abcdef')); 481 | }); 482 | it('should encode string with padding and padding char 0', function () { 483 | var parser = Parser.start().string('a', { length: 6, padd: '\u0000' }); 484 | var encoded = parser.encode({ a: 'abcd' }); 485 | assert.deepEqual(encoded, Buffer.from('abcd\u0000\u0000')); 486 | }); 487 | it('should encode string with padding and first byte of padding char', function () { 488 | var parser = Parser.start().string('a', { length: 6, padd: '1234' }); 489 | var encoded = parser.encode({ a: 'abcd' }); 490 | assert.deepEqual(encoded, Buffer.from('abcd11')); 491 | }); 492 | it('should encode string with space padding when padd char is not encoded on 1 Byte', function () { 493 | var parser = Parser.start().string('a', { length: 6, padd: 'こ' }); 494 | var encoded = parser.encode({ a: 'abcd' }); 495 | assert.deepEqual(encoded, Buffer.from('abcd ')); 496 | }); 497 | }); 498 | 499 | describe('Buffer encoder', function () { 500 | it('should encode buffer', function () { 501 | var parser = new Parser().uint8('len').buffer('raw', { 502 | length: 'len', 503 | }); 504 | 505 | var buf = Buffer.from('deadbeefdeadbeef', 'hex'); 506 | var result = parser.parse( 507 | Buffer.concat([Buffer.from([8]), buf, Buffer.from('garbage at end')]) 508 | ); 509 | 510 | assert.deepEqual(result, { 511 | len: 8, 512 | raw: buf, 513 | }); 514 | 515 | var encoded = parser.encode(result); 516 | assert.deepEqual(encoded, Buffer.concat([Buffer.from([8]), buf])); 517 | }); 518 | }); 519 | }); 520 | -------------------------------------------------------------------------------- /test/zz_composite_encoder.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var util = require('util'); 3 | var Parser = require('../dist/binary_parser').Parser; 4 | 5 | describe('Composite encoder', function () { 6 | describe('Array encoder', function () { 7 | it('should encode array of primitive types', function () { 8 | var parser = Parser.start().uint8('length').array('message', { 9 | length: 'length', 10 | type: 'uint8', 11 | }); 12 | 13 | var buffer = Buffer.from([12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); 14 | var decoded = parser.parse(buffer); 15 | assert.deepEqual(decoded, { 16 | length: 12, 17 | message: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], 18 | }); 19 | var encoded = parser.encode(decoded); 20 | assert.deepEqual(encoded, buffer); 21 | }); 22 | it('should encode array of primitive types with lengthInBytes', function () { 23 | var parser = Parser.start().uint8('length').array('message', { 24 | lengthInBytes: 'length', 25 | type: 'uint8', 26 | }); 27 | 28 | var buffer = Buffer.from([12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); 29 | var decoded = parser.parse(buffer); 30 | assert.deepEqual(decoded, { 31 | length: 12, 32 | message: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], 33 | }); 34 | var encoded = parser.encode(decoded); 35 | assert.deepEqual(encoded, buffer); 36 | }); 37 | it('should encode array of primitive types with lengthInBytes as a maximum but not minimum', function () { 38 | var parser = Parser.start().uint8('length').array('message', { 39 | lengthInBytes: 'length', 40 | type: 'uint8', 41 | }); 42 | var encoded = parser.encode({ 43 | length: 5, 44 | message: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], // Extra items in array than encoding limit 45 | }); 46 | assert.deepEqual(encoded, Buffer.from([5, 1, 2, 3, 4, 5])); 47 | encoded = parser.encode({ 48 | length: 5, 49 | message: [1, 2, 3], // Less items in array than encoding limit 50 | }); 51 | assert.deepEqual(encoded, Buffer.from([5, 1, 2, 3])); 52 | }); 53 | it('should encode array of user defined types', function () { 54 | var elementParser = new Parser().uint8('key').int16le('value'); 55 | 56 | var parser = Parser.start().uint16le('length').array('message', { 57 | length: 'length', 58 | type: elementParser, 59 | }); 60 | 61 | var buffer = Buffer.from([ 62 | 0x02, 63 | 0x00, 64 | 0xca, 65 | 0xd2, 66 | 0x04, 67 | 0xbe, 68 | 0xd3, 69 | 0x04, 70 | ]); 71 | var decoded = parser.parse(buffer); 72 | assert.deepEqual(decoded, { 73 | length: 0x02, 74 | message: [ 75 | { key: 0xca, value: 1234 }, 76 | { key: 0xbe, value: 1235 }, 77 | ], 78 | }); 79 | var encoded = parser.encode(decoded); 80 | assert.deepEqual(encoded, buffer); 81 | }); 82 | it('should encode array of user defined types with lengthInBytes', function () { 83 | var elementParser = new Parser().uint8('key').int16le('value'); 84 | 85 | var parser = Parser.start().uint16le('length').array('message', { 86 | lengthInBytes: 'length', 87 | type: elementParser, 88 | }); 89 | 90 | var buffer = Buffer.from([ 91 | 0x06, 92 | 0x00, 93 | 0xca, 94 | 0xd2, 95 | 0x04, 96 | 0xbe, 97 | 0xd3, 98 | 0x04, 99 | ]); 100 | var decoded = parser.parse(buffer); 101 | assert.deepEqual(decoded, { 102 | length: 0x06, 103 | message: [ 104 | { key: 0xca, value: 1234 }, 105 | { key: 0xbe, value: 1235 }, 106 | ], 107 | }); 108 | var encoded = parser.encode(decoded); 109 | assert.deepEqual(encoded, buffer); 110 | }); 111 | it('should encode array of user defined types with length function', function () { 112 | var elementParser = new Parser().uint8('key').int16le('value'); 113 | 114 | var parser = Parser.start() 115 | .uint16le('length') 116 | .array('message', { 117 | length: function () { 118 | return this.length; 119 | }, 120 | type: elementParser, 121 | }); 122 | 123 | var buffer = Buffer.from([ 124 | 0x02, 125 | 0x00, 126 | 0xca, 127 | 0xd2, 128 | 0x04, 129 | 0xbe, 130 | 0xd3, 131 | 0x04, 132 | ]); 133 | var decoded = parser.parse(buffer); 134 | assert.deepEqual(decoded, { 135 | length: 0x02, 136 | message: [ 137 | { key: 0xca, value: 1234 }, 138 | { key: 0xbe, value: 1235 }, 139 | ], 140 | }); 141 | var encoded = parser.encode(decoded); 142 | assert.deepEqual(encoded, buffer); 143 | }); 144 | it('should encode array of arrays', function () { 145 | var rowParser = Parser.start().uint8('length').array('cols', { 146 | length: 'length', 147 | type: 'int32le', 148 | }); 149 | 150 | var parser = Parser.start().uint8('length').array('rows', { 151 | length: 'length', 152 | type: rowParser, 153 | }); 154 | 155 | var buffer = Buffer.alloc(1 + 10 * (1 + 5 * 4)); 156 | var i, j; 157 | 158 | iterator = 0; 159 | buffer.writeUInt8(10, iterator); 160 | iterator += 1; 161 | for (i = 0; i < 10; i++) { 162 | buffer.writeUInt8(5, iterator); 163 | iterator += 1; 164 | for (j = 0; j < 5; j++) { 165 | buffer.writeInt32LE(i * j, iterator); 166 | iterator += 4; 167 | } 168 | } 169 | 170 | var decoded = parser.parse(buffer); 171 | assert.deepEqual(decoded, { 172 | length: 10, 173 | rows: [ 174 | { length: 5, cols: [0, 0, 0, 0, 0] }, 175 | { length: 5, cols: [0, 1, 2, 3, 4] }, 176 | { length: 5, cols: [0, 2, 4, 6, 8] }, 177 | { length: 5, cols: [0, 3, 6, 9, 12] }, 178 | { length: 5, cols: [0, 4, 8, 12, 16] }, 179 | { length: 5, cols: [0, 5, 10, 15, 20] }, 180 | { length: 5, cols: [0, 6, 12, 18, 24] }, 181 | { length: 5, cols: [0, 7, 14, 21, 28] }, 182 | { length: 5, cols: [0, 8, 16, 24, 32] }, 183 | { length: 5, cols: [0, 9, 18, 27, 36] }, 184 | ], 185 | }); 186 | var encoded = parser.encode(decoded); 187 | assert.deepEqual(encoded, buffer); 188 | }); 189 | it('should encode until function returns true when readUntil is function', function () { 190 | var parser = Parser.start().array('data', { 191 | readUntil: function (item, buf) { 192 | return item === 0; 193 | }, 194 | type: 'uint8', 195 | }); 196 | 197 | var buffer = Buffer.from([ 198 | 0xff, 199 | 0xff, 200 | 0xff, 201 | 0x01, 202 | 0x00, 203 | 0xff, 204 | 0xff, 205 | 0xff, 206 | 0xff, 207 | 0xff, 208 | ]); 209 | assert.deepEqual(parser.parse(buffer), { 210 | data: [0xff, 0xff, 0xff, 0x01, 0x00], 211 | }); 212 | var encoded = parser.encode({ 213 | ignore1: [0x00, 0x00], 214 | data: [0xff, 0xff, 0xff, 0x01, 0x00, 0xff, 0xff, 0x00, 0xff], 215 | ignore2: [0x01, 0x00, 0xff], 216 | }); 217 | assert.deepEqual(encoded, Buffer.from([0xff, 0xff, 0xff, 0x01, 0x00])); 218 | }); 219 | it('should not support associative arrays', function () { 220 | var parser = Parser.start() 221 | .int8('numlumps') 222 | .array('lumps', { 223 | type: Parser.start() 224 | .int32le('filepos') 225 | .int32le('size') 226 | .string('name', { length: 8, encoding: 'ascii' }), 227 | length: 'numlumps', 228 | key: 'name', 229 | }); 230 | 231 | assert.throws(function () { 232 | parser.encode({ 233 | numlumps: 2, 234 | lumps: { 235 | AAAAAAAA: { 236 | filepos: 1234, 237 | size: 5678, 238 | name: 'AAAAAAAA', 239 | }, 240 | bbbbbbbb: { 241 | filepos: 5678, 242 | size: 1234, 243 | name: 'bbbbbbbb', 244 | }, 245 | }, 246 | }); 247 | }, /Encoding associative array not supported/); 248 | }); 249 | it('should use encoder to transform encoded array', function () { 250 | var parser = Parser.start().array('data', { 251 | type: 'uint8', 252 | length: 4, 253 | formatter: function (arr) { 254 | return arr.join('.'); 255 | }, 256 | encoder: function (str) { 257 | return str.split('.'); 258 | }, 259 | }); 260 | 261 | var buffer = Buffer.from([0x0a, 0x0a, 0x01, 0x6e]); 262 | var decoded = parser.parse(buffer); 263 | assert.deepEqual(decoded, { 264 | data: '10.10.1.110', 265 | }); 266 | var encoded = parser.encode(decoded); 267 | assert.deepEqual(encoded, buffer); 268 | }); 269 | it('should be able to go into recursion', function () { 270 | var parser = Parser.start().namely('self').uint8('length').array('data', { 271 | type: 'self', 272 | length: 'length', 273 | }); 274 | 275 | var buffer = Buffer.from([1, 1, 1, 0]); 276 | var decoded = parser.parse(buffer); 277 | assert.deepEqual(decoded, { 278 | length: 1, 279 | data: [ 280 | { 281 | length: 1, 282 | data: [ 283 | { 284 | length: 1, 285 | data: [{ length: 0, data: [] }], 286 | }, 287 | ], 288 | }, 289 | ], 290 | }); 291 | var encoded = parser.encode(decoded); 292 | assert.deepEqual(encoded, buffer); 293 | }); 294 | it('should be able to go into even deeper recursion', function () { 295 | var parser = Parser.start().namely('self').uint8('length').array('data', { 296 | type: 'self', 297 | length: 'length', 298 | }); 299 | 300 | // 2 301 | // / \ 302 | // 3 1 303 | // / | \ \ 304 | // 1 0 2 0 305 | // / / \ 306 | // 0 1 0 307 | // / 308 | // 0 309 | 310 | var buffer = Buffer.from([ 311 | 2, 312 | /* 0 */ 3, 313 | /* 0 */ 1, 314 | /* 0 */ 0, 315 | /* 1 */ 0, 316 | /* 2 */ 2, 317 | /* 0 */ 1, 318 | /* 0 */ 0, 319 | /* 1 */ 0, 320 | /* 1 */ 1, 321 | /* 0 */ 0, 322 | ]); 323 | var decoded = parser.parse(buffer); 324 | assert.deepEqual(decoded, { 325 | length: 2, 326 | data: [ 327 | { 328 | length: 3, 329 | data: [ 330 | { length: 1, data: [{ length: 0, data: [] }] }, 331 | { length: 0, data: [] }, 332 | { 333 | length: 2, 334 | data: [ 335 | { length: 1, data: [{ length: 0, data: [] }] }, 336 | { length: 0, data: [] }, 337 | ], 338 | }, 339 | ], 340 | }, 341 | { 342 | length: 1, 343 | data: [{ length: 0, data: [] }], 344 | }, 345 | ], 346 | }); 347 | var encoded = parser.encode(decoded); 348 | assert.deepEqual(encoded, buffer); 349 | }); 350 | 351 | it('should allow parent parser attributes as choice key', function () { 352 | var ChildParser = Parser.start().choice('data', { 353 | tag: function (vars) { 354 | return vars.version; 355 | }, 356 | choices: { 357 | 1: Parser.start().uint8('v1'), 358 | 2: Parser.start().uint16('v2'), 359 | }, 360 | }); 361 | 362 | var ParentParser = Parser.start() 363 | .uint8('version') 364 | .nest('child', { type: ChildParser }); 365 | 366 | var buffer = Buffer.from([0x1, 0x2]); 367 | var decoded = ParentParser.parse(buffer); 368 | assert.deepEqual(decoded, { 369 | version: 1, 370 | child: { data: { v1: 2 } }, 371 | }); 372 | var encoded = ParentParser.encode(decoded); 373 | assert.deepEqual(encoded, buffer); 374 | 375 | buffer = Buffer.from([0x2, 0x3, 0x4]); 376 | decoded = ParentParser.parse(buffer); 377 | assert.deepEqual(decoded, { 378 | version: 2, 379 | child: { data: { v2: 0x0304 } }, 380 | }); 381 | encoded = ParentParser.encode(decoded); 382 | assert.deepEqual(encoded, buffer); 383 | }); 384 | }); 385 | 386 | describe('Choice encoder', function () { 387 | it('should encode choices of primitive types', function () { 388 | var parser = Parser.start() 389 | .uint8('tag1') 390 | .choice('data1', { 391 | tag: 'tag1', 392 | choices: { 393 | 0: 'int32le', 394 | 1: 'int16le', 395 | }, 396 | }) 397 | .uint8('tag2') 398 | .choice('data2', { 399 | tag: 'tag2', 400 | choices: { 401 | 0: 'int32le', 402 | 1: 'int16le', 403 | }, 404 | }); 405 | 406 | var buffer = Buffer.from([0x0, 0x4e, 0x61, 0xbc, 0x00, 0x01, 0xd2, 0x04]); 407 | var decoded = parser.parse(buffer); 408 | assert.deepEqual(decoded, { 409 | tag1: 0, 410 | data1: 12345678, 411 | tag2: 1, 412 | data2: 1234, 413 | }); 414 | var encoded = parser.encode(decoded); 415 | assert.deepEqual(encoded, buffer); 416 | }); 417 | it('should encode default choice', function () { 418 | var parser = Parser.start() 419 | .uint8('tag') 420 | .choice('data', { 421 | tag: 'tag', 422 | choices: { 423 | 0: 'int32le', 424 | 1: 'int16le', 425 | }, 426 | defaultChoice: 'uint8', 427 | }) 428 | .int32le('test'); 429 | 430 | buffer = Buffer.from([0x03, 0xff, 0x2f, 0xcb, 0x04, 0x0]); 431 | var decoded = parser.parse(buffer); 432 | assert.deepEqual(decoded, { 433 | tag: 3, 434 | data: 0xff, 435 | test: 314159, 436 | }); 437 | var encoded = parser.encode(decoded); 438 | assert.deepEqual(encoded, buffer); 439 | }); 440 | it('should parse choices of user defied types', function () { 441 | var parser = Parser.start() 442 | .uint8('tag') 443 | .choice('data', { 444 | tag: 'tag', 445 | choices: { 446 | 1: Parser.start() 447 | .uint8('length') 448 | .string('message', { length: 'length' }), 449 | 3: Parser.start().int32le('number'), 450 | }, 451 | }); 452 | 453 | var buffer = Buffer.from([ 454 | 0x1, 455 | 0xc, 456 | 0x68, 457 | 0x65, 458 | 0x6c, 459 | 0x6c, 460 | 0x6f, 461 | 0x2c, 462 | 0x20, 463 | 0x77, 464 | 0x6f, 465 | 0x72, 466 | 0x6c, 467 | 0x64, 468 | ]); 469 | var decoded = parser.parse(buffer); 470 | assert.deepEqual(decoded, { 471 | tag: 1, 472 | data: { 473 | length: 12, 474 | message: 'hello, world', 475 | }, 476 | }); 477 | var encoded = parser.encode(decoded); 478 | assert.deepEqual(encoded, buffer); 479 | buffer = Buffer.from([0x03, 0x4e, 0x61, 0xbc, 0x00]); 480 | decoded = parser.parse(buffer); 481 | assert.deepEqual(decoded, { 482 | tag: 3, 483 | data: { 484 | number: 12345678, 485 | }, 486 | }); 487 | encoded = parser.encode(decoded); 488 | assert.deepEqual(encoded, buffer); 489 | }); 490 | it('should be able to go into recursion', function () { 491 | var stop = Parser.start(); 492 | 493 | var parser = Parser.start() 494 | .namely('self') 495 | .uint8('type') 496 | .choice('data', { 497 | tag: 'type', 498 | choices: { 499 | 0: stop, 500 | 1: 'self', 501 | }, 502 | }); 503 | 504 | var buffer = Buffer.from([1, 1, 1, 0]); 505 | var decoded = parser.parse(buffer); 506 | assert.deepEqual(parser.parse(buffer), { 507 | type: 1, 508 | data: { 509 | type: 1, 510 | data: { 511 | type: 1, 512 | data: { type: 0, data: {} }, 513 | }, 514 | }, 515 | }); 516 | var encoded = parser.encode(decoded); 517 | assert.deepEqual(encoded, buffer); 518 | }); 519 | it('should be able to go into recursion with simple nesting', function () { 520 | var stop = Parser.start(); 521 | 522 | var parser = Parser.start() 523 | .namely('self') 524 | .uint8('type') 525 | .choice('data', { 526 | tag: 'type', 527 | choices: { 528 | 0: stop, 529 | 1: 'self', 530 | 2: Parser.start() 531 | .nest('left', { type: 'self' }) 532 | .nest('right', { type: stop }), 533 | }, 534 | }); 535 | 536 | var buffer = Buffer.from([2, /* left */ 1, 1, 0 /* right */]); 537 | var decoded = parser.parse(buffer); 538 | assert.deepEqual(decoded, { 539 | type: 2, 540 | data: { 541 | left: { 542 | type: 1, 543 | data: { 544 | type: 1, 545 | data: { 546 | type: 0, 547 | data: {}, 548 | }, 549 | }, 550 | }, 551 | right: {}, 552 | }, 553 | }); 554 | var encoded = parser.encode(decoded); 555 | assert.deepEqual(encoded, buffer); 556 | }); 557 | it('should be able to refer to other parsers by name', function () { 558 | var parser = Parser.start().namely('self'); 559 | 560 | var stop = Parser.start().namely('stop'); 561 | 562 | var twoCells = Parser.start() 563 | .namely('twoCells') 564 | .nest('left', { type: 'self' }) 565 | .nest('right', { type: 'stop' }); 566 | 567 | parser.uint8('type').choice('data', { 568 | tag: 'type', 569 | choices: { 570 | 0: 'stop', 571 | 1: 'self', 572 | 2: 'twoCells', 573 | }, 574 | }); 575 | 576 | var buffer = Buffer.from([2, /* left */ 1, 1, 0 /* right */]); 577 | var decoded = parser.parse(buffer); 578 | assert.deepEqual(decoded, { 579 | type: 2, 580 | data: { 581 | left: { 582 | type: 1, 583 | data: { type: 1, data: { type: 0, data: {} } }, 584 | }, 585 | right: {}, 586 | }, 587 | }); 588 | var encoded = parser.encode(decoded); 589 | assert.deepEqual(encoded, buffer); 590 | }); 591 | it('should be able to refer to other parsers both directly and by name', function () { 592 | var parser = Parser.start().namely('self'); 593 | 594 | var stop = Parser.start(); 595 | 596 | var twoCells = Parser.start() 597 | .nest('left', { type: 'self' }) 598 | .nest('right', { type: stop }); 599 | 600 | parser.uint8('type').choice('data', { 601 | tag: 'type', 602 | choices: { 603 | 0: stop, 604 | 1: 'self', 605 | 2: twoCells, 606 | }, 607 | }); 608 | 609 | var buffer = Buffer.from([2, /* left */ 1, 1, 0 /* right */]); 610 | var decoded = parser.parse(buffer); 611 | assert.deepEqual(decoded, { 612 | type: 2, 613 | data: { 614 | left: { 615 | type: 1, 616 | data: { type: 1, data: { type: 0, data: {} } }, 617 | }, 618 | right: {}, 619 | }, 620 | }); 621 | var encoded = parser.encode(decoded); 622 | assert.deepEqual(encoded, buffer); 623 | }); 624 | it('should be able to go into recursion with complex nesting', function () { 625 | var stop = Parser.start(); 626 | 627 | var parser = Parser.start() 628 | .namely('self') 629 | .uint8('type') 630 | .choice('data', { 631 | tag: 'type', 632 | choices: { 633 | 0: stop, 634 | 1: 'self', 635 | 2: Parser.start() 636 | .nest('left', { type: 'self' }) 637 | .nest('right', { type: 'self' }), 638 | 3: Parser.start() 639 | .nest('one', { type: 'self' }) 640 | .nest('two', { type: 'self' }) 641 | .nest('three', { type: 'self' }), 642 | }, 643 | }); 644 | 645 | // 2 646 | // / \ 647 | // 3 1 648 | // / | \ \ 649 | // 1 0 2 0 650 | // / / \ 651 | // 0 1 0 652 | // / 653 | // 0 654 | 655 | var buffer = Buffer.from([ 656 | 2, 657 | /* left -> */ 3, 658 | /* one -> */ 1, 659 | /* -> */ 0, 660 | /* two -> */ 0, 661 | /* three -> */ 2, 662 | /* left -> */ 1, 663 | /* -> */ 0, 664 | /* right -> */ 0, 665 | /* right -> */ 1, 666 | /* -> */ 0, 667 | ]); 668 | var decoded = parser.parse(buffer); 669 | assert.deepEqual(decoded, { 670 | type: 2, 671 | data: { 672 | left: { 673 | type: 3, 674 | data: { 675 | one: { type: 1, data: { type: 0, data: {} } }, 676 | two: { type: 0, data: {} }, 677 | three: { 678 | type: 2, 679 | data: { 680 | left: { type: 1, data: { type: 0, data: {} } }, 681 | right: { type: 0, data: {} }, 682 | }, 683 | }, 684 | }, 685 | }, 686 | right: { 687 | type: 1, 688 | data: { type: 0, data: {} }, 689 | }, 690 | }, 691 | }); 692 | var encoded = parser.encode(decoded); 693 | assert.deepEqual(encoded, buffer); 694 | }); 695 | it("should be able to 'flatten' choices when using null varName", function () { 696 | var parser = Parser.start() 697 | .uint8('tag') 698 | .choice(null, { 699 | tag: 'tag', 700 | choices: { 701 | 1: Parser.start() 702 | .uint8('length') 703 | .string('message', { length: 'length' }), 704 | 3: Parser.start().int32le('number'), 705 | }, 706 | }); 707 | 708 | var buffer = Buffer.from([ 709 | 0x1, 710 | 0xc, 711 | 0x68, 712 | 0x65, 713 | 0x6c, 714 | 0x6c, 715 | 0x6f, 716 | 0x2c, 717 | 0x20, 718 | 0x77, 719 | 0x6f, 720 | 0x72, 721 | 0x6c, 722 | 0x64, 723 | ]); 724 | var decoded = parser.parse(buffer); 725 | assert.deepEqual(decoded, { 726 | tag: 1, 727 | length: 12, 728 | message: 'hello, world', 729 | }); 730 | var encoded = parser.encode(decoded); 731 | assert.deepEqual(encoded, buffer); 732 | buffer = Buffer.from([0x03, 0x4e, 0x61, 0xbc, 0x00]); 733 | decoded = parser.parse(buffer); 734 | assert.deepEqual(decoded, { 735 | tag: 3, 736 | number: 12345678, 737 | }); 738 | encoded = parser.encode(decoded); 739 | assert.deepEqual(encoded, buffer); 740 | }); 741 | it("should be able to 'flatten' choices when omitting varName paramater", function () { 742 | var parser = Parser.start() 743 | .uint8('tag') 744 | .choice({ 745 | tag: 'tag', 746 | choices: { 747 | 1: Parser.start() 748 | .uint8('length') 749 | .string('message', { length: 'length' }), 750 | 3: Parser.start().int32le('number'), 751 | }, 752 | }); 753 | 754 | var buffer = Buffer.from([ 755 | 0x1, 756 | 0xc, 757 | 0x68, 758 | 0x65, 759 | 0x6c, 760 | 0x6c, 761 | 0x6f, 762 | 0x2c, 763 | 0x20, 764 | 0x77, 765 | 0x6f, 766 | 0x72, 767 | 0x6c, 768 | 0x64, 769 | ]); 770 | var decoded = parser.parse(buffer); 771 | assert.deepEqual(decoded, { 772 | tag: 1, 773 | length: 12, 774 | message: 'hello, world', 775 | }); 776 | var encoded = parser.encode(decoded); 777 | assert.deepEqual(encoded, buffer); 778 | buffer = Buffer.from([0x03, 0x4e, 0x61, 0xbc, 0x00]); 779 | decoded = parser.parse(buffer); 780 | assert.deepEqual(decoded, { 781 | tag: 3, 782 | number: 12345678, 783 | }); 784 | encoded = parser.encode(decoded); 785 | assert.deepEqual(encoded, buffer); 786 | }); 787 | it('should be able to use function as the choice selector', function () { 788 | var parser = Parser.start() 789 | .string('selector', { length: 4 }) 790 | .choice(null, { 791 | tag: function () { 792 | return parseInt(this.selector, 2); // string base 2 to integer decimal 793 | }, 794 | choices: { 795 | 2: Parser.start() 796 | .uint8('length') 797 | .string('message', { length: 'length' }), 798 | 7: Parser.start().int32le('number'), 799 | }, 800 | }); 801 | 802 | var buffer = Buffer.from([ 803 | 48, 804 | 48, 805 | 49, 806 | 48, 807 | 0xc, 808 | 0x68, 809 | 0x65, 810 | 0x6c, 811 | 0x6c, 812 | 0x6f, 813 | 0x2c, 814 | 0x20, 815 | 0x77, 816 | 0x6f, 817 | 0x72, 818 | 0x6c, 819 | 0x64, 820 | ]); 821 | var decoded = parser.parse(buffer); 822 | assert.deepEqual(decoded, { 823 | selector: '0010', // -> choice 2 824 | length: 12, 825 | message: 'hello, world', 826 | }); 827 | var encoded = parser.encode(decoded); 828 | assert.deepEqual(encoded, buffer); 829 | buffer = Buffer.from([48, 49, 49, 49, 0x4e, 0x61, 0xbc, 0x00]); 830 | decoded = parser.parse(buffer); 831 | assert.deepEqual(decoded, { 832 | selector: '0111', // -> choice 7 833 | number: 12345678, 834 | }); 835 | encoded = parser.encode(decoded); 836 | assert.deepEqual(encoded, buffer); 837 | }); 838 | }); 839 | 840 | describe('Nest parser', function () { 841 | it('should encode nested parsers', function () { 842 | var nameParser = new Parser() 843 | .string('firstName', { 844 | zeroTerminated: true, 845 | }) 846 | .string('lastName', { 847 | zeroTerminated: true, 848 | }); 849 | var infoParser = new Parser().uint8('age'); 850 | var personParser = new Parser() 851 | .nest('name', { 852 | type: nameParser, 853 | }) 854 | .nest('info', { 855 | type: infoParser, 856 | }); 857 | 858 | var buffer = Buffer.concat([ 859 | Buffer.from('John\0Doe\0'), 860 | Buffer.from([0x20]), 861 | ]); 862 | var person = personParser.parse(buffer); 863 | assert.deepEqual(person, { 864 | name: { 865 | firstName: 'John', 866 | lastName: 'Doe', 867 | }, 868 | info: { 869 | age: 0x20, 870 | }, 871 | }); 872 | var encoded = personParser.encode(person); 873 | assert.deepEqual(encoded, buffer); 874 | }); 875 | 876 | it('should format parsed nested parser', function () { 877 | var nameParser = new Parser() 878 | .string('firstName', { 879 | zeroTerminated: true, 880 | }) 881 | .string('lastName', { 882 | zeroTerminated: true, 883 | }); 884 | var personParser = new Parser().nest('name', { 885 | type: nameParser, 886 | formatter: function (name) { 887 | return name.firstName + ' ' + name.lastName; 888 | }, 889 | encoder: function (name) { 890 | // Reverse of aboce formatter 891 | var names = name.split(' '); 892 | return { firstName: names[0], lastName: names[1] }; 893 | }, 894 | }); 895 | 896 | var buffer = Buffer.from('John\0Doe\0'); 897 | var person = personParser.parse(buffer); 898 | assert.deepEqual(person, { 899 | name: 'John Doe', 900 | }); 901 | var encoded = personParser.encode(person); 902 | assert.deepEqual(encoded, buffer); 903 | }); 904 | 905 | it("should 'flatten' output when using null varName", function () { 906 | var parser = new Parser() 907 | .string('s1', { zeroTerminated: true }) 908 | .nest(null, { 909 | type: new Parser().string('s2', { zeroTerminated: true }), 910 | }); 911 | 912 | var buf = Buffer.from('foo\0bar\0'); 913 | var decoded = parser.parse(buf); 914 | assert.deepEqual(decoded, { s1: 'foo', s2: 'bar' }); 915 | var encoded = parser.encode(decoded); 916 | assert.deepEqual(encoded, buf); 917 | }); 918 | 919 | it("should 'flatten' output when omitting varName", function () { 920 | var parser = new Parser().string('s1', { zeroTerminated: true }).nest({ 921 | type: new Parser().string('s2', { zeroTerminated: true }), 922 | }); 923 | 924 | var buf = Buffer.from('foo\0bar\0'); 925 | var decoded = parser.parse(buf); 926 | assert.deepEqual(decoded, { s1: 'foo', s2: 'bar' }); 927 | var encoded = parser.encode(decoded); 928 | assert.deepEqual(encoded, buf); 929 | }); 930 | }); 931 | 932 | describe('Buffer encoder', function () { 933 | //this is a test for testing a fix of a bug, that removed the last byte of the 934 | //buffer parser 935 | it('should return a buffer with same size', function () { 936 | var bufferParser = new Parser().buffer('buf', { 937 | readUntil: 'eof', 938 | formatter: function (buffer) { 939 | return buffer; 940 | }, 941 | }); 942 | 943 | var buffer = Buffer.from('John\0Doe\0'); 944 | var decoded = bufferParser.parse(buffer); 945 | assert.deepEqual(decoded, { buf: buffer }); 946 | var encoded = bufferParser.encode(decoded); 947 | assert.deepEqual(encoded, buffer); 948 | }); 949 | }); 950 | 951 | describe('Constructors', function () { 952 | it('should create a custom object type', function () { 953 | function Person() { 954 | this.name = ''; 955 | } 956 | Person.prototype.toString = function () { 957 | return '[object Person]'; 958 | }; 959 | var parser = Parser.start().create(Person).string('name', { 960 | zeroTerminated: true, 961 | }); 962 | 963 | var buffer = Buffer.from('John Doe\0'); 964 | var person = parser.parse(buffer); 965 | assert.ok(person instanceof Person); 966 | assert.equal(person.name, 'John Doe'); 967 | var encoded = parser.encode(person); 968 | assert.deepEqual(encoded, buffer); 969 | }); 970 | }); 971 | 972 | describe('encode other fields after bit', function () { 973 | it('Encode uint8', function () { 974 | var buffer = Buffer.from([0, 1, 0, 4]); 975 | for (var i = 17; i <= 24; i++) { 976 | var parser = Parser.start()['bit' + i]('a').uint8('b'); 977 | var decoded = parser.parse(buffer); 978 | assert.deepEqual(decoded, { 979 | a: 1 << (i - 16), 980 | b: 4, 981 | }); 982 | var encoded = parser.encode(decoded); 983 | assert.deepEqual(encoded, buffer); 984 | } 985 | }); 986 | }); 987 | }); 988 | -------------------------------------------------------------------------------- /test/zz_encoder_bugs.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | const { SmartBuffer } = require('smart-buffer/build/smartbuffer'); 3 | var Parser = require('../dist/binary_parser').Parser; 4 | 5 | describe('Specific bugs testing', function () { 6 | describe('Array encoder with readUntil', function () { 7 | it('should limit to array length even if readUntil is never true', function () { 8 | var parser = Parser.start() 9 | .uint16('len') 10 | .array('payloads', { 11 | type: new Parser().uint8('cmd').array('params', { 12 | type: new Parser().uint8('param'), 13 | readUntil: function (item, buffer) { 14 | return buffer.length == 2; // Stop when 2 bytes left in parsed buffer 15 | }, 16 | }), 17 | lengthInBytes: function () { 18 | return this.len - 4; 19 | }, 20 | }) 21 | .uint16('crc'); 22 | 23 | var buffer = Buffer.from('0008AAB1B2B3FFFF', 'hex'); 24 | var decoded = parser.parse(buffer); 25 | 26 | assert.deepEqual(decoded, { 27 | len: 8, 28 | payloads: [ 29 | { 30 | cmd: 170, 31 | params: [ 32 | { 33 | param: 177, 34 | }, 35 | { 36 | param: 178, 37 | }, 38 | { 39 | param: 179, 40 | }, 41 | ], 42 | }, 43 | ], 44 | crc: 65535, 45 | }); 46 | 47 | var encoded; 48 | // Although readUntil is never true here, the encoding will be good 49 | assert.doesNotThrow(function () { 50 | encoded = parser.encode(decoded); 51 | }); 52 | assert.deepEqual(encoded, buffer); 53 | }); 54 | 55 | it('is not the reverse of parsing when readUntil gives false information', function () { 56 | var parser = Parser.start() 57 | .uint16('len') 58 | .array('payloads', { 59 | type: new Parser().uint8('cmd').array('params', { 60 | type: new Parser().uint8('param'), 61 | readUntil: function (item, buffer) { 62 | return buffer.length <= 2; // Stop when 2 bytes left in buffer 63 | }, 64 | }), 65 | lengthInBytes: function () { 66 | return this.len - 4; 67 | }, 68 | }) 69 | .uint16('crc'); 70 | 71 | var buffer = Buffer.from('0008AAB1B2B3FFFF', 'hex'); 72 | var decoded = parser.parse(buffer); 73 | 74 | assert.deepEqual(decoded, { 75 | len: 8, 76 | payloads: [ 77 | { 78 | cmd: 170, 79 | params: [ 80 | { 81 | param: 177, 82 | }, 83 | { 84 | param: 178, 85 | }, 86 | { 87 | param: 179, 88 | }, 89 | ], 90 | }, 91 | ], 92 | crc: 0xffff, 93 | }); 94 | 95 | var encoded = parser.encode(decoded); 96 | // Missing parms 178 and 179 as readUntil will be true at first run 97 | assert.deepEqual(encoded, Buffer.from('0008AAB1FFFF', 'hex')); 98 | }); 99 | 100 | it('should ignore readUntil when encodeUntil is provided', function () { 101 | var parser = Parser.start() 102 | .uint16('len') 103 | .array('payloads', { 104 | type: new Parser().uint8('cmd').array('params', { 105 | type: new Parser().uint8('param'), 106 | readUntil: function (item, buffer) { 107 | return buffer.length == 2; // Stop when 2 bytes left in buffer 108 | }, 109 | encodeUntil: function (item, obj) { 110 | return item.param === 178; // Stop encoding when value 178 is reached 111 | }, 112 | }), 113 | lengthInBytes: function () { 114 | return this.len - 4; 115 | }, 116 | }) 117 | .uint16('crc'); 118 | 119 | var buffer = Buffer.from('0008AAB1B2B3FFFF', 'hex'); 120 | var decoded = parser.parse(buffer); 121 | 122 | assert.deepEqual(decoded, { 123 | len: 8, 124 | payloads: [ 125 | { 126 | cmd: 170, 127 | params: [ 128 | { 129 | param: 177, 130 | }, 131 | { 132 | param: 178, 133 | }, 134 | { 135 | param: 179, 136 | }, 137 | ], 138 | }, 139 | ], 140 | crc: 0xffff, 141 | }); 142 | 143 | var encoded = parser.encode(decoded); 144 | // Missing parms 179 as encodeUntil stops at 178 145 | assert.deepEqual(encoded, Buffer.from('0008AAB1B2FFFF', 'hex')); 146 | }); 147 | 148 | it('should accept readUntil=eof and no encodeUntil provided', function () { 149 | var parser = Parser.start().array('arr', { 150 | type: 'uint8', 151 | readUntil: 'eof', // Read until end of buffer 152 | }); 153 | 154 | var buffer = Buffer.from('01020304050607', 'hex'); 155 | var decoded = parser.parse(buffer); 156 | 157 | assert.deepEqual(decoded, { 158 | arr: [1, 2, 3, 4, 5, 6, 7], 159 | }); 160 | 161 | var encoded = parser.encode(decoded); 162 | assert.deepEqual(encoded, Buffer.from('01020304050607', 'hex')); 163 | }); 164 | 165 | it('should accept empty array to encode', function () { 166 | var parser = Parser.start().array('arr', { 167 | type: 'uint8', 168 | readUntil: 'eof', // Read until end of buffer 169 | }); 170 | 171 | var buffer = Buffer.from('', 'hex'); 172 | var decoded = parser.parse(buffer); 173 | 174 | assert.deepEqual(decoded, { 175 | arr: [], 176 | }); 177 | 178 | var encoded = parser.encode(decoded); 179 | assert.deepEqual(encoded, Buffer.from('', 'hex')); 180 | }); 181 | 182 | it('should accept empty array to encode and encodeUntil function', function () { 183 | var parser = Parser.start().array('arr', { 184 | type: 'uint8', 185 | readUntil: 'eof', // Read until end of buffer 186 | encodeUntil: function (item, obj) { 187 | return false; // Never stop on content value 188 | }, 189 | }); 190 | 191 | var buffer = Buffer.from('', 'hex'); 192 | var decoded = parser.parse(buffer); 193 | 194 | assert.deepEqual(decoded, { 195 | arr: [], 196 | }); 197 | 198 | var encoded = parser.encode(decoded); 199 | assert.deepEqual(encoded, Buffer.from('', 'hex')); 200 | }); 201 | 202 | it('should accept undefined or null array', function () { 203 | var parser = Parser.start().array('arr', { 204 | type: 'uint8', 205 | readUntil: 'eof', // Read until end of buffer 206 | }); 207 | 208 | var buffer = Buffer.from('', 'hex'); 209 | var decoded = parser.parse(buffer); 210 | 211 | // Decode an empty buffer as an empty array 212 | assert.deepEqual(decoded, { 213 | arr: [], 214 | }); 215 | 216 | // Encode undefined, null or empty array as an empty buffer 217 | [{}, { arr: undefined }, { arr: null }, { arr: [] }].forEach((data) => { 218 | let encoded = parser.encode(data); 219 | assert.deepEqual(encoded, Buffer.from('', 'hex')); 220 | }); 221 | }); 222 | }); 223 | 224 | describe('Issue #19 Little endianess incorrect', function () { 225 | let binaryLiteral = function (s) { 226 | var i; 227 | var bytes = []; 228 | 229 | s = s.replace(/\s/g, ''); 230 | for (i = 0; i < s.length; i += 8) { 231 | bytes.push(parseInt(s.slice(i, i + 8), 2)); 232 | } 233 | 234 | return Buffer.from(bytes); 235 | }; 236 | it('should parse 4-byte-length bit field sequence wit little endian', function () { 237 | let buf = binaryLiteral('0000000000001111 1010000110100010'); // 000F A1A2 238 | 239 | // Parsed as two uint16 with little-endian (BYTES order) 240 | let parser1 = new Parser().uint16le('a').uint16le('b'); 241 | 242 | // Parsed as two 16 bits fields with little-endian 243 | let parser2 = new Parser().endianess('little').bit16('a').bit16('b'); 244 | 245 | let parsed1 = parser1.parse(buf); 246 | let parsed2 = parser2.parse(buf); 247 | 248 | assert.deepEqual(parsed1, { 249 | a: 0x0f00, // 000F 250 | b: 0xa2a1, // A1A2 251 | }); 252 | 253 | assert.deepEqual(parsed2, { 254 | a: 0xa1a2, // last 16 bits (but value coded as BE) 255 | b: 0x000f, // first 16 bits (but value coded as BE) 256 | }); 257 | 258 | /* This is a little confusing. The endianess with bits fields affect the order of fields */ 259 | }); 260 | it('should encode bit ranges with little endian correctly', function () { 261 | let bigParser = Parser.start() 262 | .endianess('big') 263 | .bit4('a') 264 | .bit1('b') 265 | .bit1('c') 266 | .bit1('d') 267 | .bit1('e') 268 | .uint16('f') 269 | .array('g', { type: 'uint8', readUntil: 'eof' }); 270 | let littleParser = Parser.start() 271 | .endianess('little') 272 | .bit4('a') 273 | .bit1('b') 274 | .bit1('c') 275 | .bit1('d') 276 | .bit1('e') 277 | .uint16('f') 278 | .array('g', { type: 'uint8', readUntil: 'eof' }); 279 | // Parser definition for a symetric encoding/decoding of little-endian bit fields 280 | let little2Parser = Parser.start() 281 | .endianess('little') 282 | .encoderSetOptions({ bitEndianess: true }) 283 | .bit4('a') 284 | .bit1('b') 285 | .bit1('c') 286 | .bit1('d') 287 | .bit1('e') 288 | .uint16('f') 289 | .array('g', { type: 'uint8', readUntil: 'eof' }); 290 | 291 | let data = binaryLiteral( 292 | '0011 0 1 0 1 0000000011111111 00000001 00000010 00000011' 293 | ); // 35 00FF 01 02 03 294 | // in big endian: 3 0 1 0 1 00FF 1 2 3 295 | // in little endian: 3 0 1 0 1 FF00 1 2 3 296 | // LE with encoderBitEndianess option: 297 | // 5 1 1 0 0 FF00 1 2 3 298 | 299 | //let bigDecoded = bigParser.parse(data); 300 | //let littleDecoded = littleParser.parse(data); 301 | let little2Decoded = little2Parser.parse(data); 302 | 303 | //console.log(bigDecoded); 304 | //console.log(littleDecoded); 305 | //console.log(little2Decoded); 306 | 307 | let big = { 308 | a: 3, 309 | b: 0, 310 | c: 1, 311 | d: 0, 312 | e: 1, 313 | f: 0x00ff, 314 | g: [1, 2, 3], 315 | }; 316 | let little = { 317 | a: 3, 318 | b: 0, 319 | c: 1, 320 | d: 0, 321 | e: 1, 322 | f: 0xff00, 323 | g: [1, 2, 3], 324 | }; 325 | let little2 = { 326 | a: 5, 327 | b: 1, 328 | c: 1, 329 | d: 0, 330 | e: 0, 331 | f: 0xff00, 332 | g: [1, 2, 3], 333 | }; 334 | 335 | assert.deepEqual(little2Decoded, little2); 336 | 337 | let bigEncoded = bigParser.encode(big); 338 | let littleEncoded = littleParser.encode(little); 339 | let little2Encoded = little2Parser.encode(little2); 340 | 341 | //console.log(bigEncoded); 342 | //console.log(littleEncoded); 343 | //console.log(little2Encoded); 344 | 345 | assert.deepEqual(bigEncoded, data); 346 | assert.deepEqual(littleEncoded, data); 347 | assert.deepEqual(little2Encoded, data); 348 | }); 349 | }); 350 | 351 | describe('Issue #20 Encoding fixed length null terminated or strip null strings', function () { 352 | it('should encode zero terminated fixed-length string', function () { 353 | // In that case parsing and encoding are not the exact oposite 354 | let buffer = Buffer.from( 355 | '\u0000A\u0000AB\u0000ABC\u0000ABCD\u0000ABCDE\u0000' 356 | ); 357 | let parser = Parser.start() 358 | .string('a', { length: 4, zeroTerminated: true }) 359 | .string('b', { length: 4, zeroTerminated: true }) 360 | .string('c', { length: 4, zeroTerminated: true }) 361 | .string('d', { length: 4, zeroTerminated: true }) 362 | .string('e', { length: 4, zeroTerminated: true }) 363 | .string('f', { length: 4, zeroTerminated: true }) 364 | .string('g', { length: 4, zeroTerminated: true }) 365 | .string('h', { length: 4, zeroTerminated: true }); 366 | 367 | let decoded = parser.parse(buffer); 368 | assert.deepEqual(decoded, { 369 | a: '', 370 | b: 'A', 371 | c: 'AB', 372 | d: 'ABC', 373 | e: 'ABCD', 374 | f: '', 375 | g: 'ABCD', 376 | h: 'E', 377 | }); 378 | 379 | let encoded = parser.encode(decoded); 380 | assert.deepEqual(encoded, buffer); 381 | }); 382 | 383 | it('should encode fixed-length string with stripNull', function () { 384 | let parser = Parser.start() 385 | .string('a', { length: 8, zeroTerminated: false, stripNull: true }) 386 | .string('b', { length: 8, zeroTerminated: false, stripNull: true }) 387 | .string('z', { length: 2, zeroTerminated: false, stripNull: true }); 388 | let buffer = Buffer.from('ABCD\u0000\u0000\u0000\u000012345678ZZ'); 389 | let decoded = parser.parse(buffer); 390 | assert.deepEqual(decoded, { 391 | a: 'ABCD', 392 | b: '12345678', 393 | z: 'ZZ', 394 | }); 395 | let encoded = parser.encode(decoded); 396 | assert.deepEqual(encoded, buffer); 397 | }); 398 | }); 399 | 400 | describe('Issue #23 Unable to encode uint64', function () { 401 | it('should not fail when encoding uint64', function () { 402 | let ipHeader = new Parser() 403 | .uint16('fragment_id') 404 | .uint16('fragment_total') 405 | .uint64('datetime'); 406 | 407 | let anIpHeader = { 408 | fragment_id: 1, 409 | fragment_total: 1, 410 | datetime: 4744430483355899789n, 411 | }; 412 | 413 | try { 414 | let result = ipHeader.encode(anIpHeader).toString('hex'); 415 | assert.ok(true, 'No exception'); 416 | } catch (ex) { 417 | assert.fail(ex); 418 | } 419 | }); 420 | }); 421 | }); 422 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | "outDir": "dist", /* Redirect output structure to the directory. */ 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": false, /* Enable all strict type-checking options. */ 27 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | "types": ["node"], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | } 63 | } 64 | --------------------------------------------------------------------------------