Skip to content

Commit

Permalink
issue #5 - progress on binary helper API
Browse files Browse the repository at this point in the history
  • Loading branch information
bjouhier committed Sep 18, 2014
1 parent 0f80498 commit 20232e4
Show file tree
Hide file tree
Showing 3 changed files with 169 additions and 35 deletions.
129 changes: 99 additions & 30 deletions lib/helpers/binary._js
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,24 @@
/// `var ez = require("ez-streams")`
///

var NUMBERS = [['Int8', 1], ['Int16', 2], ['Int32', 4], ['Float', 4], ['Double', 8]];

var NUMBERS = [//
['Int8', 1], ['UInt8', 1], //
['Int16', 2], ['UInt16', 2], //
['Int32', 4], ['UInt32', 4], //
['Float', 4], ['Double', 8]];

/// * `reader = ez.helpers.binary.reader(reader, options)`
/// Wraps a raw buffer reader and returns a reader with additional API to handle binary streams.
/// By default the reader is configured as big endian.
/// You can configure it as little endian by setting the `endian` option to `"little"`.
function Reader(reader, options) {
this.reader = reader;
this.options = options;
this.pos = 0;
this.buf = new Buffer(0);
}

function Writer(writer, options) {
this.writer = writer;
this.options = options;
this.pos = 0;
this.buf = new Buffer(options.bufSize > 0 ? options.bufSize : 1024);
}

