diff options
author | sowgro <tpoke.ferrari@gmail.com> | 2023-09-02 19:12:47 -0400 |
---|---|---|
committer | sowgro <tpoke.ferrari@gmail.com> | 2023-09-02 19:12:47 -0400 |
commit | e4450c8417624b71d779cb4f41692538f9165e10 (patch) | |
tree | b70826542223ecdf8a7a259f61b0a1abb8a217d8 /node_modules/busboy | |
download | sowbot3-e4450c8417624b71d779cb4f41692538f9165e10.tar.gz sowbot3-e4450c8417624b71d779cb4f41692538f9165e10.tar.bz2 sowbot3-e4450c8417624b71d779cb4f41692538f9165e10.zip |
first commit
Diffstat (limited to 'node_modules/busboy')
22 files changed, 4585 insertions, 0 deletions
diff --git a/node_modules/busboy/.eslintrc.js b/node_modules/busboy/.eslintrc.js new file mode 100644 index 0000000..be9311d --- /dev/null +++ b/node_modules/busboy/.eslintrc.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = { + extends: '@mscdex/eslint-config', +}; diff --git a/node_modules/busboy/.github/workflows/ci.yml b/node_modules/busboy/.github/workflows/ci.yml new file mode 100644 index 0000000..799bae0 --- /dev/null +++ b/node_modules/busboy/.github/workflows/ci.yml @@ -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.16.0, 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 diff --git a/node_modules/busboy/.github/workflows/lint.yml b/node_modules/busboy/.github/workflows/lint.yml new file mode 100644 index 0000000..9f9e1f5 --- /dev/null +++ b/node_modules/busboy/.github/workflows/lint.yml @@ -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 diff --git a/node_modules/busboy/LICENSE b/node_modules/busboy/LICENSE new file mode 100644 index 0000000..290762e --- /dev/null +++ b/node_modules/busboy/LICENSE @@ -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.
\ No newline at end of file diff --git a/node_modules/busboy/README.md b/node_modules/busboy/README.md new file mode 100644 index 0000000..654af30 --- /dev/null +++ b/node_modules/busboy/README.md @@ -0,0 +1,191 @@ +# Description + +A node.js module for parsing incoming HTML form data. + +Changes (breaking or otherwise) in v1.0.0 can be found [here](https://github.com/mscdex/busboy/issues/266). + +# Requirements + +* [node.js](http://nodejs.org/) -- v10.16.0 or newer + + +# Install + + npm install busboy + + +# Examples + +* Parsing (multipart) with default options: + +```js +const http = require('http'); + +const busboy = require('busboy'); + +http.createServer((req, res) => { + if (req.method === 'POST') { + console.log('POST request'); + const bb = busboy({ headers: req.headers }); + bb.on('file', (name, file, info) => { + const { filename, encoding, mimeType } = info; + console.log( + `File [${name}]: filename: %j, encoding: %j, mimeType: %j`, + filename, + encoding, + mimeType + ); + file.on('data', (data) => { + console.log(`File [${name}] got ${data.length} bytes`); + }).on('close', () => { + console.log(`File [${name}] done`); + }); + }); + bb.on('field', (name, val, info) => { + console.log(`Field [${name}]: value: %j`, val); + }); + bb.on('close', () => { + console.log('Done parsing form!'); + res.writeHead(303, { Connection: 'close', Location: '/' }); + res.end(); + }); + req.pipe(bb); + } else if (req.method === 'GET') { + res.writeHead(200, { Connection: 'close' }); + res.end(` + <html> + <head></head> + <body> + <form method="POST" enctype="multipart/form-data"> + <input type="file" name="filefield"><br /> + <input type="text" name="textfield"><br /> + <input type="submit"> + </form> + </body> + </html> + `); + } +}).listen(8000, () => { + console.log('Listening for requests'); +}); + +// Example output: +// +// Listening for requests +// < ... form submitted ... > +// POST request +// File [filefield]: filename: "logo.jpg", encoding: "binary", mime: "image/jpeg" +// File [filefield] got 11912 bytes +// Field [textfield]: value: "testing! :-)" +// File [filefield] done +// Done parsing form! +``` + +* Save all incoming files to disk: + +```js +const { randomFillSync } = require('crypto'); +const fs = require('fs'); +const http = require('http'); +const os = require('os'); +const path = require('path'); + +const busboy = require('busboy'); + +const random = (() => { + const buf = Buffer.alloc(16); + return () => randomFillSync(buf).toString('hex'); +})(); + +http.createServer((req, res) => { + if (req.method === 'POST') { + const bb = busboy({ headers: req.headers }); + bb.on('file', (name, file, info) => { + const saveTo = path.join(os.tmpdir(), `busboy-upload-${random()}`); + file.pipe(fs.createWriteStream(saveTo)); + }); + bb.on('close', () => { + res.writeHead(200, { 'Connection': 'close' }); + res.end(`That's all folks!`); + }); + req.pipe(bb); + return; + } + res.writeHead(404); + res.end(); +}).listen(8000, () => { + console.log('Listening for requests'); +}); +``` + + +# API + +## Exports + +`busboy` exports a single function: + +**( _function_ )**(< _object_ >config) - Creates and returns a new _Writable_ form parser stream. + +* Valid `config` properties: + + * **headers** - _object_ - These are the HTTP headers of the incoming request, which are used by individual parsers. + + * **highWaterMark** - _integer_ - highWaterMark to use for the parser stream. **Default:** node's _stream.Writable_ default. + + * **fileHwm** - _integer_ - highWaterMark to use for individual file streams. **Default:** node's _stream.Readable_ default. + + * **defCharset** - _string_ - Default character set to use when one isn't defined. **Default:** `'utf8'`. + + * **defParamCharset** - _string_ - For multipart forms, the default character set to use for values of part header parameters (e.g. filename) that are not extended parameters (that contain an explicit charset). **Default:** `'latin1'`. + + * **preservePath** - _boolean_ - If paths in filenames from file parts in a `'multipart/form-data'` request shall be preserved. **Default:** `false`. + + * **limits** - _object_ - Various limits on incoming data. Valid properties are: + + * **fieldNameSize** - _integer_ - Max field name size (in bytes). **Default:** `100`. + + * **fieldSize** - _integer_ - Max field value size (in bytes). **Default:** `1048576` (1MB). + + * **fields** - _integer_ - Max number of non-file fields. **Default:** `Infinity`. + + * **fileSize** - _integer_ - For multipart forms, the max file size (in bytes). **Default:** `Infinity`. + + * **files** - _integer_ - For multipart forms, the max number of file fields. **Default:** `Infinity`. + + * **parts** - _integer_ - For multipart forms, the max number of parts (fields + files). **Default:** `Infinity`. + + * **headerPairs** - _integer_ - For multipart forms, the max number of header key-value pairs to parse. **Default:** `2000` (same as node's http module). + +This function can throw exceptions if there is something wrong with the values in `config`. For example, if the Content-Type in `headers` is missing entirely, is not a supported type, or is missing the boundary for `'multipart/form-data'` requests. + +## (Special) Parser stream events + +* **file**(< _string_ >name, < _Readable_ >stream, < _object_ >info) - Emitted for each new file found. `name` contains the form field name. `stream` is a _Readable_ stream containing the file's data. No transformations/conversions (e.g. base64 to raw binary) are done on the file's data. `info` contains the following properties: + + * `filename` - _string_ - If supplied, this contains the file's filename. **WARNING:** You should almost _never_ use this value as-is (especially if you are using `preservePath: true` in your `config`) as it could contain malicious input. You are better off generating your own (safe) filenames, or at the very least using a hash of the filename. + + * `encoding` - _string_ - The file's `'Content-Transfer-Encoding'` value. + + * `mimeType` - _string_ - The file's `'Content-Type'` value. + + **Note:** If you listen for this event, you should always consume the `stream` whether you care about its contents or not (you can simply do `stream.resume();` if you want to discard/skip the contents), otherwise the `'finish'`/`'close'` event will never fire on the busboy parser stream. + However, if you aren't accepting files, you can either simply not listen for the `'file'` event at all or set `limits.files` to `0`, and any/all files will be automatically skipped (these skipped files will still count towards any configured `limits.files` and `limits.parts` limits though). + + **Note:** If a configured `limits.fileSize` limit was reached for a file, `stream` will both have a boolean property `truncated` set to `true` (best checked at the end of the stream) and emit a `'limit'` event to notify you when this happens. + +* **field**(< _string_ >name, < _string_ >value, < _object_ >info) - Emitted for each new non-file field found. `name` contains the form field name. `value` contains the string value of the field. `info` contains the following properties: + + * `nameTruncated` - _boolean_ - Whether `name` was truncated or not (due to a configured `limits.fieldNameSize` limit) + + * `valueTruncated` - _boolean_ - Whether `value` was truncated or not (due to a configured `limits.fieldSize` limit) + + * `encoding` - _string_ - The field's `'Content-Transfer-Encoding'` value. + + * `mimeType` - _string_ - The field's `'Content-Type'` value. + +* **partsLimit**() - Emitted when the configured `limits.parts` limit has been reached. No more `'file'` or `'field'` events will be emitted. + +* **filesLimit**() - Emitted when the configured `limits.files` limit has been reached. No more `'file'` events will be emitted. + +* **fieldsLimit**() - Emitted when the configured `limits.fields` limit has been reached. No more `'field'` events will be emitted. diff --git a/node_modules/busboy/bench/bench-multipart-fields-100mb-big.js b/node_modules/busboy/bench/bench-multipart-fields-100mb-big.js new file mode 100644 index 0000000..ef15729 --- /dev/null +++ b/node_modules/busboy/bench/bench-multipart-fields-100mb-big.js @@ -0,0 +1,149 @@ +'use strict'; + +function createMultipartBuffers(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), + '', + ].join('\r\n'))); + } + bufs.push(Buffer.from([ + `--${boundary}--`, + '', + ].join('\r\n'))); + return bufs; +} + +const boundary = '-----------------------------168072824752491622650073'; +const buffers = createMultipartBuffers(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 'busboy': { + const busboy = require('busboy'); + + const parser = busboy({ + limits: { + fieldSizeLimit: Infinity, + }, + headers: { + 'content-type': `multipart/form-data; boundary=${boundary}`, + }, + }); + parser.on('field', (name, val, info) => { + ++calls.partBegin; + ++calls.partData; + ++calls.partEnd; + }).on('close', () => { + ++calls.end; + console.timeEnd(moduleName); + }); + + console.time(moduleName); + for (const buf of buffers) + parser.write(buf); + break; + } + + case 'formidable': { + const { MultipartParser } = require('formidable'); + + const parser = new MultipartParser(); + parser.initWithBoundary(boundary); + parser.on('data', ({ name }) => { + ++calls[name]; + if (name === 'end') + console.timeEnd(moduleName); + }); + + console.time(moduleName); + for (const buf of buffers) + parser.write(buf); + + 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}`, + }; + + 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); + for (const buf of buffers) + req.push(buf); + req.push(null); + + break; + } + + default: + if (moduleName === undefined) + console.error('Missing parser module name'); + else + console.error(`Invalid parser module name: ${moduleName}`); + process.exit(1); +} diff --git a/node_modules/busboy/bench/bench-multipart-fields-100mb-small.js b/node_modules/busboy/bench/bench-multipart-fields-100mb-small.js new file mode 100644 index 0000000..f32d421 --- /dev/null +++ b/node_modules/busboy/bench/bench-multipart-fields-100mb-small.js @@ -0,0 +1,143 @@ +'use strict'; + +function createMultipartBuffers(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), + '', + ].join('\r\n'))); + } + bufs.push(Buffer.from([ + `--${boundary}--`, + '', + ].join('\r\n'))); + return bufs; +} + +const boundary = '-----------------------------168072824752491622650073'; +const buffers = createMultipartBuffers(boundary, (new Array(100)).fill(1)); +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 'busboy': { + const busboy = require('busboy'); + + const parser = busboy({ + limits: { + fieldSizeLimit: Infinity, + }, + headers: { + 'content-type': `multipart/form-data; boundary=${boundary}`, + }, + }); + parser.on('field', (name, val, info) => { + ++calls.partBegin; + ++calls.partData; + ++calls.partEnd; + }).on('close', () => { + ++calls.end; + console.timeEnd(moduleName); + }); + + console.time(moduleName); + for (const buf of buffers) + parser.write(buf); + break; + } + + case 'formidable': { + const { MultipartParser } = require('formidable'); + + const parser = new MultipartParser(); + parser.initWithBoundary(boundary); + parser.on('data', ({ name }) => { + ++calls[name]; + if (name === 'end') + console.timeEnd(moduleName); + }); + + console.time(moduleName); + for (const buf of buffers) + parser.write(buf); + + 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}`, + }; + + 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); + for (const buf of buffers) + req.push(buf); + req.push(null); + + break; + } + + default: + if (moduleName === undefined) + console.error('Missing parser module name'); + else + console.error(`Invalid parser module name: ${moduleName}`); + process.exit(1); +} diff --git a/node_modules/busboy/bench/bench-multipart-files-100mb-big.js b/node_modules/busboy/bench/bench-multipart-files-100mb-big.js new file mode 100644 index 0000000..b46bdee --- /dev/null +++ b/node_modules/busboy/bench/bench-multipart-files-100mb-big.js @@ -0,0 +1,154 @@ +'use strict'; + +function createMultipartBuffers(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="file${i + 1}"; ` + + `filename="random${i + 1}.bin"`, + 'content-type: application/octet-stream', + '', + '0'.repeat(mb), + '', + ].join('\r\n'))); + } + bufs.push(Buffer.from([ + `--${boundary}--`, + '', + ].join('\r\n'))); + return bufs; +} + +const boundary = '-----------------------------168072824752491622650073'; +const buffers = createMultipartBuffers(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 'busboy': { + const busboy = require('busboy'); + + const parser = busboy({ + limits: { + fieldSizeLimit: Infinity, + }, + headers: { + 'content-type': `multipart/form-data; boundary=${boundary}`, + }, + }); + parser.on('file', (name, stream, info) => { + ++calls.partBegin; + stream.on('data', (chunk) => { + ++calls.partData; + }).on('end', () => { + ++calls.partEnd; + }); + }).on('close', () => { + ++calls.end; + console.timeEnd(moduleName); + }); + + console.time(moduleName); + for (const buf of buffers) + parser.write(buf); + break; + } + + case 'formidable': { + const { MultipartParser } = require('formidable'); + + const parser = new MultipartParser(); + parser.initWithBoundary(boundary); + parser.on('data', ({ name }) => { + ++calls[name]; + if (name === 'end') + console.timeEnd(moduleName); + }); + + console.time(moduleName); + for (const buf of buffers) + parser.write(buf); + + 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}`, + }; + + 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); + for (const buf of buffers) + req.push(buf); + req.push(null); + + break; + } + + default: + if (moduleName === undefined) + console.error('Missing parser module name'); + else + console.error(`Invalid parser module name: ${moduleName}`); + process.exit(1); +} diff --git a/node_modules/busboy/bench/bench-multipart-files-100mb-small.js b/node_modules/busboy/bench/bench-multipart-files-100mb-small.js new file mode 100644 index 0000000..46b5dff --- /dev/null +++ b/node_modules/busboy/bench/bench-multipart-files-100mb-small.js @@ -0,0 +1,148 @@ +'use strict'; + +function createMultipartBuffers(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="file${i + 1}"; ` + + `filename="random${i + 1}.bin"`, + 'content-type: application/octet-stream', + '', + '0'.repeat(mb), + '', + ].join('\r\n'))); + } + bufs.push(Buffer.from([ + `--${boundary}--`, + '', + ].join('\r\n'))); + return bufs; +} + +const boundary = '-----------------------------168072824752491622650073'; +const buffers = createMultipartBuffers(boundary, (new Array(100)).fill(1)); +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 'busboy': { + const busboy = require('busboy'); + + const parser = busboy({ + limits: { + fieldSizeLimit: Infinity, + }, + headers: { + 'content-type': `multipart/form-data; boundary=${boundary}`, + }, + }); + parser.on('file', (name, stream, info) => { + ++calls.partBegin; + stream.on('data', (chunk) => { + ++calls.partData; + }).on('end', () => { + ++calls.partEnd; + }); + }).on('close', () => { + ++calls.end; + console.timeEnd(moduleName); + }); + + console.time(moduleName); + for (const buf of buffers) + parser.write(buf); + break; + } + + case 'formidable': { + const { MultipartParser } = require('formidable'); + + const parser = new MultipartParser(); + parser.initWithBoundary(boundary); + parser.on('data', ({ name }) => { + ++calls[name]; + if (name === 'end') + console.timeEnd(moduleName); + }); + + console.time(moduleName); + for (const buf of buffers) + parser.write(buf); + + 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}`, + }; + + 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); + for (const buf of buffers) + req.push(buf); + req.push(null); + + break; + } + + default: + if (moduleName === undefined) + console.error('Missing parser module name'); + else + console.error(`Invalid parser module name: ${moduleName}`); + process.exit(1); +} diff --git a/node_modules/busboy/bench/bench-urlencoded-fields-100pairs-small.js b/node_modules/busboy/bench/bench-urlencoded-fields-100pairs-small.js new file mode 100644 index 0000000..5c337df --- /dev/null +++ b/node_modules/busboy/bench/bench-urlencoded-fields-100pairs-small.js @@ -0,0 +1,101 @@ +'use strict'; + +const buffers = [ + Buffer.from( + (new Array(100)).fill('').map((_, i) => `key${i}=value${i}`).join('&') + ), +]; +const calls = { + field: 0, + end: 0, +}; + +let n = 3e3; + +const moduleName = process.argv[2]; +switch (moduleName) { + case 'busboy': { + const busboy = require('busboy'); + + console.time(moduleName); + (function next() { + const parser = busboy({ + limits: { + fieldSizeLimit: Infinity, + }, + headers: { + 'content-type': 'application/x-www-form-urlencoded; charset=utf-8', + }, + }); + parser.on('field', (name, val, info) => { + ++calls.field; + }).on('close', () => { + ++calls.end; + if (--n === 0) + console.timeEnd(moduleName); + else + process.nextTick(next); + }); + + for (const buf of buffers) + parser.write(buf); + parser.end(); + })(); + break; + } + + case 'formidable': { + const QuerystringParser = + require('formidable/src/parsers/Querystring.js'); + + console.time(moduleName); + (function next() { + const parser = new QuerystringParser(); + parser.on('data', (obj) => { + ++calls.field; + }).on('end', () => { + ++calls.end; + if (--n === 0) + console.timeEnd(moduleName); + else + process.nextTick(next); + }); + + for (const buf of buffers) + parser.write(buf); + parser.end(); + })(); + break; + } + + case 'formidable-streaming': { + const QuerystringParser = + require('formidable/src/parsers/StreamingQuerystring.js'); + + console.time(moduleName); + (function next() { + const parser = new QuerystringParser(); + parser.on('data', (obj) => { + ++calls.field; + }).on('end', () => { + ++calls.end; + if (--n === 0) + console.timeEnd(moduleName); + else + process.nextTick(next); + }); + + for (const buf of buffers) + parser.write(buf); + parser.end(); + })(); + break; + } + + default: + if (moduleName === undefined) + console.error('Missing parser module name'); + else + console.error(`Invalid parser module name: ${moduleName}`); + process.exit(1); +} diff --git a/node_modules/busboy/bench/bench-urlencoded-fields-900pairs-small-alt.js b/node_modules/busboy/bench/bench-urlencoded-fields-900pairs-small-alt.js new file mode 100644 index 0000000..1f5645c --- /dev/null +++ b/node_modules/busboy/bench/bench-urlencoded-fields-900pairs-small-alt.js @@ -0,0 +1,84 @@ +'use strict'; + +const buffers = [ + Buffer.from( + (new Array(900)).fill('').map((_, i) => `key${i}=value${i}`).join('&') + ), +]; +const calls = { + field: 0, + end: 0, +}; + +const moduleName = process.argv[2]; +switch (moduleName) { + case 'busboy': { + const busboy = require('busboy'); + + console.time(moduleName); + const parser = busboy({ + limits: { + fieldSizeLimit: Infinity, + }, + headers: { + 'content-type': 'application/x-www-form-urlencoded; charset=utf-8', + }, + }); + parser.on('field', (name, val, info) => { + ++calls.field; + }).on('close', () => { + ++calls.end; + console.timeEnd(moduleName); + }); + + for (const buf of buffers) + parser.write(buf); + parser.end(); + break; + } + + case 'formidable': { + const QuerystringParser = + require('formidable/src/parsers/Querystring.js'); + + console.time(moduleName); + const parser = new QuerystringParser(); + parser.on('data', (obj) => { + ++calls.field; + }).on('end', () => { + ++calls.end; + console.timeEnd(moduleName); + }); + + for (const buf of buffers) + parser.write(buf); + parser.end(); + break; + } + + case 'formidable-streaming': { + const QuerystringParser = + require('formidable/src/parsers/StreamingQuerystring.js'); + + console.time(moduleName); + const parser = new QuerystringParser(); + parser.on('data', (obj) => { + ++calls.field; + }).on('end', () => { + ++calls.end; + console.timeEnd(moduleName); + }); + + for (const buf of buffers) + parser.write(buf); + parser.end(); + break; + } + + default: + if (moduleName === undefined) + console.error('Missing parser module name'); + else + console.error(`Invalid parser module name: ${moduleName}`); + process.exit(1); +} diff --git a/node_modules/busboy/lib/index.js b/node_modules/busboy/lib/index.js new file mode 100644 index 0000000..873272d --- /dev/null +++ b/node_modules/busboy/lib/index.js @@ -0,0 +1,57 @@ +'use strict'; + +const { parseContentType } = require('./utils.js'); + +function getInstance(cfg) { + const headers = cfg.headers; + const conType = parseContentType(headers['content-type']); + if (!conType) + throw new Error('Malformed content type'); + + for (const type of TYPES) { + const matched = type.detect(conType); + if (!matched) + continue; + + const instanceCfg = { + limits: cfg.limits, + headers, + conType, + highWaterMark: undefined, + fileHwm: undefined, + defCharset: undefined, + defParamCharset: undefined, + preservePath: false, + }; + if (cfg.highWaterMark) + instanceCfg.highWaterMark = cfg.highWaterMark; + if (cfg.fileHwm) + instanceCfg.fileHwm = cfg.fileHwm; + instanceCfg.defCharset = cfg.defCharset; + instanceCfg.defParamCharset = cfg.defParamCharset; + instanceCfg.preservePath = cfg.preservePath; + return new type(instanceCfg); + } + + throw new Error(`Unsupported content type: ${headers['content-type']}`); +} + +// Note: types are explicitly listed here for easier bundling +// See: https://github.com/mscdex/busboy/issues/121 +const TYPES = [ + require('./types/multipart'), + require('./types/urlencoded'), +].filter(function(typemod) { return typeof typemod.detect === 'function'; }); + +module.exports = (cfg) => { + if (typeof cfg !== 'object' || cfg === null) + cfg = {}; + + if (typeof cfg.headers !== 'object' + || cfg.headers === null + || typeof cfg.headers['content-type'] !== 'string') { + throw new Error('Missing Content-Type'); + } + + return getInstance(cfg); +}; diff --git a/node_modules/busboy/lib/types/multipart.js b/node_modules/busboy/lib/types/multipart.js new file mode 100644 index 0000000..cc0d7bb --- /dev/null +++ b/node_modules/busboy/lib/types/multipart.js @@ -0,0 +1,653 @@ +'use strict'; + +const { Readable, Writable } = require('stream'); + +const StreamSearch = require('streamsearch'); + +const { + basename, + convertToUTF8, + getDecoder, + parseContentType, + parseDisposition, +} = require('../utils.js'); + +const BUF_CRLF = Buffer.from('\r\n'); +const BUF_CR = Buffer.from('\r'); +const BUF_DASH = Buffer.from('-'); + +function noop() {} + +const MAX_HEADER_PAIRS = 2000; // From node +const MAX_HEADER_SIZE = 16 * 1024; // From node (its default value) + +const HPARSER_NAME = 0; +const HPARSER_PRE_OWS = 1; +const HPARSER_VALUE = 2; +class HeaderParser { + constructor(cb) { + this.header = Object.create(null); + this.pairCount = 0; + this.byteCount = 0; + this.state = HPARSER_NAME; + this.name = ''; + this.value = ''; + this.crlf = 0; + this.cb = cb; + } + + reset() { + this.header = Object.create(null); + this.pairCount = 0; + this.byteCount = 0; + this.state = HPARSER_NAME; + this.name = ''; + this.value = ''; + this.crlf = 0; + } + + push(chunk, pos, end) { + let start = pos; + while (pos < end) { + switch (this.state) { + case HPARSER_NAME: { + let done = false; + for (; pos < end; ++pos) { + if (this.byteCount === MAX_HEADER_SIZE) + return -1; + ++this.byteCount; + const code = chunk[pos]; + if (TOKEN[code] !== 1) { + if (code !== 58/* ':' */) + return -1; + this.name += chunk.latin1Slice(start, pos); + if (this.name.length === 0) + return -1; + ++pos; + done = true; + this.state = HPARSER_PRE_OWS; + break; + } + } + if (!done) { + this.name += chunk.latin1Slice(start, pos); + break; + } + // FALLTHROUGH + } + case HPARSER_PRE_OWS: { + // Skip optional whitespace + let done = false; + for (; pos < end; ++pos) { + if (this.byteCount === MAX_HEADER_SIZE) + return -1; + ++this.byteCount; + const code = chunk[pos]; + if (code !== 32/* ' ' */ && code !== 9/* '\t' */) { + start = pos; + done = true; + this.state = HPARSER_VALUE; + break; + } + } + if (!done) + break; + // FALLTHROUGH + } + case HPARSER_VALUE: + switch (this.crlf) { + case 0: // Nothing yet + for (; pos < end; ++pos) { + if (this.byteCount === MAX_HEADER_SIZE) + return -1; + ++this.byteCount; + const code = chunk[pos]; + if (FIELD_VCHAR[code] !== 1) { + if (code !== 13/* '\r' */) + return -1; + ++this.crlf; + break; + } + } + this.value += chunk.latin1Slice(start, pos++); + break; + case 1: // Received CR + if (this.byteCount === MAX_HEADER_SIZE) + return -1; + ++this.byteCount; + if (chunk[pos++] !== 10/* '\n' */) + return -1; + ++this.crlf; + break; + case 2: { // Received CR LF + if (this.byteCount === MAX_HEADER_SIZE) + return -1; + ++this.byteCount; + const code = chunk[pos]; + if (code === 32/* ' ' */ || code === 9/* '\t' */) { + // Folded value + start = pos; + this.crlf = 0; + } else { + if (++this.pairCount < MAX_HEADER_PAIRS) { + this.name = this.name.toLowerCase(); + if (this.header[this.name] === undefined) + this.header[this.name] = [this.value]; + else + this.header[this.name].push(this.value); + } + if (code === 13/* '\r' */) { + ++this.crlf; + ++pos; + } else { + // Assume start of next header field name + start = pos; + this.crlf = 0; + this.state = HPARSER_NAME; + this.name = ''; + this.value = ''; + } + } + break; + } + case 3: { // Received CR LF CR + if (this.byteCount === MAX_HEADER_SIZE) + return -1; + ++this.byteCount; + if (chunk[pos++] !== 10/* '\n' */) + return -1; + // End of header + const header = this.header; + this.reset(); + this.cb(header); + return pos; + } + } + break; + } + } + + return pos; + } +} + +class FileStream extends Readable { + constructor(opts, owner) { + super(opts); + this.truncated = false; + this._readcb = null; + this.once('end', () => { + // We need to make sure that we call any outstanding _writecb() that is + // associated with this file so that processing of the rest of the form + // can continue. This may not happen if the file stream ends right after + // backpressure kicks in, so we force it here. + this._read(); + if (--owner._fileEndsLeft === 0 && owner._finalcb) { + const cb = owner._finalcb; + owner._finalcb = null; + // Make sure other 'end' event handlers get a chance to be executed + // before busboy's 'finish' event is emitted + process.nextTick(cb); + } + }); + } + _read(n) { + const cb = this._readcb; + if (cb) { + this._readcb = null; + cb(); + } + } +} + +const ignoreData = { + push: (chunk, pos) => {}, + destroy: () => {}, +}; + +function callAndUnsetCb(self, err) { + const cb = self._writecb; + self._writecb = null; + if (err) + self.destroy(err); + else if (cb) + cb(); +} + +function nullDecoder(val, hint) { + return val; +} + +class Multipart extends Writable { + constructor(cfg) { + const streamOpts = { + autoDestroy: true, + emitClose: true, + highWaterMark: (typeof cfg.highWaterMark === 'number' + ? cfg.highWaterMark + : undefined), + }; + super(streamOpts); + + if (!cfg.conType.params || typeof cfg.conType.params.boundary !== 'string') + throw new Error('Multipart: Boundary not found'); + + const boundary = cfg.conType.params.boundary; + const paramDecoder = (typeof cfg.defParamCharset === 'string' + && cfg.defParamCharset + ? getDecoder(cfg.defParamCharset) + : nullDecoder); + const defCharset = (cfg.defCharset || 'utf8'); + const preservePath = cfg.preservePath; + const fileOpts = { + autoDestroy: true, + emitClose: true, + highWaterMark: (typeof cfg.fileHwm === 'number' + ? cfg.fileHwm + : undefined), + }; + + const limits = cfg.limits; + const fieldSizeLimit = (limits && typeof limits.fieldSize === 'number' + ? limits.fieldSize + : 1 * 1024 * 1024); + const fileSizeLimit = (limits && typeof limits.fileSize === 'number' + ? limits.fileSize + : Infinity); + const filesLimit = (limits && typeof limits.files === 'number' + ? limits.files + : Infinity); + const fieldsLimit = (limits && typeof limits.fields === 'number' + ? limits.fields + : Infinity); + const partsLimit = (limits && typeof limits.parts === 'number' + ? limits.parts + : Infinity); + + let parts = -1; // Account for initial boundary + let fields = 0; + let files = 0; + let skipPart = false; + + this._fileEndsLeft = 0; + this._fileStream = undefined; + this._complete = false; + let fileSize = 0; + + let field; + let fieldSize = 0; + let partCharset; + let partEncoding; + let partType; + let partName; + let partTruncated = false; + + let hitFilesLimit = false; + let hitFieldsLimit = false; + + this._hparser = null; + const hparser = new HeaderParser((header) => { + this._hparser = null; + skipPart = false; + + partType = 'text/plain'; + partCharset = defCharset; + partEncoding = '7bit'; + partName = undefined; + partTruncated = false; + + let filename; + if (!header['content-disposition']) { + skipPart = true; + return; + } + + const disp = parseDisposition(header['content-disposition'][0], + paramDecoder); + if (!disp || disp.type !== 'form-data') { + skipPart = true; + return; + } + + if (disp.params) { + if (disp.params.name) + partName = disp.params.name; + + if (disp.params['filename*']) + filename = disp.params['filename*']; + else if (disp.params.filename) + filename = disp.params.filename; + + if (filename !== undefined && !preservePath) + filename = basename(filename); + } + + if (header['content-type']) { + const conType = parseContentType(header['content-type'][0]); + if (conType) { + partType = `${conType.type}/${conType.subtype}`; + if (conType.params && typeof conType.params.charset === 'string') + partCharset = conType.params.charset.toLowerCase(); + } + } + + if (header['content-transfer-encoding']) + partEncoding = header['content-transfer-encoding'][0].toLowerCase(); + + if (partType === 'application/octet-stream' || filename !== undefined) { + // File + + if (files === filesLimit) { + if (!hitFilesLimit) { + hitFilesLimit = true; + this.emit('filesLimit'); + } + skipPart = true; + return; + } + ++files; + + if (this.listenerCount('file') === 0) { + skipPart = true; + return; + } + + fileSize = 0; + this._fileStream = new FileStream(fileOpts, this); + ++this._fileEndsLeft; + this.emit( + 'file', + partName, + this._fileStream, + { filename, + encoding: partEncoding, + mimeType: partType } + ); + } else { + // Non-file + + if (fields === fieldsLimit) { + if (!hitFieldsLimit) { + hitFieldsLimit = true; + this.emit('fieldsLimit'); + } + skipPart = true; + return; + } + ++fields; + + if (this.listenerCount('field') === 0) { + skipPart = true; + return; + } + + field = []; + fieldSize = 0; + } + }); + + let matchPostBoundary = 0; + const ssCb = (isMatch, data, start, end, isDataSafe) => { +retrydata: + while (data) { + if (this._hparser !== null) { + const ret = this._hparser.push(data, start, end); + if (ret === -1) { + this._hparser = null; + hparser.reset(); + this.emit('error', new Error('Malformed part header')); + break; + } + start = ret; + } + + if (start === end) + break; + + if (matchPostBoundary !== 0) { + if (matchPostBoundary === 1) { + switch (data[start]) { + case 45: // '-' + // Try matching '--' after boundary + matchPostBoundary = 2; + ++start; + break; + case 13: // '\r' + // Try matching CR LF before header + matchPostBoundary = 3; + ++start; + break; + default: + matchPostBoundary = 0; + } + if (start === end) + return; + } + + if (matchPostBoundary === 2) { + matchPostBoundary = 0; + if (data[start] === 45/* '-' */) { + // End of multipart data + this._complete = true; + this._bparser = ignoreData; + return; + } + // We saw something other than '-', so put the dash we consumed + // "back" + const writecb = this._writecb; + this._writecb = noop; + ssCb(false, BUF_DASH, 0, 1, false); + this._writecb = writecb; + } else if (matchPostBoundary === 3) { + matchPostBoundary = 0; + if (data[start] === 10/* '\n' */) { + ++start; + if (parts >= partsLimit) + break; + // Prepare the header parser + this._hparser = hparser; + if (start === end) + break; + // Process the remaining data as a header + continue retrydata; + } else { + // We saw something other than LF, so put the CR we consumed + // "back" + const writecb = this._writecb; + this._writecb = noop; + ssCb(false, BUF_CR, 0, 1, false); + this._writecb = writecb; + } + } + } + + if (!skipPart) { + if (this._fileStream) { + let chunk; + const actualLen = Math.min(end - start, fileSizeLimit - fileSize); + if (!isDataSafe) { + chunk = Buffer.allocUnsafe(actualLen); + data.copy(chunk, 0, start, start + actualLen); + } else { + chunk = data.slice(start, start + actualLen); + } + + fileSize += chunk.length; + if (fileSize === fileSizeLimit) { + if (chunk.length > 0) + this._fileStream.push(chunk); + this._fileStream.emit('limit'); + this._fileStream.truncated = true; + skipPart = true; + } else if (!this._fileStream.push(chunk)) { + if (this._writecb) + this._fileStream._readcb = this._writecb; + this._writecb = null; + } + } else if (field !== undefined) { + let chunk; + const actualLen = Math.min( + end - start, + fieldSizeLimit - fieldSize + ); + if (!isDataSafe) { + chunk = Buffer.allocUnsafe(actualLen); + data.copy(chunk, 0, start, start + actualLen); + } else { + chunk = data.slice(start, start + actualLen); + } + + fieldSize += actualLen; + field.push(chunk); + if (fieldSize === fieldSizeLimit) { + skipPart = true; + partTruncated = true; + } + } + } + + break; + } + + if (isMatch) { + matchPostBoundary = 1; + + if (this._fileStream) { + // End the active file stream if the previous part was a file + this._fileStream.push(null); + this._fileStream = null; + } else if (field !== undefined) { + let data; + switch (field.length) { + case 0: + data = ''; + break; + case 1: + data = convertToUTF8(field[0], partCharset, 0); + break; + default: + data = convertToUTF8( + Buffer.concat(field, fieldSize), + partCharset, + 0 + ); + } + field = undefined; + fieldSize = 0; + this.emit( + 'field', + partName, + data, + { nameTruncated: false, + valueTruncated: partTruncated, + encoding: partEncoding, + mimeType: partType } + ); + } + + if (++parts === partsLimit) + this.emit('partsLimit'); + } + }; + this._bparser = new StreamSearch(`\r\n--${boundary}`, ssCb); + + this._writecb = null; + this._finalcb = null; + + // Just in case there is no preamble + this.write(BUF_CRLF); + } + + static detect(conType) { + return (conType.type === 'multipart' && conType.subtype === 'form-data'); + } + + _write(chunk, enc, cb) { + this._writecb = cb; + this._bparser.push(chunk, 0); + if (this._writecb) + callAndUnsetCb(this); + } + + _destroy(err, cb) { + this._hparser = null; + this._bparser = ignoreData; + if (!err) + err = checkEndState(this); + const fileStream = this._fileStream; + if (fileStream) { + this._fileStream = null; + fileStream.destroy(err); + } + cb(err); + } + + _final(cb) { + this._bparser.destroy(); + if (!this._complete) + return cb(new Error('Unexpected end of form')); + if (this._fileEndsLeft) + this._finalcb = finalcb.bind(null, this, cb); + else + finalcb(this, cb); + } +} + +function finalcb(self, cb, err) { + if (err) + return cb(err); + err = checkEndState(self); + cb(err); +} + +function checkEndState(self) { + if (self._hparser) + return new Error('Malformed part header'); + const fileStream = self._fileStream; + if (fileStream) { + self._fileStream = null; + fileStream.destroy(new Error('Unexpected end of file')); + } + if (!self._complete) + return new Error('Unexpected end of form'); +} + +const TOKEN = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +]; + +const FIELD_VCHAR = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, +]; + +module.exports = Multipart; diff --git a/node_modules/busboy/lib/types/urlencoded.js b/node_modules/busboy/lib/types/urlencoded.js new file mode 100644 index 0000000..5c463a2 --- /dev/null +++ b/node_modules/busboy/lib/types/urlencoded.js @@ -0,0 +1,350 @@ +'use strict'; + +const { Writable } = require('stream'); + +const { getDecoder } = require('../utils.js'); + +class URLEncoded extends Writable { + constructor(cfg) { + const streamOpts = { + autoDestroy: true, + emitClose: true, + highWaterMark: (typeof cfg.highWaterMark === 'number' + ? cfg.highWaterMark + : undefined), + }; + super(streamOpts); + + let charset = (cfg.defCharset || 'utf8'); + if (cfg.conType.params && typeof cfg.conType.params.charset === 'string') + charset = cfg.conType.params.charset; + + this.charset = charset; + + const limits = cfg.limits; + this.fieldSizeLimit = (limits && typeof limits.fieldSize === 'number' + ? limits.fieldSize + : 1 * 1024 * 1024); + this.fieldsLimit = (limits && typeof limits.fields === 'number' + ? limits.fields + : Infinity); + this.fieldNameSizeLimit = ( + limits && typeof limits.fieldNameSize === 'number' + ? limits.fieldNameSize + : 100 + ); + + this._inKey = true; + this._keyTrunc = false; + this._valTrunc = false; + this._bytesKey = 0; + this._bytesVal = 0; + this._fields = 0; + this._key = ''; + this._val = ''; + this._byte = -2; + this._lastPos = 0; + this._encode = 0; + this._decoder = getDecoder(charset); + } + + static detect(conType) { + return (conType.type === 'application' + && conType.subtype === 'x-www-form-urlencoded'); + } + + _write(chunk, enc, cb) { + if (this._fields >= this.fieldsLimit) + return cb(); + + let i = 0; + const len = chunk.length; + this._lastPos = 0; + + // Check if we last ended mid-percent-encoded byte + if (this._byte !== -2) { + i = readPctEnc(this, chunk, i, len); + if (i === -1) + return cb(new Error('Malformed urlencoded form')); + if (i >= len) + return cb(); + if (this._inKey) + ++this._bytesKey; + else + ++this._bytesVal; + } + +main: + while (i < len) { + if (this._inKey) { + // Parsing key + + i = skipKeyBytes(this, chunk, i, len); + + while (i < len) { + switch (chunk[i]) { + case 61: // '=' + if (this._lastPos < i) + this._key += chunk.latin1Slice(this._lastPos, i); + this._lastPos = ++i; + this._key = this._decoder(this._key, this._encode); + this._encode = 0; + this._inKey = false; + continue main; + case 38: // '&' + if (this._lastPos < i) + this._key += chunk.latin1Slice(this._lastPos, i); + this._lastPos = ++i; + this._key = this._decoder(this._key, this._encode); + this._encode = 0; + if (this._bytesKey > 0) { + this.emit( + 'field', + this._key, + '', + { nameTruncated: this._keyTrunc, + valueTruncated: false, + encoding: this.charset, + mimeType: 'text/plain' } + ); + } + this._key = ''; + this._val = ''; + this._keyTrunc = false; + this._valTrunc = false; + this._bytesKey = 0; + this._bytesVal = 0; + if (++this._fields >= this.fieldsLimit) { + this.emit('fieldsLimit'); + return cb(); + } + continue; + case 43: // '+' + if (this._lastPos < i) + this._key += chunk.latin1Slice(this._lastPos, i); + this._key += ' '; + this._lastPos = i + 1; + break; + case 37: // '%' + if (this._encode === 0) + this._encode = 1; + if (this._lastPos < i) + this._key += chunk.latin1Slice(this._lastPos, i); + this._lastPos = i + 1; + this._byte = -1; + i = readPctEnc(this, chunk, i + 1, len); + if (i === -1) + return cb(new Error('Malformed urlencoded form')); + if (i >= len) + return cb(); + ++this._bytesKey; + i = skipKeyBytes(this, chunk, i, len); + continue; + } + ++i; + ++this._bytesKey; + i = skipKeyBytes(this, chunk, i, len); + } + if (this._lastPos < i) + this._key += chunk.latin1Slice(this._lastPos, i); + } else { + // Parsing value + + i = skipValBytes(this, chunk, i, len); + + while (i < len) { + switch (chunk[i]) { + case 38: // '&' + if (this._lastPos < i) + this._val += chunk.latin1Slice(this._lastPos, i); + this._lastPos = ++i; + this._inKey = true; + this._val = this._decoder(this._val, this._encode); + this._encode = 0; + if (this._bytesKey > 0 || this._bytesVal > 0) { + this.emit( + 'field', + this._key, + this._val, + { nameTruncated: this._keyTrunc, + valueTruncated: this._valTrunc, + encoding: this.charset, + mimeType: 'text/plain' } + ); + } + this._key = ''; + this._val = ''; + this._keyTrunc = false; + this._valTrunc = false; + this._bytesKey = 0; + this._bytesVal = 0; + if (++this._fields >= this.fieldsLimit) { + this.emit('fieldsLimit'); + return cb(); + } + continue main; + case 43: // '+' + if (this._lastPos < i) + this._val += chunk.latin1Slice(this._lastPos, i); + this._val += ' '; + this._lastPos = i + 1; + break; + case 37: // '%' + if (this._encode === 0) + this._encode = 1; + if (this._lastPos < i) + this._val += chunk.latin1Slice(this._lastPos, i); + this._lastPos = i + 1; + this._byte = -1; + i = readPctEnc(this, chunk, i + 1, len); + if (i === -1) + return cb(new Error('Malformed urlencoded form')); + if (i >= len) + return cb(); + ++this._bytesVal; + i = skipValBytes(this, chunk, i, len); + continue; + } + ++i; + ++this._bytesVal; + i = skipValBytes(this, chunk, i, len); + } + if (this._lastPos < i) + this._val += chunk.latin1Slice(this._lastPos, i); + } + } + + cb(); + } + + _final(cb) { + if (this._byte !== -2) + return cb(new Error('Malformed urlencoded form')); + if (!this._inKey || this._bytesKey > 0 || this._bytesVal > 0) { + if (this._inKey) + this._key = this._decoder(this._key, this._encode); + else + this._val = this._decoder(this._val, this._encode); + this.emit( + 'field', + this._key, + this._val, + { nameTruncated: this._keyTrunc, + valueTruncated: this._valTrunc, + encoding: this.charset, + mimeType: 'text/plain' } + ); + } + cb(); + } +} + +function readPctEnc(self, chunk, pos, len) { + if (pos >= len) + return len; + + if (self._byte === -1) { + // We saw a '%' but no hex characters yet + const hexUpper = HEX_VALUES[chunk[pos++]]; + if (hexUpper === -1) + return -1; + + if (hexUpper >= 8) + self._encode = 2; // Indicate high bits detected + + if (pos < len) { + // Both hex characters are in this chunk + const hexLower = HEX_VALUES[chunk[pos++]]; + if (hexLower === -1) + return -1; + + if (self._inKey) + self._key += String.fromCharCode((hexUpper << 4) + hexLower); + else + self._val += String.fromCharCode((hexUpper << 4) + hexLower); + + self._byte = -2; + self._lastPos = pos; + } else { + // Only one hex character was available in this chunk + self._byte = hexUpper; + } + } else { + // We saw only one hex character so far + const hexLower = HEX_VALUES[chunk[pos++]]; + if (hexLower === -1) + return -1; + + if (self._inKey) + self._key += String.fromCharCode((self._byte << 4) + hexLower); + else + self._val += String.fromCharCode((self._byte << 4) + hexLower); + + self._byte = -2; + self._lastPos = pos; + } + + return pos; +} + +function skipKeyBytes(self, chunk, pos, len) { + // Skip bytes if we've truncated + if (self._bytesKey > self.fieldNameSizeLimit) { + if (!self._keyTrunc) { + if (self._lastPos < pos) + self._key += chunk.latin1Slice(self._lastPos, pos - 1); + } + self._keyTrunc = true; + for (; pos < len; ++pos) { + const code = chunk[pos]; + if (code === 61/* '=' */ || code === 38/* '&' */) + break; + ++self._bytesKey; + } + self._lastPos = pos; + } + + return pos; +} + +function skipValBytes(self, chunk, pos, len) { + // Skip bytes if we've truncated + if (self._bytesVal > self.fieldSizeLimit) { + if (!self._valTrunc) { + if (self._lastPos < pos) + self._val += chunk.latin1Slice(self._lastPos, pos - 1); + } + self._valTrunc = true; + for (; pos < len; ++pos) { + if (chunk[pos] === 38/* '&' */) + break; + ++self._bytesVal; + } + self._lastPos = pos; + } + + return pos; +} + +/* eslint-disable no-multi-spaces */ +const HEX_VALUES = [ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, +]; +/* eslint-enable no-multi-spaces */ + +module.exports = URLEncoded; diff --git a/node_modules/busboy/lib/utils.js b/node_modules/busboy/lib/utils.js new file mode 100644 index 0000000..8274f6c --- /dev/null +++ b/node_modules/busboy/lib/utils.js @@ -0,0 +1,596 @@ +'use strict'; + +function parseContentType(str) { + if (str.length === 0) + return; + + const params = Object.create(null); + let i = 0; + + // Parse type + for (; i < str.length; ++i) { + const code = str.charCodeAt(i); + if (TOKEN[code] !== 1) { + if (code !== 47/* '/' */ || i === 0) + return; + break; + } + } + // Check for type without subtype + if (i === str.length) + return; + + const type = str.slice(0, i).toLowerCase(); + + // Parse subtype + const subtypeStart = ++i; + for (; i < str.length; ++i) { + const code = str.charCodeAt(i); + if (TOKEN[code] !== 1) { + // Make sure we have a subtype + if (i === subtypeStart) + return; + + if (parseContentTypeParams(str, i, params) === undefined) + return; + break; + } + } + // Make sure we have a subtype + if (i === subtypeStart) + return; + + const subtype = str.slice(subtypeStart, i).toLowerCase(); + + return { type, subtype, params }; +} + +function parseContentTypeParams(str, i, params) { + while (i < str.length) { + // Consume whitespace + for (; i < str.length; ++i) { + const code = str.charCodeAt(i); + if (code !== 32/* ' ' */ && code !== 9/* '\t' */) + break; + } + + // Ended on whitespace + if (i === str.length) + break; + + // Check for malformed parameter + if (str.charCodeAt(i++) !== 59/* ';' */) + return; + + // Consume whitespace + for (; i < str.length; ++i) { + const code = str.charCodeAt(i); + if (code !== 32/* ' ' */ && code !== 9/* '\t' */) + break; + } + + // Ended on whitespace (malformed) + if (i === str.length) + return; + + let name; + const nameStart = i; + // Parse parameter name + for (; i < str.length; ++i) { + const code = str.charCodeAt(i); + if (TOKEN[code] !== 1) { + if (code !== 61/* '=' */) + return; + break; + } + } + + // No value (malformed) + if (i === str.length) + return; + + name = str.slice(nameStart, i); + ++i; // Skip over '=' + + // No value (malformed) + if (i === str.length) + return; + + let value = ''; + let valueStart; + if (str.charCodeAt(i) === 34/* '"' */) { + valueStart = ++i; + let escaping = false; + // Parse quoted value + for (; i < str.length; ++i) { + const code = str.charCodeAt(i); + if (code === 92/* '\\' */) { + if (escaping) { + valueStart = i; + escaping = false; + } else { + value += str.slice(valueStart, i); + escaping = true; + } + continue; + } + if (code === 34/* '"' */) { + if (escaping) { + valueStart = i; + escaping = false; + continue; + } + value += str.slice(valueStart, i); + break; + } + if (escaping) { + valueStart = i - 1; + escaping = false; + } + // Invalid unescaped quoted character (malformed) + if (QDTEXT[code] !== 1) + return; + } + + // No end quote (malformed) + if (i === str.length) + return; + + ++i; // Skip over double quote + } else { + valueStart = i; + // Parse unquoted value + for (; i < str.length; ++i) { + const code = str.charCodeAt(i); + if (TOKEN[code] !== 1) { + // No value (malformed) + if (i === valueStart) + return; + break; + } + } + value = str.slice(valueStart, i); + } + + name = name.toLowerCase(); + if (params[name] === undefined) + params[name] = value; + } + + return params; +} + +function parseDisposition(str, defDecoder) { + if (str.length === 0) + return; + + const params = Object.create(null); + let i = 0; + + for (; i < str.length; ++i) { + const code = str.charCodeAt(i); + if (TOKEN[code] !== 1) { + if (parseDispositionParams(str, i, params, defDecoder) === undefined) + return; + break; + } + } + + const type = str.slice(0, i).toLowerCase(); + + return { type, params }; +} + +function parseDispositionParams(str, i, params, defDecoder) { + while (i < str.length) { + // Consume whitespace + for (; i < str.length; ++i) { + const code = str.charCodeAt(i); + if (code !== 32/* ' ' */ && code !== 9/* '\t' */) + break; + } + + // Ended on whitespace + if (i === str.length) + break; + + // Check for malformed parameter + if (str.charCodeAt(i++) !== 59/* ';' */) + return; + + // Consume whitespace + for (; i < str.length; ++i) { + const code = str.charCodeAt(i); + if (code !== 32/* ' ' */ && code !== 9/* '\t' */) + break; + } + + // Ended on whitespace (malformed) + if (i === str.length) + return; + + let name; + const nameStart = i; + // Parse parameter name + for (; i < str.length; ++i) { + const code = str.charCodeAt(i); + if (TOKEN[code] !== 1) { + if (code === 61/* '=' */) + break; + return; + } + } + + // No value (malformed) + if (i === str.length) + return; + + let value = ''; + let valueStart; + let charset; + //~ let lang; + name = str.slice(nameStart, i); + if (name.charCodeAt(name.length - 1) === 42/* '*' */) { + // Extended value + + const charsetStart = ++i; + // Parse charset name + for (; i < str.length; ++i) { + const code = str.charCodeAt(i); + if (CHARSET[code] !== 1) { + if (code !== 39/* '\'' */) + return; + break; + } + } + + // Incomplete charset (malformed) + if (i === str.length) + return; + + charset = str.slice(charsetStart, i); + ++i; // Skip over the '\'' + + //~ const langStart = ++i; + // Parse language name + for (; i < str.length; ++i) { + const code = str.charCodeAt(i); + if (code === 39/* '\'' */) + break; + } + + // Incomplete language (malformed) + if (i === str.length) + return; + + //~ lang = str.slice(langStart, i); + ++i; // Skip over the '\'' + + // No value (malformed) + if (i === str.length) + return; + + valueStart = i; + + let encode = 0; + // Parse value + for (; i < str.length; ++i) { + const code = str.charCodeAt(i); + if (EXTENDED_VALUE[code] !== 1) { + if (code === 37/* '%' */) { + let hexUpper; + let hexLower; + if (i + 2 < str.length + && (hexUpper = HEX_VALUES[str.charCodeAt(i + 1)]) !== -1 + && (hexLower = HEX_VALUES[str.charCodeAt(i + 2)]) !== -1) { + const byteVal = (hexUpper << 4) + hexLower; + value += str.slice(valueStart, i); + value += String.fromCharCode(byteVal); + i += 2; + valueStart = i + 1; + if (byteVal >= 128) + encode = 2; + else if (encode === 0) + encode = 1; + continue; + } + // '%' disallowed in non-percent encoded contexts (malformed) + return; + } + break; + } + } + + value += str.slice(valueStart, i); + value = convertToUTF8(value, charset, encode); + if (value === undefined) + return; + } else { + // Non-extended value + + ++i; // Skip over '=' + + // No value (malformed) + if (i === str.length) + return; + + if (str.charCodeAt(i) === 34/* '"' */) { + valueStart = ++i; + let escaping = false; + // Parse quoted value + for (; i < str.length; ++i) { + const code = str.charCodeAt(i); + if (code === 92/* '\\' */) { + if (escaping) { + valueStart = i; + escaping = false; + } else { + value += str.slice(valueStart, i); + escaping = true; + } + continue; + } + if (code === 34/* '"' */) { + if (escaping) { + valueStart = i; + escaping = false; + continue; + } + value += str.slice(valueStart, i); + break; + } + if (escaping) { + valueStart = i - 1; + escaping = false; + } + // Invalid unescaped quoted character (malformed) + if (QDTEXT[code] !== 1) + return; + } + + // No end quote (malformed) + if (i === str.length) + return; + + ++i; // Skip over double quote + } else { + valueStart = i; + // Parse unquoted value + for (; i < str.length; ++i) { + const code = str.charCodeAt(i); + if (TOKEN[code] !== 1) { + // No value (malformed) + if (i === valueStart) + return; + break; + } + } + value = str.slice(valueStart, i); + } + + value = defDecoder(value, 2); + if (value === undefined) + return; + } + + name = name.toLowerCase(); + if (params[name] === undefined) + params[name] = value; + } + + return params; +} + +function getDecoder(charset) { + let lc; + while (true) { + switch (charset) { + case 'utf-8': + case 'utf8': + return decoders.utf8; + case 'latin1': + case 'ascii': // TODO: Make these a separate, strict decoder? + case 'us-ascii': + case 'iso-8859-1': + case 'iso8859-1': + case 'iso88591': + case 'iso_8859-1': + case 'windows-1252': + case 'iso_8859-1:1987': + case 'cp1252': + case 'x-cp1252': + return decoders.latin1; + case 'utf16le': + case 'utf-16le': + case 'ucs2': + case 'ucs-2': + return decoders.utf16le; + case 'base64': + return decoders.base64; + default: + if (lc === undefined) { + lc = true; + charset = charset.toLowerCase(); + continue; + } + return decoders.other.bind(charset); + } + } +} + +const decoders = { + utf8: (data, hint) => { + if (data.length === 0) + return ''; + if (typeof data === 'string') { + // If `data` never had any percent-encoded bytes or never had any that + // were outside of the ASCII range, then we can safely just return the + // input since UTF-8 is ASCII compatible + if (hint < 2) + return data; + + data = Buffer.from(data, 'latin1'); + } + return data.utf8Slice(0, data.length); + }, + + latin1: (data, hint) => { + if (data.length === 0) + return ''; + if (typeof data === 'string') + return data; + return data.latin1Slice(0, data.length); + }, + + utf16le: (data, hint) => { + if (data.length === 0) + return ''; + if (typeof data === 'string') + data = Buffer.from(data, 'latin1'); + return data.ucs2Slice(0, data.length); + }, + + base64: (data, hint) => { + if (data.length === 0) + return ''; + if (typeof data === 'string') + data = Buffer.from(data, 'latin1'); + return data.base64Slice(0, data.length); + }, + + other: (data, hint) => { + if (data.length === 0) + return ''; + if (typeof data === 'string') + data = Buffer.from(data, 'latin1'); + try { + const decoder = new TextDecoder(this); + return decoder.decode(data); + } catch {} + }, +}; + +function convertToUTF8(data, charset, hint) { + const decode = getDecoder(charset); + if (decode) + return decode(data, hint); +} + +function basename(path) { + if (typeof path !== 'string') + return ''; + for (let i = path.length - 1; i >= 0; --i) { + switch (path.charCodeAt(i)) { + case 0x2F: // '/' + case 0x5C: // '\' + path = path.slice(i + 1); + return (path === '..' || path === '.' ? '' : path); + } + } + return (path === '..' || path === '.' ? '' : path); +} + +const TOKEN = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +]; + +const QDTEXT = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, +]; + +const CHARSET = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +]; + +const EXTENDED_VALUE = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +]; + +/* eslint-disable no-multi-spaces */ +const HEX_VALUES = [ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, +]; +/* eslint-enable no-multi-spaces */ + +module.exports = { + basename, + convertToUTF8, + getDecoder, + parseContentType, + parseDisposition, +}; diff --git a/node_modules/busboy/package.json b/node_modules/busboy/package.json new file mode 100644 index 0000000..ac2577f --- /dev/null +++ b/node_modules/busboy/package.json @@ -0,0 +1,22 @@ +{ "name": "busboy", + "version": "1.6.0", + "author": "Brian White <mscdex@mscdex.net>", + "description": "A streaming parser for HTML form data for node.js", + "main": "./lib/index.js", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "devDependencies": { + "@mscdex/eslint-config": "^1.1.0", + "eslint": "^7.32.0" + }, + "scripts": { + "test": "node test/test.js", + "lint": "eslint --cache --report-unused-disable-directives --ext=.js .eslintrc.js lib test bench", + "lint:fix": "npm run lint -- --fix" + }, + "engines": { "node": ">=10.16.0" }, + "keywords": [ "uploads", "forms", "multipart", "form-data" ], + "licenses": [ { "type": "MIT", "url": "http://github.com/mscdex/busboy/raw/master/LICENSE" } ], + "repository": { "type": "git", "url": "http://github.com/mscdex/busboy.git" } +} diff --git a/node_modules/busboy/test/common.js b/node_modules/busboy/test/common.js new file mode 100644 index 0000000..fb82ad8 --- /dev/null +++ b/node_modules/busboy/test/common.js @@ -0,0 +1,109 @@ +'use strict'; + +const assert = require('assert'); +const { inspect } = require('util'); + +const mustCallChecks = []; + +function noop() {} + +function runCallChecks(exitCode) { + if (exitCode !== 0) return; + + const failed = mustCallChecks.filter((context) => { + if ('minimum' in context) { + context.messageSegment = `at least ${context.minimum}`; + return context.actual < context.minimum; + } + context.messageSegment = `exactly ${context.exact}`; + return context.actual !== context.exact; + }); + + failed.forEach((context) => { + console.error('Mismatched %s function calls. Expected %s, actual %d.', + context.name, + context.messageSegment, + context.actual); + console.error(context.stack.split('\n').slice(2).join('\n')); + }); + + if (failed.length) + process.exit(1); +} + +function mustCall(fn, exact) { + return _mustCallInner(fn, exact, 'exact'); +} + +function mustCallAtLeast(fn, minimum) { + return _mustCallInner(fn, minimum, 'minimum'); +} + +function _mustCallInner(fn, criteria = 1, field) { + if (process._exiting) + throw new Error('Cannot use common.mustCall*() in process exit handler'); + + if (typeof fn === 'number') { + criteria = fn; + fn = noop; + } else if (fn === undefined) { + fn = noop; + } + + if (typeof criteria !== 'number') + throw new TypeError(`Invalid ${field} value: ${criteria}`); + + const context = { + [field]: criteria, + actual: 0, + stack: inspect(new Error()), + name: fn.name || '<anonymous>' + }; + + // Add the exit listener only once to avoid listener leak warnings + if (mustCallChecks.length === 0) + process.on('exit', runCallChecks); + + mustCallChecks.push(context); + + function wrapped(...args) { + ++context.actual; + return fn.call(this, ...args); + } + // TODO: remove origFn? + wrapped.origFn = fn; + + return wrapped; +} + +function getCallSite(top) { + const originalStackFormatter = Error.prepareStackTrace; + Error.prepareStackTrace = (err, stack) => + `${stack[0].getFileName()}:${stack[0].getLineNumber()}`; + const err = new Error(); + Error.captureStackTrace(err, top); + // With the V8 Error API, the stack is not formatted until it is accessed + // eslint-disable-next-line no-unused-expressions + err.stack; + Error.prepareStackTrace = originalStackFormatter; + return err.stack; +} + +function mustNotCall(msg) { + const callSite = getCallSite(mustNotCall); + return function mustNotCall(...args) { + args = args.map(inspect).join(', '); + const argsInfo = (args.length > 0 + ? `\ncalled with arguments: ${args}` + : ''); + assert.fail( + `${msg || 'function should not have been called'} at ${callSite}` + + argsInfo); + }; +} + +module.exports = { + mustCall, + mustCallAtLeast, + mustNotCall, +}; diff --git a/node_modules/busboy/test/test-types-multipart-charsets.js b/node_modules/busboy/test/test-types-multipart-charsets.js new file mode 100644 index 0000000..ed9c38a --- /dev/null +++ b/node_modules/busboy/test/test-types-multipart-charsets.js @@ -0,0 +1,94 @@ +'use strict'; + +const assert = require('assert'); +const { inspect } = require('util'); + +const { mustCall } = require(`${__dirname}/common.js`); + +const busboy = require('..'); + +const input = Buffer.from([ + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + 'name="upload_file_0"; filename="テスト.dat"', + 'Content-Type: application/octet-stream', + '', + 'A'.repeat(1023), + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--' +].join('\r\n')); +const boundary = '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k'; +const expected = [ + { type: 'file', + name: 'upload_file_0', + data: Buffer.from('A'.repeat(1023)), + info: { + filename: 'テスト.dat', + encoding: '7bit', + mimeType: 'application/octet-stream', + }, + limited: false, + }, +]; +const bb = busboy({ + defParamCharset: 'utf8', + headers: { + 'content-type': `multipart/form-data; boundary=${boundary}`, + } +}); +const results = []; + +bb.on('field', (name, val, info) => { + results.push({ type: 'field', name, val, info }); +}); + +bb.on('file', (name, stream, info) => { + const data = []; + let nb = 0; + const file = { + type: 'file', + name, + data: null, + info, + limited: false, + }; + results.push(file); + stream.on('data', (d) => { + data.push(d); + nb += d.length; + }).on('limit', () => { + file.limited = true; + }).on('close', () => { + file.data = Buffer.concat(data, nb); + assert.strictEqual(stream.truncated, file.limited); + }).once('error', (err) => { + file.err = err.message; + }); +}); + +bb.on('error', (err) => { + results.push({ error: err.message }); +}); + +bb.on('partsLimit', () => { + results.push('partsLimit'); +}); + +bb.on('filesLimit', () => { + results.push('filesLimit'); +}); + +bb.on('fieldsLimit', () => { + results.push('fieldsLimit'); +}); + +bb.on('close', mustCall(() => { + assert.deepStrictEqual( + results, + expected, + 'Results mismatch.\n' + + `Parsed: ${inspect(results)}\n` + + `Expected: ${inspect(expected)}` + ); +})); + +bb.end(input); diff --git a/node_modules/busboy/test/test-types-multipart-stream-pause.js b/node_modules/busboy/test/test-types-multipart-stream-pause.js new file mode 100644 index 0000000..df7268a --- /dev/null +++ b/node_modules/busboy/test/test-types-multipart-stream-pause.js @@ -0,0 +1,102 @@ +'use strict'; + +const assert = require('assert'); +const { randomFillSync } = require('crypto'); +const { inspect } = require('util'); + +const busboy = require('..'); + +const { mustCall } = require('./common.js'); + +const BOUNDARY = 'u2KxIV5yF1y+xUspOQCCZopaVgeV6Jxihv35XQJmuTx8X3sh'; + +function formDataSection(key, value) { + return Buffer.from( + `\r\n--${BOUNDARY}` + + `\r\nContent-Disposition: form-data; name="${key}"` + + `\r\n\r\n${value}` + ); +} + +function formDataFile(key, filename, contentType) { + const buf = Buffer.allocUnsafe(100000); + return Buffer.concat([ + Buffer.from(`\r\n--${BOUNDARY}\r\n`), + Buffer.from(`Content-Disposition: form-data; name="${key}"` + + `; filename="${filename}"\r\n`), + Buffer.from(`Content-Type: ${contentType}\r\n\r\n`), + randomFillSync(buf) + ]); +} + +const reqChunks = [ + Buffer.concat([ + formDataFile('file', 'file.bin', 'application/octet-stream'), + formDataSection('foo', 'foo value'), + ]), + formDataSection('bar', 'bar value'), + Buffer.from(`\r\n--${BOUNDARY}--\r\n`) +]; +const bb = busboy({ + headers: { + 'content-type': `multipart/form-data; boundary=${BOUNDARY}` + } +}); +const expected = [ + { type: 'file', + name: 'file', + info: { + filename: 'file.bin', + encoding: '7bit', + mimeType: 'application/octet-stream', + }, + }, + { type: 'field', + name: 'foo', + val: 'foo value', + info: { + nameTruncated: false, + valueTruncated: false, + encoding: '7bit', + mimeType: 'text/plain', + }, + }, + { type: 'field', + name: 'bar', + val: 'bar value', + info: { + nameTruncated: false, + valueTruncated: false, + encoding: '7bit', + mimeType: 'text/plain', + }, + }, +]; +const results = []; + +bb.on('field', (name, val, info) => { + results.push({ type: 'field', name, val, info }); +}); + +bb.on('file', (name, stream, info) => { + results.push({ type: 'file', name, info }); + // Simulate a pipe where the destination is pausing (perhaps due to waiting + // for file system write to finish) + setTimeout(() => { + stream.resume(); + }, 10); +}); + +bb.on('close', mustCall(() => { + assert.deepStrictEqual( + results, + expected, + 'Results mismatch.\n' + + `Parsed: ${inspect(results)}\n` + + `Expected: ${inspect(expected)}` + ); +})); + +for (const chunk of reqChunks) + bb.write(chunk); +bb.end(); diff --git a/node_modules/busboy/test/test-types-multipart.js b/node_modules/busboy/test/test-types-multipart.js new file mode 100644 index 0000000..9755642 --- /dev/null +++ b/node_modules/busboy/test/test-types-multipart.js @@ -0,0 +1,1053 @@ +'use strict'; + +const assert = require('assert'); +const { inspect } = require('util'); + +const busboy = require('..'); + +const active = new Map(); + +const tests = [ + { source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="file_name_0"', + '', + 'super alpha file', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="file_name_1"', + '', + 'super beta file', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + 'name="upload_file_0"; filename="1k_a.dat"', + 'Content-Type: application/octet-stream', + '', + 'A'.repeat(1023), + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + 'name="upload_file_1"; filename="1k_b.dat"', + 'Content-Type: application/octet-stream', + '', + 'B'.repeat(1023), + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--' + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + expected: [ + { type: 'field', + name: 'file_name_0', + val: 'super alpha file', + info: { + nameTruncated: false, + valueTruncated: false, + encoding: '7bit', + mimeType: 'text/plain', + }, + }, + { type: 'field', + name: 'file_name_1', + val: 'super beta file', + info: { + nameTruncated: false, + valueTruncated: false, + encoding: '7bit', + mimeType: 'text/plain', + }, + }, + { type: 'file', + name: 'upload_file_0', + data: Buffer.from('A'.repeat(1023)), + info: { + filename: '1k_a.dat', + encoding: '7bit', + mimeType: 'application/octet-stream', + }, + limited: false, + }, + { type: 'file', + name: 'upload_file_1', + data: Buffer.from('B'.repeat(1023)), + info: { + filename: '1k_b.dat', + encoding: '7bit', + mimeType: 'application/octet-stream', + }, + limited: false, + }, + ], + what: 'Fields and files' + }, + { source: [ + ['------WebKitFormBoundaryTB2MiQ36fnSJlrhY', + 'Content-Disposition: form-data; name="cont"', + '', + 'some random content', + '------WebKitFormBoundaryTB2MiQ36fnSJlrhY', + 'Content-Disposition: form-data; name="pass"', + '', + 'some random pass', + '------WebKitFormBoundaryTB2MiQ36fnSJlrhY', + 'Content-Disposition: form-data; name=bit', + '', + '2', + '------WebKitFormBoundaryTB2MiQ36fnSJlrhY--' + ].join('\r\n') + ], + boundary: '----WebKitFormBoundaryTB2MiQ36fnSJlrhY', + expected: [ + { type: 'field', + name: 'cont', + val: 'some random content', + info: { + nameTruncated: false, + valueTruncated: false, + encoding: '7bit', + mimeType: 'text/plain', + }, + }, + { type: 'field', + name: 'pass', + val: 'some random pass', + info: { + nameTruncated: false, + valueTruncated: false, + encoding: '7bit', + mimeType: 'text/plain', + }, + }, + { type: 'field', + name: 'bit', + val: '2', + info: { + nameTruncated: false, + valueTruncated: false, + encoding: '7bit', + mimeType: 'text/plain', + }, + }, + ], + what: 'Fields only' + }, + { source: [ + '' + ], + boundary: '----WebKitFormBoundaryTB2MiQ36fnSJlrhY', + expected: [ + { error: 'Unexpected end of form' }, + ], + what: 'No fields and no files' + }, + { source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="file_name_0"', + '', + 'super alpha file', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + 'name="upload_file_0"; filename="1k_a.dat"', + 'Content-Type: application/octet-stream', + '', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--' + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + limits: { + fileSize: 13, + fieldSize: 5 + }, + expected: [ + { type: 'field', + name: 'file_name_0', + val: 'super', + info: { + nameTruncated: false, + valueTruncated: true, + encoding: '7bit', + mimeType: 'text/plain', + }, + }, + { type: 'file', + name: 'upload_file_0', + data: Buffer.from('ABCDEFGHIJKLM'), + info: { + filename: '1k_a.dat', + encoding: '7bit', + mimeType: 'application/octet-stream', + }, + limited: true, + }, + ], + what: 'Fields and files (limits)' + }, + { source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="file_name_0"', + '', + 'super alpha file', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + 'name="upload_file_0"; filename="1k_a.dat"', + 'Content-Type: application/octet-stream', + '', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--' + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + limits: { + files: 0 + }, + expected: [ + { type: 'field', + name: 'file_name_0', + val: 'super alpha file', + info: { + nameTruncated: false, + valueTruncated: false, + encoding: '7bit', + mimeType: 'text/plain', + }, + }, + 'filesLimit', + ], + what: 'Fields and files (limits: 0 files)' + }, + { source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="file_name_0"', + '', + 'super alpha file', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="file_name_1"', + '', + 'super beta file', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + 'name="upload_file_0"; filename="1k_a.dat"', + 'Content-Type: application/octet-stream', + '', + 'A'.repeat(1023), + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + 'name="upload_file_1"; filename="1k_b.dat"', + 'Content-Type: application/octet-stream', + '', + 'B'.repeat(1023), + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--' + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + expected: [ + { type: 'field', + name: 'file_name_0', + val: 'super alpha file', + info: { + nameTruncated: false, + valueTruncated: false, + encoding: '7bit', + mimeType: 'text/plain', + }, + }, + { type: 'field', + name: 'file_name_1', + val: 'super beta file', + info: { + nameTruncated: false, + valueTruncated: false, + encoding: '7bit', + mimeType: 'text/plain', + }, + }, + ], + events: ['field'], + what: 'Fields and (ignored) files' + }, + { source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + 'name="upload_file_0"; filename="/tmp/1k_a.dat"', + 'Content-Type: application/octet-stream', + '', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + 'name="upload_file_1"; filename="C:\\files\\1k_b.dat"', + 'Content-Type: application/octet-stream', + '', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + 'name="upload_file_2"; filename="relative/1k_c.dat"', + 'Content-Type: application/octet-stream', + '', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--' + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + expected: [ + { type: 'file', + name: 'upload_file_0', + data: Buffer.from('ABCDEFGHIJKLMNOPQRSTUVWXYZ'), + info: { + filename: '1k_a.dat', + encoding: '7bit', + mimeType: 'application/octet-stream', + }, + limited: false, + }, + { type: 'file', + name: 'upload_file_1', + data: Buffer.from('ABCDEFGHIJKLMNOPQRSTUVWXYZ'), + info: { + filename: '1k_b.dat', + encoding: '7bit', + mimeType: 'application/octet-stream', + }, + limited: false, + }, + { type: 'file', + name: 'upload_file_2', + data: Buffer.from('ABCDEFGHIJKLMNOPQRSTUVWXYZ'), + info: { + filename: '1k_c.dat', + encoding: '7bit', + mimeType: 'application/octet-stream', + }, + limited: false, + }, + ], + what: 'Files with filenames containing paths' + }, + { source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + 'name="upload_file_0"; filename="/absolute/1k_a.dat"', + 'Content-Type: application/octet-stream', + '', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + 'name="upload_file_1"; filename="C:\\absolute\\1k_b.dat"', + 'Content-Type: application/octet-stream', + '', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + 'name="upload_file_2"; filename="relative/1k_c.dat"', + 'Content-Type: application/octet-stream', + '', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--' + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + preservePath: true, + expected: [ + { type: 'file', + name: 'upload_file_0', + data: Buffer.from('ABCDEFGHIJKLMNOPQRSTUVWXYZ'), + info: { + filename: '/absolute/1k_a.dat', + encoding: '7bit', + mimeType: 'application/octet-stream', + }, + limited: false, + }, + { type: 'file', + name: 'upload_file_1', + data: Buffer.from('ABCDEFGHIJKLMNOPQRSTUVWXYZ'), + info: { + filename: 'C:\\absolute\\1k_b.dat', + encoding: '7bit', + mimeType: 'application/octet-stream', + }, + limited: false, + }, + { type: 'file', + name: 'upload_file_2', + data: Buffer.from('ABCDEFGHIJKLMNOPQRSTUVWXYZ'), + info: { + filename: 'relative/1k_c.dat', + encoding: '7bit', + mimeType: 'application/octet-stream', + }, + limited: false, + }, + ], + what: 'Paths to be preserved through the preservePath option' + }, + { source: [ + ['------WebKitFormBoundaryTB2MiQ36fnSJlrhY', + 'Content-Disposition: form-data; name="cont"', + 'Content-Type: ', + '', + 'some random content', + '------WebKitFormBoundaryTB2MiQ36fnSJlrhY', + 'Content-Disposition: ', + '', + 'some random pass', + '------WebKitFormBoundaryTB2MiQ36fnSJlrhY--' + ].join('\r\n') + ], + boundary: '----WebKitFormBoundaryTB2MiQ36fnSJlrhY', + expected: [ + { type: 'field', + name: 'cont', + val: 'some random content', + info: { + nameTruncated: false, + valueTruncated: false, + encoding: '7bit', + mimeType: 'text/plain', + }, + }, + ], + what: 'Empty content-type and empty content-disposition' + }, + { source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + 'name="file"; filename*=utf-8\'\'n%C3%A4me.txt', + 'Content-Type: application/octet-stream', + '', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--' + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + expected: [ + { type: 'file', + name: 'file', + data: Buffer.from('ABCDEFGHIJKLMNOPQRSTUVWXYZ'), + info: { + filename: 'näme.txt', + encoding: '7bit', + mimeType: 'application/octet-stream', + }, + limited: false, + }, + ], + what: 'Unicode filenames' + }, + { source: [ + ['--asdasdasdasd\r\n', + 'Content-Type: text/plain\r\n', + 'Content-Disposition: form-data; name="foo"\r\n', + '\r\n', + 'asd\r\n', + '--asdasdasdasd--' + ].join(':)') + ], + boundary: 'asdasdasdasd', + expected: [ + { error: 'Malformed part header' }, + { error: 'Unexpected end of form' }, + ], + what: 'Stopped mid-header' + }, + { source: [ + ['------WebKitFormBoundaryTB2MiQ36fnSJlrhY', + 'Content-Disposition: form-data; name="cont"', + 'Content-Type: application/json', + '', + '{}', + '------WebKitFormBoundaryTB2MiQ36fnSJlrhY--', + ].join('\r\n') + ], + boundary: '----WebKitFormBoundaryTB2MiQ36fnSJlrhY', + expected: [ + { type: 'field', + name: 'cont', + val: '{}', + info: { + nameTruncated: false, + valueTruncated: false, + encoding: '7bit', + mimeType: 'application/json', + }, + }, + ], + what: 'content-type for fields' + }, + { source: [ + '------WebKitFormBoundaryTB2MiQ36fnSJlrhY--', + ], + boundary: '----WebKitFormBoundaryTB2MiQ36fnSJlrhY', + expected: [], + what: 'empty form' + }, + { source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + 'name=upload_file_0; filename="1k_a.dat"', + 'Content-Type: application/octet-stream', + 'Content-Transfer-Encoding: binary', + '', + '', + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + expected: [ + { type: 'file', + name: 'upload_file_0', + data: Buffer.alloc(0), + info: { + filename: '1k_a.dat', + encoding: 'binary', + mimeType: 'application/octet-stream', + }, + limited: false, + err: 'Unexpected end of form', + }, + { error: 'Unexpected end of form' }, + ], + what: 'Stopped mid-file #1' + }, + { source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + 'name=upload_file_0; filename="1k_a.dat"', + 'Content-Type: application/octet-stream', + '', + 'a', + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + expected: [ + { type: 'file', + name: 'upload_file_0', + data: Buffer.from('a'), + info: { + filename: '1k_a.dat', + encoding: '7bit', + mimeType: 'application/octet-stream', + }, + limited: false, + err: 'Unexpected end of form', + }, + { error: 'Unexpected end of form' }, + ], + what: 'Stopped mid-file #2' + }, + { source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + 'name="upload_file_0"; filename="notes.txt"', + 'Content-Type: text/plain; charset=utf8', + '', + 'a', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--', + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + expected: [ + { type: 'file', + name: 'upload_file_0', + data: Buffer.from('a'), + info: { + filename: 'notes.txt', + encoding: '7bit', + mimeType: 'text/plain', + }, + limited: false, + }, + ], + what: 'Text file with charset' + }, + { source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + 'name="upload_file_0"; filename="notes.txt"', + 'Content-Type: ', + ' text/plain; charset=utf8', + '', + 'a', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--', + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + expected: [ + { type: 'file', + name: 'upload_file_0', + data: Buffer.from('a'), + info: { + filename: 'notes.txt', + encoding: '7bit', + mimeType: 'text/plain', + }, + limited: false, + }, + ], + what: 'Folded header value' + }, + { source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Type: text/plain; charset=utf8', + '', + 'a', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--', + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + expected: [], + what: 'No Content-Disposition' + }, + { source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="file_name_0"', + '', + 'a'.repeat(64 * 1024), + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + 'name="upload_file_0"; filename="notes.txt"', + 'Content-Type: ', + ' text/plain; charset=utf8', + '', + 'bc', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--', + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + limits: { + fieldSize: Infinity, + }, + expected: [ + { type: 'file', + name: 'upload_file_0', + data: Buffer.from('bc'), + info: { + filename: 'notes.txt', + encoding: '7bit', + mimeType: 'text/plain', + }, + limited: false, + }, + ], + events: [ 'file' ], + what: 'Skip field parts if no listener' + }, + { source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="file_name_0"', + '', + 'a', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + 'name="upload_file_0"; filename="notes.txt"', + 'Content-Type: ', + ' text/plain; charset=utf8', + '', + 'bc', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--', + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + limits: { + parts: 1, + }, + expected: [ + { type: 'field', + name: 'file_name_0', + val: 'a', + info: { + nameTruncated: false, + valueTruncated: false, + encoding: '7bit', + mimeType: 'text/plain', + }, + }, + 'partsLimit', + ], + what: 'Parts limit' + }, + { source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="file_name_0"', + '', + 'a', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="file_name_1"', + '', + 'b', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--', + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + limits: { + fields: 1, + }, + expected: [ + { type: 'field', + name: 'file_name_0', + val: 'a', + info: { + nameTruncated: false, + valueTruncated: false, + encoding: '7bit', + mimeType: 'text/plain', + }, + }, + 'fieldsLimit', + ], + what: 'Fields limit' + }, + { source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + 'name="upload_file_0"; filename="notes.txt"', + 'Content-Type: text/plain; charset=utf8', + '', + 'ab', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + 'name="upload_file_1"; filename="notes2.txt"', + 'Content-Type: text/plain; charset=utf8', + '', + 'cd', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--', + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + limits: { + files: 1, + }, + expected: [ + { type: 'file', + name: 'upload_file_0', + data: Buffer.from('ab'), + info: { + filename: 'notes.txt', + encoding: '7bit', + mimeType: 'text/plain', + }, + limited: false, + }, + 'filesLimit', + ], + what: 'Files limit' + }, + { source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + `name="upload_file_0"; filename="${'a'.repeat(64 * 1024)}.txt"`, + 'Content-Type: text/plain; charset=utf8', + '', + 'ab', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + 'name="upload_file_1"; filename="notes2.txt"', + 'Content-Type: text/plain; charset=utf8', + '', + 'cd', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--', + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + expected: [ + { error: 'Malformed part header' }, + { type: 'file', + name: 'upload_file_1', + data: Buffer.from('cd'), + info: { + filename: 'notes2.txt', + encoding: '7bit', + mimeType: 'text/plain', + }, + limited: false, + }, + ], + what: 'Oversized part header' + }, + { source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + 'name="upload_file_0"; filename="notes.txt"', + 'Content-Type: text/plain; charset=utf8', + '', + 'a'.repeat(31) + '\r', + ].join('\r\n'), + 'b'.repeat(40), + '\r\n-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--', + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + fileHwm: 32, + expected: [ + { type: 'file', + name: 'upload_file_0', + data: Buffer.from('a'.repeat(31) + '\r' + 'b'.repeat(40)), + info: { + filename: 'notes.txt', + encoding: '7bit', + mimeType: 'text/plain', + }, + limited: false, + }, + ], + what: 'Lookbehind data should not stall file streams' + }, + { source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + `name="upload_file_0"; filename="${'a'.repeat(8 * 1024)}.txt"`, + 'Content-Type: text/plain; charset=utf8', + '', + 'ab', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + `name="upload_file_1"; filename="${'b'.repeat(8 * 1024)}.txt"`, + 'Content-Type: text/plain; charset=utf8', + '', + 'cd', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + `name="upload_file_2"; filename="${'c'.repeat(8 * 1024)}.txt"`, + 'Content-Type: text/plain; charset=utf8', + '', + 'ef', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--', + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + expected: [ + { type: 'file', + name: 'upload_file_0', + data: Buffer.from('ab'), + info: { + filename: `${'a'.repeat(8 * 1024)}.txt`, + encoding: '7bit', + mimeType: 'text/plain', + }, + limited: false, + }, + { type: 'file', + name: 'upload_file_1', + data: Buffer.from('cd'), + info: { + filename: `${'b'.repeat(8 * 1024)}.txt`, + encoding: '7bit', + mimeType: 'text/plain', + }, + limited: false, + }, + { type: 'file', + name: 'upload_file_2', + data: Buffer.from('ef'), + info: { + filename: `${'c'.repeat(8 * 1024)}.txt`, + encoding: '7bit', + mimeType: 'text/plain', + }, + limited: false, + }, + ], + what: 'Header size limit should be per part' + }, + { source: [ + '\r\n--d1bf46b3-aa33-4061-b28d-6c5ced8b08ee\r\n', + 'Content-Type: application/gzip\r\n' + + 'Content-Encoding: gzip\r\n' + + 'Content-Disposition: form-data; name=batch-1; filename=batch-1' + + '\r\n\r\n', + '\r\n--d1bf46b3-aa33-4061-b28d-6c5ced8b08ee--', + ], + boundary: 'd1bf46b3-aa33-4061-b28d-6c5ced8b08ee', + expected: [ + { type: 'file', + name: 'batch-1', + data: Buffer.alloc(0), + info: { + filename: 'batch-1', + encoding: '7bit', + mimeType: 'application/gzip', + }, + limited: false, + }, + ], + what: 'Empty part' + }, +]; + +for (const test of tests) { + active.set(test, 1); + + const { what, boundary, events, limits, preservePath, fileHwm } = test; + const bb = busboy({ + fileHwm, + limits, + preservePath, + headers: { + 'content-type': `multipart/form-data; boundary=${boundary}`, + } + }); + const results = []; + + if (events === undefined || events.includes('field')) { + bb.on('field', (name, val, info) => { + results.push({ type: 'field', name, val, info }); + }); + } + + if (events === undefined || events.includes('file')) { + bb.on('file', (name, stream, info) => { + const data = []; + let nb = 0; + const file = { + type: 'file', + name, + data: null, + info, + limited: false, + }; + results.push(file); + stream.on('data', (d) => { + data.push(d); + nb += d.length; + }).on('limit', () => { + file.limited = true; + }).on('close', () => { + file.data = Buffer.concat(data, nb); + assert.strictEqual(stream.truncated, file.limited); + }).once('error', (err) => { + file.err = err.message; + }); + }); + } + + bb.on('error', (err) => { + results.push({ error: err.message }); + }); + + bb.on('partsLimit', () => { + results.push('partsLimit'); + }); + + bb.on('filesLimit', () => { + results.push('filesLimit'); + }); + + bb.on('fieldsLimit', () => { + results.push('fieldsLimit'); + }); + + bb.on('close', () => { + active.delete(test); + + assert.deepStrictEqual( + results, + test.expected, + `[${what}] Results mismatch.\n` + + `Parsed: ${inspect(results)}\n` + + `Expected: ${inspect(test.expected)}` + ); + }); + + for (const src of test.source) { + const buf = (typeof src === 'string' ? Buffer.from(src, 'utf8') : src); + bb.write(buf); + } + bb.end(); +} + +// Byte-by-byte versions +for (let test of tests) { + test = { ...test }; + test.what += ' (byte-by-byte)'; + active.set(test, 1); + + const { what, boundary, events, limits, preservePath, fileHwm } = test; + const bb = busboy({ + fileHwm, + limits, + preservePath, + headers: { + 'content-type': `multipart/form-data; boundary=${boundary}`, + } + }); + const results = []; + + if (events === undefined || events.includes('field')) { + bb.on('field', (name, val, info) => { + results.push({ type: 'field', name, val, info }); + }); + } + + if (events === undefined || events.includes('file')) { + bb.on('file', (name, stream, info) => { + const data = []; + let nb = 0; + const file = { + type: 'file', + name, + data: null, + info, + limited: false, + }; + results.push(file); + stream.on('data', (d) => { + data.push(d); + nb += d.length; + }).on('limit', () => { + file.limited = true; + }).on('close', () => { + file.data = Buffer.concat(data, nb); + assert.strictEqual(stream.truncated, file.limited); + }).once('error', (err) => { + file.err = err.message; + }); + }); + } + + bb.on('error', (err) => { + results.push({ error: err.message }); + }); + + bb.on('partsLimit', () => { + results.push('partsLimit'); + }); + + bb.on('filesLimit', () => { + results.push('filesLimit'); + }); + + bb.on('fieldsLimit', () => { + results.push('fieldsLimit'); + }); + + bb.on('close', () => { + active.delete(test); + + assert.deepStrictEqual( + results, + test.expected, + `[${what}] Results mismatch.\n` + + `Parsed: ${inspect(results)}\n` + + `Expected: ${inspect(test.expected)}` + ); + }); + + for (const src of test.source) { + const buf = (typeof src === 'string' ? Buffer.from(src, 'utf8') : src); + for (let i = 0; i < buf.length; ++i) + bb.write(buf.slice(i, i + 1)); + } + bb.end(); +} + +{ + let exception = false; + process.once('uncaughtException', (ex) => { + exception = true; + throw ex; + }); + process.on('exit', () => { + if (exception || active.size === 0) + return; + process.exitCode = 1; + console.error('=========================='); + console.error(`${active.size} test(s) did not finish:`); + console.error('=========================='); + console.error(Array.from(active.keys()).map((v) => v.what).join('\n')); + }); +} diff --git a/node_modules/busboy/test/test-types-urlencoded.js b/node_modules/busboy/test/test-types-urlencoded.js new file mode 100644 index 0000000..c35962b --- /dev/null +++ b/node_modules/busboy/test/test-types-urlencoded.js @@ -0,0 +1,488 @@ +'use strict'; + +const assert = require('assert'); +const { transcode } = require('buffer'); +const { inspect } = require('util'); + +const busboy = require('..'); + +const active = new Map(); + +const tests = [ + { source: ['foo'], + expected: [ + ['foo', + '', + { nameTruncated: false, + valueTruncated: false, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ], + what: 'Unassigned value' + }, + { source: ['foo=bar'], + expected: [ + ['foo', + 'bar', + { nameTruncated: false, + valueTruncated: false, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ], + what: 'Assigned value' + }, + { source: ['foo&bar=baz'], + expected: [ + ['foo', + '', + { nameTruncated: false, + valueTruncated: false, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ['bar', + 'baz', + { nameTruncated: false, + valueTruncated: false, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ], + what: 'Unassigned and assigned value' + }, + { source: ['foo=bar&baz'], + expected: [ + ['foo', + 'bar', + { nameTruncated: false, + valueTruncated: false, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ['baz', + '', + { nameTruncated: false, + valueTruncated: false, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ], + what: 'Assigned and unassigned value' + }, + { source: ['foo=bar&baz=bla'], + expected: [ + ['foo', + 'bar', + { nameTruncated: false, + valueTruncated: false, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ['baz', + 'bla', + { nameTruncated: false, + valueTruncated: false, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ], + what: 'Two assigned values' + }, + { source: ['foo&bar'], + expected: [ + ['foo', + '', + { nameTruncated: false, + valueTruncated: false, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ['bar', + '', + { nameTruncated: false, + valueTruncated: false, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ], + what: 'Two unassigned values' + }, + { source: ['foo&bar&'], + expected: [ + ['foo', + '', + { nameTruncated: false, + valueTruncated: false, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ['bar', + '', + { nameTruncated: false, + valueTruncated: false, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ], + what: 'Two unassigned values and ampersand' + }, + { source: ['foo+1=bar+baz%2Bquux'], + expected: [ + ['foo 1', + 'bar baz+quux', + { nameTruncated: false, + valueTruncated: false, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ], + what: 'Assigned key and value with (plus) space' + }, + { source: ['foo=bar%20baz%21'], + expected: [ + ['foo', + 'bar baz!', + { nameTruncated: false, + valueTruncated: false, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ], + what: 'Assigned value with encoded bytes' + }, + { source: ['foo%20bar=baz%20bla%21'], + expected: [ + ['foo bar', + 'baz bla!', + { nameTruncated: false, + valueTruncated: false, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ], + what: 'Assigned value with encoded bytes #2' + }, + { source: ['foo=bar%20baz%21&num=1000'], + expected: [ + ['foo', + 'bar baz!', + { nameTruncated: false, + valueTruncated: false, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ['num', + '1000', + { nameTruncated: false, + valueTruncated: false, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ], + what: 'Two assigned values, one with encoded bytes' + }, + { source: [ + Array.from(transcode(Buffer.from('foo'), 'utf8', 'utf16le')).map( + (n) => `%${n.toString(16).padStart(2, '0')}` + ).join(''), + '=', + Array.from(transcode(Buffer.from('😀!'), 'utf8', 'utf16le')).map( + (n) => `%${n.toString(16).padStart(2, '0')}` + ).join(''), + ], + expected: [ + ['foo', + '😀!', + { nameTruncated: false, + valueTruncated: false, + encoding: 'UTF-16LE', + mimeType: 'text/plain' }, + ], + ], + charset: 'UTF-16LE', + what: 'Encoded value with multi-byte charset' + }, + { source: [ + 'foo=<', + Array.from(transcode(Buffer.from('©:^þ'), 'utf8', 'latin1')).map( + (n) => `%${n.toString(16).padStart(2, '0')}` + ).join(''), + ], + expected: [ + ['foo', + '<©:^þ', + { nameTruncated: false, + valueTruncated: false, + encoding: 'ISO-8859-1', + mimeType: 'text/plain' }, + ], + ], + charset: 'ISO-8859-1', + what: 'Encoded value with single-byte, ASCII-compatible, non-UTF8 charset' + }, + { source: ['foo=bar&baz=bla'], + expected: [], + what: 'Limits: zero fields', + limits: { fields: 0 } + }, + { source: ['foo=bar&baz=bla'], + expected: [ + ['foo', + 'bar', + { nameTruncated: false, + valueTruncated: false, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ], + what: 'Limits: one field', + limits: { fields: 1 } + }, + { source: ['foo=bar&baz=bla'], + expected: [ + ['foo', + 'bar', + { nameTruncated: false, + valueTruncated: false, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ['baz', + 'bla', + { nameTruncated: false, + valueTruncated: false, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ], + what: 'Limits: field part lengths match limits', + limits: { fieldNameSize: 3, fieldSize: 3 } + }, + { source: ['foo=bar&baz=bla'], + expected: [ + ['fo', + 'bar', + { nameTruncated: true, + valueTruncated: false, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ['ba', + 'bla', + { nameTruncated: true, + valueTruncated: false, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ], + what: 'Limits: truncated field name', + limits: { fieldNameSize: 2 } + }, + { source: ['foo=bar&baz=bla'], + expected: [ + ['foo', + 'ba', + { nameTruncated: false, + valueTruncated: true, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ['baz', + 'bl', + { nameTruncated: false, + valueTruncated: true, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ], + what: 'Limits: truncated field value', + limits: { fieldSize: 2 } + }, + { source: ['foo=bar&baz=bla'], + expected: [ + ['fo', + 'ba', + { nameTruncated: true, + valueTruncated: true, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ['ba', + 'bl', + { nameTruncated: true, + valueTruncated: true, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ], + what: 'Limits: truncated field name and value', + limits: { fieldNameSize: 2, fieldSize: 2 } + }, + { source: ['foo=bar&baz=bla'], + expected: [ + ['fo', + '', + { nameTruncated: true, + valueTruncated: true, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ['ba', + '', + { nameTruncated: true, + valueTruncated: true, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ], + what: 'Limits: truncated field name and zero value limit', + limits: { fieldNameSize: 2, fieldSize: 0 } + }, + { source: ['foo=bar&baz=bla'], + expected: [ + ['', + '', + { nameTruncated: true, + valueTruncated: true, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ['', + '', + { nameTruncated: true, + valueTruncated: true, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ], + what: 'Limits: truncated zero field name and zero value limit', + limits: { fieldNameSize: 0, fieldSize: 0 } + }, + { source: ['&'], + expected: [], + what: 'Ampersand' + }, + { source: ['&&&&&'], + expected: [], + what: 'Many ampersands' + }, + { source: ['='], + expected: [ + ['', + '', + { nameTruncated: false, + valueTruncated: false, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ], + what: 'Assigned value, empty name and value' + }, + { source: [''], + expected: [], + what: 'Nothing' + }, +]; + +for (const test of tests) { + active.set(test, 1); + + const { what } = test; + const charset = test.charset || 'utf-8'; + const bb = busboy({ + limits: test.limits, + headers: { + 'content-type': `application/x-www-form-urlencoded; charset=${charset}`, + }, + }); + const results = []; + + bb.on('field', (key, val, info) => { + results.push([key, val, info]); + }); + + bb.on('file', () => { + throw new Error(`[${what}] Unexpected file`); + }); + + bb.on('close', () => { + active.delete(test); + + assert.deepStrictEqual( + results, + test.expected, + `[${what}] Results mismatch.\n` + + `Parsed: ${inspect(results)}\n` + + `Expected: ${inspect(test.expected)}` + ); + }); + + for (const src of test.source) { + const buf = (typeof src === 'string' ? Buffer.from(src, 'utf8') : src); + bb.write(buf); + } + bb.end(); +} + +// Byte-by-byte versions +for (let test of tests) { + test = { ...test }; + test.what += ' (byte-by-byte)'; + active.set(test, 1); + + const { what } = test; + const charset = test.charset || 'utf-8'; + const bb = busboy({ + limits: test.limits, + headers: { + 'content-type': `application/x-www-form-urlencoded; charset="${charset}"`, + }, + }); + const results = []; + + bb.on('field', (key, val, info) => { + results.push([key, val, info]); + }); + + bb.on('file', () => { + throw new Error(`[${what}] Unexpected file`); + }); + + bb.on('close', () => { + active.delete(test); + + assert.deepStrictEqual( + results, + test.expected, + `[${what}] Results mismatch.\n` + + `Parsed: ${inspect(results)}\n` + + `Expected: ${inspect(test.expected)}` + ); + }); + + for (const src of test.source) { + const buf = (typeof src === 'string' ? Buffer.from(src, 'utf8') : src); + for (let i = 0; i < buf.length; ++i) + bb.write(buf.slice(i, i + 1)); + } + bb.end(); +} + +{ + let exception = false; + process.once('uncaughtException', (ex) => { + exception = true; + throw ex; + }); + process.on('exit', () => { + if (exception || active.size === 0) + return; + process.exitCode = 1; + console.error('=========================='); + console.error(`${active.size} test(s) did not finish:`); + console.error('=========================='); + console.error(Array.from(active.keys()).map((v) => v.what).join('\n')); + }); +} diff --git a/node_modules/busboy/test/test.js b/node_modules/busboy/test/test.js new file mode 100644 index 0000000..d0380f2 --- /dev/null +++ b/node_modules/busboy/test/test.js @@ -0,0 +1,20 @@ +'use strict'; + +const { spawnSync } = require('child_process'); +const { readdirSync } = require('fs'); +const { join } = require('path'); + +const files = readdirSync(__dirname).sort(); +for (const filename of files) { + if (filename.startsWith('test-')) { + const path = join(__dirname, filename); + console.log(`> Running ${filename} ...`); + const result = spawnSync(`${process.argv0} ${path}`, { + shell: true, + stdio: 'inherit', + windowsHide: true + }); + if (result.status !== 0) + process.exitCode = 1; + } +} |