From e4c1f88a41d64a19a13897e6a4d1ba270a187987 Mon Sep 17 00:00:00 2001 From: Autarc Date: Sat, 11 Jul 2015 16:52:45 +0200 Subject: [PATCH 1/4] Enable Strings as choice keys --- lib/binary_parser.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/binary_parser.js b/lib/binary_parser.js index 978e9484..23b3694a 100644 --- a/lib/binary_parser.js +++ b/lib/binary_parser.js @@ -148,9 +148,6 @@ Parser.prototype.choice = function(varName, options) { throw new Error('Choices option of array is not defined.'); } Object.keys(options.choices).forEach(function(key) { - if (isNaN(parseInt(key, 10))) { - throw new Error('Key of choices must be a number.'); - } if (!options.choices[key]) { throw new Error('Choice Case ' + key + ' of ' + varName + ' is not valid.'); } @@ -524,7 +521,7 @@ Parser.prototype.generateChoice = function(ctx) { Object.keys(this.options.choices).forEach(function(tag) { var type = this.options.choices[tag]; - ctx.pushCode('case {0}:', tag); + ctx.pushCode('case ' + (isNaN(tag) ? '"{0}"' : '{0}') + ':', tag); this.generateChoiceCase(ctx, this.varName, type); ctx.pushCode('break;'); }, this); From 5dd275278dd57c81a4c8d18661f2ff9915f221a9 Mon Sep 17 00:00:00 2001 From: Autarc Date: Sat, 11 Jul 2015 20:40:51 +0200 Subject: [PATCH 2/4] Add $parent property for passing scoped references --- README.md | 8 ++++++-- lib/binary_parser.js | 24 ++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c11e6c4a..0bac94dc 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,8 @@ Parse bytes as an array. `options` is an object; following options are available Use number for statically sized arrays. - `readUntil` - (either `length` or `readUntil` is required) If `'eof'`, then this parser reads until the end of `Buffer` object. If function it reads until the function returns true. +- `$parent` - (Optional) An array with selected properties from the parental scope. References can be + accessed inside functions using `this.$parent[...]`. ```javascript var parser = new Parser() @@ -193,9 +195,11 @@ Combining `choice` with `array` is useful for parsing a typical - `tag` - (Required) The value used to determine which parser to use from the `choices` Can be a string pointing to another field or a function. -- `choices` - (Required) An object which key is an integer and value is the parser which is executed +- `choices` - (Required) An object which key is an integer/string and value is the parser which is executed when `tag` equals the key value. - `defaultChoice` - (Optional) In case of the tag value doesn't match any of `choices` use this parser. +- `$parent` - (Optional) An array with selected properties from the parental scope. References can be +accessed inside functions using `this.$parent[...]`. ```javascript var parser1 = ...; @@ -254,7 +258,7 @@ These are common options that can be specified in all parsers. ```javascript var parser = new Parser() .array('ipv4', { - type: uint8, + type: 'uint8', length: '4', formatter: function(arr) { return arr.join('.'); } }); diff --git a/lib/binary_parser.js b/lib/binary_parser.js index 23b3694a..faacc8be 100644 --- a/lib/binary_parser.js +++ b/lib/binary_parser.js @@ -482,7 +482,17 @@ Parser.prototype.generateArray = function(ctx) { ctx.pushCode('var {0} = buffer.read{1}(offset);', item, NAME_MAP[type]); ctx.pushCode('offset += {0};', PRIMITIVE_TYPES[NAME_MAP[type]]); } else if (type instanceof Parser) { - ctx.pushCode('var {0} = {};', item); + if (!this.options.$parent) { + ctx.pushCode('var {0} = {};', item); + } else { + ctx.pushCode('var {0} = { "$parent": {1} };', item, + '[' + this.options.$parent.map(function(prop){ return '"' + prop + '"'; }).toString() + + '].reduce(function($parent, key){\ + $parent[key] = ' + ctx.generateVariable() + '[key];\ + return $parent;\ + }, {})' + ); + } ctx.pushScope(item); type.generate(ctx); @@ -516,7 +526,17 @@ Parser.prototype.generateChoiceCase = function(ctx, varName, type) { Parser.prototype.generateChoice = function(ctx) { var tag = ctx.generateOption(this.options.tag); - ctx.pushCode('{0} = {};', ctx.generateVariable(this.varName)); + if (!this.options.$parent) { + ctx.pushCode('{0} = {};', ctx.generateVariable(this.varName)); + } else { + ctx.pushCode('{0} = { "$parent": {1} };', ctx.generateVariable(this.varName), + '[' + this.options.$parent.map(function(prop){ return '"' + prop + '"'; }).toString() + + '].reduce(function($parent, key){\ + $parent[key] = ' + ctx.generateVariable() + '[key];\ + return $parent;\ + }, {})' + ); + } ctx.pushCode('switch({0}) {', tag); Object.keys(this.options.choices).forEach(function(tag) { var type = this.options.choices[tag]; From 165c61a35c1b43991121f3fdf34315cbf611fd58 Mon Sep 17 00:00:00 2001 From: Autarc Date: Wed, 15 Jul 2015 10:34:42 +0200 Subject: [PATCH 3/4] improve skip parameter description --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0bac94dc..2d492492 100644 --- a/README.md +++ b/README.md @@ -225,7 +225,7 @@ Nest a parser in this position. Parse result of the nested parser is stored in t - `type` - (Required) A `Parser` object. ### skip(length) -Skip parsing for `length` bytes. +Skip parsing of bytes. `length` can be either a number, a string or a function. ### endianess(endianess) Define what endianess to use in this parser. `endianess` can be either `'little'` or `'big'`. From f8a4ac273491e526721d4cdfc06660545d1cbac3 Mon Sep 17 00:00:00 2001 From: Autarc Date: Wed, 22 Jul 2015 14:00:39 +0200 Subject: [PATCH 4/4] Added 'loop' call to API --- README.md | 31 +++++++++++++++++-- lib/binary_parser.js | 66 +++++++++++++++++++++++++++++++++++----- test/composite_parser.js | 18 +++++++++++ 3 files changed, 105 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 2d492492..486d2ee2 100644 --- a/README.md +++ b/README.md @@ -211,13 +211,38 @@ var parser = new Parser() .choice('data', { tag: 'tagValue', choices: { - 1: parser1, // When tagValue == 1, execute parser1 - 4: parser2, // When tagValue == 4, execute parser2 - 5: parser3 // When tagValue == 5, execute parser3 + 1: parser1, // if tagValue == 1, execute parser1 + 4: parser2, // if tagValue == 4, execute parser2 + 5: parser3 // if tagValue == 5, execute parser3 } }); ``` +### loop(lookahead, [,options]) +Choose and execute parsers which are matching the referenced tag value. +The `lookahead` parser provides information about upcoming fields to determine the selection. +As long as one of the `choices` options fits, it continues parsing the buffer. + +- `tag` - (Required) The value used to determine which parser to use from the `choices` + Can be a string pointing to another field or a function. +- `choices` - (Required) An object which key is an integer/string and value is the parser which is executed + when `tag` equals the key value. The key of a choice is used as the property name and will hold an array + of results if multiple instances are matched. + +```javascript +var parser1 = ...; +var parser2 = ...; + +var parser = new Parser() + .loop(new Parser().skip(4).string('type', { length: 4 })), { + tag: 'type', + choices: { + ftyp: parser1, // if type == 'ftyp', execute parser1 + moov: parser2 // if type == 'ftyp', execute parser2 + } + }); +``` + ### nest(name [,options]) Nest a parser in this position. Parse result of the nested parser is stored in the variable `name`. diff --git a/lib/binary_parser.js b/lib/binary_parser.js index faacc8be..ae650ffc 100644 --- a/lib/binary_parser.js +++ b/lib/binary_parser.js @@ -28,7 +28,8 @@ var SPECIAL_TYPES = { 'Skip' : null, 'Choice' : null, 'Nest' : null, - 'Bit' : null + 'Bit' : null, + 'Loop' : null }; var BIT_RANGE = []; @@ -171,6 +172,17 @@ Parser.prototype.nest = function(varName, options) { return this.setNextParser('nest', varName, options); }; +Parser.prototype.loop = function (lookahead, options) { + if (!(lookahead instanceof Parser)) { + throw new Error('The lookahead function must be a Parser object.'); + } + if (!options.tag) { + throw new Error('Tag option of loop is not defined.'); + } + + return this.setNextParser('loop', lookahead, options); +}; + Parser.prototype.endianess = function(endianess) { switch (endianess.toLowerCase()) { case 'little': @@ -288,13 +300,16 @@ Parser.prototype.setNextParser = function(type, varName, options) { // Call code generator for this parser Parser.prototype.generate = function(ctx) { + if (this.type) { this['generate' + this.type](ctx); - this.generateAssert(ctx); + if (this.options.assert) { + this.generateAssert(ctx); + } } - var varName = ctx.generateVariable(this.varName); if (this.options.formatter) { + var varName = ctx.generateVariable(this.varName); this.generateFormatter(ctx, varName, this.options.formatter); } @@ -302,10 +317,6 @@ Parser.prototype.generate = function(ctx) { }; Parser.prototype.generateAssert = function(ctx) { - if (!this.options.assert) { - return; - } - var varName = ctx.generateVariable(this.varName); switch (typeof this.options.assert) { @@ -568,6 +579,47 @@ Parser.prototype.generateFormatter = function(ctx, varName, formatter) { } }; +Parser.prototype.generateLoop = function(ctx) { + var originalScopes = ctx.scopes; + var scopeReference = originalScopes[0].join('.'); + var loopVar = ctx.generateTmpVariable(); + var tagVar = ctx.generateTmpVariable(); + var caseVar = ctx.generateTmpVariable(); + ctx.pushCode('var {0} = true;', loopVar); + ctx.pushCode('var {0} = {};', tagVar); + ctx.pushCode('var {0} = {};', caseVar); + + ctx.pushCode('while ({0}) {', loopVar); + ctx.scopes = [[tagVar]]; + var before = ctx.code; + this.varName.generate(ctx); + ctx.scopes = [[]]; + ctx.pushCode('offset -= {0};', + ctx.code.replace(before, '').match(/offset \+= (\d+);/g).reduce(function(length, modifier) { + return length + parseInt(modifier.replace(/\D+/, ''), 10); + }, 0) + ); + ctx.pushCode('switch({0}.{1}) {', tagVar, this.options.tag); + Object.keys(this.options.choices).forEach(function(tag) { + var type = this.options.choices[tag]; + + ctx.pushCode('case ' + (isNaN(tag) ? '"{0}"' : '{0}') + ':', tag); + ctx.pushCode('{0} = {};', caseVar); + this.generateChoiceCase(ctx, caseVar, type); + ctx.pushCode('if (!{0}.{1}) {\ + {0}.{1} = {2}; } else {\ + if (!Array.isArray({0}.{1})) {0}.{1} = [{0}.{1}];\ + {0}.{1}.push({2}); }', scopeReference, tag, caseVar); + ctx.pushCode('break;'); + }, this); + ctx.pushCode('default:'); + ctx.pushCode('{0} = false;', loopVar); + ctx.pushCode('}'); + + ctx.pushCode('}'); + ctx.scopes = originalScopes; +}; + Parser.prototype.isInteger = function() { return !!this.type.match(/U?Int[8|16|32][BE|LE]?|Bit\d+/); }; diff --git a/test/composite_parser.js b/test/composite_parser.js index 700541dd..b402e265 100644 --- a/test/composite_parser.js +++ b/test/composite_parser.js @@ -262,6 +262,24 @@ describe('Composite parser', function(){ }); }); + describe('Loop parser', function(){ + it('should parse looped parsers', function(){ + var parser = Parser.start() + .loop(Parser.start().uint8('tag'), { + tag: 'tag', + choices: { + 0: 'int32le', + 1: 'int16le' + } + }); + var buffer = new Buffer([0x0, 0x4e, 0x61, 0xbc, 0x00, 0x01, 0xd2, 0x04]); + assert.deepEqual(parser.parse(buffer), { + 0: 12345678, + 1: 1234 + }); + }); + }); + describe('Nest parser', function() { it('should parse nested parsers', function() { var nameParser = new Parser()