diff --git a/README.md b/README.md index bad9a6c6..455b98bc 100644 --- a/README.md +++ b/README.md @@ -429,10 +429,10 @@ parser.parse(buffer); ``` ### wrapped(name[, options]) -Read data then wrap it by transforming it by a function for further parsing. -It works similarly to a buffer where it reads a block of data. But instead of returning the buffer it +Read data then wrap it by transforming it by a function for further parsing. +It works similarly to a buffer where it reads a block of data. But instead of returning the buffer it will pass it on to a parser for further processing. -- `wrapper` - (Required) A function taking a buffer and returning a buffer (`(x: Buffer | Uint8Array ) => Buffer | Uint8Array`) +- `wrapper` - (Required) A function taking a buffer and returning a buffer (`(x: Buffer | Uint8Array ) => Buffer | Uint8Array`) transforming the buffer into a buffer expected by `type`. - `type` - (Required) A `Parser` object to parse the result of wrapper. - `length ` - (either `length` or `readUntil` is required) Length of the @@ -455,11 +455,11 @@ var textParser = Parser.start() var mainParser = Parser.start() // Read length of the data to wrap .uint32le('length') - // Read wrapped data + // Read wrapped data .wrapped('wrappedData', { // Indicate how much data to read, like buffer() length: 'length', - // Define function to pre-process the data buffer + // Define function to pre-process the data buffer wrapper: function (buffer) { // E.g. decompress data and return it for further parsing return zlib.inflateRawSync(buffer); @@ -519,6 +519,42 @@ These options can be used in all parsers. }); ``` +### Context variables +You can use some special fields while parsing to traverse your structure. These +context variables will be removed after the parsing process: +- `$parent` - This field references the parent structure. This variable will be + `null` while parsing the root structure. + ```js + var parser = new Parser() + .nest("header", { + type: new Parser().uint32("length"), + }) + .array("data", { + type: "int32", + length: function() { + return this.$parent.header.length + } + }) + ``` +- `$root` - This field references the root structure. + ```js + var parser = new Parser() + .nest("header", { + type: new Parser().uint32("length"), + }) + .nest("data", { + type: new Parser() + .uint32("value") + .array("data", { + type: "int32", + length: function() { + return this.$root.header.length + } + }), + }) + + ``` + ## Examples See `example/` for real-world examples. diff --git a/lib/binary_parser.ts b/lib/binary_parser.ts index 80c2e595..9d161384 100644 --- a/lib/binary_parser.ts +++ b/lib/binary_parser.ts @@ -457,11 +457,11 @@ export class Parser { return this; } - skip(length: number, options?: ParserOptions) { + skip(length: ParserOptions['length'], options?: ParserOptions) { return this.seek(length, options); } - seek(relOffset: number, options?: ParserOptions) { + seek(relOffset: ParserOptions['length'], options?: ParserOptions) { if (options && options.assert) { throw new Error('assert option on seek is not allowed.'); } @@ -571,7 +571,7 @@ export class Parser { return this.setNextParser('choice', varName as string, options); } - nest(varName: string | ParserOptions, options: ParserOptions) { + nest(varName: string | ParserOptions, options?: ParserOptions) { if (typeof options !== 'object' && typeof varName === 'object') { options = varName; varName = null; @@ -670,34 +670,38 @@ export class Parser { private addRawCode(ctx: Context) { ctx.pushCode('var offset = 0;'); - - if (this.constructorFn) { - ctx.pushCode('var vars = new constructorFn();'); - } else { - ctx.pushCode('var vars = {};'); - } + ctx.pushCode( + `var vars = ${this.constructorFn ? 'new constructorFn()' : '{}'};` + ); + ctx.pushCode('vars.$parent = null;'); + ctx.pushCode('vars.$root = vars;'); this.generate(ctx); this.resolveReferences(ctx); + ctx.pushCode('delete vars.$parent;'); + ctx.pushCode('delete vars.$root;'); ctx.pushCode('return vars;'); } private addAliasedCode(ctx: Context) { - ctx.pushCode(`function ${FUNCTION_PREFIX + this.alias}(offset) {`); - - if (this.constructorFn) { - ctx.pushCode('var vars = new constructorFn();'); - } else { - ctx.pushCode('var vars = {};'); - } + ctx.pushCode( + `function ${FUNCTION_PREFIX + this.alias}(offset, parent, root) {` + ); + ctx.pushCode( + `var vars = ${this.constructorFn ? 'new constructorFn()' : '{}'};` + ); + ctx.pushCode('vars.$parent = parent || null;'); + ctx.pushCode('vars.$root = root || vars;'); this.generate(ctx); ctx.markResolved(this.alias); this.resolveReferences(ctx); + ctx.pushCode('delete vars.$parent;'); + ctx.pushCode('delete vars.$root;'); ctx.pushCode('return { offset: offset, result: vars };'); ctx.pushCode('}'); @@ -1088,18 +1092,28 @@ export class Parser { ); ctx.pushCode(`offset += ${PRIMITIVE_SIZES[type as PrimitiveTypes]};`); } else { + const parentVar = ctx.generateVariable(); const tempVar = ctx.generateTmpVariable(); - ctx.pushCode(`var ${tempVar} = ${FUNCTION_PREFIX + type}(offset);`); + ctx.pushCode( + `var ${tempVar} = ${ + FUNCTION_PREFIX + type + }(offset, ${parentVar}, ${parentVar}.$root);` + ); ctx.pushCode( `var ${item} = ${tempVar}.result; offset = ${tempVar}.offset;` ); if (type !== this.alias) ctx.addReference(type); } } else if (type instanceof Parser) { + const parentVar = ctx.generateVariable(); ctx.pushCode(`var ${item} = {};`); ctx.pushScope(item); + ctx.pushCode(`${item}.$parent = ${parentVar};`); + ctx.pushCode(`${item}.$root = ${parentVar}.$root;`); type.generate(ctx); + ctx.pushCode(`delete ${item}.$parent`); + ctx.pushCode(`delete ${item}.$root`); ctx.popScope(); } @@ -1136,7 +1150,11 @@ export class Parser { ctx.pushCode(`offset += ${PRIMITIVE_SIZES[type as PrimitiveTypes]}`); } else { const tempVar = ctx.generateTmpVariable(); - ctx.pushCode(`var ${tempVar} = ${FUNCTION_PREFIX + type}(offset);`); + ctx.pushCode( + `var ${tempVar} = ${ + FUNCTION_PREFIX + type + }(offset, ${varName}.$parent, ${varName}.$root);` + ); ctx.pushCode( `${varName} = ${tempVar}.result; offset = ${tempVar}.offset;` ); @@ -1151,8 +1169,14 @@ export class Parser { private generateChoice(ctx: Context) { const tag = ctx.generateOption(this.options.tag); + const nestVar = ctx.generateVariable(this.varName); + if (this.varName) { - ctx.pushCode(`${ctx.generateVariable(this.varName)} = {};`); + ctx.pushCode(`${nestVar} = {};`); + + const parentVar = ctx.generateVariable(); + ctx.pushCode(`${nestVar}.$parent = ${parentVar};`); + ctx.pushCode(`${nestVar}.$root = ${parentVar}.$root;`); } ctx.pushCode(`switch(${tag}) {`); Object.keys(this.options.choices).forEach((tag) => { @@ -1169,6 +1193,11 @@ export class Parser { ctx.generateError(`"Met undefined tag value " + ${tag} + " at choice"`); } ctx.pushCode('}'); + + if (this.varName) { + ctx.pushCode(`delete ${nestVar}.$parent`); + ctx.pushCode(`delete ${nestVar}.$root`); + } } private generateNest(ctx: Context) { @@ -1176,15 +1205,25 @@ export class Parser { if (this.options.type instanceof Parser) { if (this.varName) { + const parentVar = ctx.generateVariable(); ctx.pushCode(`${nestVar} = {};`); + ctx.pushCode(`${nestVar}.$parent = ${parentVar};`); + ctx.pushCode(`${nestVar}.$root = ${parentVar}.$root;`); } ctx.pushPath(this.varName); this.options.type.generate(ctx); ctx.popPath(this.varName); + if (this.varName) { + ctx.pushCode(`delete ${nestVar}.$parent`); + ctx.pushCode(`delete ${nestVar}.$root`); + } } else if (aliasRegistry[this.options.type]) { + const parentVar = ctx.generateVariable(); const tempVar = ctx.generateTmpVariable(); ctx.pushCode( - `var ${tempVar} = ${FUNCTION_PREFIX + this.options.type}(offset);` + `var ${tempVar} = ${ + FUNCTION_PREFIX + this.options.type + }(offset, ${parentVar}, ${parentVar}.$root);` ); ctx.pushCode( `${nestVar} = ${tempVar}.result; offset = ${tempVar}.offset;` @@ -1283,14 +1322,22 @@ export class Parser { ctx.pushCode(`offset = ${offset};`); if (this.options.type instanceof Parser) { + const parentVar = ctx.generateVariable(); ctx.pushCode(`${nestVar} = {};`); + ctx.pushCode(`${nestVar}.$parent = ${parentVar};`); + ctx.pushCode(`${nestVar}.$root = ${parentVar}.$root;`); ctx.pushPath(this.varName); this.options.type.generate(ctx); ctx.popPath(this.varName); + ctx.pushCode(`delete ${nestVar}.$parent`); + ctx.pushCode(`delete ${nestVar}.$root`); } else if (aliasRegistry[this.options.type]) { + const parentVar = ctx.generateVariable(); const tempVar = ctx.generateTmpVariable(); ctx.pushCode( - `var ${tempVar} = ${FUNCTION_PREFIX + this.options.type}(offset);` + `var ${tempVar} = ${ + FUNCTION_PREFIX + this.options.type + }(offset, ${parentVar}, ${parentVar}.$root);` ); ctx.pushCode( `${nestVar} = ${tempVar}.result; offset = ${tempVar}.offset;` diff --git a/test/composite_parser.js b/test/composite_parser.js index 2f476834..16d4f075 100644 --- a/test/composite_parser.js +++ b/test/composite_parser.js @@ -62,6 +62,74 @@ const suite = (Buffer) => ], }); }); + it('should parse array of user defined types and have access to parent context', function () { + var elementParser = new Parser().uint8('key').array('value', { + type: "uint8", + length: function () { + return this.$parent.valueLength; + } + }); + + var parser = Parser.start().uint16le('length').uint16le('valueLength').array('message', { + length: 'length', + type: elementParser, + }); + + var buffer = Buffer.from([ + 0x02, + 0x00, + 0x02, + 0x00, + 0xca, + 0xd2, + 0x04, + 0xbe, + 0xd3, + 0x04, + ]); + assert.deepStrictEqual(parser.parse(buffer), { + length: 0x02, + valueLength: 0x02, + message: [ + { key: 0xca, value: [0xd2, 0x04] }, + { key: 0xbe, value: [0xd3, 0x04] }, + ], + }); + }); + it('should parse array of user defined types and have access to root context', function () { + var elementParser = new Parser().uint8('key').nest("data", { + type: new Parser().array('value', { + type: "uint8", + length: "$root.valueLength" + }) + }); + + var parser = Parser.start().uint16le('length').uint16le('valueLength').array('message', { + length: 'length', + type: elementParser, + }); + + var buffer = Buffer.from([ + 0x02, + 0x00, + 0x02, + 0x00, + 0xca, + 0xd2, + 0x04, + 0xbe, + 0xd3, + 0x04, + ]); + assert.deepStrictEqual(parser.parse(buffer), { + length: 0x02, + valueLength: 0x02, + message: [ + { key: 0xca, data: {value: [0xd2, 0x04]} }, + { key: 0xbe, data: {value: [0xd3, 0x04]} }, + ], + }); + }); it('should parse array of user defined types with lengthInBytes', function () { var elementParser = new Parser().uint8('key').int16le('value'); @@ -493,7 +561,7 @@ const suite = (Buffer) => test: 314159, }); }); - it('should parse choices of user defied types', function () { + it('should parse choices of user defined types', function () { var parser = Parser.start() .uint8('tag') .choice('data', { @@ -761,7 +829,7 @@ const suite = (Buffer) => number: 12345678, }); }); - it("should be able to 'flatten' choices when omitting varName paramater", function () { + it("should be able to 'flatten' choices when omitting varName parameter", function () { var parser = Parser.start() .uint8('tag') .choice({ @@ -846,6 +914,60 @@ const suite = (Buffer) => number: 12345678, }); }); + it('should be able to use parsing context', function () { + var parser = Parser.start() + .uint8('tag') + .uint8('items') + .choice('data', { + tag: 'tag', + choices: { + 1: Parser.start() + .uint8('length') + .string('message', { length: 'length' }) + .array('value', { type: "uint8", length: "$parent.items"}), + 3: Parser.start().int32le('number'), + }, + }); + + var buffer = Buffer.from([ + 0x1, + 0x2, + 0xc, + 0x68, + 0x65, + 0x6c, + 0x6c, + 0x6f, + 0x2c, + 0x20, + 0x77, + 0x6f, + 0x72, + 0x6c, + 0x64, + 0x01, + 0x02, + 0x02, + 0x02, + ]); + assert.deepStrictEqual(parser.parse(buffer), { + tag: 1, + items: 2, + data: { + length: 12, + message: 'hello, world', + value: [0x01, 0x02], + }, + }); + buffer = Buffer.from([0x03, 0x0, 0x4e, 0x61, 0xbc, 0x00]); + assert.deepStrictEqual(parser.parse(buffer), { + tag: 3, + items: 0, + data: { + number: 12345678, + }, + }); + }); }); describe('Nest parser', function () { @@ -923,6 +1045,46 @@ const suite = (Buffer) => assert.deepStrictEqual(parser.parse(buf), { s1: 'foo', s2: 'bar' }); }); + + it('should be able to use parsing context', function () { + var parser = Parser.start() + .uint8('items') + .nest('data', { + type: Parser.start() + .uint8('length') + .string('message', { length: 'length' }) + .array('value', { type: "uint8", length: "$parent.items"}), + }); + + var buffer = Buffer.from([ + 0x2, + 0xc, + 0x68, + 0x65, + 0x6c, + 0x6c, + 0x6f, + 0x2c, + 0x20, + 0x77, + 0x6f, + 0x72, + 0x6c, + 0x64, + 0x01, + 0x02, + 0x02, + 0x02, + ]); + assert.deepStrictEqual(parser.parse(buffer), { + items: 2, + data: { + length: 12, + message: 'hello, world', + value: [0x01, 0x02], + }, + }); + }); }); describe('Constructors', function () {