Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
---------------------

Expand All @@ -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
---------------

Expand Down
146 changes: 146 additions & 0 deletions example-framing.js
Original file line number Diff line number Diff line change
@@ -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
]));
119 changes: 119 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}});

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
};
9 changes: 8 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down Expand Up @@ -32,5 +32,12 @@
"emit"
],
"author": "Conrad Pankoff <deoxxa@fknsrs.biz> (http://www.fknsrs.biz/)",
"contributors": [
{
"name": "Artem Pylypchuk",
"email": "articicejuice@gmail.com",
"url": "https://github.com/articice/"
}
],
"license": "BSD"
}