From 20232e4e6b8fe5f71c04f1ab8654f17ae8854aeb Mon Sep 17 00:00:00 2001 From: Bruno Jouhier Date: Thu, 18 Sep 2014 22:36:09 +0200 Subject: [PATCH] issue #5 - progress on binary helper API --- lib/helpers/binary._js | 129 +++++++++++++++++++++++++++--------- lib/helpers/binary.md | 59 +++++++++++++++-- test/server/binary-test._js | 16 ++++- 3 files changed, 169 insertions(+), 35 deletions(-) diff --git a/lib/helpers/binary._js b/lib/helpers/binary._js index ea72bdf..b2bc403 100644 --- a/lib/helpers/binary._js +++ b/lib/helpers/binary._js @@ -5,9 +5,16 @@ /// `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; @@ -15,13 +22,7 @@ function Reader(reader, options) { 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; @@ -41,38 +42,94 @@ 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(_); @@ -80,6 +137,10 @@ Writer.prototype.ensure = function(_, 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(_); @@ -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); @@ -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); diff --git a/lib/helpers/binary.md b/lib/helpers/binary.md index 0b6289d..29504a9 100644 --- a/lib/helpers/binary.md +++ b/lib/helpers/binary.md @@ -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. diff --git a/test/server/binary-test._js b/test/server/binary-test._js index 88fa4b4..8422ce1 100644 --- a/test/server/binary-test._js +++ b/test/server/binary-test._js @@ -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); @@ -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();