UNPKG

@idio/dicer

Version:

[fork] A Very Fast Streaming Multipart Parser For Node.JS Written In ES6 And Optimised With JavaScript Compiler.

252 lines (238 loc) 7.33 kB
import { Writable } from 'stream' import StreamSearch from './streamsearch' import PartStream from './PartStream' import HeaderParser from './HeaderParser' const DASH = 45 const B_ONEDASH = Buffer.from('-') const B_CRLF = Buffer.from('\r\n') const EMPTY_FN = () => {} /** * @implements {_idio.Dicer} */ export default class Dicer extends Writable { /** * @param {!_idio.DicerConfig} [cfg] Options for the program. * @param {string} [cfg.boundary] This is the boundary used to detect the beginning of a new part. * @param {boolean} [cfg.headerFirst=false] If true, preamble header parsing will be performed first. Default `false`. * @param {boolean} [cfg.partHwm] High watermark for parsing parts. * @param {number} [cfg.maxHeaderPairs=2000] The maximum number of header key=>value pairs to parse. Default `2000`. */ constructor(cfg) { super(/** @type {!stream.WritableOptions|undefined} */ (cfg)) if (!cfg || (!cfg.headerFirst && typeof cfg.boundary != 'string')) throw new TypeError('Boundary required') if (typeof cfg.boundary == 'string') this.setBoundary(cfg.boundary) else /** @type {!StreamSearch|undefined} */ this._bparser = undefined this._headerFirst = cfg.headerFirst this._dashes = 0 this._parts = 0 this._finished = false this._realFinish = false this._isPreamble = true this._justMatched = false this._firstWrite = true this._inHeader = true /** * @type {!PartStream|undefined} */ this._part = undefined this._cb = undefined this._ignoreData = false this._partOpts = /** @type {!stream.ReadableOptions} */ (typeof cfg.partHwm == 'number' ? { highWaterMark: cfg.partHwm } : {}) this._pause = false this._hparser = new HeaderParser(cfg) this._hparser.on('header', (header) => { this._inHeader = false this._part.emit('header', header) }) } emit(ev) { if (ev == 'finish' && !this._realFinish) { if (!this._finished) { process.nextTick(() => { this.emit('error', new Error('Unexpected end of multipart data')) if (this._part && !this._ignoreData) { var type = (this._isPreamble ? 'Preamble' : 'Part') this._part.emit('error', new Error(type + ' terminated early due to unexpected end of multipart data')) this._part.push(null) process.nextTick(() => { this._realFinish = true this.emit('finish') this._realFinish = false }) return } this._realFinish = true this.emit('finish') this._realFinish = false }) } } else Writable.prototype.emit.apply(this, arguments) return false } _write(data, encoding, cb) { // ignore unexpected data (e.g. extra trailer data after finished) if (!this._hparser && !this._bparser) return cb() if (this._headerFirst && this._isPreamble) { if (!this._part) { this._part = new PartStream(this._partOpts) if (this._events['preamble']) this.emit('preamble', this._part) else this._ignore() } const r = this._hparser.push(data) if (!this._inHeader && r !== undefined && r < data.length) data = data.slice(r) else return cb() } // allows for "easier" testing if (this._firstWrite) { // this.start = +new Date this._bparser.push(B_CRLF) this._firstWrite = false } // this.start = +new Date this._bparser.push(data) if (this._pause) this._cb = cb else cb() } reset() { this._part = undefined this._bparser = undefined this._hparser = undefined } setBoundary(boundary) { this._bparser = new StreamSearch('\r\n--' + boundary) this._bparser.on('info', (isMatch, data, start, end) => { this._oninfo(isMatch, data, start, end) // const duration = +new Date - this.start // console.log('found in %sms', duration) }) } _ignore() { if (this._part && !this._ignoreData) { this._ignoreData = true this._part.on('error', EMPTY_FN) // we must perform some kind of read on the stream even though we are // ignoring the data, otherwise node's Readable stream will not emit 'end' // after pushing null to the stream this._part.resume() } } _oninfo(isMatch, data, start, end) { var buf, i = 0, r, ev, shouldWriteMore = true if (!this._part && this._justMatched && data) { while (this._dashes < 2 && (start + i) < end) { if (data[start + i] === DASH) { ++i ++this._dashes } else { if (this._dashes) buf = B_ONEDASH this._dashes = 0 break } } if (this._dashes === 2) { if ((start + i) < end && this._events.trailer) this.emit('trailer', data.slice(start + i, end)) this.reset() this._finished = true // no more parts will be added if (this._parts === 0) { this._realFinish = true this.emit('finish') this._realFinish = false } } if (this._dashes) return } if (this._justMatched) this._justMatched = false if (!this._part) { this._part = new PartStream(this._partOpts) this._part._read = () => { this._unpause() } ev = this._isPreamble ? 'preamble' : 'part' if (this._events[ev]) this.emit(ev, this._part) else this._ignore() if (!this._isPreamble) this._inHeader = true } if (data && start < end && !this._ignoreData) { if (this._isPreamble || !this._inHeader) { if (buf) shouldWriteMore = this._part.push(buf) shouldWriteMore = this._part.push(data.slice(start, end)) if (!shouldWriteMore) this._pause = true } else if (!this._isPreamble && this._inHeader) { if (buf) this._hparser.push(buf) r = this._hparser.push(data.slice(start, end)) if (!this._inHeader && r !== undefined && r < end) this._oninfo(false, data, start + r, end) } } if (isMatch) { this._hparser.reset() if (this._isPreamble) this._isPreamble = false else { ++this._parts this._part.on('end', () => { if (--this._parts === 0) { if (this._finished) { this._realFinish = true this.emit('finish') this._realFinish = false } else { this._unpause() } } }) } this._part.push(null) this._part = undefined this._ignoreData = false this._justMatched = true this._dashes = 0 } } _unpause() { if (!this._pause) return this._pause = false if (this._cb) { const cb = this._cb this._cb = undefined cb() } } } /** * @suppress {nonStandardJsDocs} * @typedef {import('../types').DicerConfig} _idio.DicerConfig */ /** * @suppress {nonStandardJsDocs} * @typedef {import('stream').ReadableOptions} stream.ReadableOptions */ /** * @license MIT dicer by Brian White * https://github.com/mscdex/dicer */