// internal API
Reader.prototype.ensure = function(_, len) {
if (this.buf === undefined) return false;
if (this.pos + len <= this.buf.length) return len;
Expand All @@ -41,45 +42,105 @@ Reader.prototype.ensure = function(_, len) {
return Math.min(this.buf.length, len);
}

Reader.prototype.read = function(_, len) {
/// * `buf = reader.read(_, len)`
/// returns the `len` next bytes of the stream.
/// returns a buffer of length `len`, except at the end of the stream.
/// The last chunk of the stream may have less than `len` bytes and afterwards the call
/// returns `undefined`.
/// If the `len` parameter is omitted, the call returns the next available chunk of data.
// peekOnly is internal and not documented
Reader.prototype.read = function(_, len, peekOnly) {
if (len === undefined) {
if (this.pos < this.buf.length) return this.read(_, this.buf.length - this.pos);
else {
this.buf = this.reader.read(_);
this.pos = this.buf ? this.buf.length : 0;
this.pos = this.buf && !peekOnly ? this.buf.length : 0;
return this.buf;
}
}
var l = this.ensure(_, len);
if (l === 0 && len > 0) return undefined;
var result = this.buf.slice(this.pos, this.pos + l);
this.pos += l;
if (!peekOnly) this.pos += l;
return result;
}

function intReader(name, len) {
/// * `buf = reader.peek(_, len)`
/// Same as `read` but does not advance the read pointer.
/// Another `read` would read the same data again.
Reader.prototype.peek = function(_, len) {
return this.read(_, len, true);
}

/// * reader.unread(len)`
/// Unread the last `len` bytes read.
/// `len` cannot exceed the size of the last read.
Reader.prototype.unread = function(len) {
if (!(len <= this.pos)) throw new Error("invalid unread: expected <= " + this.pos + ", got " + len);
this.pos -= len;
}

/// * `val = reader.readInt8(_)`
/// * `val = reader.readUInt8(_)`
/// * `val = reader.readInt16(_)`
/// * `val = reader.readUInt16(_)`
/// * `val = reader.readInt32(_)`
/// * `val = reader.readUInt32(_)`
/// * `val = reader.readFloat(_)`
/// * `val = reader.readDouble(_)`
/// Specialized readers for numbers.
///
/// * `val = reader.peekInt8(_)`
/// * `val = reader.peekUInt8(_)`
/// * `val = reader.peekInt16(_)`
/// * `val = reader.peekUInt16(_)`
/// * `val = reader.peekInt32(_)`
/// * `val = reader.peekUInt32(_)`
/// * `val = reader.peekFloat(_)`
/// * `val = reader.peekDouble(_)`
/// Specialized peekers for numbers.
function numberReader(name, len, peekOnly) {
return function(_) {
var got = this.ensure(_, len);
if (got === 0) return undefined;
if (got < len) throw new Error("unexpected EOF: expected " + len + ", got " + got);
var result = this.buf[name](this.pos);
this.pos += len;
if (!peekOnly) this.pos += len;
return result;
};
}

/// * `writer = ez.helpers.binary.writer(writer, options)`
/// Wraps a raw buffer writer and returns a writer with additional API to handle binary streams.
/// By default the writer is configured as big endian.
/// You can configure it as little endian by setting the `endian` option to `"little"`.
/// The `bufSize` option controls the size of the intermediate buffer.
function Writer(writer, options) {
this.writer = writer;
this.options = options;
this.pos = 0;
this.buf = new Buffer(options.bufSize > 0 ? options.bufSize : 16384);
}

/// * `writer.flush(_)`
/// Flushes the buffer to the wrapped writer.
Writer.prototype.flush = function(_) {
this.writer.write(_, this.buf.slice(0, this.pos));
this.pos = 0;
}

// internal call
Writer.prototype.ensure = function(_, len) {
if (this.pos + len > this.buf.length) {
this.flush(_);
if (len > this.buf.length) this.buf = new Buffer(len);
}
}

/// * `writer.write(_, buf)`
/// Writes `buf`.
/// Note: writes are buffered.
/// Use the `flush(_)` call if you need to flush before the end of the stream.
Writer.prototype.write = function(_, buf) {
if (buf === undefined || buf.length > this.buf.length) {
this.flush(_);
Expand All @@ -91,7 +152,16 @@ Writer.prototype.write = function(_, buf) {
}
}

function intWriter(name, len) {
/// * `writer.writeInt8(_, val)`
/// * `writer.writeUInt8(_, val)`
/// * `writer.writeInt16(_, val)`
/// * `writer.writeUInt16(_, val)`
/// * `writer.writeInt32(_, val)`
/// * `writer.writeUInt32(_, val)`
/// * `writer.writeFloat(_, val)`
/// * `writer.writeDouble(_, val)`
/// Specialized writers for numbers.
function numberWriter(name, len) {
return function(_, val) {
this.ensure(_, len);
this.buf[name](val, this.pos);
Expand All @@ -103,39 +173,38 @@ NUMBERS.forEach(function(pair) {
var len = pair[1];
var names = len > 1 ? [pair[0] + 'BE', pair[0] + 'LE'] : [pair[0]];
names.forEach(function(name) {
Reader.prototype['read' + name] = intReader('read' + name, len);
Reader.prototype['readU' + name] = intReader('readU' + name, len);
Writer.prototype['write' + name] = intWriter('write' + name, len);
Writer.prototype['writeU' + name] = intWriter('writeU' + name, len);
Reader.prototype['read' + name] = numberReader('read' + name, len, false);
Reader.prototype['peek' + name] = numberReader('read' + name, len, true);
Writer.prototype['write' + name] = numberWriter('write' + name, len);
});
});

function makeEndian(base, verb, suffix) {
function makeEndian(base, verbs, suffix) {
var construct = function() {
base.apply(this, arguments);
}
construct.prototype = Object.create(base.prototype);
NUMBERS.slice(1).forEach(function(pair) {
construct.prototype[verb + pair[0]] = base.prototype[verb + pair[0] + suffix];
construct.prototype[verb + 'U' + pair[0]] = base.prototype[verb + 'U' + pair[0] + suffix];
verbs.forEach(function(verb) {
construct.prototype[verb + pair[0]] = base.prototype[verb + pair[0] + suffix];
});
});
return construct;
}

var ReaderLE = makeEndian(Reader, 'read', 'LE');
var ReaderBE = makeEndian(Reader, 'read', 'BE');
var WriterLE = makeEndian(Writer, 'write', 'LE');
var WriterBE = makeEndian(Writer, 'write', 'BE');
require('../reader').decorate(Reader.prototype);
require('../writer').decorate(Writer.prototype);
var ReaderLE = makeEndian(Reader, ['read', 'peek'], 'LE');
var ReaderBE = makeEndian(Reader, ['read', 'peek'], 'BE');
var WriterLE = makeEndian(Writer, ['write'], 'LE');
var WriterBE = makeEndian(Writer, ['write'], 'BE');

module.exports = {
/// * `mapper = ez.mappers.convert.stringify(encoding)`
/// returns a mapper that converts to string
// Documentation above, next to the constructor
reader: function(reader, options) {
options = options || {};
return new (options.endian === 'little' ? ReaderLE : ReaderBE)(reader, options);
},
/// * `mapper = ez.mappers.convert.bufferify(encoding)`
/// returns a mapper that converts to buffer
writer: function(writer, options) {
options = options || {};
return new (options.endian === 'little' ? WriterLE : WriterBE)(writer, options);
Expand Down
59 changes: 55 additions & 4 deletions lib/helpers/binary.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,58 @@

`var ez = require("ez-streams")`

* `mapper = ez.mappers.convert.stringify(encoding)`
returns a mapper that converts to string
* `mapper = ez.mappers.convert.bufferify(encoding)`
returns a mapper that converts to buffer
* `reader = ez.helpers.binary.reader(reader, options)`
Wraps a raw buffer reader and returns a reader with additional API to handle binary streams.
By default the reader is configured as big endian.
You can configure it as little endian by setting the `endian` option to `"little"`.
* `buf = reader.read(_, len)`
returns the `len` next bytes of the stream.
returns a buffer of length `len`, except at the end of the stream.
The last chunk of the stream may have less than `len` bytes and afterwards the call
returns `undefined`.
If the `len` parameter is omitted, the call returns the next available chunk of data.
* `buf = reader.peek(_, len)`
Same as `read` but does not advance the read pointer.
Another `read` would read the same data again.
* reader.unread(len)`
Unread the last `len` bytes read.
`len` cannot exceed the size of the last read.
* `val = reader.readInt8(_)`
* `val = reader.readUInt8(_)`
* `val = reader.readInt16(_)`
* `val = reader.readUInt16(_)`
* `val = reader.readInt32(_)`
* `val = reader.readUInt32(_)`
* `val = reader.readFloat(_)`
* `val = reader.readDouble(_)`
Specialized readers for numbers.

* `val = reader.peekInt8(_)`
* `val = reader.peekUInt8(_)`
* `val = reader.peekInt16(_)`
* `val = reader.peekUInt16(_)`
* `val = reader.peekInt32(_)`
* `val = reader.peekUInt32(_)`
* `val = reader.peekFloat(_)`
* `val = reader.peekDouble(_)`
Specialized peekers for numbers.
* `writer = ez.helpers.binary.writer(writer, options)`
Wraps a raw buffer writer and returns a writer with additional API to handle binary streams.
By default the writer is configured as big endian.
You can configure it as little endian by setting the `endian` option to `"little"`.
The `bufSize` option controls the size of the intermediate buffer.
* `writer.flush(_)`
Flushes the buffer to the wrapped writer.
* `writer.write(_, buf)`
Writes `buf`.
Note: writes are buffered.
Use the `flush(_)` call if you need to flush before the end of the stream.
* `writer.writeInt8(_, val)`
* `writer.writeUInt8(_, val)`
* `writer.writeInt16(_, val)`
* `writer.writeUInt16(_, val)`
* `writer.writeInt32(_, val)`
* `writer.writeUInt32(_, val)`
* `writer.writeFloat(_, val)`
* `writer.writeDouble(_, val)`
Specialized writers for numbers.
16 changes: 15 additions & 1 deletion test/server/binary-test._js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@
QUnit.module(module.id);

var ez = require("ez-streams");
var TESTBUF = new Buffer([1, 4, 9, 16, 25, 36, 49, 64, 81, 100]);

asyncTest("roundtrip", 7, function(_) {
function eqbuf(b1, b2) {
equals(b1.toString('hex'), b2.toString('hex'));
}

asyncTest("roundtrip", 13, function(_) {
var dst = ez.devices.buffer.writer();
var writer = ez.helpers.binary.writer(dst, { bufsize: 3 });
writer.write(_, TESTBUF);
writer.writeInt8(_, 1);
writer.writeInt16(_, 2);
writer.writeInt32(_, 3);
Expand All @@ -17,11 +23,19 @@ asyncTest("roundtrip", 7, function(_) {

var src = ez.devices.buffer.reader(result).transform(ez.transforms.cut(5));
var reader = ez.helpers.binary.reader(src);
eqbuf(reader.read(_, 7), TESTBUF.slice(0, 7), 'read 7');
reader.unread(3);
eqbuf(reader.peek(_, 5), TESTBUF.slice(4, 9), 'unread 3 then peek 5');
eqbuf(reader.read(_, 6), TESTBUF.slice(4), 'read 6');
equals(reader.readInt8(_), 1, 'int8 roundtrip');
equals(reader.peekInt16(_), 2, 'int16 roundtrip (peek)');
equals(reader.readInt16(_), 2, 'int16 roundtrip');
equals(reader.readInt32(_), 3, 'int32 roundtrip');
equals(reader.readFloat(_), 0.5, 'float roundtrip');
equals(reader.peekDouble(_), 0.125, 'double roundtrip (peek)');
equals(reader.readDouble(_), 0.125, 'double roundtrip');
reader.unread(8);
equals(reader.readDouble(_), 0.125, 'double roundtrip (after unread)');
equals(reader.readInt8(_), 5, 'int8 roundtrip again');
equals(reader.read(_), undefined, 'EOF roundtrip');
start();
Expand Down

0 comments on commit 20232e4

Please sign in to comment.