├── .gitignore ├── .travis.yml ├── History.md ├── LICENSE ├── README.md ├── index.js ├── package.json └── test ├── transform.js └── writable.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | ?.js 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.8" 4 | - "0.10" 5 | - "0.12" 6 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | 0.3.1 / 2015-06-04 3 | ================== 4 | 5 | * package: allow any "debug" v2 6 | 7 | 0.3.0 / 2014-06-22 8 | ================== 9 | 10 | * add a `_skipBytes()` function 11 | * History: fix changelog output 12 | 13 | 0.2.0 / 2014-06-21 14 | ================== 15 | 16 | * index: emit an "error" event when data is written with no parsing function is in place 17 | * package: fix "main" field 18 | * rename "lib/parser.js" to "index.js" 19 | * README: use svg for Travis badge 20 | 21 | 0.1.2 / 2014-06-16 22 | ================== 23 | 24 | * parser: use %o debug v1 formatting 25 | 26 | 0.1.1 / 2014-06-16 27 | ================== 28 | 29 | * package: pin "readable-stream" to v1.0 30 | * package: update "debug" to v1.0.0 31 | * travis: test node v0.11 32 | * travis: don't test node v0.9 33 | 34 | 0.1.0 / 2013-06-04 35 | ================== 36 | 37 | * travis: test node v0.10 38 | * test: add test case from #3 39 | * parser: add jsdocs for the `process()` function 40 | * parser: use a thunk-based "trampoline" technique to prevent stack overflows on synchronous parsers (fixes #3) 41 | 42 | 0.0.5 / 2013-03-06 43 | ================== 44 | 45 | * Update for node v0.9.12 streams2 API Writable/Transform API changes 46 | 47 | 0.0.4 / 2013-02-23 48 | ================== 49 | 50 | * Don't allow `_bytes(0)` 51 | * Fix tests on node v0.8.x 52 | 53 | 0.0.3 / 2013-02-10 54 | ================== 55 | 56 | * Allow `_passthrough(Infinity)` 57 | * Add MIT license file 58 | 59 | 0.0.2 / 2013-02-08 60 | ================== 61 | 62 | * Add support for asynchronous callback functions 63 | 64 | 0.0.1 / 2013-02-05 65 | ================== 66 | 67 | * Initial release 68 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2013 Nathan Rajlich 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the "Software"), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | node-stream-parser 2 | ================== 3 | ### Generic interruptible "parser" mixin for Transform & Writable streams 4 | [![Build Status](https://secure.travis-ci.org/TooTallNate/node-stream-parser.svg)](http://travis-ci.org/TooTallNate/node-stream-parser) 5 | 6 | This module offers the `stream-parser` mixin, which provides an easy-to-use API 7 | for parsing bytes from `Writable` and/or `Transform` stream instances. This module 8 | is great for implementing streaming parsers for standardized file formats. 9 | 10 | For `Writable` streams, the parser takes control over the `_write` callback 11 | function. For `Transform` streams, the parser controls the `_transform` callback 12 | function. 13 | 14 | Installation 15 | ------------ 16 | 17 | ``` bash 18 | $ npm install stream-parser 19 | ``` 20 | 21 | 22 | Example 23 | ------- 24 | 25 | Let's create a quick `Transform` stream subclass that utilizes the parser's 26 | `_bytes()` and `_passthrough()` functions to parse a theoretical file format that 27 | has an 8-byte header we want to parse, and then pass through the rest of the data. 28 | 29 | ``` javascript 30 | var Parser = require('stream-parser'); 31 | var inherits = require('util').inherits; 32 | var Transform = require('stream').Transform; 33 | 34 | // create a Transform stream subclass 35 | function MyParser () { 36 | Transform.call(this); 37 | 38 | // buffer the first 8 bytes written 39 | this._bytes(8, this.onheader); 40 | } 41 | inherits(MyParser, Transform); 42 | 43 | // mixin stream-parser into MyParser's `prototype` 44 | Parser(MyParser.prototype); 45 | 46 | // invoked when the first 8 bytes have been received 47 | MyParser.prototype.onheader = function (buffer, output) { 48 | // parse the "buffer" into a useful "header" object 49 | var header = {}; 50 | header.type = buffer.readUInt32LE(0); 51 | header.name = buffer.toString('utf8', 4); 52 | this.emit('header', header); 53 | 54 | // it's usually a good idea to queue the next "piece" within the callback 55 | this._passthrough(Infinity); 56 | }; 57 | 58 | 59 | // now we can *use* it! 60 | var parser = new MyParser(); 61 | parser.on('header', function (header) { 62 | console.error('got "header"', header); 63 | }); 64 | process.stdin.pipe(parser).pipe(process.stdout); 65 | ``` 66 | 67 | Here's an example of manually creating a `Transform` stream and turning it into a 68 | "pass through" stream equivalent to the one built into node core: 69 | 70 | ``` javascript 71 | var Parser = require('stream-parser'); 72 | var Transform = require('stream').Transform; 73 | 74 | // create a Transform instance and extend it with "stream-parser" 75 | var p = new Transform(); 76 | Parser(p); 77 | 78 | // pass through `Infinity` bytes... forever... 79 | p._passthrough(Infinity); 80 | 81 | // now `p` is equivalent to a stream.PassThrough instance 82 | process.stdin.pipe(p).pipe(process.stdout); 83 | ``` 84 | 85 | See the `test` directory for some more example code in the test cases. 86 | 87 | A list of known concrete implementations is here (send pull requests for more!): 88 | 89 | * [node-icy][] 90 | * [node-throttle][] 91 | * [node-flv][] 92 | * [node-wav][] 93 | 94 | API 95 | --- 96 | 97 | - [Parser()](#parser) 98 | - [._bytes(n, cb)](#_bytesn-cb) 99 | - [._skipBytes(n, cb)](#_skipbytesn-cb) 100 | - [._passthrough(n, cb)](#_passthroughn-cb) 101 | 102 | ## Parser() 103 | 104 | The `Parser` stream mixin works with either `Writable` or `Transform` stream 105 | instances/subclasses. Provides a convenient generic "parsing" API: 106 | 107 | ```js 108 | _bytes(n, cb) - buffers "n" bytes and then calls "cb" with the "chunk" 109 | _skipBytes(n, cb) - skips "n" bytes and then calls "cb" when done 110 | ``` 111 | 112 | If you extend a `Transform` stream, then the `_passthrough()` function is also 113 | added: 114 | 115 | ```js 116 | _passthrough(n, cb) - passes through "n" bytes untouched and then calls "cb" 117 | ``` 118 | 119 | ### ._bytes(n, cb) 120 | 121 | Buffers `n` bytes and then invokes `cb` once that amount has been collected. 122 | 123 | ### ._skipBytes(n, cb) 124 | 125 | Skips over the next `n` bytes and then invokes `cb` once that amount has been 126 | discarded. 127 | 128 | ### ._passthrough(n, cb) 129 | 130 | Passes through `n` bytes to the readable side of this stream untouched, 131 | then invokes `cb` once that amount has been passed through. This function is only defined 132 | when stream-parser is extending a `Transform` stream. 133 | 134 | [node-icy]: https://github.com/TooTallNate/node-icy 135 | [node-throttle]: https://github.com/TooTallNate/node-throttle 136 | [node-flv]: https://github.com/TooTallNate/node-flv 137 | [node-wav]: https://github.com/TooTallNate/node-wav 138 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var assert = require('assert'); 7 | var debug = require('debug')('stream-parser'); 8 | 9 | /** 10 | * Module exports. 11 | */ 12 | 13 | module.exports = Parser; 14 | 15 | /** 16 | * Parser states. 17 | */ 18 | 19 | var INIT = -1; 20 | var BUFFERING = 0; 21 | var SKIPPING = 1; 22 | var PASSTHROUGH = 2; 23 | 24 | /** 25 | * The `Parser` stream mixin works with either `Writable` or `Transform` stream 26 | * instances/subclasses. Provides a convenient generic "parsing" API: 27 | * 28 | * _bytes(n, cb) - buffers "n" bytes and then calls "cb" with the "chunk" 29 | * _skipBytes(n, cb) - skips "n" bytes and then calls "cb" when done 30 | * 31 | * If you extend a `Transform` stream, then the `_passthrough()` function is also 32 | * added: 33 | * 34 | * _passthrough(n, cb) - passes through "n" bytes untouched and then calls "cb" 35 | * 36 | * @param {Stream} stream Transform or Writable stream instance to extend 37 | * @api public 38 | */ 39 | 40 | function Parser (stream) { 41 | var isTransform = stream && 'function' == typeof stream._transform; 42 | var isWritable = stream && 'function' == typeof stream._write; 43 | 44 | if (!isTransform && !isWritable) throw new Error('must pass a Writable or Transform stream in'); 45 | debug('extending Parser into stream'); 46 | 47 | // Transform streams and Writable streams get `_bytes()` and `_skipBytes()` 48 | stream._bytes = _bytes; 49 | stream._skipBytes = _skipBytes; 50 | 51 | // only Transform streams get the `_passthrough()` function 52 | if (isTransform) stream._passthrough = _passthrough; 53 | 54 | // take control of the streams2 callback functions for this stream 55 | if (isTransform) { 56 | stream._transform = transform; 57 | } else { 58 | stream._write = write; 59 | } 60 | } 61 | 62 | function init (stream) { 63 | debug('initializing parser stream'); 64 | 65 | // number of bytes left to parser for the next "chunk" 66 | stream._parserBytesLeft = 0; 67 | 68 | // array of Buffer instances that make up the next "chunk" 69 | stream._parserBuffers = []; 70 | 71 | // number of bytes parsed so far for the next "chunk" 72 | stream._parserBuffered = 0; 73 | 74 | // flag that keeps track of if what the parser should do with bytes received 75 | stream._parserState = INIT; 76 | 77 | // the callback for the next "chunk" 78 | stream._parserCallback = null; 79 | 80 | // XXX: backwards compat with the old Transform API... remove at some point.. 81 | if ('function' == typeof stream.push) { 82 | stream._parserOutput = stream.push.bind(stream); 83 | } 84 | 85 | stream._parserInit = true; 86 | } 87 | 88 | /** 89 | * Buffers `n` bytes and then invokes `fn` once that amount has been collected. 90 | * 91 | * @param {Number} n the number of bytes to buffer 92 | * @param {Function} fn callback function to invoke when `n` bytes are buffered 93 | * @api public 94 | */ 95 | 96 | function _bytes (n, fn) { 97 | assert(!this._parserCallback, 'there is already a "callback" set!'); 98 | assert(isFinite(n) && n > 0, 'can only buffer a finite number of bytes > 0, got "' + n + '"'); 99 | if (!this._parserInit) init(this); 100 | debug('buffering %o bytes', n); 101 | this._parserBytesLeft = n; 102 | this._parserCallback = fn; 103 | this._parserState = BUFFERING; 104 | } 105 | 106 | /** 107 | * Skips over the next `n` bytes, then invokes `fn` once that amount has 108 | * been discarded. 109 | * 110 | * @param {Number} n the number of bytes to discard 111 | * @param {Function} fn callback function to invoke when `n` bytes have been skipped 112 | * @api public 113 | */ 114 | 115 | function _skipBytes (n, fn) { 116 | assert(!this._parserCallback, 'there is already a "callback" set!'); 117 | assert(n > 0, 'can only skip > 0 bytes, got "' + n + '"'); 118 | if (!this._parserInit) init(this); 119 | debug('skipping %o bytes', n); 120 | this._parserBytesLeft = n; 121 | this._parserCallback = fn; 122 | this._parserState = SKIPPING; 123 | } 124 | 125 | /** 126 | * Passes through `n` bytes to the readable side of this stream untouched, 127 | * then invokes `fn` once that amount has been passed through. 128 | * 129 | * @param {Number} n the number of bytes to pass through 130 | * @param {Function} fn callback function to invoke when `n` bytes have passed through 131 | * @api public 132 | */ 133 | 134 | function _passthrough (n, fn) { 135 | assert(!this._parserCallback, 'There is already a "callback" set!'); 136 | assert(n > 0, 'can only pass through > 0 bytes, got "' + n + '"'); 137 | if (!this._parserInit) init(this); 138 | debug('passing through %o bytes', n); 139 | this._parserBytesLeft = n; 140 | this._parserCallback = fn; 141 | this._parserState = PASSTHROUGH; 142 | } 143 | 144 | /** 145 | * The `_write()` callback function implementation. 146 | * 147 | * @api private 148 | */ 149 | 150 | function write (chunk, encoding, fn) { 151 | if (!this._parserInit) init(this); 152 | debug('write(%o bytes)', chunk.length); 153 | 154 | // XXX: old Writable stream API compat... remove at some point... 155 | if ('function' == typeof encoding) fn = encoding; 156 | 157 | data(this, chunk, null, fn); 158 | } 159 | 160 | /** 161 | * The `_transform()` callback function implementation. 162 | * 163 | * @api private 164 | */ 165 | 166 | 167 | function transform (chunk, output, fn) { 168 | if (!this._parserInit) init(this); 169 | debug('transform(%o bytes)', chunk.length); 170 | 171 | // XXX: old Transform stream API compat... remove at some point... 172 | if ('function' != typeof output) { 173 | output = this._parserOutput; 174 | } 175 | 176 | data(this, chunk, output, fn); 177 | } 178 | 179 | /** 180 | * The internal buffering/passthrough logic... 181 | * 182 | * This `_data` function get's "trampolined" to prevent stack overflows for tight 183 | * loops. This technique requires us to return a "thunk" function for any 184 | * synchronous action. Async stuff breaks the trampoline, but that's ok since it's 185 | * working with a new stack at that point anyway. 186 | * 187 | * @api private 188 | */ 189 | 190 | function _data (stream, chunk, output, fn) { 191 | if (stream._parserBytesLeft <= 0) { 192 | return fn(new Error('got data but not currently parsing anything')); 193 | } 194 | 195 | if (chunk.length <= stream._parserBytesLeft) { 196 | // small buffer fits within the "_parserBytesLeft" window 197 | return function () { 198 | return process(stream, chunk, output, fn); 199 | }; 200 | } else { 201 | // large buffer needs to be sliced on "_parserBytesLeft" and processed 202 | return function () { 203 | var b = chunk.slice(0, stream._parserBytesLeft); 204 | return process(stream, b, output, function (err) { 205 | if (err) return fn(err); 206 | if (chunk.length > b.length) { 207 | return function () { 208 | return _data(stream, chunk.slice(b.length), output, fn); 209 | }; 210 | } 211 | }); 212 | }; 213 | } 214 | } 215 | 216 | /** 217 | * The internal `process` function gets called by the `data` function when 218 | * something "interesting" happens. This function takes care of buffering the 219 | * bytes when buffering, passing through the bytes when doing that, and invoking 220 | * the user callback when the number of bytes has been reached. 221 | * 222 | * @api private 223 | */ 224 | 225 | function process (stream, chunk, output, fn) { 226 | stream._parserBytesLeft -= chunk.length; 227 | debug('%o bytes left for stream piece', stream._parserBytesLeft); 228 | 229 | if (stream._parserState === BUFFERING) { 230 | // buffer 231 | stream._parserBuffers.push(chunk); 232 | stream._parserBuffered += chunk.length; 233 | } else if (stream._parserState === PASSTHROUGH) { 234 | // passthrough 235 | output(chunk); 236 | } 237 | // don't need to do anything for the SKIPPING case 238 | 239 | if (0 === stream._parserBytesLeft) { 240 | // done with stream "piece", invoke the callback 241 | var cb = stream._parserCallback; 242 | if (cb && stream._parserState === BUFFERING && stream._parserBuffers.length > 1) { 243 | chunk = Buffer.concat(stream._parserBuffers, stream._parserBuffered); 244 | } 245 | if (stream._parserState !== BUFFERING) { 246 | chunk = null; 247 | } 248 | stream._parserCallback = null; 249 | stream._parserBuffered = 0; 250 | stream._parserState = INIT; 251 | stream._parserBuffers.splice(0); // empty 252 | 253 | if (cb) { 254 | var args = []; 255 | if (chunk) { 256 | // buffered 257 | args.push(chunk); 258 | } else { 259 | // passthrough 260 | } 261 | if (output) { 262 | // on a Transform stream, has "output" function 263 | args.push(output); 264 | } 265 | var async = cb.length > args.length; 266 | if (async) { 267 | args.push(trampoline(fn)); 268 | } 269 | // invoke cb 270 | var rtn = cb.apply(stream, args); 271 | if (!async || fn === rtn) return fn; 272 | } 273 | } else { 274 | // need more bytes 275 | return fn; 276 | } 277 | } 278 | 279 | var data = trampoline(_data); 280 | 281 | /** 282 | * Generic thunk-based "trampoline" helper function. 283 | * 284 | * @param {Function} input function 285 | * @return {Function} "trampolined" function 286 | * @api private 287 | */ 288 | 289 | function trampoline (fn) { 290 | return function () { 291 | var result = fn.apply(this, arguments); 292 | 293 | while ('function' == typeof result) { 294 | result = result(); 295 | } 296 | 297 | return result; 298 | }; 299 | } 300 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stream-parser", 3 | "version": "0.3.1", 4 | "description": "Generic interruptible \"parser\" mixin for Transform & Writable streams", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha --reporter spec" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/TooTallNate/node-stream-parser.git" 12 | }, 13 | "author": "Nathan Rajlich (http://tootallnate.net)", 14 | "license": "MIT", 15 | "dependencies": { 16 | "debug": "2" 17 | }, 18 | "devDependencies": { 19 | "mocha": "*", 20 | "readable-stream": "2" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/transform.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var assert = require('assert'); 7 | var Parser = require('../'); 8 | var Transform = require('stream').Transform; 9 | 10 | // for node v0.6.x-v0.8.x support 11 | if (!Transform) Transform = require('readable-stream/transform'); 12 | 13 | describe('Transform stream', function () { 14 | 15 | it('should have the `_bytes()` function', function () { 16 | var t = new Transform(); 17 | Parser(t); 18 | assert.equal('function', typeof t._bytes); 19 | }); 20 | 21 | it('should have the `_skipBytes()` function', function () { 22 | var t = new Transform(); 23 | Parser(t); 24 | assert.equal('function', typeof t._skipBytes); 25 | }); 26 | 27 | it('should have the `_passthrough()` function', function () { 28 | var t = new Transform(); 29 | Parser(t); 30 | assert.equal('function', typeof t._passthrough); 31 | }); 32 | 33 | it('should read 2 bytes, pass through 2 bytes', function (done) { 34 | var t = new Transform(); 35 | Parser(t); 36 | var gotBytes = false; 37 | var gotPassthrough = false; 38 | var gotData = false; 39 | 40 | // read 2 bytes 41 | t._bytes(2, read); 42 | function read (chunk, output) { 43 | assert.equal(2, chunk.length); 44 | assert.equal(0, chunk[0]); 45 | assert.equal(1, chunk[1]); 46 | gotBytes = true; 47 | t._passthrough(2, passthrough); 48 | } 49 | function passthrough (output) { 50 | gotPassthrough = true; 51 | } 52 | 53 | t.on('data', function (data) { 54 | assert.equal(2, data.length); 55 | assert.equal(2, data[0]); 56 | assert.equal(3, data[1]); 57 | gotData = true; 58 | }); 59 | 60 | t.on('end', function () { 61 | assert(gotBytes); 62 | assert(gotPassthrough); 63 | assert(gotData); 64 | done(); 65 | }); 66 | 67 | t.end(new Buffer([ 0, 1, 2, 3 ])); 68 | }); 69 | 70 | it('should allow you to pass through Infinity bytes', function (done) { 71 | var t = new Transform(); 72 | Parser(t); 73 | t._passthrough(Infinity); 74 | var out = []; 75 | t.on('data', function (data) { 76 | out.push(data); 77 | }); 78 | t.on('end', function () { 79 | assert.equal('hello world', Buffer.concat(out).toString()); 80 | done(); 81 | }); 82 | t.end('hello world'); 83 | }); 84 | 85 | it('should *not* allow you to buffer Infinity bytes', function () { 86 | // buffering to Infinity would just be silly... 87 | var t = new Transform(); 88 | Parser(t); 89 | assert.throws(function () { 90 | t._bytes(Infinity); 91 | }); 92 | }); 93 | 94 | it('should not cause stack overflow', function (done) { 95 | // this one does an admirable amount of CPU work... 96 | this.test.slow(500); 97 | this.test.timeout(1000); 98 | 99 | var t = new Transform(); 100 | Parser(t); 101 | 102 | var bytes = 65536; 103 | t._bytes(1, read); 104 | function read() { 105 | // Any downstream pipe consumer (writable) which doesn't do any async actions. 106 | // e.g. console.log, or simply capturing data into an in-memory data-structure. 107 | if (--bytes) { 108 | t._bytes(1, read); 109 | } else { 110 | done(); 111 | } 112 | } 113 | 114 | var b = new Buffer(bytes); 115 | b.fill('h'); 116 | t.end(b); 117 | }); 118 | 119 | describe('async', function () { 120 | 121 | it('should accept a callback function for `_passthrough()`', function (done) { 122 | var t = new Transform(); 123 | var data = 'test', _data; 124 | Parser(t); 125 | t._passthrough(data.length, function (output, fn) { 126 | setTimeout(fn, 25); 127 | }); 128 | 129 | t.on('data', function (data) { 130 | _data = data; 131 | }); 132 | t.on('end', function () { 133 | assert.equal(data, _data); 134 | done(); 135 | }); 136 | t.end(data); 137 | t.resume(); 138 | }); 139 | 140 | it('should accept a callback function for `_bytes()`', function (done) { 141 | var t = new Transform(); 142 | var data = 'test'; 143 | Parser(t); 144 | t._bytes(data.length, function (chunk, output, fn) { 145 | setTimeout(fn, 25); 146 | }); 147 | 148 | t.on('end', function () { 149 | done(); 150 | }); 151 | t.end(data); 152 | t.resume(); 153 | }); 154 | 155 | it('should work switching between async and sync callbacks', function (done) { 156 | var firstCalled, secondCalled, thirdCalled; 157 | 158 | // create a 6 byte Buffer. The first 4 will be the int 159 | // `1337`. The last 2 will be whatever... 160 | var val = 1337; 161 | var buf = new Buffer(6); 162 | buf.writeUInt32LE(val, 0); 163 | 164 | var t = new Transform(); 165 | Parser(t); 166 | 167 | // first read 4 bytes, with an async callback 168 | function first (chunk, output, fn) { 169 | firstCalled = true; 170 | assert.equal(chunk.length, 4); 171 | assert.equal(val, chunk.readUInt32LE(0)); 172 | 173 | t._bytes(1, second); 174 | setTimeout(fn, 10); 175 | } 176 | 177 | // second read 1 byte, sync callback 178 | function second (chunk) { 179 | secondCalled = true; 180 | assert.equal(chunk.length, 1); 181 | t._bytes(1, third); 182 | } 183 | 184 | // third read 1 byte, async callback 185 | function third (chunk, output, fn) { 186 | thirdCalled = true; 187 | assert.equal(chunk.length, 1); 188 | setTimeout(fn, 10); 189 | } 190 | 191 | t.on('finish', function () { 192 | assert(firstCalled); 193 | assert(secondCalled); 194 | assert(thirdCalled); 195 | done(); 196 | }); 197 | 198 | t._bytes(4, first); 199 | t.write(buf); 200 | t.end(); 201 | }); 202 | 203 | }); 204 | 205 | }); 206 | -------------------------------------------------------------------------------- /test/writable.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var Parser = require('../'); 7 | var assert = require('assert'); 8 | var inherits = require('util').inherits; 9 | var Writable = require('stream').Writable; 10 | 11 | // for node v0.6.x-v0.8.x support 12 | if (!Writable) Writable = require('readable-stream/writable'); 13 | 14 | describe('Writable streams', function () { 15 | 16 | var val = 1337; 17 | var buf = new Buffer(4); 18 | buf.writeUInt32LE(val, 0); 19 | 20 | it('should have the `_bytes()` function', function () { 21 | var w = new Writable(); 22 | Parser(w); 23 | assert.equal('function', typeof w._bytes); 24 | }); 25 | 26 | it('should have the `_skipBytes()` function', function () { 27 | var w = new Writable(); 28 | Parser(w); 29 | assert.equal('function', typeof w._skipBytes); 30 | }); 31 | 32 | it('should *not* have the `_passthrough()` function', function () { 33 | var w = new Writable(); 34 | Parser(w); 35 | assert.notEqual('function', typeof w._passthrough); 36 | }); 37 | 38 | it('should read 4 bytes in one chunk', function (done) { 39 | var w = new Writable(); 40 | Parser(w); 41 | 42 | // read 4 bytes 43 | w._bytes(4, function (chunk) { 44 | assert.equal(chunk.length, buf.length); 45 | assert.equal(val, chunk.readUInt32LE(0)); 46 | done(); 47 | }); 48 | 49 | w.end(buf); 50 | }); 51 | 52 | it('should read 4 bytes in multiple chunks', function (done) { 53 | var w = new Writable(); 54 | Parser(w); 55 | 56 | // read 4 bytes 57 | w._bytes(4, function (chunk) { 58 | assert.equal(chunk.length, buf.length); 59 | assert.equal(val, chunk.readUInt32LE(0)); 60 | done(); 61 | }); 62 | 63 | for (var i = 0; i < buf.length; i++) { 64 | w.write(new Buffer([ buf[i] ])); 65 | } 66 | w.end(); 67 | }); 68 | 69 | it('should read 1 byte, 2 bytes, then 3 bytes', function (done) { 70 | var w = new Writable(); 71 | Parser(w); 72 | 73 | // read 1 byte 74 | w._bytes(1, readone); 75 | function readone (chunk) { 76 | assert.equal(1, chunk.length); 77 | assert.equal(0, chunk[0]); 78 | w._bytes(2, readtwo); 79 | } 80 | function readtwo (chunk) { 81 | assert.equal(2, chunk.length); 82 | assert.equal(0, chunk[0]); 83 | assert.equal(1, chunk[1]); 84 | w._bytes(3, readthree); 85 | } 86 | function readthree (chunk) { 87 | assert.equal(3, chunk.length); 88 | assert.equal(0, chunk[0]); 89 | assert.equal(1, chunk[1]); 90 | assert.equal(2, chunk[2]); 91 | done(); 92 | } 93 | 94 | w.end(new Buffer([ 0, 0, 1, 0, 1, 2 ])); 95 | }); 96 | 97 | it('should work when mixing in to a subclass\' `prototype`', function (done) { 98 | function MyWritable () { 99 | Writable.call(this); 100 | this._bytes(2, this.onbytes); 101 | } 102 | inherits(MyWritable, Writable); 103 | 104 | // mixin to the `prototype` 105 | Parser(MyWritable.prototype); 106 | 107 | var count = 2; 108 | MyWritable.prototype.onbytes = function (buf) { 109 | assert.equal(2, buf.length); 110 | assert.equal(0, buf[0]); 111 | assert.equal(1, buf[1]); 112 | --count; 113 | if (!count) done(); 114 | }; 115 | 116 | var a = new MyWritable(); 117 | var b = new MyWritable(); 118 | 119 | // interleave write()s 120 | a.write(new Buffer([ 0 ])); 121 | b.write(new Buffer([ 0 ])); 122 | a.write(new Buffer([ 1 ])); 123 | b.write(new Buffer([ 1 ])); 124 | a.end(); 125 | b.end(); 126 | }); 127 | 128 | it('should *not* allow you to buffer Infinity bytes', function () { 129 | // buffering to Infinity would just be silly... 130 | var w = new Writable(); 131 | Parser(w); 132 | assert.throws(function () { 133 | w._bytes(Infinity); 134 | }); 135 | }); 136 | 137 | it('should skip 3 bytes then buffer 3 bytes', function (done) { 138 | var w = new Writable(); 139 | Parser(w); 140 | 141 | w._skipBytes(3, function () { 142 | assert.equal(arguments.length, 0); 143 | w._bytes(3, function (data) { 144 | assert.equal(arguments.length, 1); 145 | assert.equal(data.toString('ascii'), 'lo\n'); 146 | done(); 147 | }); 148 | }); 149 | 150 | w.end('hello\n'); 151 | }); 152 | 153 | describe('async', function () { 154 | 155 | it('should accept a callback function for `_bytes()`', function (done) { 156 | var w = new Writable(); 157 | var data = 'test'; 158 | Parser(w); 159 | w._bytes(data.length, function (chunk, fn) { 160 | setTimeout(fn, 25); 161 | }); 162 | w.on('finish', function () { 163 | done(); 164 | }); 165 | w.end(data); 166 | }); 167 | 168 | it('should emit an "error" event when data is written with no parsing function', function (done) { 169 | var w = new Writable(); 170 | Parser(w); 171 | w.once('error', function (err) { 172 | assert(err); 173 | done(); 174 | }); 175 | w.write('a'); 176 | }); 177 | 178 | }); 179 | 180 | describe('FrameParser', function () { 181 | function FrameParser () { 182 | Writable.call(this); 183 | this._bytes(1, this.onsize); 184 | } 185 | inherits(FrameParser, Writable); 186 | 187 | // mixin to the `prototype` 188 | Parser(FrameParser.prototype); 189 | 190 | FrameParser.prototype.onsize = function (buf) { 191 | var size = buf.readUInt8(0); 192 | this._bytes(size, this.onframe); 193 | }; 194 | 195 | FrameParser.prototype.onframe = function (buf) { 196 | this.emit('frame', buf.toString()); 197 | 198 | // begin parsing the next "frame" 199 | this._bytes(1, this.onsize); 200 | }; 201 | 202 | it('should emit 1 "frame" event', function (done) { 203 | var p = new FrameParser(); 204 | var s = 'a string'; 205 | p.on('frame', function (frame) { 206 | assert.equal(s, frame); 207 | done(); 208 | }); 209 | p.write(new Buffer([ s.length ])); 210 | p.write(new Buffer(s)); 211 | p.end(); 212 | }); 213 | 214 | it('should emit 2 "frame" events', function (done) { 215 | var p = new FrameParser(); 216 | var s = 'a string'; 217 | var s2 = 'done'; 218 | var count = 0; 219 | p.on('frame', function (frame) { 220 | count++; 221 | if (s2 == frame) { 222 | assert.equal(2, count); 223 | done(); 224 | } 225 | }); 226 | p.write(new Buffer([ s.length ])); 227 | p.write(new Buffer(s)); 228 | p.write(new Buffer([ s2.length ])); 229 | p.write(new Buffer(s2)); 230 | p.end(); 231 | }); 232 | 233 | }); 234 | 235 | }); 236 | --------------------------------------------------------------------------------