pax_global_header00006660000000000000000000000064142275302040014511gustar00rootroot0000000000000052 comment=9aadb7afbcb8c70c81c93b1018313c1b1835afb0 busboy-1.6.0/000077500000000000000000000000001422753020400130205ustar00rootroot00000000000000busboy-1.6.0/.eslintrc.js000066400000000000000000000001111422753020400152500ustar00rootroot00000000000000'use strict'; module.exports = { extends: '@mscdex/eslint-config', }; busboy-1.6.0/.github/000077500000000000000000000000001422753020400143605ustar00rootroot00000000000000busboy-1.6.0/.github/workflows/000077500000000000000000000000001422753020400164155ustar00rootroot00000000000000busboy-1.6.0/.github/workflows/ci.yml000066400000000000000000000010061422753020400175300ustar00rootroot00000000000000name: 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 busboy-1.6.0/.github/workflows/lint.yml000066400000000000000000000007271422753020400201140ustar00rootroot00000000000000name: 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 busboy-1.6.0/LICENSE000066400000000000000000000020531422753020400140250ustar00rootroot00000000000000Copyright 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.busboy-1.6.0/README.md000066400000000000000000000173661422753020400143140ustar00rootroot00000000000000# 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(`


`); } }).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. busboy-1.6.0/bench/000077500000000000000000000000001422753020400140775ustar00rootroot00000000000000busboy-1.6.0/bench/bench-multipart-fields-100mb-big.js000066400000000000000000000061521422753020400223570ustar00rootroot00000000000000'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); } busboy-1.6.0/bench/bench-multipart-fields-100mb-small.js000066400000000000000000000061411422753020400227240ustar00rootroot00000000000000'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); } busboy-1.6.0/bench/bench-multipart-files-100mb-big.js000066400000000000000000000064251422753020400222160ustar00rootroot00000000000000'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); } busboy-1.6.0/bench/bench-multipart-files-100mb-small.js000066400000000000000000000064141422753020400225630ustar00rootroot00000000000000'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); } busboy-1.6.0/bench/bench-urlencoded-fields-100pairs-small.js000066400000000000000000000042431422753020400235500ustar00rootroot00000000000000'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); } busboy-1.6.0/bench/bench-urlencoded-fields-900pairs-small-alt.js000066400000000000000000000034201422753020400243320ustar00rootroot00000000000000'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); } busboy-1.6.0/lib/000077500000000000000000000000001422753020400135665ustar00rootroot00000000000000busboy-1.6.0/lib/index.js000066400000000000000000000030501422753020400152310ustar00rootroot00000000000000'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); }; busboy-1.6.0/lib/types/000077500000000000000000000000001422753020400147325ustar00rootroot00000000000000busboy-1.6.0/lib/types/multipart.js000066400000000000000000000450511422753020400173160ustar00rootroot00000000000000'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; busboy-1.6.0/lib/types/urlencoded.js000066400000000000000000000247211422753020400174220ustar00rootroot00000000000000'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; busboy-1.6.0/lib/utils.js000066400000000000000000000375341422753020400153000ustar00rootroot00000000000000'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, }; busboy-1.6.0/package.json000066400000000000000000000014521422753020400153100ustar00rootroot00000000000000{ "name": "busboy", "version": "1.6.0", "author": "Brian White ", "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" } } busboy-1.6.0/test/000077500000000000000000000000001422753020400137775ustar00rootroot00000000000000busboy-1.6.0/test/common.js000066400000000000000000000053571422753020400156370ustar00rootroot00000000000000'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 || '' }; // 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, }; busboy-1.6.0/test/test-types-multipart-charsets.js000066400000000000000000000040151422753020400223070ustar00rootroot00000000000000'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); busboy-1.6.0/test/test-types-multipart-stream-pause.js000066400000000000000000000045011422753020400231010ustar00rootroot00000000000000'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(); busboy-1.6.0/test/test-types-multipart.js000066400000000000000000000724751422753020400205140ustar00rootroot00000000000000'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')); }); } busboy-1.6.0/test/test-types-urlencoded.js000066400000000000000000000257121422753020400206070ustar00rootroot00000000000000'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')); }); } busboy-1.6.0/test/test.js000066400000000000000000000010331422753020400153110ustar00rootroot00000000000000'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; } }