Initial commit
This commit is contained in:
+5
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
extends: '@mscdex/eslint-config',
|
||||
};
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
tests-linux:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node-version: [10.x, 12.x, 14.x, 16.x]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Install module
|
||||
run: npm install
|
||||
- name: Run tests
|
||||
run: npm test
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
name: lint
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [ master ]
|
||||
|
||||
env:
|
||||
NODE_VERSION: 16.x
|
||||
|
||||
jobs:
|
||||
lint-js:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
- name: Install ESLint + ESLint configs/plugins
|
||||
run: npm install --only=dev
|
||||
- name: Lint files
|
||||
run: npm run lint
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
Copyright Brian White. All rights reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to
|
||||
deal in the Software without restriction, including without limitation the
|
||||
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
IN THE SOFTWARE.
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
|
||||
Description
|
||||
===========
|
||||
|
||||
A very fast streaming multipart parser for node.js.
|
||||
|
||||
Benchmarks can be found [here](https://github.com/mscdex/dicer/wiki/Benchmarks).
|
||||
|
||||
|
||||
Requirements
|
||||
============
|
||||
|
||||
* [node.js](http://nodejs.org/) -- v10.0.0 or newer
|
||||
|
||||
|
||||
Install
|
||||
============
|
||||
|
||||
npm install dicer
|
||||
|
||||
|
||||
Examples
|
||||
========
|
||||
|
||||
* Parse an HTTP form upload
|
||||
|
||||
```js
|
||||
const { inspect } = require('util');
|
||||
const http = require('http');
|
||||
|
||||
const Dicer = require('dicer');
|
||||
|
||||
// Quick and dirty way to parse multipart boundary
|
||||
const RE_BOUNDARY =
|
||||
/^multipart\/.+?(?:; boundary=(?:(?:"(.+)")|(?:([^\s]+))))$/i;
|
||||
const HTML = Buffer.from(`
|
||||
<html><head></head><body>
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
<input type="text" name="textfield"><br />
|
||||
<input type="file" name="filefield"><br />
|
||||
<input type="submit">
|
||||
</form>
|
||||
</body></html>
|
||||
`);
|
||||
const PORT = 8080;
|
||||
|
||||
http.createServer((req, res) => {
|
||||
let m;
|
||||
if (req.method === 'POST'
|
||||
&& req.headers['content-type']
|
||||
&& (m = RE_BOUNDARY.exec(req.headers['content-type']))) {
|
||||
const d = new Dicer({ boundary: m[1] || m[2] });
|
||||
|
||||
d.on('part', (p) => {
|
||||
console.log('New part!');
|
||||
p.on('header', (header) => {
|
||||
for (const h in header) {
|
||||
console.log(
|
||||
`Part header: k: ${inspect(h)}, v: ${inspect(header[h])}`
|
||||
);
|
||||
}
|
||||
});
|
||||
p.on('data', (data) => {
|
||||
console.log(`Part data: ${inspect(data.toString())}`);
|
||||
});
|
||||
p.on('end', () => {
|
||||
console.log('End of part\n');
|
||||
});
|
||||
});
|
||||
d.on('finish', () => {
|
||||
console.log('End of parts');
|
||||
res.writeHead(200);
|
||||
res.end('Form submission successful!');
|
||||
});
|
||||
req.pipe(d);
|
||||
} else if (req.method === 'GET' && req.url === '/') {
|
||||
res.writeHead(200);
|
||||
res.end(HTML);
|
||||
} else {
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
}
|
||||
}).listen(PORT, () => {
|
||||
console.log(`Listening for requests on port ${PORT}`);
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
API
|
||||
===
|
||||
|
||||
_Dicer_ is a _Writable_ stream
|
||||
|
||||
Dicer (special) events
|
||||
----------------------
|
||||
|
||||
* **finish**() - Emitted when all parts have been parsed and the Dicer instance has been ended.
|
||||
|
||||
* **part**(< _PartStream_ >stream) - Emitted when a new part has been found.
|
||||
|
||||
* **preamble**(< _PartStream_ >stream) - Emitted for preamble if you should happen to need it (can usually be ignored).
|
||||
|
||||
* **trailer**(< _Buffer_ >data) - Emitted when trailing data was found after the terminating boundary (as with the preamble, this can usually be ignored too).
|
||||
|
||||
|
||||
Dicer methods
|
||||
-------------
|
||||
|
||||
* **(constructor)**(< _object_ >config) - Creates and returns a new Dicer instance with the following valid `config` settings:
|
||||
|
||||
* **boundary** - _string_ - This is the boundary used to detect the beginning of a new part.
|
||||
|
||||
* **headerFirst** - _boolean_ - If true, preamble header parsing will be performed first.
|
||||
|
||||
* **maxHeaderPairs** - _integer_ - The maximum number of header key=>value pairs to parse **Default:** 2000 (same as node's http).
|
||||
|
||||
* **setBoundary**(< _string_ >boundary) - _(void)_ - Sets the boundary to use for parsing and performs some initialization needed for parsing. You should only need to use this if you set `headerFirst` to true in the constructor and are parsing the boundary from the preamble header.
|
||||
|
||||
|
||||
|
||||
_PartStream_ is a _Readable_ stream
|
||||
|
||||
PartStream (special) events
|
||||
---------------------------
|
||||
|
||||
* **header**(< _object_ >header) - An object containing the header for this particular part. Each property value is an _array_ of one or more string values.
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
'use strict';
|
||||
|
||||
function createMultipartBuffer(boundary, sizes) {
|
||||
const bufs = [];
|
||||
for (let i = 0; i < sizes.length; ++i) {
|
||||
const mb = sizes[i] * 1024 * 1024;
|
||||
bufs.push(Buffer.from([
|
||||
`--${boundary}`,
|
||||
`content-disposition: form-data; name="field${i + 1}"`,
|
||||
'',
|
||||
'0'.repeat(mb),
|
||||
`--${boundary}--`,
|
||||
'',
|
||||
].join('\r\n')));
|
||||
}
|
||||
return Buffer.concat(bufs);
|
||||
}
|
||||
|
||||
const boundary = '-----------------------------168072824752491622650073';
|
||||
const buffer = createMultipartBuffer(boundary, [
|
||||
10,
|
||||
10,
|
||||
10,
|
||||
20,
|
||||
50,
|
||||
]);
|
||||
const calls = {
|
||||
partBegin: 0,
|
||||
headerField: 0,
|
||||
headerValue: 0,
|
||||
headerEnd: 0,
|
||||
headersEnd: 0,
|
||||
partData: 0,
|
||||
partEnd: 0,
|
||||
end: 0,
|
||||
};
|
||||
|
||||
const moduleName = process.argv[2];
|
||||
switch (moduleName) {
|
||||
case 'dicer': {
|
||||
const Dicer = require('..');
|
||||
|
||||
const parser = new Dicer({ boundary });
|
||||
parser.on('part', (p) => {
|
||||
++calls.partBegin;
|
||||
p.on('header', (header) => {
|
||||
++calls.headersEnd;
|
||||
});
|
||||
p.on('data', (data) => {
|
||||
++calls.partData;
|
||||
});
|
||||
p.on('end', () => {
|
||||
++calls.partEnd;
|
||||
});
|
||||
}).on('end', () => {
|
||||
++calls.end;
|
||||
});
|
||||
|
||||
console.time(moduleName);
|
||||
parser.write(buffer);
|
||||
console.timeEnd(moduleName);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'formidable': {
|
||||
const { MultipartParser } = require('formidable');
|
||||
|
||||
const parser = new MultipartParser();
|
||||
parser.initWithBoundary(boundary);
|
||||
parser.on('data', ({ name }) => {
|
||||
++calls[name];
|
||||
});
|
||||
|
||||
console.time(moduleName);
|
||||
parser.write(buffer);
|
||||
console.timeEnd(moduleName);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'multiparty': {
|
||||
const { Readable } = require('stream');
|
||||
|
||||
const { Form } = require('multiparty');
|
||||
|
||||
const form = new Form({
|
||||
maxFieldsSize: Infinity,
|
||||
maxFields: Infinity,
|
||||
maxFilesSize: Infinity,
|
||||
autoFields: false,
|
||||
autoFiles: false,
|
||||
});
|
||||
|
||||
const req = new Readable({ read: () => {} });
|
||||
req.headers = {
|
||||
'content-type': `multipart/form-data; boundary=${boundary}`,
|
||||
};
|
||||
req.push(buffer);
|
||||
req.push(null);
|
||||
|
||||
function hijack(name, fn) {
|
||||
const oldFn = form[name];
|
||||
form[name] = function() {
|
||||
fn();
|
||||
return oldFn.apply(this, arguments);
|
||||
};
|
||||
}
|
||||
|
||||
hijack('onParseHeaderField', () => {
|
||||
++calls.headerField;
|
||||
});
|
||||
hijack('onParseHeaderValue', () => {
|
||||
++calls.headerValue;
|
||||
});
|
||||
hijack('onParsePartBegin', () => {
|
||||
++calls.partBegin;
|
||||
});
|
||||
hijack('onParsePartData', () => {
|
||||
++calls.partData;
|
||||
});
|
||||
hijack('onParsePartEnd', () => {
|
||||
++calls.partEnd;
|
||||
});
|
||||
|
||||
form.on('close', () => {
|
||||
++calls.end;
|
||||
console.timeEnd(moduleName);
|
||||
}).on('part', (p) => p.resume());
|
||||
|
||||
console.time(moduleName);
|
||||
form.parse(req);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
if (moduleName === undefined)
|
||||
console.error('Missing parser module name');
|
||||
else
|
||||
console.error(`Invalid parser module name: ${moduleName}`);
|
||||
process.exit(1);
|
||||
}
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
'use strict';
|
||||
|
||||
const { Writable } = require('stream');
|
||||
|
||||
const StreamSearch = require('streamsearch');
|
||||
|
||||
const PartStream = require('./PartStream');
|
||||
const HeaderParser = require('./HeaderParser');
|
||||
|
||||
const DASH = 45;
|
||||
const B_ONEDASH = Buffer.from('-');
|
||||
const B_CRLF = Buffer.from('\r\n');
|
||||
const EMPTY_FN = () => {};
|
||||
|
||||
class Dicer extends Writable {
|
||||
constructor(cfg) {
|
||||
super(cfg);
|
||||
|
||||
if (!cfg || (!cfg.headerFirst && typeof cfg.boundary !== 'string'))
|
||||
throw new TypeError('Boundary required');
|
||||
|
||||
if (typeof cfg.boundary === 'string')
|
||||
this.setBoundary(cfg.boundary);
|
||||
else
|
||||
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;
|
||||
this._part = undefined;
|
||||
this._cb = undefined;
|
||||
this._ignoreData = false;
|
||||
this._partOpts = (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);
|
||||
});
|
||||
this._hparser.on('error', (err) => {
|
||||
if (this._part && !this._ignoreData) {
|
||||
this._part.emit('error', err);
|
||||
this._part.push(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
emit(ev) {
|
||||
if (ev !== 'finish' || this._realFinish) {
|
||||
Writable.prototype.emit.apply(this, arguments);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._finished)
|
||||
return;
|
||||
|
||||
process.nextTick(() => {
|
||||
this.emit('error', new Error('Unexpected end of multipart data'));
|
||||
|
||||
if (this._part && !this._ignoreData) {
|
||||
const 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;
|
||||
});
|
||||
}
|
||||
|
||||
_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
|
||||
ignore(this);
|
||||
}
|
||||
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._bparser.push(B_CRLF);
|
||||
this._firstWrite = false;
|
||||
}
|
||||
|
||||
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}`, onInfo.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
function onInfo(isMatch, data, start, end) {
|
||||
let buf;
|
||||
let i = 0;
|
||||
let r;
|
||||
let ev;
|
||||
let 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 = (n) => {
|
||||
unpause(this);
|
||||
};
|
||||
ev = this._isPreamble ? 'preamble' : 'part';
|
||||
if (this._events[ev])
|
||||
this.emit(ev, this._part);
|
||||
else
|
||||
ignore(this);
|
||||
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)
|
||||
onInfo.call(this, 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 {
|
||||
unpause(this);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
this._part.push(null);
|
||||
this._part = undefined;
|
||||
this._ignoreData = false;
|
||||
this._justMatched = true;
|
||||
this._dashes = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function ignore(self) {
|
||||
if (self._part && !self._ignoreData) {
|
||||
self._ignoreData = true;
|
||||
self._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
|
||||
self._part.resume();
|
||||
}
|
||||
}
|
||||
|
||||
function unpause(self) {
|
||||
if (!self._pause)
|
||||
return;
|
||||
|
||||
self._pause = false;
|
||||
if (self._cb) {
|
||||
const cb = self._cb;
|
||||
self._cb = undefined;
|
||||
cb();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Dicer;
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
'use strict';
|
||||
|
||||
const EventEmitter = require('events');
|
||||
|
||||
const StreamSearch = require('streamsearch');
|
||||
|
||||
const B_DCRLF = Buffer.from('\r\n\r\n');
|
||||
const RE_CRLF = /\r\n/g;
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const RE_HDR = /^([^:]+):[ \t]?([\x00-\xFF]+)?$/;
|
||||
const MAX_HEADER_PAIRS = 2000; // From node's http.js
|
||||
const MAX_HEADER_SIZE = 80 * 1024; // From node's http_parser
|
||||
|
||||
class HeaderParser extends EventEmitter {
|
||||
constructor(cfg) {
|
||||
super();
|
||||
|
||||
this.nread = 0;
|
||||
this.maxed = false;
|
||||
this.npairs = 0;
|
||||
this.maxHeaderPairs = (cfg && typeof cfg.maxHeaderPairs === 'number'
|
||||
? cfg.maxHeaderPairs
|
||||
: MAX_HEADER_PAIRS);
|
||||
this.buffer = '';
|
||||
this.header = {};
|
||||
this.finished = false;
|
||||
this.ss = new StreamSearch(B_DCRLF, (isMatch, data, start, end) => {
|
||||
if (data && !this.maxed) {
|
||||
if (this.nread + (end - start) > MAX_HEADER_SIZE) {
|
||||
end = (MAX_HEADER_SIZE - this.nread);
|
||||
this.nread = MAX_HEADER_SIZE;
|
||||
} else {
|
||||
this.nread += (end - start);
|
||||
}
|
||||
|
||||
if (this.nread === MAX_HEADER_SIZE)
|
||||
this.maxed = true;
|
||||
|
||||
this.buffer += data.toString('latin1', start, end);
|
||||
}
|
||||
if (isMatch)
|
||||
this._finish();
|
||||
});
|
||||
}
|
||||
|
||||
push(data) {
|
||||
const r = this.ss.push(data);
|
||||
if (this.finished)
|
||||
return r;
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.finished = false;
|
||||
this.buffer = '';
|
||||
this.header = {};
|
||||
this.ss.reset();
|
||||
}
|
||||
|
||||
_finish() {
|
||||
let hadError = false;
|
||||
if (this.buffer)
|
||||
hadError = !parseHeader(this);
|
||||
this.ss.matches = this.ss.maxMatches;
|
||||
const header = this.header;
|
||||
this.header = {};
|
||||
this.buffer = '';
|
||||
this.finished = true;
|
||||
this.nread = this.npairs = 0;
|
||||
this.maxed = false;
|
||||
if (!hadError)
|
||||
this.emit('header', header);
|
||||
}
|
||||
}
|
||||
|
||||
function parseHeader(self) {
|
||||
if (self.npairs === self.maxHeaderPairs)
|
||||
return true;
|
||||
|
||||
const lines = self.buffer.split(RE_CRLF);
|
||||
const len = lines.length;
|
||||
let m;
|
||||
let h;
|
||||
let modded = false;
|
||||
|
||||
for (let i = 0; i < len; ++i) {
|
||||
if (lines[i].length === 0)
|
||||
continue;
|
||||
|
||||
if (lines[i][0] === '\t' || lines[i][0] === ' ') {
|
||||
// Folded header content
|
||||
// RFC2822 says to just remove the CRLF and not the whitespace following
|
||||
// it, so we follow the RFC and include the leading whitespace ...
|
||||
if (!h) {
|
||||
self.emit('error', new Error('Unexpected folded header value'));
|
||||
return false;
|
||||
}
|
||||
self.header[h][self.header[h].length - 1] += lines[i];
|
||||
} else {
|
||||
m = RE_HDR.exec(lines[i]);
|
||||
if (m) {
|
||||
h = m[1].toLowerCase();
|
||||
if (m[2]) {
|
||||
if (self.header[h] === undefined)
|
||||
self.header[h] = [m[2]];
|
||||
else
|
||||
self.header[h].push(m[2]);
|
||||
} else {
|
||||
self.header[h] = [''];
|
||||
}
|
||||
if (++self.npairs === self.maxHeaderPairs)
|
||||
break;
|
||||
} else {
|
||||
self.buffer = lines[i];
|
||||
modded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!modded)
|
||||
self.buffer = '';
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
module.exports = HeaderParser;
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
const { Readable } = require('stream');
|
||||
|
||||
class PartStream extends Readable {
|
||||
_read(n) {}
|
||||
}
|
||||
|
||||
module.exports = PartStream;
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"_from": "dicer@^0.3.0",
|
||||
"_id": "[email protected]",
|
||||
"_inBundle": false,
|
||||
"_integrity": "sha512-ObioMtXnmjYs3aRtpIJt9rgQSPCIhKVkFPip+E9GUDyWl8N435znUxK/JfNwGZJ2wnn5JKQ7Ly3vOK5Q5dylGA==",
|
||||
"_location": "/dicer",
|
||||
"_phantomChildren": {},
|
||||
"_requested": {
|
||||
"type": "range",
|
||||
"registry": true,
|
||||
"raw": "dicer@^0.3.0",
|
||||
"name": "dicer",
|
||||
"escapedName": "dicer",
|
||||
"rawSpec": "^0.3.0",
|
||||
"saveSpec": null,
|
||||
"fetchSpec": "^0.3.0"
|
||||
},
|
||||
"_requiredBy": [
|
||||
"/firebase-admin"
|
||||
],
|
||||
"_resolved": "https://registry.npmjs.org/dicer/-/dicer-0.3.1.tgz",
|
||||
"_shasum": "abf28921e3475bc5e801e74e0159fd94f927ba97",
|
||||
"_spec": "dicer@^0.3.0",
|
||||
"_where": "/Users/talksik/Development/nirvana-server/node_modules/firebase-admin",
|
||||
"author": {
|
||||
"name": "Brian White",
|
||||
"email": "[email protected]"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/mscdex/dicer/issues"
|
||||
},
|
||||
"bundleDependencies": false,
|
||||
"dependencies": {
|
||||
"streamsearch": "^1.1.0"
|
||||
},
|
||||
"deprecated": false,
|
||||
"description": "A very fast streaming multipart parser for node.js",
|
||||
"devDependencies": {
|
||||
"@mscdex/eslint-config": "^1.1.0",
|
||||
"eslint": "^7.32.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"homepage": "https://github.com/mscdex/dicer#readme",
|
||||
"keywords": [
|
||||
"parser",
|
||||
"parse",
|
||||
"parsing",
|
||||
"multipart",
|
||||
"form-data",
|
||||
"streaming"
|
||||
],
|
||||
"licenses": [
|
||||
{
|
||||
"type": "MIT",
|
||||
"url": "http://github.com/mscdex/dicer/raw/master/LICENSE"
|
||||
}
|
||||
],
|
||||
"main": "./lib/Dicer.js",
|
||||
"name": "dicer",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+ssh://[email protected]/mscdex/dicer.git"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint --cache --report-unused-disable-directives --ext=.js .eslintrc.js lib test",
|
||||
"lint:fix": "npm run lint -- --fix",
|
||||
"test": "node test/test.js"
|
||||
},
|
||||
"version": "0.3.1"
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
------WebKitFormBoundaryWLHCs9qmcJJoyjKR
|
||||
Content-Disposition: form-data; name="_method"
|
||||
|
||||
put
|
||||
------WebKitFormBoundaryWLHCs9qmcJJoyjKR
|
||||
Content-Disposition: form-data; name="profile[blog]"
|
||||
|
||||
|
||||
------WebKitFormBoundaryWLHCs9qmcJJoyjKR
|
||||
Content-Disposition: form-data; name="profile[public_email]"
|
||||
|
||||
|
||||
------WebKitFormBoundaryWLHCs9qmcJJoyjKR
|
||||
Content-Disposition: form-data; name="profile[interests]"
|
||||
|
||||
|
||||
------WebKitFormBoundaryWLHCs9qmcJJoyjKR
|
||||
Content-Disposition: form-data; name="profile[bio]"
|
||||
|
||||
hello
|
||||
|
||||
"quote"
|
||||
------WebKitFormBoundaryWLHCs9qmcJJoyjKR
|
||||
Content-Disposition: form-data; name="commit"
|
||||
|
||||
Save
|
||||
------WebKitFormBoundaryWLHCs9qmcJJoyjKR
|
||||
Content-Disposition: form-data; name="media"; filename=""
|
||||
Content-Type: application/octet-stream
|
||||
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
put
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"content-disposition": ["form-data; name=\"_method\""]}
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"content-disposition": ["form-data; name=\"profile[blog]\""]}
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"content-disposition": ["form-data; name=\"profile[public_email]\""]}
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"content-disposition": ["form-data; name=\"profile[interests]\""]}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
hello
|
||||
|
||||
"quote"
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"content-disposition": ["form-data; name=\"profile[bio]\""]}
|
||||
+1
@@ -0,0 +1 @@
|
||||
Save
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"content-disposition": ["form-data; name=\"commit\""]}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
{"content-disposition": ["form-data; name=\"media\"; filename=\"\""],
|
||||
"content-type": ["application/octet-stream"]}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
------WebKitFormBoundaryWLHCs9qmcJJoyjKR
|
||||
Content-Disposition: form-data; name="_method"
|
||||
|
||||
put
|
||||
------WebKitFormBoundaryWLHCs9qmcJJoyjKR
|
||||
Content-Disposition: form-data; name="profile[blog]"
|
||||
|
||||
|
||||
------WebKitFormBoundaryWLHCs9qmcJJoyjKR
|
||||
Content-Disposition: form-data; name="profile[public_email]"
|
||||
|
||||
|
||||
------WebKitFormBoundaryWLHCs9qmcJJoyjKR
|
||||
Content-Disposition: form-data; name="profile[interests]"
|
||||
|
||||
|
||||
------WebKitFormBoundaryWLHCs9qmcJJoyjKR
|
||||
Content-Disposition: form-data; name="profile[bio]"
|
||||
|
||||
hello
|
||||
|
||||
"quote"
|
||||
------WebKitFormBoundaryWLHCs9qmcJJoyjKR
|
||||
Content-Disposition: form-data; name="media"; filename=""
|
||||
Content-Type: application/octet-stream
|
||||
|
||||
|
||||
------WebKitFormBoundaryWLHCs9qmcJJoyjKR
|
||||
Content-Disposition: form-data; name="commit"
|
||||
|
||||
Save
|
||||
------WebKitFormBoundaryWLHCs9qmcJJoyjKR--
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
|
||||
------WebKitFormBoundaryWLHCs9qmcJJoyjKR
|
||||
Content-Disposition: form-data; name="_method"
|
||||
|
||||
put
|
||||
------WebKitFormBoundaryWLHCs9qmcJJoyjKR
|
||||
Content-Disposition: form-data; name="profile[blog]"
|
||||
|
||||
|
||||
------WebKitFormBoundaryWLHCs9qmcJJoyjKR
|
||||
Content-Disposition: form-data; name="profile[public_email]"
|
||||
|
||||
|
||||
------WebKitFormBoundaryWLHCs9qmcJJoyjKR
|
||||
Content-Disposition: form-data; name="profile[interests]"
|
||||
|
||||
|
||||
------WebKitFormBoundaryWLHCs9qmcJJoyjKR
|
||||
Content-Disposition: form-data; name="profile[bio]"
|
||||
|
||||
hello
|
||||
|
||||
"quote"
|
||||
------WebKitFormBoundaryWLHCs9qmcJJoyjKR
|
||||
Content-Disposition: form-data; name="media"; filename=""
|
||||
Content-Type: application/octet-stream
|
||||
|
||||
|
||||
------WebKitFormBoundaryWLHCs9qmcJJoyjKR
|
||||
Content-Disposition: form-data; name="commit"
|
||||
|
||||
Save
|
||||
------WebKitFormBoundaryWLHCs9qmcJJoyjKR--
|
||||
+1
@@ -0,0 +1 @@
|
||||
Preamble terminated early due to unexpected end of multipart data
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
------WebKitFormBoundaryWLHCs9qmcJJoyjKR
|
||||
Content-Disposition: form-data; name="_method"
|
||||
|
||||
put
|
||||
------WebKitFormBoundaryWLHCs9qmcJJoyjKR
|
||||
Content-Disposition: form-data; name="profile[blog]"
|
||||
|
||||
|
||||
------WebKitFormBoundaryWLHCs9qmcJJoyjKR
|
||||
Content-Disposition: form-data; name="profile[public_email]"
|
||||
|
||||
|
||||
------WebKitFormBoundaryWLHCs9qmcJJoyjKR
|
||||
Content-Disposition: form-data; name="profile[interests]"
|
||||
|
||||
|
||||
------WebKitFormBoundaryWLHCs9qmcJJoyjKR
|
||||
Content-Disposition: form-data; name="profile[bio]"
|
||||
|
||||
hello
|
||||
|
||||
"quote"
|
||||
------WebKitFormBoundaryWLHCs9qmcJJoyjKR
|
||||
Content-Disposition: form-data; name="media"; filename=""
|
||||
Content-Type: application/octet-stream
|
||||
|
||||
|
||||
------WebKitFormBoundaryWLHCs9qmcJJoyjKR
|
||||
Content-Disposition: form-data; name="commit"
|
||||
|
||||
Save
|
||||
------WebKitFormBoundaryWLHCs9qmcJJoyjKR--
|
||||
+1
@@ -0,0 +1 @@
|
||||
put
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"content-disposition": ["form-data; name=\"_method\""]}
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"content-disposition": ["form-data; name=\"profile[blog]\""]}
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"content-disposition": ["form-data; name=\"profile[public_email]\""]}
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"content-disposition": ["form-data; name=\"profile[interests]\""]}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
hello
|
||||
|
||||
"quote"
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"content-disposition": ["form-data; name=\"profile[bio]\""]}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
{"content-disposition": ["form-data; name=\"media\"; filename=\"\""],
|
||||
"content-type": ["application/octet-stream"]}
|
||||
+1
@@ -0,0 +1 @@
|
||||
Save
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"content-disposition": ["form-data; name=\"commit\""]}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
User-Agent: foo bar baz
|
||||
Content-Type: multipart/form-data; boundary=AaB03x
|
||||
|
||||
--AaB03x
|
||||
Content-Disposition: form-data; name="foo"
|
||||
|
||||
bar
|
||||
--AaB03x
|
||||
Content-Disposition: form-data; name="files"
|
||||
Content-Type: multipart/mixed, boundary=BbC04y
|
||||
|
||||
--BbC04y
|
||||
Content-Disposition: attachment; filename="file.txt"
|
||||
Content-Type: text/plain
|
||||
|
||||
contents
|
||||
--BbC04y
|
||||
Content-Disposition: attachment; filename="flowers.jpg"
|
||||
Content-Type: image/jpeg
|
||||
Content-Transfer-Encoding: binary
|
||||
|
||||
contents
|
||||
--BbC04y--
|
||||
--AaB03x--
|
||||
+1
@@ -0,0 +1 @@
|
||||
bar
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"content-disposition": ["form-data; name=\"foo\""]}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
--BbC04y
|
||||
Content-Disposition: attachment; filename="file.txt"
|
||||
Content-Type: text/plain
|
||||
|
||||
contents
|
||||
--BbC04y
|
||||
Content-Disposition: attachment; filename="flowers.jpg"
|
||||
Content-Type: image/jpeg
|
||||
Content-Transfer-Encoding: binary
|
||||
|
||||
contents
|
||||
--BbC04y--
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
{"content-disposition": ["form-data; name=\"files\""],
|
||||
"content-type": ["multipart/mixed, boundary=BbC04y"]}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
{"user-agent": ["foo bar baz"],
|
||||
"content-type": ["multipart/form-data; boundary=AaB03x"]}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
--AaB03x
|
||||
Content-Disposition: form-data; name="foo"
|
||||
|
||||
bar
|
||||
--AaB03x
|
||||
Content-Disposition: form-data; name="files"
|
||||
Content-Type: multipart/mixed, boundary=BbC04y
|
||||
|
||||
--BbC04y
|
||||
Content-Disposition: attachment; filename="file.txt"
|
||||
Content-Type: text/plain
|
||||
|
||||
contents
|
||||
--BbC04y
|
||||
Content-Disposition: attachment; filename="flowers.jpg"
|
||||
Content-Type: image/jpeg
|
||||
Content-Transfer-Encoding: binary
|
||||
|
||||
contents
|
||||
--BbC04y--
|
||||
--AaB03x--
|
||||
+1
@@ -0,0 +1 @@
|
||||
bar
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"content-disposition": ["form-data; name=\"foo\""]}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
--BbC04y
|
||||
Content-Disposition: attachment; filename="file.txt"
|
||||
Content-Type: text/plain
|
||||
|
||||
contents
|
||||
--BbC04y
|
||||
Content-Disposition: attachment; filename="flowers.jpg"
|
||||
Content-Type: image/jpeg
|
||||
Content-Transfer-Encoding: binary
|
||||
|
||||
contents
|
||||
--BbC04y--
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
{"content-disposition": ["form-data; name=\"files\""],
|
||||
"content-type": ["multipart/mixed, boundary=BbC04y"]}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
|
||||
const Dicer = require('..');
|
||||
|
||||
const CRLF = '\r\n';
|
||||
const boundary = 'boundary';
|
||||
|
||||
const writeSep = `--${boundary}`;
|
||||
|
||||
const writePart = [
|
||||
writeSep,
|
||||
'Content-Type: text/plain',
|
||||
'Content-Length: 0'
|
||||
].join(CRLF) + `${CRLF}${CRLF}some data${CRLF}`;
|
||||
|
||||
const writeEnd = `--${CRLF}`;
|
||||
|
||||
let firedEnd = false;
|
||||
let firedFinish = false;
|
||||
|
||||
const dicer = new Dicer({ boundary });
|
||||
dicer.on('part', partListener);
|
||||
dicer.on('finish', finishListener);
|
||||
dicer.write(writePart + writeSep);
|
||||
|
||||
function partListener(partReadStream) {
|
||||
partReadStream.on('data', () => {});
|
||||
partReadStream.on('end', partEndListener);
|
||||
}
|
||||
|
||||
function partEndListener() {
|
||||
firedEnd = true;
|
||||
setImmediate(afterEnd);
|
||||
}
|
||||
|
||||
function afterEnd() {
|
||||
dicer.end(writeEnd);
|
||||
setImmediate(afterWrite);
|
||||
}
|
||||
|
||||
function finishListener() {
|
||||
assert(firedEnd, 'Failed to end before finishing');
|
||||
firedFinish = true;
|
||||
test2();
|
||||
}
|
||||
|
||||
function afterWrite() {
|
||||
assert(firedFinish, 'Failed to finish');
|
||||
}
|
||||
|
||||
let isPausePush = true;
|
||||
|
||||
let firedPauseCallback = false;
|
||||
let firedPauseFinish = false;
|
||||
|
||||
let dicer2 = null;
|
||||
|
||||
function test2() {
|
||||
dicer2 = new Dicer({ boundary });
|
||||
dicer2.on('part', pausePartListener);
|
||||
dicer2.on('finish', pauseFinish);
|
||||
dicer2.write(writePart + writeSep, 'utf8', pausePartCallback);
|
||||
setImmediate(pauseAfterWrite);
|
||||
}
|
||||
|
||||
function pausePartListener(partReadStream) {
|
||||
partReadStream.on('data', () => {});
|
||||
partReadStream.on('end', () => {});
|
||||
const realPush = partReadStream.push;
|
||||
partReadStream.push = (...args) => {
|
||||
realPush.apply(partReadStream, args);
|
||||
if (!isPausePush)
|
||||
return true;
|
||||
isPausePush = false;
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
function pauseAfterWrite() {
|
||||
dicer2.end(writeEnd);
|
||||
setImmediate(pauseAfterEnd);
|
||||
}
|
||||
|
||||
function pauseAfterEnd() {
|
||||
assert(firedPauseCallback, 'Failed to call callback after pause');
|
||||
assert(firedPauseFinish, 'Failed to finish after pause');
|
||||
}
|
||||
|
||||
function pauseFinish() {
|
||||
firedPauseFinish = true;
|
||||
}
|
||||
|
||||
function pausePartCallback() {
|
||||
firedPauseCallback = true;
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
const path = require('path');
|
||||
|
||||
const HeaderParser = require('../lib/HeaderParser');
|
||||
|
||||
const DCRLF = '\r\n\r\n';
|
||||
const MAXED_BUFFER = Buffer.allocUnsafe(128 * 1024);
|
||||
MAXED_BUFFER.fill(0x41); // 'A'
|
||||
|
||||
const group = path.basename(__filename, '.js') + '/';
|
||||
|
||||
function makeMsg(what, msg) {
|
||||
return `[${group}${what}]: ${msg}`;
|
||||
}
|
||||
|
||||
[
|
||||
{ source: DCRLF,
|
||||
expected: {},
|
||||
what: 'No header'
|
||||
},
|
||||
{ source: [
|
||||
'Content-Type:\t text/plain',
|
||||
'Content-Length:0',
|
||||
].join('\r\n') + DCRLF,
|
||||
expected: {
|
||||
'content-type': [' text/plain'],
|
||||
'content-length': ['0'],
|
||||
},
|
||||
what: 'Value spacing',
|
||||
},
|
||||
{ source: [
|
||||
'Content-Type:\r\n text/plain',
|
||||
'Foo:\r\n bar\r\n baz',
|
||||
].join('\r\n') + DCRLF,
|
||||
expected: {
|
||||
'content-type': [' text/plain'],
|
||||
'foo': [' bar baz'],
|
||||
},
|
||||
what: 'Folded values',
|
||||
},
|
||||
{ source: [
|
||||
'Content-Type:',
|
||||
'Foo: ',
|
||||
].join('\r\n') + DCRLF,
|
||||
expected: {
|
||||
'content-type': [''],
|
||||
'foo': [''],
|
||||
},
|
||||
what: 'Empty values',
|
||||
},
|
||||
{ source: MAXED_BUFFER.toString('ascii') + DCRLF,
|
||||
expected: {},
|
||||
what: 'Max header size (single chunk)',
|
||||
},
|
||||
{ source: [
|
||||
'ABCDEFGHIJ',
|
||||
MAXED_BUFFER.toString('ascii'),
|
||||
DCRLF,
|
||||
],
|
||||
expected: {},
|
||||
what: 'Max header size (multiple chunks #1)',
|
||||
},
|
||||
{ source: [
|
||||
MAXED_BUFFER.toString('ascii'),
|
||||
MAXED_BUFFER.toString('ascii'),
|
||||
DCRLF,
|
||||
],
|
||||
expected: {},
|
||||
what: 'Max header size (multiple chunk #2)',
|
||||
},
|
||||
].forEach((v) => {
|
||||
const parser = new HeaderParser();
|
||||
let fired = false;
|
||||
|
||||
parser.on('header', (header) => {
|
||||
assert(!fired, makeMsg(v.what, 'Header event fired more than once'));
|
||||
fired = true;
|
||||
assert.deepEqual(header,
|
||||
v.expected,
|
||||
makeMsg(v.what, 'Parsed result mismatch'));
|
||||
});
|
||||
if (!Array.isArray(v.source))
|
||||
v.source = [v.source];
|
||||
for (const chunk of v.source)
|
||||
parser.push(chunk);
|
||||
assert(fired, makeMsg(v.what, 'Did not receive header from parser'));
|
||||
});
|
||||
|
||||
{
|
||||
const source = [
|
||||
' Content-Disposition: form-data; name="bildbeschreibung"',
|
||||
DCRLF,
|
||||
DCRLF,
|
||||
DCRLF,
|
||||
];
|
||||
const parser = new HeaderParser();
|
||||
let hadError = false;
|
||||
|
||||
parser.on('header', (header) => {
|
||||
assert(false, 'Should not have seen header');
|
||||
});
|
||||
parser.on('error', (err) => {
|
||||
assert(!hadError, 'Unexpected multiple errors');
|
||||
hadError = true;
|
||||
assert(/unexpected folded/i.test(err.message),
|
||||
`Wrong error message: ${err.message}`);
|
||||
});
|
||||
for (const chunk of source)
|
||||
parser.push(chunk);
|
||||
assert(hadError, 'Expected error');
|
||||
}
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { inspect } = require('util');
|
||||
|
||||
const Dicer = require('..');
|
||||
|
||||
const FIXTURES_ROOT = `${__dirname}/fixtures/`;
|
||||
|
||||
let t = 0;
|
||||
const group = path.basename(__filename, '.js') + '/';
|
||||
|
||||
function makeMsg(what, msg) {
|
||||
return '[' + group + what + ']: ' + msg;
|
||||
}
|
||||
|
||||
process.on('exit', () => {
|
||||
assert(t === tests.length,
|
||||
makeMsg('_exit', 'Only ran ' + t + '/' + tests.length + ' tests'));
|
||||
});
|
||||
|
||||
const tests = [
|
||||
{ source: 'many',
|
||||
opts: { boundary: '----WebKitFormBoundaryWLHCs9qmcJJoyjKR' },
|
||||
chsize: 16,
|
||||
nparts: 7,
|
||||
what: 'Extra trailer data pushed after finished'
|
||||
},
|
||||
];
|
||||
|
||||
function next() {
|
||||
if (t === tests.length)
|
||||
return;
|
||||
const v = tests[t];
|
||||
const fixtureBase = FIXTURES_ROOT + v.source;
|
||||
let n = 0;
|
||||
const buffer = Buffer.allocUnsafe(v.chsize);
|
||||
const state = { parts: [] };
|
||||
const fd = fs.openSync(fixtureBase + '/original', 'r');
|
||||
|
||||
const dicer = new Dicer(v.opts);
|
||||
let error;
|
||||
let partErrors = 0;
|
||||
let finishes = 0;
|
||||
|
||||
dicer.on('part', (p) => {
|
||||
const part = {
|
||||
body: undefined,
|
||||
bodylen: 0,
|
||||
error: undefined,
|
||||
header: undefined
|
||||
};
|
||||
|
||||
p.on('header', (h) => {
|
||||
part.header = h;
|
||||
}).on('data', (data) => {
|
||||
// Make a copy because we are using readSync which re-uses a buffer ...
|
||||
const copy = Buffer.allocUnsafe(data.length);
|
||||
data.copy(copy);
|
||||
data = copy;
|
||||
if (!part.body)
|
||||
part.body = [ data ];
|
||||
else
|
||||
part.body.push(data);
|
||||
part.bodylen += data.length;
|
||||
}).on('error', (err) => {
|
||||
part.error = err;
|
||||
++partErrors;
|
||||
}).on('end', () => {
|
||||
if (part.body)
|
||||
part.body = Buffer.concat(part.body, part.bodylen);
|
||||
state.parts.push(part);
|
||||
});
|
||||
}).on('error', (err) => {
|
||||
error = err;
|
||||
}).on('finish', () => {
|
||||
assert(finishes++ === 0, makeMsg(v.what, 'finish emitted multiple times'));
|
||||
|
||||
if (v.dicerError)
|
||||
assert(error !== undefined, makeMsg(v.what, 'Expected error'));
|
||||
else
|
||||
assert(error === undefined, makeMsg(v.what, 'Unexpected error'));
|
||||
|
||||
if (v.events && v.events.indexOf('part') > -1) {
|
||||
assert.equal(state.parts.length,
|
||||
v.nparts,
|
||||
makeMsg(v.what,
|
||||
'Part count mismatch:\nActual: '
|
||||
+ state.parts.length
|
||||
+ '\nExpected: '
|
||||
+ v.nparts));
|
||||
|
||||
if (!v.npartErrors)
|
||||
v.npartErrors = 0;
|
||||
assert.equal(partErrors,
|
||||
v.npartErrors,
|
||||
makeMsg(v.what,
|
||||
'Part errors mismatch:\nActual: '
|
||||
+ partErrors
|
||||
+ '\nExpected: '
|
||||
+ v.npartErrors));
|
||||
|
||||
for (let i = 0, header, body; i < v.nparts; ++i) {
|
||||
if (fs.existsSync(fixtureBase + '/part' + (i + 1))) {
|
||||
body = fs.readFileSync(fixtureBase + '/part' + (i + 1));
|
||||
if (body.length === 0)
|
||||
body = undefined;
|
||||
} else {
|
||||
body = undefined;
|
||||
}
|
||||
assert.deepEqual(state.parts[i].body,
|
||||
body,
|
||||
makeMsg(v.what,
|
||||
'Part #' + (i + 1) + ' body mismatch'));
|
||||
if (fs.existsSync(fixtureBase + '/part' + (i + 1) + '.header')) {
|
||||
header = fs.readFileSync(fixtureBase
|
||||
+ '/part' + (i + 1) + '.header', 'latin1');
|
||||
header = JSON.parse(header);
|
||||
} else {
|
||||
header = undefined;
|
||||
}
|
||||
assert.deepEqual(state.parts[i].header,
|
||||
header,
|
||||
makeMsg(v.what,
|
||||
'Part #' + (i + 1)
|
||||
+ ' parsed header mismatch:\nActual: '
|
||||
+ inspect(state.parts[i].header)
|
||||
+ '\nExpected: '
|
||||
+ inspect(header)));
|
||||
}
|
||||
}
|
||||
++t;
|
||||
next();
|
||||
});
|
||||
|
||||
while (true) {
|
||||
n = fs.readSync(fd, buffer, 0, buffer.length, null);
|
||||
if (n === 0) {
|
||||
setTimeout(() => {
|
||||
dicer.write('\r\n\r\n\r\n');
|
||||
dicer.end();
|
||||
}, 50);
|
||||
break;
|
||||
}
|
||||
dicer.write(n === buffer.length ? buffer : buffer.slice(0, n));
|
||||
}
|
||||
fs.closeSync(fd);
|
||||
}
|
||||
next();
|
||||
+235
@@ -0,0 +1,235 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { inspect } = require('util');
|
||||
|
||||
const Dicer = require('..');
|
||||
|
||||
const FIXTURES_ROOT = `${__dirname}/fixtures/`;
|
||||
|
||||
let t = 0;
|
||||
const group = path.basename(__filename, '.js') + '/';
|
||||
|
||||
function makeMsg(what, msg) {
|
||||
return '[' + group + what + ']: ' + msg;
|
||||
}
|
||||
|
||||
process.on('exit', () => {
|
||||
assert(t === tests.length,
|
||||
makeMsg('_exit', 'Only ran ' + t + '/' + tests.length + ' tests'));
|
||||
});
|
||||
|
||||
const tests = [
|
||||
{ source: 'many',
|
||||
opts: { boundary: '----WebKitFormBoundaryWLHCs9qmcJJoyjKR' },
|
||||
chsize: 16,
|
||||
nparts: 0,
|
||||
what: 'No preamble or part listeners'
|
||||
},
|
||||
];
|
||||
|
||||
function next() {
|
||||
if (t === tests.length)
|
||||
return;
|
||||
const v = tests[t];
|
||||
const fixtureBase = FIXTURES_ROOT + v.source;
|
||||
let n = 0;
|
||||
const buffer = Buffer.allocUnsafe(v.chsize);
|
||||
const state = { done: false, parts: [], preamble: undefined };
|
||||
const fd = fs.openSync(fixtureBase + '/original', 'r');
|
||||
|
||||
const dicer = new Dicer(v.opts);
|
||||
let error;
|
||||
let partErrors = 0;
|
||||
let finishes = 0;
|
||||
|
||||
if (v.events && v.events.indexOf('preamble') > -1) {
|
||||
dicer.on('preamble', (p) => {
|
||||
const preamble = {
|
||||
body: undefined,
|
||||
bodylen: 0,
|
||||
error: undefined,
|
||||
header: undefined
|
||||
};
|
||||
|
||||
p.on('header', (h) => {
|
||||
preamble.header = h;
|
||||
}).on('data', (data) => {
|
||||
// Make a copy because we are using readSync which re-uses a buffer ...
|
||||
const copy = Buffer.allocUnsafe(data.length);
|
||||
data.copy(copy);
|
||||
data = copy;
|
||||
if (!preamble.body)
|
||||
preamble.body = [ data ];
|
||||
else
|
||||
preamble.body.push(data);
|
||||
preamble.bodylen += data.length;
|
||||
}).on('error', (err) => {
|
||||
preamble.error = err;
|
||||
}).on('end', () => {
|
||||
if (preamble.body)
|
||||
preamble.body = Buffer.concat(preamble.body, preamble.bodylen);
|
||||
if (preamble.body || preamble.header)
|
||||
state.preamble = preamble;
|
||||
});
|
||||
});
|
||||
}
|
||||
if (v.events && v.events.indexOf('part') > -1) {
|
||||
dicer.on('part', (p) => {
|
||||
const part = {
|
||||
body: undefined,
|
||||
bodylen: 0,
|
||||
error: undefined,
|
||||
header: undefined
|
||||
};
|
||||
|
||||
p.on('header', (h) => {
|
||||
part.header = h;
|
||||
}).on('data', (data) => {
|
||||
// Make a copy because we are using readSync which re-uses a buffer ...
|
||||
const copy = Buffer.allocUnsafe(data.length);
|
||||
data.copy(copy);
|
||||
data = copy;
|
||||
if (!part.body)
|
||||
part.body = [ data ];
|
||||
else
|
||||
part.body.push(data);
|
||||
part.bodylen += data.length;
|
||||
}).on('error', (err) => {
|
||||
part.error = err;
|
||||
++partErrors;
|
||||
}).on('end', () => {
|
||||
if (part.body)
|
||||
part.body = Buffer.concat(part.body, part.bodylen);
|
||||
state.parts.push(part);
|
||||
});
|
||||
});
|
||||
}
|
||||
dicer.on('error', (err) => {
|
||||
error = err;
|
||||
}).on('finish', () => {
|
||||
assert(finishes++ === 0, makeMsg(v.what, 'finish emitted multiple times'));
|
||||
|
||||
if (v.dicerError)
|
||||
assert(error !== undefined, makeMsg(v.what, 'Expected error'));
|
||||
else
|
||||
assert(error === undefined, makeMsg(v.what, 'Unexpected error'));
|
||||
|
||||
if (v.events && v.events.indexOf('preamble') > -1) {
|
||||
let preamble;
|
||||
if (fs.existsSync(fixtureBase + '/preamble')) {
|
||||
const prebody = fs.readFileSync(fixtureBase + '/preamble');
|
||||
if (prebody.length) {
|
||||
preamble = {
|
||||
body: prebody,
|
||||
bodylen: prebody.length,
|
||||
error: undefined,
|
||||
header: undefined
|
||||
};
|
||||
}
|
||||
}
|
||||
if (fs.existsSync(fixtureBase + '/preamble.header')) {
|
||||
const prehead = JSON.parse(fs.readFileSync(
|
||||
fixtureBase + '/preamble.header', 'latin1'
|
||||
));
|
||||
if (!preamble) {
|
||||
preamble = {
|
||||
body: undefined,
|
||||
bodylen: 0,
|
||||
error: undefined,
|
||||
header: prehead
|
||||
};
|
||||
} else {
|
||||
preamble.header = prehead;
|
||||
}
|
||||
}
|
||||
if (fs.existsSync(fixtureBase + '/preamble.error')) {
|
||||
const err = new Error(fs.readFileSync(
|
||||
fixtureBase + '/preamble.error', 'latin1'
|
||||
));
|
||||
if (!preamble) {
|
||||
preamble = {
|
||||
body: undefined,
|
||||
bodylen: 0,
|
||||
error: err,
|
||||
header: undefined
|
||||
};
|
||||
} else {
|
||||
preamble.error = err;
|
||||
}
|
||||
}
|
||||
|
||||
assert.deepEqual(state.preamble,
|
||||
preamble,
|
||||
makeMsg(v.what,
|
||||
'Preamble mismatch:\nActual:'
|
||||
+ inspect(state.preamble)
|
||||
+ '\nExpected: '
|
||||
+ inspect(preamble)));
|
||||
}
|
||||
|
||||
if (v.events && v.events.indexOf('part') > -1) {
|
||||
assert.equal(state.parts.length,
|
||||
v.nparts,
|
||||
makeMsg(v.what,
|
||||
'Part count mismatch:\nActual: '
|
||||
+ state.parts.length
|
||||
+ '\nExpected: '
|
||||
+ v.nparts));
|
||||
|
||||
if (!v.npartErrors)
|
||||
v.npartErrors = 0;
|
||||
assert.equal(partErrors,
|
||||
v.npartErrors,
|
||||
makeMsg(v.what,
|
||||
'Part errors mismatch:\nActual: '
|
||||
+ partErrors
|
||||
+ '\nExpected: '
|
||||
+ v.npartErrors));
|
||||
|
||||
for (let i = 0, header, body; i < v.nparts; ++i) {
|
||||
if (fs.existsSync(fixtureBase + '/part' + (i + 1))) {
|
||||
body = fs.readFileSync(fixtureBase + '/part' + (i + 1));
|
||||
if (body.length === 0)
|
||||
body = undefined;
|
||||
} else {
|
||||
body = undefined;
|
||||
}
|
||||
assert.deepEqual(state.parts[i].body,
|
||||
body,
|
||||
makeMsg(v.what,
|
||||
'Part #' + (i + 1) + ' body mismatch'));
|
||||
if (fs.existsSync(fixtureBase + '/part' + (i + 1) + '.header')) {
|
||||
header = fs.readFileSync(fixtureBase
|
||||
+ '/part' + (i + 1) + '.header', 'latin1');
|
||||
header = JSON.parse(header);
|
||||
} else {
|
||||
header = undefined;
|
||||
}
|
||||
assert.deepEqual(state.parts[i].header,
|
||||
header,
|
||||
makeMsg(v.what,
|
||||
'Part #' + (i + 1)
|
||||
+ ' parsed header mismatch:\nActual: '
|
||||
+ inspect(state.parts[i].header)
|
||||
+ '\nExpected: '
|
||||
+ inspect(header)));
|
||||
}
|
||||
}
|
||||
++t;
|
||||
next();
|
||||
});
|
||||
|
||||
while (true) {
|
||||
n = fs.readSync(fd, buffer, 0, buffer.length, null);
|
||||
if (n === 0) {
|
||||
dicer.end();
|
||||
break;
|
||||
}
|
||||
dicer.write(n === buffer.length ? buffer : buffer.slice(0, n));
|
||||
}
|
||||
fs.closeSync(fd);
|
||||
}
|
||||
next();
|
||||
+249
@@ -0,0 +1,249 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { inspect } = require('util');
|
||||
|
||||
const Dicer = require('..');
|
||||
|
||||
const FIXTURES_ROOT = `${__dirname}/fixtures/`;
|
||||
|
||||
let t = 0;
|
||||
const group = path.basename(__filename, '.js') + '/';
|
||||
|
||||
function makeMsg(what, msg) {
|
||||
return '[' + group + what + ']: ' + msg;
|
||||
}
|
||||
|
||||
process.on('exit', function() {
|
||||
assert(t === tests.length,
|
||||
makeMsg('_exit', 'Only ran ' + t + '/' + tests.length + ' tests'));
|
||||
});
|
||||
|
||||
const tests = [
|
||||
{ source: 'nested',
|
||||
opts: { boundary: 'AaB03x' },
|
||||
chsize: 32,
|
||||
nparts: 2,
|
||||
what: 'One nested multipart'
|
||||
},
|
||||
{ source: 'many',
|
||||
opts: { boundary: '----WebKitFormBoundaryWLHCs9qmcJJoyjKR' },
|
||||
chsize: 16,
|
||||
nparts: 7,
|
||||
what: 'Many parts'
|
||||
},
|
||||
{ source: 'many-wrongboundary',
|
||||
opts: { boundary: 'LOLOLOL' },
|
||||
chsize: 8,
|
||||
nparts: 0,
|
||||
dicerError: true,
|
||||
what: 'Many parts, wrong boundary'
|
||||
},
|
||||
{ source: 'many-noend',
|
||||
opts: { boundary: '----WebKitFormBoundaryWLHCs9qmcJJoyjKR' },
|
||||
chsize: 16,
|
||||
nparts: 7,
|
||||
npartErrors: 1,
|
||||
dicerError: true,
|
||||
what: 'Many parts, end boundary missing, 1 file open'
|
||||
},
|
||||
{ source: 'nested-full',
|
||||
opts: { boundary: 'AaB03x', headerFirst: true },
|
||||
chsize: 32,
|
||||
nparts: 2,
|
||||
what: 'One nested multipart with preceding header'
|
||||
},
|
||||
{ source: 'nested-full',
|
||||
opts: { headerFirst: true },
|
||||
chsize: 32,
|
||||
nparts: 2,
|
||||
setBoundary: 'AaB03x',
|
||||
what: 'One nested multipart with preceding header, using setBoundary'
|
||||
},
|
||||
];
|
||||
|
||||
function next() {
|
||||
if (t === tests.length)
|
||||
return;
|
||||
const v = tests[t];
|
||||
const fixtureBase = FIXTURES_ROOT + v.source;
|
||||
const state = { parts: [], preamble: undefined };
|
||||
|
||||
const dicer = new Dicer(v.opts);
|
||||
let error;
|
||||
let partErrors = 0;
|
||||
let finishes = 0;
|
||||
|
||||
dicer.on('preamble', (p) => {
|
||||
const preamble = {
|
||||
body: undefined,
|
||||
bodylen: 0,
|
||||
error: undefined,
|
||||
header: undefined
|
||||
};
|
||||
|
||||
p.on('header', (h) => {
|
||||
preamble.header = h;
|
||||
if (v.setBoundary)
|
||||
dicer.setBoundary(v.setBoundary);
|
||||
}).on('data', (data) => {
|
||||
// Make a copy because we are using readSync which re-uses a buffer ...
|
||||
const copy = Buffer.allocUnsafe(data.length);
|
||||
data.copy(copy);
|
||||
data = copy;
|
||||
if (!preamble.body)
|
||||
preamble.body = [ data ];
|
||||
else
|
||||
preamble.body.push(data);
|
||||
preamble.bodylen += data.length;
|
||||
}).on('error', (err) => {
|
||||
preamble.error = err;
|
||||
}).on('end', () => {
|
||||
if (preamble.body)
|
||||
preamble.body = Buffer.concat(preamble.body, preamble.bodylen);
|
||||
if (preamble.body || preamble.header)
|
||||
state.preamble = preamble;
|
||||
});
|
||||
});
|
||||
dicer.on('part', (p) => {
|
||||
const part = {
|
||||
body: undefined,
|
||||
bodylen: 0,
|
||||
error: undefined,
|
||||
header: undefined
|
||||
};
|
||||
|
||||
p.on('header', (h) => {
|
||||
part.header = h;
|
||||
}).on('data', (data) => {
|
||||
if (!part.body)
|
||||
part.body = [ data ];
|
||||
else
|
||||
part.body.push(data);
|
||||
part.bodylen += data.length;
|
||||
}).on('error', (err) => {
|
||||
part.error = err;
|
||||
++partErrors;
|
||||
}).on('end', () => {
|
||||
if (part.body)
|
||||
part.body = Buffer.concat(part.body, part.bodylen);
|
||||
state.parts.push(part);
|
||||
});
|
||||
}).on('error', (err) => {
|
||||
error = err;
|
||||
}).on('finish', () => {
|
||||
assert(finishes++ === 0, makeMsg(v.what, 'finish emitted multiple times'));
|
||||
|
||||
if (v.dicerError) {
|
||||
assert(error !== undefined, makeMsg(v.what, 'Expected error'));
|
||||
} else {
|
||||
assert(error === undefined,
|
||||
makeMsg(v.what, 'Unexpected error: ' + error));
|
||||
}
|
||||
|
||||
let preamble;
|
||||
if (fs.existsSync(fixtureBase + '/preamble')) {
|
||||
const prebody = fs.readFileSync(fixtureBase + '/preamble');
|
||||
if (prebody.length) {
|
||||
preamble = {
|
||||
body: prebody,
|
||||
bodylen: prebody.length,
|
||||
error: undefined,
|
||||
header: undefined
|
||||
};
|
||||
}
|
||||
}
|
||||
if (fs.existsSync(fixtureBase + '/preamble.header')) {
|
||||
const prehead = JSON.parse(fs.readFileSync(
|
||||
fixtureBase + '/preamble.header', 'latin1'
|
||||
));
|
||||
if (!preamble) {
|
||||
preamble = {
|
||||
body: undefined,
|
||||
bodylen: 0,
|
||||
error: undefined,
|
||||
header: prehead
|
||||
};
|
||||
} else {
|
||||
preamble.header = prehead;
|
||||
}
|
||||
}
|
||||
if (fs.existsSync(fixtureBase + '/preamble.error')) {
|
||||
const err = new Error(fs.readFileSync(
|
||||
fixtureBase + '/preamble.error', 'latin1'
|
||||
));
|
||||
if (!preamble) {
|
||||
preamble = {
|
||||
body: undefined,
|
||||
bodylen: 0,
|
||||
error: err,
|
||||
header: undefined
|
||||
};
|
||||
} else {
|
||||
preamble.error = err;
|
||||
}
|
||||
}
|
||||
|
||||
assert.deepEqual(state.preamble,
|
||||
preamble,
|
||||
makeMsg(v.what,
|
||||
'Preamble mismatch:\nActual:'
|
||||
+ inspect(state.preamble)
|
||||
+ '\nExpected: '
|
||||
+ inspect(preamble)));
|
||||
|
||||
assert.equal(state.parts.length,
|
||||
v.nparts,
|
||||
makeMsg(v.what,
|
||||
'Part count mismatch:\nActual: '
|
||||
+ state.parts.length
|
||||
+ '\nExpected: '
|
||||
+ v.nparts));
|
||||
|
||||
if (!v.npartErrors)
|
||||
v.npartErrors = 0;
|
||||
assert.equal(partErrors,
|
||||
v.npartErrors,
|
||||
makeMsg(v.what,
|
||||
'Part errors mismatch:\nActual: '
|
||||
+ partErrors
|
||||
+ '\nExpected: '
|
||||
+ v.npartErrors));
|
||||
|
||||
for (let i = 0, header, body; i < v.nparts; ++i) {
|
||||
if (fs.existsSync(fixtureBase + '/part' + (i + 1))) {
|
||||
body = fs.readFileSync(fixtureBase + '/part' + (i + 1));
|
||||
if (body.length === 0)
|
||||
body = undefined;
|
||||
} else {
|
||||
body = undefined;
|
||||
}
|
||||
assert.deepEqual(state.parts[i].body,
|
||||
body,
|
||||
makeMsg(v.what,
|
||||
'Part #' + (i + 1) + ' body mismatch'));
|
||||
if (fs.existsSync(fixtureBase + '/part' + (i + 1) + '.header')) {
|
||||
header = fs.readFileSync(fixtureBase
|
||||
+ '/part' + (i + 1) + '.header', 'latin1');
|
||||
header = JSON.parse(header);
|
||||
} else {
|
||||
header = undefined;
|
||||
}
|
||||
assert.deepEqual(state.parts[i].header,
|
||||
header,
|
||||
makeMsg(v.what,
|
||||
'Part #' + (i + 1)
|
||||
+ ' parsed header mismatch:\nActual: '
|
||||
+ inspect(state.parts[i].header)
|
||||
+ '\nExpected: '
|
||||
+ inspect(header)));
|
||||
}
|
||||
++t;
|
||||
next();
|
||||
});
|
||||
|
||||
fs.createReadStream(fixtureBase + '/original').pipe(dicer);
|
||||
}
|
||||
next();
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
require('fs').readdirSync(__dirname).forEach((f) => {
|
||||
if (f.substr(0, 5) === 'test-')
|
||||
require(`./${f}`);
|
||||
});
|
||||
Reference in New Issue
Block a user