diff --git a/README.md b/README.md index e23f057..f5c26ef 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 @@ -118,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 --------------------- @@ -135,6 +155,14 @@ 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 +---------------------- + +* `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 --------------- diff --git a/example-framing.js b/example-framing.js new file mode 100644 index 0000000..9dab5a0 --- /dev/null +++ b/example-framing.js @@ -0,0 +1,146 @@ +#!/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 () { + + this.deleteVar('packet_length'); + + this.parse('packet_data', 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() { + this.deleteVar('packet_data'); + 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 diff --git a/index.js b/index.js index 04ba4d1..1d92427 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,71 @@ 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 = []; + + 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) { + 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(); + + if (!Buffer.isBuffer(job.new_buffer)) throw new Error('Buffer expected for parse()'); + + 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_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.skip_end); + else + 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); this.jobs.shift(); @@ -145,6 +211,26 @@ 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 === "rest") { + offset = this._exec_rest_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 +289,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 +388,32 @@ 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; +}; + +Dissolve.prototype["rest"] = function(name, skip_end) { + + if (!skip_end) skip_end = 0; + + this.jobs.push({ + type: "rest", + name: name, + skip_end: skip_end, + }); + + return this; +}; \ No newline at end of file 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" }