From 7e684478a36d91d9b4215a743a68beae1a02b245 Mon Sep 17 00:00:00 2001 From: Artem Pylypchuk Date: Thu, 1 Sep 2016 16:08:30 +0300 Subject: [PATCH 1/8] Add static buffer parsing routine (using Buffer object and stacking of Buffers) --- index.js | 87 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/index.js b/index.js index 04ba4d1..429b0ae 100644 --- a/index.js +++ b/index.js @@ -19,6 +19,7 @@ var Dissolve = module.exports = function Dissolve(options) { this.vars_list = []; this._buffer = new BufferList(); + this._buffers_stack = []; }; Dissolve.prototype = Object.create(stream.Transform.prototype, {constructor: {value: Dissolve}}); @@ -107,6 +108,57 @@ Dissolve.prototype._exec_loop = function _exec_loop(job) { Array.prototype.push.apply(this.jobs, jobs); }; +Dissolve.prototype._exec_parse = function _exec_parse(job, curr_offset) { + this.jobs.shift(); + + var jobs = this.jobs; + this.jobs = []; + + this.jobs.push({type: "store", offset: curr_offset, new_buffer: job.buffer}); + + if (job.name) { + this.jobs.push({type: "down", into: job.name}); + this.jobs.push({type: "tap", args: job.args || [], fn: job.fn}); + this.jobs.push({type: "up"}); + } else { + job.fn.apply(this, job.args || []); + } + + this.jobs.push({type: "retrieve"}); + + Array.prototype.push.apply(this.jobs, jobs); +}; + +Dissolve.prototype._exec_store_buffer = function (job, offset) { + this.jobs.shift(); + + var tmp = this._buffer; + this._buffers_stack.push({ + buffer: this._buffer, + offset: offset + }); + this._buffer = job.new_buffer; + + return 0; +} + +Dissolve.prototype._exec_pop_buffer = function (job, offset) { + + this.jobs.shift(); + + // check for buffer overrun + if (offset < this._buffer.length) + throw new Error("Static buffer parsing - not all data consumed"); + + delete(this._buffer); + + //({this._buffer, offset} = this._buffers_stack.pop()); + var obj = this._buffers_stack.pop(); + this._buffer = obj.buffer; + + return obj.offset; +} + Dissolve.prototype._exec_string = function _exec_string(job, offset, length) { this.vars[job.name] = this._buffer.toString("utf8", offset, offset + length); this.jobs.shift(); @@ -145,6 +197,21 @@ Dissolve.prototype._transform = function _transform(input, encoding, done) { continue; } + if (job.type === "store") { + offset = this._exec_store_buffer(job, offset); + continue; + } + + if (job.type === "retrieve") { + offset = this._exec_pop_buffer(job, offset); + continue; + } + + if (job.type === "parse") { + this._exec_parse(job); + continue; + } + var length; if (typeof job.length === "string") { length = this.vars[job.length]; @@ -203,6 +270,10 @@ Dissolve.prototype._transform = function _transform(input, encoding, done) { offset += length; } + //parsing static buffer of exact length should never reach consume (after job retrieval loop) + if (this._buffer instanceof Buffer) + throw new Error("Static buffer parsing underrun, processing procedure went on to parse upper level buffer"); + this._buffer.consume(offset); if (this.jobs.length === 0) { @@ -298,3 +369,19 @@ Dissolve.prototype._transform = function _transform(input, encoding, done) { return this; }; }); + +Dissolve.prototype["parse"] = function(buffer, name, fn) { + if (typeof name === "function") { + fn = name; + name = null; + } + + this.jobs.push({ + type: "parse", + name: name, + fn: fn, + buffer: buffer + }); + + return this; +}; \ No newline at end of file From 8fa97c384efa3e727fae9e221ef8ee1577876cc1 Mon Sep 17 00:00:00 2001 From: Artem Pylypchuk Date: Fri, 2 Sep 2016 11:59:18 +0300 Subject: [PATCH 2/8] Add rest (of static buffer) parse function (with skip_end option) --- index.js | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 429b0ae..e46e025 100644 --- a/index.js +++ b/index.js @@ -159,6 +159,17 @@ Dissolve.prototype._exec_pop_buffer = function (job, offset) { return obj.offset; } +Dissolve.prototype._exec_rest_buffer = function(job, offset) { + this.jobs.shift(); + + if (this._buffer instanceof Buffer) + this.vars[job.name] = this._buffer.slice(offset, this._buffer.length - job.end); + else + throw new Error("Rest of a non-static buffer requested"); + + return this._buffer.length - job.end; +} + Dissolve.prototype._exec_string = function _exec_string(job, offset, length) { this.vars[job.name] = this._buffer.toString("utf8", offset, offset + length); this.jobs.shift(); @@ -207,6 +218,11 @@ Dissolve.prototype._transform = function _transform(input, encoding, done) { continue; } + if (job.type === "rest") { + offset = this._exec_rest_buffer(job, offset); + continue; + } + if (job.type === "parse") { this._exec_parse(job); continue; @@ -384,4 +400,17 @@ Dissolve.prototype["parse"] = function(buffer, name, fn) { }); return this; -}; \ No newline at end of file +}; + +Dissolve.prototype["rest"] = function(name, skip_end) { + + if (!skip_end) skip_end = 0; + + this.jobs.push({ + type: "rest", + name: name, + end: skip_end + }); + + return this; +} \ No newline at end of file From 17e40bdd3f57673fe6744e1ae1d8ce7cb8c11afc Mon Sep 17 00:00:00 2001 From: Artem Pylypchuk Date: Thu, 8 Sep 2016 10:14:50 +0300 Subject: [PATCH 3/8] Rename end into skip_end (follow naming conventions) --- index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index e46e025..7e8cf0e 100644 --- a/index.js +++ b/index.js @@ -163,11 +163,11 @@ Dissolve.prototype._exec_rest_buffer = function(job, offset) { this.jobs.shift(); if (this._buffer instanceof Buffer) - this.vars[job.name] = this._buffer.slice(offset, this._buffer.length - job.end); + this.vars[job.name] = this._buffer.slice(offset, this._buffer.length - job.skip_end); else throw new Error("Rest of a non-static buffer requested"); - return this._buffer.length - job.end; + return this._buffer.length - job.skip_end; } Dissolve.prototype._exec_string = function _exec_string(job, offset, length) { @@ -409,7 +409,7 @@ Dissolve.prototype["rest"] = function(name, skip_end) { this.jobs.push({ type: "rest", name: name, - end: skip_end + skip_end: skip_end }); return this; From 374709f2ebf05abad51bcba8b7ac28bf261a49c9 Mon Sep 17 00:00:00 2001 From: Artem Pylypchuk Date: Tue, 29 Nov 2016 23:00:46 +0200 Subject: [PATCH 4/8] Add static buffer parsing documentation and example --- README.md | 9 ++- example-framing.js | 147 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 example-framing.js diff --git a/README.md b/README.md index e23f057..bce99d8 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,8 @@ Usage Also see [example.js](https://github.com/deoxxa/dissolve/blob/master/example.js), [example-complex.js](https://github.com/deoxxa/dissolve/blob/master/example-complex.js) -and [example-loop.js](https://github.com/deoxxa/dissolve/blob/master/example-loop.js). +[example-loop.js](https://github.com/deoxxa/dissolve/blob/master/example-loop.js) +and [example-framing.js](https://github.com/deoxxa/dissolve/blob/master/example-framing.js). ```javascript #!/usr/bin/env node @@ -135,6 +136,12 @@ previously-set `this.vars` entry. If it's a number, it will be used as-is. * `string(name, length)` - utf8 string slice * `skip(length)` - skip `length` bytes +Static Buffer Parsing Methods +---------------------- + +* `parse(buffer, name, callback)` - tap into and parse binary slice (e.g. from `buffer()`). Just like in `tap()`, the optional `name` parameter makes parser put variables into a child object named after `name`. +* `rest(name, skip_end)` - when parsing a binary slice with `parse()` - cut out remaining part and store into variable. Useful when current parse pointer position or remaining length is not easy to calculate). Optionally - throw away `skip_end` bytes at the end. + Numeric Methods --------------- diff --git a/example-framing.js b/example-framing.js new file mode 100644 index 0000000..65dc6cf --- /dev/null +++ b/example-framing.js @@ -0,0 +1,147 @@ +#!/usr/bin/env node + +/* + * this example demonstrates TCP framing by copying entire frame into a static buffer + * and parsing of data segments inside, including cases when offset/length are not easy to calculate + */ + +var Dissolve = require("./index"), + util = require("util"); + +function Parser() { + Dissolve.call(this); + + this.loop(function () { + this.magicNumber('uint16be', 0x9005, 'header') + .uint16be('packet_length') + .loop(function (endPacket_cb) { + this.buffer('packet_data', 'packet_length').tap(function () { + var packet = this.vars.packet_data; + + this.deleteVar('packet_length'); + this.deleteVar('packet_data'); + + this.parse(packet, function () { + this.loop(function (lastSegment_cb) { + this.uint8('segment_type').tap(function () { + var segment_type; + switch (segment_type = this.vars.segment_type) { + + case 0x01: //error status segment + this + .uint8('ErrorCode') + .nonterminatedString('message'); + lastSegment_cb(); + break; + + case 0x10: //successful execution segment, includes id, hex_id and text + this + .uint8('id') + .hexString(2, 'hex_id') + .terminatedString('text', 'ascii'); + lastSegment_cb(); + break; + + case 0x00: //control segment + this.tap('EventInfo', function () { + this + .uint8('Event') + .hexString(5, 'NextBlock'); + }); + break; + + default: + throw new Error("Encountered unknown segment type: 0x" + segment_type.toString(16)); + } + this.deleteVar('segment_type'); + }); + }); + }); + }).tap(function packet_end() { + endPacket_cb(); + }); + }) + .tap(function () { + this.push(this.vars); + this.vars = {}; + }); + }); +}; + + + +util.inherits(Parser, Dissolve); + +Parser.prototype.magicNumber = function magic(type, expected_value, ref_name) { + var fn = this[type]; + var name = 'magic_'+expected_value.toString(16); + fn.call(this, name) + .tap(function() { + if (this.vars[name] !== expected_value) + throw new Error("Magic value '"+ref_name+"' not matched!"); + + this.deleteVar(name); + }); + + return this; +}; + +Parser.prototype.hexString = function(length, name) { + + this.buffer(name, length).tap(function() { + var string = this.vars[name]; + + if (!(typeof(string) !== "string" || Buffer.isBuffer(string))) + throw new Error("String expected"); + + this.vars[name] = string.toString('hex').toUpperCase(); + }); + + return this; +}; + +Parser.prototype.terminatedString = function (name, encoding) { + this.rest(name, 1) + .tap(function() { + this.vars[name] = this.vars[name].toString(encoding); + }) + .magicNumber('uint8', 0x01, name+' reply EOM'); + + return this; +}; + +Parser.prototype.nonterminatedString = function (name, encoding) { + this.rest(name) + .tap(function() { + this.vars[name] = this.vars[name].toString(encoding); + }); + + return this; +}; + +Parser.prototype.deleteVar = function(name) { + delete(this.vars[name]); +}; + +var parser = new Parser(); + +parser.on("readable", function() { + var e; + while (e = parser.read()) { + console.log(JSON.stringify(e, null, 2)); + } +}); + +parser.write(new Buffer([ + 0x90, 0x05, 0, 16, //header + + 0x00, 0x02, 0x00, 0xca, 0xfe, 0xba, 0xbe, //control + 0x10, 0xaa, 0xba, 0xad, 0x74, 0x65, 0x73, 0x74, 0x01 //successful execution +])); + +parser.write(new Buffer([ + 0x90, 0x05, 0, 15, //header + + 0x00, 0x02, 0x00, 0xba, 0xad, 0xf0, 0x0d, //control + 0x01, 0xff, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x21 //error +])); \ No newline at end of file From aaaccc0a298bf0e508d7c64bcce05b4a85c02fd3 Mon Sep 17 00:00:00 2001 From: Artem Pylypchuk Date: Tue, 29 Nov 2016 23:19:43 +0200 Subject: [PATCH 5/8] Add this.vars property name shortcut to parse(), add type check to store_buffer --- README.md | 2 +- example-framing.js | 5 ++--- index.js | 5 ++++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index bce99d8..d54d868 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,7 @@ previously-set `this.vars` entry. If it's a number, it will be used as-is. Static Buffer Parsing Methods ---------------------- -* `parse(buffer, name, callback)` - tap into and parse binary slice (e.g. from `buffer()`). Just like in `tap()`, the optional `name` parameter makes parser put variables into a child object named after `name`. +* `parse(buffer, name, callback)` - tap into and parse binary slice (e.g. from `buffer()`). Just like in `tap()`, the optional `name` parameter makes parser put variables into a child object named after `name`. If `buffer` is a string, it is assumed that it is a name of buffer variable in `this.vars`. * `rest(name, skip_end)` - when parsing a binary slice with `parse()` - cut out remaining part and store into variable. Useful when current parse pointer position or remaining length is not easy to calculate). Optionally - throw away `skip_end` bytes at the end. Numeric Methods diff --git a/example-framing.js b/example-framing.js index 65dc6cf..9dab5a0 100644 --- a/example-framing.js +++ b/example-framing.js @@ -16,12 +16,10 @@ function Parser() { .uint16be('packet_length') .loop(function (endPacket_cb) { this.buffer('packet_data', 'packet_length').tap(function () { - var packet = this.vars.packet_data; this.deleteVar('packet_length'); - this.deleteVar('packet_data'); - this.parse(packet, function () { + this.parse('packet_data', function () { this.loop(function (lastSegment_cb) { this.uint8('segment_type').tap(function () { var segment_type; @@ -58,6 +56,7 @@ function Parser() { }); }); }).tap(function packet_end() { + this.deleteVar('packet_data'); endPacket_cb(); }); }) diff --git a/index.js b/index.js index 7e8cf0e..e344047 100644 --- a/index.js +++ b/index.js @@ -114,6 +114,8 @@ Dissolve.prototype._exec_parse = function _exec_parse(job, curr_offset) { var jobs = this.jobs; this.jobs = []; + if (typeof job.buffer == "string") job.buffer = this.vars[job.buffer]; + this.jobs.push({type: "store", offset: curr_offset, new_buffer: job.buffer}); if (job.name) { @@ -132,7 +134,8 @@ Dissolve.prototype._exec_parse = function _exec_parse(job, curr_offset) { Dissolve.prototype._exec_store_buffer = function (job, offset) { this.jobs.shift(); - var tmp = this._buffer; + if (!Buffer.isBuffer(job.new_buffer)) throw new Error('Buffer expected for parse()'); + this._buffers_stack.push({ buffer: this._buffer, offset: offset From 4abba06769fe7fcaebccb06e3c91808359d4e211 Mon Sep 17 00:00:00 2001 From: Artem Pylypchuk Date: Tue, 29 Nov 2016 23:22:32 +0200 Subject: [PATCH 6/8] fix commas and semicolons --- index.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/index.js b/index.js index e344047..1d92427 100644 --- a/index.js +++ b/index.js @@ -138,12 +138,12 @@ Dissolve.prototype._exec_store_buffer = function (job, offset) { this._buffers_stack.push({ buffer: this._buffer, - offset: offset + offset: offset, }); this._buffer = job.new_buffer; return 0; -} +}; Dissolve.prototype._exec_pop_buffer = function (job, offset) { @@ -160,7 +160,7 @@ Dissolve.prototype._exec_pop_buffer = function (job, offset) { this._buffer = obj.buffer; return obj.offset; -} +}; Dissolve.prototype._exec_rest_buffer = function(job, offset) { this.jobs.shift(); @@ -171,7 +171,7 @@ Dissolve.prototype._exec_rest_buffer = function(job, offset) { throw new Error("Rest of a non-static buffer requested"); return this._buffer.length - job.skip_end; -} +}; Dissolve.prototype._exec_string = function _exec_string(job, offset, length) { this.vars[job.name] = this._buffer.toString("utf8", offset, offset + length); @@ -399,7 +399,7 @@ Dissolve.prototype["parse"] = function(buffer, name, fn) { type: "parse", name: name, fn: fn, - buffer: buffer + buffer: buffer, }); return this; @@ -412,8 +412,8 @@ Dissolve.prototype["rest"] = function(name, skip_end) { this.jobs.push({ type: "rest", name: name, - skip_end: skip_end + skip_end: skip_end, }); return this; -} \ No newline at end of file +}; \ No newline at end of file From 65bcc02938353879f5a9f63c77685071560f4f8c Mon Sep 17 00:00:00 2001 From: Artem Pylypchuk Date: Tue, 29 Nov 2016 23:43:13 +0200 Subject: [PATCH 7/8] version bump, add contributor --- package.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 77d7ca2..6942f37 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dissolve", - "version": "0.3.3", + "version": "0.3.4", "description": "Parse and consume binary streams with a neat DSL", "main": "index.js", "engines": { @@ -32,5 +32,12 @@ "emit" ], "author": "Conrad Pankoff (http://www.fknsrs.biz/)", + "contributors": [ + { + "name": "Artem Pylypchuk", + "email": "articicejuice@gmail.com", + "url": "https://github.com/articice/" + } + ], "license": "BSD" } From f374cfe4538ebdde570723a7fe3b1b91f97b5e9f Mon Sep 17 00:00:00 2001 From: Artem Pylypchuk Date: Wed, 30 Nov 2016 01:52:52 +0200 Subject: [PATCH 8/8] Improve description of parse() --- README.md | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d54d868..f5c26ef 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,25 @@ the examples will make that explanation a lot clearer. The same semantics for job ordering and "scoping" apply as for `tap`. +Parse +----- + +`parse(buffer, name, callback)` + +Use this static buffer and parse it (e.g. a binary slice cut by the `buffer` method). +Sets aside currently parsed stream or static buffer (puts onto stack). + +`callback` is expected to consume precisely the entire length of static buffer, +using all available parsing methods (including `parse` itself). +If not all data is consumed, or an attempt is made to consume more +than is available, errors are thrown. + +Just like in `tap`, the optional `name` parameter makes parser put variables +into a child object named after `name`. + +If `buffer` is a string, it is assumed that it is a name of buffer variable in `this.vars`. + + Basic Parsing Methods --------------------- @@ -139,8 +158,10 @@ previously-set `this.vars` entry. If it's a number, it will be used as-is. Static Buffer Parsing Methods ---------------------- -* `parse(buffer, name, callback)` - tap into and parse binary slice (e.g. from `buffer()`). Just like in `tap()`, the optional `name` parameter makes parser put variables into a child object named after `name`. If `buffer` is a string, it is assumed that it is a name of buffer variable in `this.vars`. -* `rest(name, skip_end)` - when parsing a binary slice with `parse()` - cut out remaining part and store into variable. Useful when current parse pointer position or remaining length is not easy to calculate). Optionally - throw away `skip_end` bytes at the end. +* `rest(name, skip_end)` - when parsing a binary slice with `parse` - cut out remaining part +and store into variable. Useful when current parse pointer position or remaining length is +not easy to calculate. Optionally - leave `skip_end` bytes at the end, to be parsed by other +methods later. Numeric Methods ---------------