diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-21 20:56:19 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-21 20:56:19 +0000 |
commit | 0b6210cd37b68b94252cb798598b12974a20e1c1 (patch) | |
tree | e371686554a877842d95aa94f100bee552ff2a8e /fastify-busboy | |
parent | Initial commit. (diff) | |
download | node-undici-0b6210cd37b68b94252cb798598b12974a20e1c1.tar.xz node-undici-0b6210cd37b68b94252cb798598b12974a20e1c1.zip |
Adding upstream version 5.28.2+dfsg1+~cs23.11.12.3.upstream/5.28.2+dfsg1+_cs23.11.12.3upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'fastify-busboy')
116 files changed, 6996 insertions, 0 deletions
diff --git a/fastify-busboy/.eslintrc.js b/fastify-busboy/.eslintrc.js new file mode 100644 index 0000000..4b904cd --- /dev/null +++ b/fastify-busboy/.eslintrc.js @@ -0,0 +1,27 @@ +module.exports = { + ignorePatterns: [ + 'bench', + 'deps/encoding' + ], + extends: [ + 'standard', + 'eslint:recommended', + 'plugin:n/recommended' + ], + rules: { + 'no-unused-vars': [1, { vars: 'all', args: 'none' }], + 'n/no-missing-require': 1, + 'no-constant-condition': 'off', + 'no-var': 'off', + 'no-redeclare': 1, + 'no-fallthrough': 1, + 'no-control-regex': 1, + 'no-empty': 'off', + 'prefer-const': 'off' + }, + env: { + node: true, + mocha: true, + es6: true + } +} diff --git a/fastify-busboy/.gitattributes b/fastify-busboy/.gitattributes new file mode 100644 index 0000000..49b4f89 --- /dev/null +++ b/fastify-busboy/.gitattributes @@ -0,0 +1,2 @@ +* text=false +*.header -crlf diff --git a/fastify-busboy/.github/dependabot.yml b/fastify-busboy/.github/dependabot.yml new file mode 100644 index 0000000..dfa7fa6 --- /dev/null +++ b/fastify-busboy/.github/dependabot.yml @@ -0,0 +1,13 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + open-pull-requests-limit: 10 + + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 diff --git a/fastify-busboy/.github/workflows/ci.yml b/fastify-busboy/.github/workflows/ci.yml new file mode 100644 index 0000000..babd56d --- /dev/null +++ b/fastify-busboy/.github/workflows/ci.yml @@ -0,0 +1,22 @@ +name: CI + +on: + push: + branches: + - main + - master + - next + - 'v*' + paths-ignore: + - 'docs/**' + - '*.md' + pull_request: + paths-ignore: + - 'docs/**' + - '*.md' + +jobs: + test: + uses: fastify/workflows/.github/workflows/plugins-ci.yml@v3 + with: + license-check: true diff --git a/fastify-busboy/.github/workflows/coverage.yml b/fastify-busboy/.github/workflows/coverage.yml new file mode 100644 index 0000000..3d7b943 --- /dev/null +++ b/fastify-busboy/.github/workflows/coverage.yml @@ -0,0 +1,44 @@ +--- + +name: coverage + +on: + push: + branches: + - master + +jobs: + build: + runs-on: ubuntu-latest + name: coverage + + strategy: + matrix: + node-version: [16.x] + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Setup Node ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + always-auth: false + node-version: ${{ matrix.node-version }} + + - name: Run npm install + run: npm install + + - name: Run Tests + run: npm run test:coverage + + - name: Generate LCOV + run: npm run coveralls + + - name: Update Coveralls + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + if: success() diff --git a/fastify-busboy/.github/workflows/linting.yml b/fastify-busboy/.github/workflows/linting.yml new file mode 100644 index 0000000..407f53e --- /dev/null +++ b/fastify-busboy/.github/workflows/linting.yml @@ -0,0 +1,35 @@ +--- + +name: Linting and Types + +on: + pull_request: + branches: + - master + +jobs: + build: + runs-on: ubuntu-latest + name: Linting and Types + + strategy: + matrix: + node-version: [16.x] + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Setup Node ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + always-auth: false + node-version: ${{ matrix.node-version }} + + - name: Run npm install + run: npm install + + - name: Run lint:everything + run: npm run lint:everything diff --git a/fastify-busboy/.gitignore b/fastify-busboy/.gitignore new file mode 100644 index 0000000..6e49526 --- /dev/null +++ b/fastify-busboy/.gitignore @@ -0,0 +1,152 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# Vim swap files +*.swp + +# macOS files +.DS_Store + +# Clinic +.clinic + +# lock files +bun.lockb +package-lock.json +pnpm-lock.yaml +yarn.lock + +# editor files +.vscode +.idea + +/benchmarks/node_modules/ +/benchmarks/package-lock.json diff --git a/fastify-busboy/.taprc b/fastify-busboy/.taprc new file mode 100644 index 0000000..30a802b --- /dev/null +++ b/fastify-busboy/.taprc @@ -0,0 +1,4 @@ +files: + - test/**/*.test.js + +coverage: false
\ No newline at end of file diff --git a/fastify-busboy/CHANGELOG.md b/fastify-busboy/CHANGELOG.md new file mode 100644 index 0000000..6d10297 --- /dev/null +++ b/fastify-busboy/CHANGELOG.md @@ -0,0 +1,28 @@ +# Changelog + +Major changes since the last busboy release (0.3.1): + +# 1.1.0 - 09 June, 2022 + +* Fix potential ReDOS-Attack-Vector in Headerparser (#72) +* Improve array parse performances (#69) +* Export Dicer library (#90) + +# 1.0.0 - 04 December, 2021 + +* Prevent malformed headers from crashing the web server (#34) +* Prevent empty parts from hanging the process (#55) +* Use non-deprecated Buffer creation (#8, #10) +* Include TypeScript types in the package itself (#13) +* Make `busboy` importable both as ESM and as CJS module (#61) +* Improve performance (#21, #32, #36) +* Set `autoDestroy` to `false` by default in order to avoid regressions when upgrading from Node.js 12 to Node.js 14 (#9) +* Add option `isPartAFile`, to make the file-detection configurable (#53) +* Add property `bytesRead` on FileStreams (#51) +* Add and expose headerSize limit (#64) +* Throw an error on non-number limit (#7) +* Use the native TextDecoder and the package `text-decoding` for fallback if Node.js does not support the requested encoding (#50) +* Integrate `dicer` dependency into `busboy` itself (#14) +* Convert tests to Mocha (#11, #12, #22, #23) +* Implement better benchmarks (#40, #54) +* Use JavaScript Standard style (#44, #45) diff --git a/fastify-busboy/LICENSE b/fastify-busboy/LICENSE new file mode 100644 index 0000000..290762e --- /dev/null +++ b/fastify-busboy/LICENSE @@ -0,0 +1,19 @@ +Copyright Brian White. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE.
\ No newline at end of file diff --git a/fastify-busboy/README.md b/fastify-busboy/README.md new file mode 100644 index 0000000..c74e618 --- /dev/null +++ b/fastify-busboy/README.md @@ -0,0 +1,271 @@ +# busboy + +<div align="center"> + +[![Build Status](https://github.com/fastify/busboy/workflows/ci/badge.svg)](https://github.com/fastify/busboy/actions) +[![Coverage Status](https://coveralls.io/repos/fastify/busboy/badge.svg?branch=master)](https://coveralls.io/r/fastify/busboy?branch=master) +[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](https://standardjs.com/) +[![Security Responsible Disclosure](https://img.shields.io/badge/Security-Responsible%20Disclosure-yellow.svg)](https://github.com/nodejs/security-wg/blob/HEAD/processes/responsible_disclosure_template.md) + +</div> + +<div align="center"> + +[![NPM version](https://img.shields.io/npm/v/@fastify/busboy.svg?style=flat)](https://www.npmjs.com/package/@fastify/busboy) +[![NPM downloads](https://img.shields.io/npm/dm/@fastify/busboy.svg?style=flat)](https://www.npmjs.com/package/@fastify/busboy) + +</div> + +Description +=========== + +A Node.js module for parsing incoming HTML form data. + +This is an officially supported fork by [fastify](https://github.com/fastify/) organization of the amazing library [originally created](https://github.com/mscdex/busboy) by Brian White, +aimed at addressing long-standing issues with it. + +Benchmark (Mean time for 500 Kb payload, 2000 cycles, 1000 cycle warmup): + +| Library | Version | Mean time in nanoseconds (less is better) | +|-----------------------|---------|-------------------------------------------| +| busboy | 0.3.1 | `340114` | +| @fastify/busboy | 1.0.0 | `270984` | + +[Changelog](https://github.com/fastify/busboy/blob/master/CHANGELOG.md) since busboy 0.31. + +Requirements +============ + +* [Node.js](http://nodejs.org/) 10+ + + +Install +======= + + npm i @fastify/busboy + + +Examples +======== + +* Parsing (multipart) with default options: + +```javascript +const http = require('node:http'); +const { inspect } = require('node:util'); +const Busboy = require('busboy'); + +http.createServer((req, res) => { + if (req.method === 'POST') { + const busboy = new Busboy({ headers: req.headers }); + busboy.on('file', (fieldname, file, filename, encoding, mimetype) => { + console.log(`File [${fieldname}]: filename: ${filename}, encoding: ${encoding}, mimetype: ${mimetype}`); + file.on('data', data => { + console.log(`File [${fieldname}] got ${data.length} bytes`); + }); + file.on('end', () => { + console.log(`File [${fieldname}] Finished`); + }); + }); + busboy.on('field', (fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) => { + console.log(`Field [${fieldname}]: value: ${inspect(val)}`); + }); + busboy.on('finish', () => { + console.log('Done parsing form!'); + res.writeHead(303, { Connection: 'close', Location: '/' }); + res.end(); + }); + req.pipe(busboy); + } else if (req.method === 'GET') { + res.writeHead(200, { Connection: 'close' }); + res.end(`<html><head></head><body> + <form method="POST" enctype="multipart/form-data"> + <input type="text" name="textfield"><br> + <input type="file" name="filefield"><br> + <input type="submit"> + </form> + </body></html>`); + } +}).listen(8000, () => { + console.log('Listening for requests'); +}); + +// Example output, using http://nodejs.org/images/ryan-speaker.jpg as the file: +// +// Listening for requests +// File [filefield]: filename: ryan-speaker.jpg, encoding: binary +// File [filefield] got 11971 bytes +// Field [textfield]: value: 'testing! :-)' +// File [filefield] Finished +// Done parsing form! +``` + +* Save all incoming files to disk: + +```javascript +const http = require('node:http'); +const path = require('node:path'); +const os = require('node:os'); +const fs = require('node:fs'); + +const Busboy = require('busboy'); + +http.createServer(function(req, res) { + if (req.method === 'POST') { + const busboy = new Busboy({ headers: req.headers }); + busboy.on('file', function(fieldname, file, filename, encoding, mimetype) { + var saveTo = path.join(os.tmpdir(), path.basename(fieldname)); + file.pipe(fs.createWriteStream(saveTo)); + }); + busboy.on('finish', function() { + res.writeHead(200, { 'Connection': 'close' }); + res.end("That's all folks!"); + }); + return req.pipe(busboy); + } + res.writeHead(404); + res.end(); +}).listen(8000, function() { + console.log('Listening for requests'); +}); +``` + +* Parsing (urlencoded) with default options: + +```javascript +const http = require('node:http'); +const { inspect } = require('node:util'); + +const Busboy = require('busboy'); + +http.createServer(function(req, res) { + if (req.method === 'POST') { + const busboy = new Busboy({ headers: req.headers }); + busboy.on('file', function(fieldname, file, filename, encoding, mimetype) { + console.log('File [' + fieldname + ']: filename: ' + filename); + file.on('data', function(data) { + console.log('File [' + fieldname + '] got ' + data.length + ' bytes'); + }); + file.on('end', function() { + console.log('File [' + fieldname + '] Finished'); + }); + }); + busboy.on('field', function(fieldname, val, fieldnameTruncated, valTruncated) { + console.log('Field [' + fieldname + ']: value: ' + inspect(val)); + }); + busboy.on('finish', function() { + console.log('Done parsing form!'); + res.writeHead(303, { Connection: 'close', Location: '/' }); + res.end(); + }); + req.pipe(busboy); + } else if (req.method === 'GET') { + res.writeHead(200, { Connection: 'close' }); + res.end('<html><head></head><body>\ + <form method="POST">\ + <input type="text" name="textfield"><br />\ + <select name="selectfield">\ + <option value="1">1</option>\ + <option value="10">10</option>\ + <option value="100">100</option>\ + <option value="9001">9001</option>\ + </select><br />\ + <input type="checkbox" name="checkfield">Node.js rules!<br />\ + <input type="submit">\ + </form>\ + </body></html>'); + } +}).listen(8000, function() { + console.log('Listening for requests'); +}); + +// Example output: +// +// Listening for requests +// Field [textfield]: value: 'testing! :-)' +// Field [selectfield]: value: '9001' +// Field [checkfield]: value: 'on' +// Done parsing form! +``` + + +API +=== + +_Busboy_ is a _Writable_ stream + +Busboy (special) events +----------------------- + +* **file**(< _string_ >fieldname, < _ReadableStream_ >stream, < _string_ >filename, < _string_ >transferEncoding, < _string_ >mimeType) - Emitted for each new file form field found. `transferEncoding` contains the 'Content-Transfer-Encoding' value for the file stream. `mimeType` contains the 'Content-Type' value for the file stream. + * Note: if you listen for this event, you should always handle the `stream` no matter if you care about the file contents or not (e.g. you can simply just do `stream.resume();` if you want to discard the contents), otherwise the 'finish' event will never fire on the Busboy instance. However, if you don't care about **any** incoming files, you can simply not listen for the 'file' event at all and any/all files will be automatically and safely discarded (these discarded files do still count towards `files` and `parts` limits). + * If a configured file size limit was reached, `stream` will both have a boolean property `truncated` (best checked at the end of the stream) and emit a 'limit' event to notify you when this happens. + * The property `bytesRead` informs about the number of bytes that have been read so far. + +* **field**(< _string_ >fieldname, < _string_ >value, < _boolean_ >fieldnameTruncated, < _boolean_ >valueTruncated, < _string_ >transferEncoding, < _string_ >mimeType) - Emitted for each new non-file field found. + +* **partsLimit**() - Emitted when specified `parts` limit has been reached. No more 'file' or 'field' events will be emitted. + +* **filesLimit**() - Emitted when specified `files` limit has been reached. No more 'file' events will be emitted. + +* **fieldsLimit**() - Emitted when specified `fields` limit has been reached. No more 'field' events will be emitted. + + +Busboy methods +-------------- + +* **(constructor)**(< _object_ >config) - Creates and returns a new Busboy instance. + + * The constructor takes the following valid `config` settings: + + * **headers** - _object_ - These are the HTTP headers of the incoming request, which are used by individual parsers. + + * **autoDestroy** - _boolean_ - Whether this stream should automatically call .destroy() on itself after ending. (Default: false). + + * **highWaterMark** - _integer_ - highWaterMark to use for this Busboy instance (Default: WritableStream default). + + * **fileHwm** - _integer_ - highWaterMark to use for file streams (Default: ReadableStream default). + + * **defCharset** - _string_ - Default character set to use when one isn't defined (Default: 'utf8'). + + * **preservePath** - _boolean_ - If paths in the multipart 'filename' field shall be preserved. (Default: false). + + * **isPartAFile** - __function__ - Use this function to override the default file detection functionality. It has following parameters: + + * fieldName - __string__ The name of the field. + + * contentType - __string__ The content-type of the part, e.g. `text/plain`, `image/jpeg`, `application/octet-stream` + + * fileName - __string__ The name of a file supplied by the part. + + (Default: `(fieldName, contentType, fileName) => (contentType === 'application/octet-stream' || fileName !== undefined)`) + + * **limits** - _object_ - Various limits on incoming data. Valid properties are: + + * **fieldNameSize** - _integer_ - Max field name size (in bytes) (Default: 100 bytes). + + * **fieldSize** - _integer_ - Max field value size (in bytes) (Default: 1 MiB, which is 1024 x 1024 bytes). + + * **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 + + * **headerSize** - _integer_ - For multipart forms, the max size of a multipart header **Default:** 81920. + + * The constructor can throw errors: + + * **Busboy expected an options-Object.** - Busboy expected an Object as first parameters. + + * **Busboy expected an options-Object with headers-attribute.** - The first parameter is lacking of a headers-attribute. + + * **Limit $limit is not a valid number** - Busboy expected the desired limit to be of type number. Busboy throws this Error to prevent a potential security issue by falling silently back to the Busboy-defaults. Potential source for this Error can be the direct use of environment variables without transforming them to the type number. + + * **Unsupported Content-Type.** - The `Content-Type` isn't one Busboy can parse. + + * **Missing Content-Type-header.** - The provided headers don't include `Content-Type` at all. diff --git a/fastify-busboy/bench/busboy-form-bench-latin1.js b/fastify-busboy/bench/busboy-form-bench-latin1.js new file mode 100644 index 0000000..33634ad --- /dev/null +++ b/fastify-busboy/bench/busboy-form-bench-latin1.js @@ -0,0 +1,32 @@ +'use strict' + +const Busboy = require('busboy'); +const { createMultipartBufferForEncodingBench } = require("./createMultipartBufferForEncodingBench"); + + for (var i = 0, il = 10000; i < il; i++) { // eslint-disable-line no-var + const boundary = '-----------------------------168072824752491622650073', + busboy = new Busboy({ + headers: { + 'content-type': 'multipart/form-data; boundary=' + boundary + } + }), + buffer = createMultipartBufferForEncodingBench(boundary, 100, 'iso-8859-1'), + mb = buffer.length / 1048576; + + let processedData = 0; + busboy.on('file', (field, file, filename, encoding, mimetype) => { + file.resume() + }) + + busboy.on('error', function (err) { + }) + busboy.on('finish', function () { + }) + + const start = +new Date(); + const result = busboy.write(buffer, () => { }); + busboy.end(); + const duration = +new Date - start; + const mbPerSec = (mb / (duration / 1000)).toFixed(2); + console.log(mbPerSec + ' mb/sec'); + }
\ No newline at end of file diff --git a/fastify-busboy/bench/busboy-form-bench-utf8.js b/fastify-busboy/bench/busboy-form-bench-utf8.js new file mode 100644 index 0000000..1e6e0b7 --- /dev/null +++ b/fastify-busboy/bench/busboy-form-bench-utf8.js @@ -0,0 +1,32 @@ +'use strict' + +const Busboy = require('busboy'); +const { createMultipartBufferForEncodingBench } = require("./createMultipartBufferForEncodingBench"); + + for (var i = 0, il = 10000; i < il; i++) { // eslint-disable-line no-var + const boundary = '-----------------------------168072824752491622650073', + busboy = new Busboy({ + headers: { + 'content-type': 'multipart/form-data; boundary=' + boundary + } + }), + buffer = createMultipartBufferForEncodingBench(boundary, 100, 'utf-8'), + mb = buffer.length / 1048576; + + let processedData = 0; + busboy.on('file', (field, file, filename, encoding, mimetype) => { + file.resume() + }) + + busboy.on('error', function (err) { + }) + busboy.on('finish', function () { + }) + + const start = +new Date(); + const result = busboy.write(buffer, () => { }); + busboy.end(); + const duration = +new Date - start; + const mbPerSec = (mb / (duration / 1000)).toFixed(2); + console.log(mbPerSec + ' mb/sec'); + }
\ No newline at end of file diff --git a/fastify-busboy/bench/createMultipartBufferForEncodingBench.js b/fastify-busboy/bench/createMultipartBufferForEncodingBench.js new file mode 100644 index 0000000..9d20f8f --- /dev/null +++ b/fastify-busboy/bench/createMultipartBufferForEncodingBench.js @@ -0,0 +1,23 @@ +'use strict' + +function createMultipartBufferForEncodingBench(boundary, amount, charset) { + const filename = charset === 'utf-8' ? 'utf-8\'\'%c2%a3%20and%20%e2%82%ac%20rates' : `${charset}\'en\'%A3%20rates`; + const head = '--' + boundary + '\r\n' + + 'content-disposition: form-data; name="field1"\r\n' + + 'content-type: text/plain;charset=' + charset + '; filename*=' + filename + '\r\n' + + '\r\n', tail = '\r\n--' + boundary + '--\r\n', buffer = Buffer.concat([Buffer.from(head), Buffer.from(` +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus pretium leo ex, vitae dignissim felis viverra non. Praesent id quam ac elit tincidunt porttitor sed eget magna. Vivamus nibh ipsum, malesuada in eros sit amet, rutrum mattis leo. Ut nunc justo, ornare a finibus in, consectetur euismod sapien. Praesent facilisis, odio consectetur facilisis varius, tellus justo tristique sapien, non porttitor eros massa quis nibh. Nam blandit orci ac efficitur cursus. Nunc non mollis sapien, sit amet pretium odio. Nam vestibulum lectus ac orci egestas aliquet. Duis nec nibh quis augue consequat vulputate a a dui. + +Aenean nec laoreet dolor, commodo aliquam leo. Quisque at placerat sem. In scelerisque cursus dolor, ac aliquam metus malesuada in. Vestibulum lacinia dolor purus, at convallis ipsum iaculis id. Integer bibendum sem neque, at bibendum enim lobortis eu. Cras pretium arcu eget congue cursus. Curabitur blandit ultricies mollis. Sed lacinia quis felis ut fringilla. + +Nulla vitae lobortis metus. Morbi gravida risus tortor, in pulvinar massa lobortis vitae. Etiam vitae massa libero. Sed id tincidunt elit. Quisque congue felis vel aliquam varius. Sed a massa vitae lectus vehicula lacinia vitae ac justo. In commodo sodales nisi finibus vulputate. Suspendisse viverra, est eget fringilla gravida, nulla justo vulputate lorem, at eleifend nisi urna a eros. Sed sit amet ipsum vehicula, venenatis urna ac, interdum felis. + +Cras semper mi magna, nec iaculis neque rhoncus at. In sit amet odio sed libero fringilla commodo. Sed hendrerit pulvinar turpis sed porta. Pellentesque consequat scelerisque sapien nec iaculis. Aenean sed nunc a purus laoreet efficitur id eu orci. Mauris tincidunt auctor congue. Aliquam nisi ligula, facilisis a molestie sed, luctus vitae mauris. Mauris at facilisis elit. Maecenas sodales pretium nisi in sodales. Cras nec blandit enim. Praesent in lacus et nibh varius suscipit in sit amet nibh. + +Nam hendrerit justo eu lectus molestie, sit amet fringilla ipsum semper. Maecenas sit amet nunc elementum, interdum nunc eu, euismod ipsum. Vestibulum ut mauris sapien. Praesent nec felis ex. Fusce vel leo lobortis, mattis sem a, ullamcorper dolor. Aliquam erat volutpat. Fusce feugiat odio ut feugiat volutpat. Vestibulum magna ante, tempor in volutpat ut, gravida vitae justo. Praesent vitae eleifend eros. Integer feugiat molestie dolor, et pretium enim accumsan sit amet. Sed quis suscipit dui. Integer gravida dolor elit, sit amet fringilla odio commodo at. Quisque ut eleifend risus. Nunc mollis velit quis lectus laoreet pellentesque.\r\n\r\n`)]); + + const buffers = new Array(amount).fill(buffer); + buffers.push(Buffer.from(tail)); + return Buffer.concat(buffers); +} +exports.createMultipartBufferForEncodingBench = createMultipartBufferForEncodingBench; diff --git a/fastify-busboy/bench/dicer/dicer-bench-multipart-parser.js b/fastify-busboy/bench/dicer/dicer-bench-multipart-parser.js new file mode 100644 index 0000000..d24f599 --- /dev/null +++ b/fastify-busboy/bench/dicer/dicer-bench-multipart-parser.js @@ -0,0 +1,60 @@ +'use strict' + +const Dicer = require('../../deps/dicer/lib/Dicer') + +function createMultipartBuffer(boundary, size) { + const head = + '--' + boundary + '\r\n' + + 'content-disposition: form-data; name="field1"\r\n' + + '\r\n' + , tail = '\r\n--' + boundary + '--\r\n' + , buffer = Buffer.allocUnsafe(size); + + buffer.write(head, 0, 'ascii'); + buffer.write(tail, buffer.length - tail.length, 'ascii'); + return buffer; +} + +for (var i = 0, il = 10; i < il; i++) { // eslint-disable-line no-var + const boundary = '-----------------------------168072824752491622650073', + d = new Dicer({ boundary: boundary }), + mb = 100, + buffer = createMultipartBuffer(boundary, mb * 1024 * 1024), + callbacks = + { + partBegin: -1, + partEnd: -1, + headerField: -1, + headerValue: -1, + partData: -1, + end: -1, + }; + + + d.on('part', function (p) { + callbacks.partBegin++; + p.on('header', function (header) { + /*for (var h in header) + console.log('Part header: k: ' + inspect(h) + ', v: ' + inspect(header[h]));*/ + }); + p.on('data', function (data) { + callbacks.partData++; + //console.log('Part data: ' + inspect(data.toString())); + }); + p.on('end', function () { + //console.log('End of part\n'); + callbacks.partEnd++; + }); + }); + d.on('end', function () { + //console.log('End of parts'); + callbacks.end++; + }); + + const start = +new Date(); + d.write(buffer); + const duration = +new Date - start; + const mbPerSec = (mb / (duration / 1000)).toFixed(2); + + console.log(mbPerSec + ' mb/sec'); +} diff --git a/fastify-busboy/bench/dicer/formidable-bench-multipart-parser.js b/fastify-busboy/bench/dicer/formidable-bench-multipart-parser.js new file mode 100644 index 0000000..0470771 --- /dev/null +++ b/fastify-busboy/bench/dicer/formidable-bench-multipart-parser.js @@ -0,0 +1,71 @@ +'use strict' + +require('../node_modules/formidable/test/common'); +var multipartParser = require('../node_modules/formidable/lib/multipart_parser'), + MultipartParser = multipartParser.MultipartParser, + parser = new MultipartParser(), + boundary = '-----------------------------168072824752491622650073', + mb = 100, + buffer = createMultipartBuffer(boundary, mb * 1024 * 1024), + callbacks = + { partBegin: -1, + partEnd: -1, + headerField: -1, + headerValue: -1, + partData: -1, + end: -1, + }; + + +parser.initWithBoundary(boundary); +parser.onHeaderField = function() { + callbacks.headerField++; +}; + +parser.onHeaderValue = function() { + callbacks.headerValue++; +}; + +parser.onPartBegin = function() { + callbacks.partBegin++; +}; + +parser.onPartData = function() { + callbacks.partData++; +}; + +parser.onPartEnd = function() { + callbacks.partEnd++; +}; + +parser.onEnd = function() { + callbacks.end++; +}; + +var start = +new Date(), + nparsed = parser.write(buffer), + duration = +new Date - start, + mbPerSec = (mb / (duration / 1000)).toFixed(2); + +console.log(mbPerSec+' mb/sec'); + +//assert.equal(nparsed, buffer.length); + +function createMultipartBuffer(boundary, size) { + var head = + '--'+boundary+'\r\n' + + 'content-disposition: form-data; name="field1"\r\n' + + '\r\n' + , tail = '\r\n--'+boundary+'--\r\n' + , buffer = Buffer.allocUnsafe(size); + + buffer.write(head, 'ascii', 0); + buffer.write(tail, 'ascii', buffer.length - tail.length); + return buffer; +} + +process.on('exit', function() { + /*for (var k in callbacks) { + assert.equal(0, callbacks[k], k+' count off by '+callbacks[k]); + }*/ +}); diff --git a/fastify-busboy/bench/dicer/multipartser-bench-multipart-parser.js b/fastify-busboy/bench/dicer/multipartser-bench-multipart-parser.js new file mode 100644 index 0000000..40ca00b --- /dev/null +++ b/fastify-busboy/bench/dicer/multipartser-bench-multipart-parser.js @@ -0,0 +1,57 @@ +'use strict' + +var multipartser = require('multipartser'), + boundary = '-----------------------------168072824752491622650073', + parser = multipartser(), + mb = 100, + buffer = createMultipartBuffer(boundary, mb * 1024 * 1024), + callbacks = + { partBegin: -1, + partEnd: -1, + headerField: -1, + headerValue: -1, + partData: -1, + end: -1, + }; + +parser.boundary( boundary ); + +parser.on( 'part', function ( part ) { +}); + +parser.on( 'end', function () { + //console.log( 'completed parsing' ); +}); + +parser.on( 'error', function ( error ) { + console.error( error ); +}); + +var start = +new Date(), + nparsed = parser.data(buffer), + nend = parser.end(), + duration = +new Date - start, + mbPerSec = (mb / (duration / 1000)).toFixed(2); + +console.log(mbPerSec+' mb/sec'); + +//assert.equal(nparsed, buffer.length); + +function createMultipartBuffer(boundary, size) { + var head = + '--'+boundary+'\r\n' + + 'content-disposition: form-data; name="field1"\r\n' + + '\r\n' + , tail = '\r\n--'+boundary+'--\r\n' + , buffer = Buffer.allocUnsafe(size); + + buffer.write(head, 'ascii', 0); + buffer.write(tail, 'ascii', buffer.length - tail.length); + return buffer; +} + +process.on('exit', function() { + /*for (var k in callbacks) { + assert.equal(0, callbacks[k], k+' count off by '+callbacks[k]); + }*/ +}); diff --git a/fastify-busboy/bench/dicer/multiparty-bench-multipart-parser.js b/fastify-busboy/bench/dicer/multiparty-bench-multipart-parser.js new file mode 100644 index 0000000..ab79ec0 --- /dev/null +++ b/fastify-busboy/bench/dicer/multiparty-bench-multipart-parser.js @@ -0,0 +1,78 @@ +'use strict' + +var assert = require('node:assert'), + Form = require('multiparty').Form, + boundary = '-----------------------------168072824752491622650073', + mb = 100, + buffer = createMultipartBuffer(boundary, mb * 1024 * 1024), + callbacks = + { partBegin: -1, + partEnd: -1, + headerField: -1, + headerValue: -1, + partData: -1, + end: -1, + }; + +var form = new Form({ boundary: boundary }); + +hijack('onParseHeaderField', function() { + callbacks.headerField++; +}); + +hijack('onParseHeaderValue', function() { + callbacks.headerValue++; +}); + +hijack('onParsePartBegin', function() { + callbacks.partBegin++; +}); + +hijack('onParsePartData', function() { + callbacks.partData++; +}); + +hijack('onParsePartEnd', function() { + callbacks.partEnd++; +}); + +form.on('finish', function() { + callbacks.end++; +}); + +var start = new Date(); +form.write(buffer, function(err) { + var duration = new Date() - start; + assert.ifError(err); + var mbPerSec = (mb / (duration / 1000)).toFixed(2); + console.log(mbPerSec+' mb/sec'); +}); + +//assert.equal(nparsed, buffer.length); + +function createMultipartBuffer(boundary, size) { + var head = + '--'+boundary+'\r\n' + + 'content-disposition: form-data; name="field1"\r\n' + + '\r\n' + , tail = '\r\n--'+boundary+'--\r\n' + , buffer = Buffer.allocUnsafe(size); + + buffer.write(head, 'ascii', 0); + buffer.write(tail, 'ascii', buffer.length - tail.length); + return buffer; +} + +process.on('exit', function() { + /*for (var k in callbacks) { + assert.equal(0, callbacks[k], k+' count off by '+callbacks[k]); + }*/ +}); + +function hijack(name, fn) { + var oldFn = form[name]; + form[name] = function() { + fn(); + return oldFn.apply(this, arguments); + }; +} diff --git a/fastify-busboy/bench/dicer/parted-bench-multipart-parser.js b/fastify-busboy/bench/dicer/parted-bench-multipart-parser.js new file mode 100644 index 0000000..e0a4670 --- /dev/null +++ b/fastify-busboy/bench/dicer/parted-bench-multipart-parser.js @@ -0,0 +1,65 @@ +'use strict' + +// A special, edited version of the multipart parser from parted is needed here +// because otherwise it attempts to do some things above and beyond just parsing +// -- like saving to disk and whatnot + +var assert = require('node:assert'); +var Parser = require('./parted-multipart'), + boundary = '-----------------------------168072824752491622650073', + parser = new Parser('boundary=' + boundary), + mb = 100, + buffer = createMultipartBuffer(boundary, mb * 1024 * 1024), + callbacks = + { partBegin: -1, + partEnd: -1, + headerField: -1, + headerValue: -1, + partData: -1, + end: -1, + }; + + +parser.on('header', function() { + //callbacks.headerField++; +}); + +parser.on('data', function() { + //callbacks.partBegin++; +}); + +parser.on('part', function() { + +}); + +parser.on('end', function() { + //callbacks.end++; +}); + +var start = +new Date(), + nparsed = parser.write(buffer), + duration = +new Date - start, + mbPerSec = (mb / (duration / 1000)).toFixed(2); + +console.log(mbPerSec+' mb/sec'); + +//assert.equal(nparsed, buffer.length); + +function createMultipartBuffer(boundary, size) { + var head = + '--'+boundary+'\r\n' + + 'content-disposition: form-data; name="field1"\r\n' + + '\r\n' + , tail = '\r\n--'+boundary+'--\r\n' + , buffer = Buffer.allocUnsafe(size); + + buffer.write(head, 'ascii', 0); + buffer.write(tail, 'ascii', buffer.length - tail.length); + return buffer; +} + +process.on('exit', function() { + /*for (var k in callbacks) { + assert.equal(0, callbacks[k], k+' count off by '+callbacks[k]); + }*/ +}); diff --git a/fastify-busboy/bench/dicer/parted-multipart.js b/fastify-busboy/bench/dicer/parted-multipart.js new file mode 100644 index 0000000..f214ff4 --- /dev/null +++ b/fastify-busboy/bench/dicer/parted-multipart.js @@ -0,0 +1,486 @@ +'use strict' + +/** + * Parted (https://github.com/chjj/parted) + * A streaming multipart state parser. + * Copyright (c) 2011, Christopher Jeffrey. (MIT Licensed) + */ + +var fs = require('node:fs') + , path = require('node:path') + , EventEmitter = require('node:events').EventEmitter + , StringDecoder = require('node:string_decoder').StringDecoder + , set = require('qs').set + , each = Array.prototype.forEach; + +/** + * Character Constants + */ + +var DASH = '-'.charCodeAt(0) + , CR = '\r'.charCodeAt(0) + , LF = '\n'.charCodeAt(0) + , COLON = ':'.charCodeAt(0) + , SPACE = ' '.charCodeAt(0); + +/** + * Parser + */ + +var Parser = function(type, options) { + if (!(this instanceof Parser)) { + return new Parser(type, options); + } + + EventEmitter.call(this); + + this.writable = true; + this.readable = true; + + this.options = options || {}; + + var key = grab(type, 'boundary'); + if (!key) { + return this._error('No boundary key found.'); + } + + this.key = Buffer.allocUnsafe('\r\n--' + key); + + this._key = {}; + each.call(this.key, function(ch) { + this._key[ch] = true; + }, this); + + this.state = 'start'; + this.pending = 0; + this.written = 0; + this.writtenDisk = 0; + this.buff = Buffer.allocUnsafe(200); + + this.preamble = true; + this.epilogue = false; + + this._reset(); +}; + +Parser.prototype.__proto__ = EventEmitter.prototype; + +/** + * Parsing + */ + +Parser.prototype.write = function(data) { + if (!this.writable + || this.epilogue) return; + + try { + this._parse(data); + } catch (e) { + this._error(e); + } + + return true; +}; + +Parser.prototype.end = function(data) { + if (!this.writable) return; + + if (data) this.write(data); + + if (!this.epilogue) { + return this._error('Message underflow.'); + } + + return true; +}; + +Parser.prototype._parse = function(data) { + var i = 0 + , len = data.length + , buff = this.buff + , key = this.key + , ch + , val + , j; + + for (; i < len; i++) { + if (this.pos >= 200) { + return this._error('Potential buffer overflow.'); + } + + ch = data[i]; + + switch (this.state) { + case 'start': + switch (ch) { + case DASH: + this.pos = 3; + this.state = 'key'; + break; + default: + break; + } + break; + case 'key': + if (this.pos === key.length) { + this.state = 'key_end'; + i--; + } else if (ch !== key[this.pos]) { + if (this.preamble) { + this.state = 'start'; + i--; + } else { + this.state = 'body'; + val = this.pos - i; + if (val > 0) { + this._write(key.slice(0, val)); + } + i--; + } + } else { + this.pos++; + } + break; + case 'key_end': + switch (ch) { + case CR: + this.state = 'key_line_end'; + break; + case DASH: + this.state = 'key_dash_end'; + break; + default: + return this._error('Expected CR or DASH.'); + } + break; + case 'key_line_end': + switch (ch) { + case LF: + if (this.preamble) { + this.preamble = false; + } else { + this._finish(); + } + this.state = 'header_name'; + this.pos = 0; + break; + default: + return this._error('Expected CR.'); + } + break; + case 'key_dash_end': + switch (ch) { + case DASH: + this.epilogue = true; + this._finish(); + return; + default: + return this._error('Expected DASH.'); + } + case 'header_name': + switch (ch) { + case COLON: + this.header = buff.toString('ascii', 0, this.pos); + this.pos = 0; + this.state = 'header_val'; + break; + default: + buff[this.pos++] = ch | 32; + break; + } + break; + case 'header_val': + switch (ch) { + case CR: + this.state = 'header_val_end'; + break; + case SPACE: + if (this.pos === 0) { + break; + } + // FALL-THROUGH + default: + buff[this.pos++] = ch; + break; + } + break; + case 'header_val_end': + switch (ch) { + case LF: + val = buff.toString('ascii', 0, this.pos); + this._header(this.header, val); + this.pos = 0; + this.state = 'header_end'; + break; + default: + return this._error('Expected LF.'); + } + break; + case 'header_end': + switch (ch) { + case CR: + this.state = 'head_end'; + break; + default: + this.state = 'header_name'; + i--; + break; + } + break; + case 'head_end': + switch (ch) { + case LF: + this.state = 'body'; + i++; + if (i >= len) return; + data = data.slice(i); + i = -1; + len = data.length; + break; + default: + return this._error('Expected LF.'); + } + break; + case 'body': + switch (ch) { + case CR: + if (i > 0) { + this._write(data.slice(0, i)); + } + this.pos = 1; + this.state = 'key'; + data = data.slice(i); + i = 0; + len = data.length; + break; + default: + // boyer-moore-like algorithm + // at felixge's suggestion + while ((j = i + key.length - 1) < len) { + if (this._key[data[j]]) break; + i = j; + } + break; + } + break; + } + } + + if (this.state === 'body') { + this._write(data); + } +}; + +Parser.prototype._header = function(name, val) { + /*if (name === 'content-disposition') { + this.field = grab(val, 'name'); + this.file = grab(val, 'filename'); + + if (this.file) { + this.data = stream(this.file, this.options.path); + } else { + this.decode = new StringDecoder('utf8'); + this.data = ''; + } + }*/ + + return this.emit('header', name, val); +}; + +Parser.prototype._write = function(data) { + /*if (this.data == null) { + return this._error('No disposition.'); + } + + if (this.file) { + this.data.write(data); + this.writtenDisk += data.length; + } else { + this.data += this.decode.write(data); + this.written += data.length; + }*/ + + this.emit('data', data); +}; + +Parser.prototype._reset = function() { + this.pos = 0; + this.decode = null; + this.field = null; + this.data = null; + this.file = null; + this.header = null; +}; + +Parser.prototype._error = function(err) { + this.destroy(); + this.emit('error', typeof err === 'string' + ? new Error(err) + : err); +}; + +Parser.prototype.destroy = function(err) { + this.writable = false; + this.readable = false; + this._reset(); +}; + +Parser.prototype._finish = function() { + var self = this + , field = this.field + , data = this.data + , file = this.file + , part; + + this.pending++; + + this._reset(); + + if (data && data.path) { + part = data.path; + data.end(next); + } else { + part = data; + next(); + } + + function next() { + if (!self.readable) return; + + self.pending--; + + self.emit('part', field, part); + + if (data && data.path) { + self.emit('file', field, part, file); + } + + if (self.epilogue && !self.pending) { + self.emit('end'); + self.destroy(); + } + } +}; + +/** + * Uploads + */ + +Parser.root = process.platform === 'win32' + ? 'C:/Temp' + : '/tmp'; + +/** + * Middleware + */ + +Parser.middleware = function(options) { + options = options || {}; + return function(req, res, next) { + if (options.ensureBody) { + req.body = {}; + } + + if (req.method === 'GET' + || req.method === 'HEAD' + || req._multipart) return next(); + + req._multipart = true; + + var type = req.headers['content-type']; + + if (type) type = type.split(';', 1)[0].trim().toLowerCase(); + + if (type === 'multipart/form-data') { + Parser.handle(req, res, next, options); + } else { + next(); + } + }; +}; + +/** + * Handler + */ + +Parser.handle = function(req, res, next, options) { + var parser = new Parser(req.headers['content-type'], options) + , diskLimit = options.diskLimit + , limit = options.limit + , parts = {} + , files = {}; + + parser.on('error', function(err) { + req.destroy(); + next(err); + }); + + parser.on('part', function(field, part) { + set(parts, field, part); + }); + + parser.on('file', function(field, path, name) { + set(files, field, { + path: path, + name: name, + toString: function() { + return path; + } + }); + }); + + parser.on('data', function() { + if (this.writtenDisk > diskLimit || this.written > limit) { + this.emit('error', new Error('Overflow.')); + this.destroy(); + } + }); + + parser.on('end', next); + + req.body = parts; + req.files = files; + req.pipe(parser); +}; + +/** + * Helpers + */ + +var isWindows = process.platform === 'win32'; + +var stream = function(name, dir) { + var ext = path.extname(name) || '' + , name = path.basename(name, ext) || '' + , dir = dir || Parser.root + , tag; + + tag = Math.random().toString(36).substring(2); + + name = name.substring(0, 200) + '.' + tag; + name = path.join(dir, name) + ext.substring(0, 6); + name = name.replace(/\0/g, ''); + + if (isWindows) { + name = name.replace(/[:*<>|"?]/g, ''); + } + + return fs.createWriteStream(name); +}; + +var grab = function(str, name) { + if (!str) return; + + var rx = new RegExp('\\b' + name + '\\s*=\\s*("[^"]+"|\'[^\']+\'|[^;,]+)', 'i') + , cap = rx.exec(str); + + if (cap) { + return cap[1].trim().replace(/^['"]|['"]$/g, ''); + } +}; + +/** + * Expose + */ + +module.exports = Parser; diff --git a/fastify-busboy/bench/fastify-busboy-form-bench-latin1.js b/fastify-busboy/bench/fastify-busboy-form-bench-latin1.js new file mode 100644 index 0000000..7ca5f44 --- /dev/null +++ b/fastify-busboy/bench/fastify-busboy-form-bench-latin1.js @@ -0,0 +1,31 @@ +'use strict' + +const Busboy = require('../lib/main'); +const { createMultipartBufferForEncodingBench } = require("./createMultipartBufferForEncodingBench"); + + for (var i = 0, il = 10000; i < il; i++) { // eslint-disable-line no-var + const boundary = '-----------------------------168072824752491622650073', + busboy = new Busboy({ + headers: { + 'content-type': 'multipart/form-data; boundary=' + boundary + } + }), + buffer = createMultipartBufferForEncodingBench(boundary, 100, 'iso-8859-1'), + mb = buffer.length / 1048576; + + busboy.on('file', (field, file, filename, encoding, mimetype) => { + file.resume() + }) + + busboy.on('error', function (err) { + }) + busboy.on('finish', function () { + }) + + const start = +new Date(); + busboy.write(buffer, () => { }); + busboy.end(); + const duration = +new Date - start; + const mbPerSec = (mb / (duration / 1000)).toFixed(2); + console.log(mbPerSec + ' mb/sec'); + } diff --git a/fastify-busboy/bench/fastify-busboy-form-bench-utf8.js b/fastify-busboy/bench/fastify-busboy-form-bench-utf8.js new file mode 100644 index 0000000..6c35071 --- /dev/null +++ b/fastify-busboy/bench/fastify-busboy-form-bench-utf8.js @@ -0,0 +1,31 @@ +'use strict' + +const Busboy = require('../lib/main'); +const { createMultipartBufferForEncodingBench } = require("./createMultipartBufferForEncodingBench"); + + for (var i = 0, il = 10000; i < il; i++) { // eslint-disable-line no-var + const boundary = '-----------------------------168072824752491622650073', + busboy = new Busboy({ + headers: { + 'content-type': 'multipart/form-data; boundary=' + boundary + } + }), + buffer = createMultipartBufferForEncodingBench(boundary, 100, 'utf-8'), + mb = buffer.length / 1048576; + + busboy.on('file', (field, file, filename, encoding, mimetype) => { + file.resume() + }) + + busboy.on('error', function (err) { + }) + busboy.on('finish', function () { + }) + + const start = +new Date(); + busboy.write(buffer, () => { }); + busboy.end(); + const duration = +new Date - start; + const mbPerSec = (mb / (duration / 1000)).toFixed(2); + console.log(mbPerSec + ' mb/sec'); + } diff --git a/fastify-busboy/bench/parse-params.js b/fastify-busboy/bench/parse-params.js new file mode 100644 index 0000000..439a372 --- /dev/null +++ b/fastify-busboy/bench/parse-params.js @@ -0,0 +1,21 @@ +'use strict' + +const parseParams = require('../lib/utils/parseParams') +const { Bench } = require('tinybench'); +const bench = new Bench(); + +const simple = 'video/ogg' +const complex = "'text/plain; filename*=utf-8''%c2%a3%20and%20%e2%82%ac%20rates'" + +bench + .add(simple, function () { parseParams(simple) }) + .add(complex, function () { parseParams(complex) }) + .run() + .then((tasks) => { + const errors = tasks.map(t => t.result?.error).filter((t) => t) + if (errors.length) { + errors.map((e) => console.error(e)) + } else { + console.table(bench.table()) + } + }) diff --git a/fastify-busboy/benchmarks/_results/Busboy_comparison-busboy-Node_12.json b/fastify-busboy/benchmarks/_results/Busboy_comparison-busboy-Node_12.json new file mode 100644 index 0000000..69468dd --- /dev/null +++ b/fastify-busboy/benchmarks/_results/Busboy_comparison-busboy-Node_12.json @@ -0,0 +1,10 @@ +{ + "runtimeVersion": "12.22.7, V8 7.8.279.23-node.56", + "benchmarkName": "Busboy comparison", + "benchmarkEntryName": "busboy", + "benchmarkCycles": 10, + "benchmarkCycleSamples": 50, + "warmupCycles": 10, + "meanTimeNs": 1945927.3472222222, + "meanTimeMs": 1.9459273472222223 +}
\ No newline at end of file diff --git a/fastify-busboy/benchmarks/_results/Busboy_comparison-busboy-Node_16.json b/fastify-busboy/benchmarks/_results/Busboy_comparison-busboy-Node_16.json new file mode 100644 index 0000000..b4c492a --- /dev/null +++ b/fastify-busboy/benchmarks/_results/Busboy_comparison-busboy-Node_16.json @@ -0,0 +1,10 @@ +{ + "runtimeVersion": "16.13.0, V8 9.4.146.19-node.13", + "benchmarkName": "Busboy comparison", + "benchmarkEntryName": "busboy", + "benchmarkCycles": 2000, + "benchmarkCycleSamples": 50, + "warmupCycles": 1000, + "meanTimeNs": 340114.0411908194, + "meanTimeMs": 0.3401140411908194 +}
\ No newline at end of file diff --git a/fastify-busboy/benchmarks/_results/Busboy_comparison-fastify-busboy-Node_16.json b/fastify-busboy/benchmarks/_results/Busboy_comparison-fastify-busboy-Node_16.json new file mode 100644 index 0000000..30f5d1e --- /dev/null +++ b/fastify-busboy/benchmarks/_results/Busboy_comparison-fastify-busboy-Node_16.json @@ -0,0 +1,10 @@ +{ + "runtimeVersion": "16.13.0, V8 9.4.146.19-node.13", + "benchmarkName": "Busboy comparison", + "benchmarkEntryName": "fastify-busboy", + "benchmarkCycles": 2000, + "benchmarkCycleSamples": 50, + "warmupCycles": 1000, + "meanTimeNs": 270984.48082281026, + "meanTimeMs": 0.27098448082281024 +}
\ No newline at end of file diff --git a/fastify-busboy/benchmarks/busboy/contestants/busboy.js b/fastify-busboy/benchmarks/busboy/contestants/busboy.js new file mode 100644 index 0000000..6cb3414 --- /dev/null +++ b/fastify-busboy/benchmarks/busboy/contestants/busboy.js @@ -0,0 +1,40 @@ +'use strict' + +const Busboy = require('busboy') +const { buffer, boundary } = require('../data') + +function process () { + const busboy = Busboy({ + headers: { + 'content-type': 'multipart/form-data; boundary=' + boundary + } + }) + let processedData = '' + + return new Promise((resolve, reject) => { + busboy.on('file', (field, file, filename, encoding, mimetype) => { + // console.log('read file') + file.on('data', (data) => { + processedData += data.toString() + // console.log(`File [${filename}] got ${data.length} bytes`); + }) + file.on('end', (fieldname) => { + // console.log(`File [${fieldname}] Finished`); + }) + }) + + busboy.on('error', function (err) { + reject(err) + }) + busboy.on('finish', function () { + resolve(processedData) + }) + busboy.write(buffer, () => { }) + + busboy.end() + }) +} + +module.exports = { + process +} diff --git a/fastify-busboy/benchmarks/busboy/contestants/fastify-busboy.js b/fastify-busboy/benchmarks/busboy/contestants/fastify-busboy.js new file mode 100644 index 0000000..6750f77 --- /dev/null +++ b/fastify-busboy/benchmarks/busboy/contestants/fastify-busboy.js @@ -0,0 +1,41 @@ +'use strict' + +const Busboy = require('../../../lib/main') +const { buffer, boundary } = require('../data') + +function process () { + const busboy = new Busboy({ + headers: { + 'content-type': 'multipart/form-data; boundary=' + boundary + } + }) + + let processedData = '' + + return new Promise((resolve, reject) => { + busboy.on('file', (field, file, filename, encoding, mimetype) => { + // console.log('read file') + file.on('data', (data) => { + processedData += data.toString() + // console.log(`File [${filename}] got ${data.length} bytes`); + }) + file.on('end', (fieldname) => { + // console.log(`File [${fieldname}] Finished`); + }) + }) + + busboy.on('error', function (err) { + reject(err) + }) + busboy.on('finish', function () { + resolve(processedData) + }) + busboy.write(buffer, () => { }) + + busboy.end() + }) +} + +module.exports = { + process +} diff --git a/fastify-busboy/benchmarks/busboy/data.js b/fastify-busboy/benchmarks/busboy/data.js new file mode 100644 index 0000000..4fdefae --- /dev/null +++ b/fastify-busboy/benchmarks/busboy/data.js @@ -0,0 +1,34 @@ +'use strict'
+
+const boundary = '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k'
+const randomContent = Buffer.from(makeString(1024 * 500), 'utf8')
+const buffer = createMultipartBuffer(boundary)
+
+function makeString (length) {
+ let result = ''
+ const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
+ const charactersLength = characters.length
+ for (var i = 0; i < length; i++) { // eslint-disable-line no-var
+ result += characters.charAt(Math.floor(Math.random() *
+ charactersLength))
+ }
+ return result
+}
+
+function createMultipartBuffer (boundary) {
+ const payload = [
+ '--' + boundary,
+ 'Content-Disposition: form-data; name="upload_file_0"; filename="1k_a.dat"',
+ 'Content-Type: application/octet-stream',
+ '',
+ randomContent,
+ '--' + boundary + '--'
+ ].join('\r\n')
+ return Buffer.from(payload, 'ascii')
+}
+
+module.exports = {
+ boundary,
+ buffer,
+ randomContent
+}
diff --git a/fastify-busboy/benchmarks/busboy/executioner.js b/fastify-busboy/benchmarks/busboy/executioner.js new file mode 100644 index 0000000..524912c --- /dev/null +++ b/fastify-busboy/benchmarks/busboy/executioner.js @@ -0,0 +1,50 @@ +'use strict' + +const { process: processBusboy } = require('./contestants/busboy') +const { process: processFastify } = require('./contestants/fastify-busboy') +const { getCommonBuilder } = require('../common/commonBuilder') +const { validateAccuracy } = require('./validator') +const { resolveContestant } = require('../common/contestantResolver') +const { outputResults } = require('../common/resultUtils') + +const contestants = { + busboy: measureBusboy, + fastify: measureFastify +} + +async function measureBusboy () { + const benchmark = getCommonBuilder() + .benchmarkName('Busboy comparison') + .benchmarkEntryName('busboy') + .asyncFunctionUnderTest(processBusboy) + .build() + const benchmarkResults = await benchmark.executeAsync() + outputResults(benchmark, benchmarkResults) +} + +async function measureFastify () { + const benchmark = getCommonBuilder() + .benchmarkName('Busboy comparison') + .benchmarkEntryName('fastify-busboy') + .asyncFunctionUnderTest(processFastify) + .build() + const benchmarkResults = await benchmark.executeAsync() + outputResults(benchmark, benchmarkResults) +} + +function execute () { + return validateAccuracy(processBusboy()) + .then(() => { + return validateAccuracy(processFastify()) + }) + .then(() => { + const contestant = resolveContestant(contestants) + return contestant() + }).then(() => { + console.log('all done') + }).catch((err) => { + console.error(`Something went wrong: ${err.message}`) + }) +} + +execute() diff --git a/fastify-busboy/benchmarks/busboy/regenerate.cmd b/fastify-busboy/benchmarks/busboy/regenerate.cmd new file mode 100644 index 0000000..87c0768 --- /dev/null +++ b/fastify-busboy/benchmarks/busboy/regenerate.cmd @@ -0,0 +1,17 @@ +rem Make sure to run this in Admin account
+rem
+call npm run install-node
+timeout /t 2
+call nvm use 17.2.0
+timeout /t 2
+call npm run benchmark-all
+call nvm use 16.13.1
+timeout /t 2
+call npm run benchmark-all
+call nvm use 14.18.2
+timeout /t 2
+call npm run benchmark-all
+call nvm use 12.22.7
+timeout /t 2
+call npm run benchmark-all
+call npm run combine-results
diff --git a/fastify-busboy/benchmarks/busboy/validator.js b/fastify-busboy/benchmarks/busboy/validator.js new file mode 100644 index 0000000..a86cc33 --- /dev/null +++ b/fastify-busboy/benchmarks/busboy/validator.js @@ -0,0 +1,15 @@ +'use strict'
+
+const { validateEqual } = require('validation-utils')
+const { randomContent } = require('./data')
+
+const EXPECTED_RESULT = randomContent.toString()
+
+async function validateAccuracy (actualResultPromise) {
+ const result = await actualResultPromise
+ validateEqual(result, EXPECTED_RESULT)
+}
+
+module.exports = {
+ validateAccuracy
+}
diff --git a/fastify-busboy/benchmarks/common/commonBuilder.js b/fastify-busboy/benchmarks/common/commonBuilder.js new file mode 100644 index 0000000..b5707aa --- /dev/null +++ b/fastify-busboy/benchmarks/common/commonBuilder.js @@ -0,0 +1,46 @@ +'use strict' + +const { validateNotNil } = require('validation-utils') +const { BenchmarkBuilder } = require('photofinish') +const getopts = require('getopts') + +const options = getopts(process.argv.slice(1), { + alias: { + preset: 'p' + }, + default: {} +}) + +const PRESET = { + LOW: (builder) => { + return builder + .warmupCycles(1000) + .benchmarkCycles(1000) + }, + + MEDIUM: (builder) => { + return builder + .warmupCycles(1000) + .benchmarkCycles(2000) + }, + + HIGH: (builder) => { + return builder + .warmupCycles(1000) + .benchmarkCycles(10000) + } +} + +function getCommonBuilder () { + const presetId = options.preset || 'MEDIUM' + const preset = validateNotNil(PRESET[presetId.toUpperCase()], `Unknown preset: ${presetId}`) + + const builder = new BenchmarkBuilder() + preset(builder) + return builder + .benchmarkCycleSamples(50) +} + +module.exports = { + getCommonBuilder +} diff --git a/fastify-busboy/benchmarks/common/contestantResolver.js b/fastify-busboy/benchmarks/common/contestantResolver.js new file mode 100644 index 0000000..7cfc90e --- /dev/null +++ b/fastify-busboy/benchmarks/common/contestantResolver.js @@ -0,0 +1,26 @@ +'use strict'
+
+const getopts = require('getopts')
+
+const options = getopts(process.argv.slice(1), {
+ alias: {
+ contestant: 'c'
+ },
+ default: {}
+})
+
+function resolveContestant (contestants) {
+ const contestantId = options.contestant
+ const contestant = Number.isFinite(contestantId)
+ ? Object.values(contestants)[contestantId]
+ : contestants[contestantId]
+
+ if (!contestant) {
+ throw new Error(`Unknown contestant ${contestantId}`)
+ }
+ return contestant
+}
+
+module.exports = {
+ resolveContestant
+}
diff --git a/fastify-busboy/benchmarks/common/executionUtils.js b/fastify-busboy/benchmarks/common/executionUtils.js new file mode 100644 index 0000000..8c52ec8 --- /dev/null +++ b/fastify-busboy/benchmarks/common/executionUtils.js @@ -0,0 +1,18 @@ +'use strict'
+
+const { getCommonBuilder } = require('./commonBuilder')
+const { outputResults } = require('./resultUtils')
+
+function getMeasureFn (constestandId, fn) {
+ return () => {
+ const benchmark = getCommonBuilder()
+ .benchmarkEntryName(constestandId)
+ .functionUnderTest(fn).build()
+ const benchmarkResults = benchmark.execute()
+ outputResults(benchmark, benchmarkResults)
+ }
+}
+
+module.exports = {
+ getMeasureFn
+}
diff --git a/fastify-busboy/benchmarks/common/resultUtils.js b/fastify-busboy/benchmarks/common/resultUtils.js new file mode 100644 index 0000000..ec7bce7 --- /dev/null +++ b/fastify-busboy/benchmarks/common/resultUtils.js @@ -0,0 +1,17 @@ +'use strict' + +const { exportResults } = require('photofinish') + +function outputResults (benchmark, benchmarkResults) { + console.log( + `Mean time for ${ + benchmark.benchmarkEntryName + } is ${benchmarkResults.meanTime.getTimeInNanoSeconds()} nanoseconds` + ) + + exportResults(benchmarkResults, { exportPath: '_results' }) +} + +module.exports = { + outputResults +} diff --git a/fastify-busboy/benchmarks/common/resultsCombinator.js b/fastify-busboy/benchmarks/common/resultsCombinator.js new file mode 100644 index 0000000..253211b --- /dev/null +++ b/fastify-busboy/benchmarks/common/resultsCombinator.js @@ -0,0 +1,54 @@ +'use strict' + +const fs = require('node:fs') +const path = require('node:path') +const getopts = require('getopts') +const systemInformation = require('systeminformation') +const { loadResults } = require('photofinish') + +const options = getopts(process.argv.slice(1), { + alias: { + resultsDir: 'r', + precision: 'p' + }, + default: {} +}) + +const { generateTable } = require('photofinish') + +async function getSpecs () { + const cpuInfo = await systemInformation.cpu() + + return { + cpu: { + brand: cpuInfo.brand, + speed: `${cpuInfo.speed} GHz` + } + } +} + +async function saveTable () { + const baseResultsDir = options.resultsDir + const benchmarkResults = await loadResults(baseResultsDir) + + const table = generateTable(benchmarkResults, { + precision: options.precision, + sortBy: [ + { field: 'meanTimeNs', order: 'asc' } + ] + }) + + const specs = await getSpecs() + + console.log(specs) + console.log(table) + + const targetFilePath = path.resolve(baseResultsDir, 'results.md') + fs.writeFileSync( + targetFilePath, + `${table}` + + `\n\n**Specs**: ${specs.cpu.brand} (${specs.cpu.speed})` + ) +} + +saveTable() diff --git a/fastify-busboy/benchmarks/package.json b/fastify-busboy/benchmarks/package.json new file mode 100644 index 0000000..2574b8b --- /dev/null +++ b/fastify-busboy/benchmarks/package.json @@ -0,0 +1,21 @@ +{ + "name": "busboy-benchmarks", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "getopts": "^2.3.0", + "photofinish": "^1.8.0", + "systeminformation": "^5.9.15", + "tslib": "^2.3.1", + "validation-utils": "^7.0.0" + }, + "scripts": { + "install-node": "nvm install 17.2.0 && nvm install 16.13.1 && nvm install 14.18.2 && nvm install 12.22.7", + "benchmark-busboy": "node busboy/executioner.js -c 0", + "benchmark-fastify": "node busboy/executioner.js -c 1", + "benchmark-all": "npm run benchmark-busboy -- -p high && npm run benchmark-fastify -- -p high", + "benchmark-all-medium": "npm run benchmark-busboy -- -p medium && npm run benchmark-fastify -- -p medium", + "benchmark-all-low": "npm run benchmark-busboy -- -p low && npm run benchmark-fastify -- -p low", + "combine-results": "node common/resultsCombinator.js -r _results -p 6" + } +} diff --git a/fastify-busboy/deps/dicer/LICENSE b/fastify-busboy/deps/dicer/LICENSE new file mode 100644 index 0000000..290762e --- /dev/null +++ b/fastify-busboy/deps/dicer/LICENSE @@ -0,0 +1,19 @@ +Copyright Brian White. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE.
\ No newline at end of file diff --git a/fastify-busboy/deps/dicer/lib/Dicer.js b/fastify-busboy/deps/dicer/lib/Dicer.js new file mode 100644 index 0000000..79da160 --- /dev/null +++ b/fastify-busboy/deps/dicer/lib/Dicer.js @@ -0,0 +1,207 @@ +'use strict' + +const WritableStream = require('node:stream').Writable +const inherits = require('node:util').inherits + +const StreamSearch = require('../../streamsearch/sbmh') + +const PartStream = require('./PartStream') +const HeaderParser = require('./HeaderParser') + +const DASH = 45 +const B_ONEDASH = Buffer.from('-') +const B_CRLF = Buffer.from('\r\n') +const EMPTY_FN = function () {} + +function Dicer (cfg) { + if (!(this instanceof Dicer)) { return new Dicer(cfg) } + WritableStream.call(this, cfg) + + if (!cfg || (!cfg.headerFirst && typeof cfg.boundary !== 'string')) { throw new TypeError('Boundary required') } + + if (typeof cfg.boundary === 'string') { this.setBoundary(cfg.boundary) } else { this._bparser = undefined } + + this._headerFirst = cfg.headerFirst + + this._dashes = 0 + this._parts = 0 + this._finished = false + this._realFinish = false + this._isPreamble = true + this._justMatched = false + this._firstWrite = true + this._inHeader = true + this._part = undefined + this._cb = undefined + this._ignoreData = false + this._partOpts = { highWaterMark: cfg.partHwm } + this._pause = false + + const self = this + this._hparser = new HeaderParser(cfg) + this._hparser.on('header', function (header) { + self._inHeader = false + self._part.emit('header', header) + }) +} +inherits(Dicer, WritableStream) + +Dicer.prototype.emit = function (ev) { + if (ev === 'finish' && !this._realFinish) { + if (!this._finished) { + const self = this + process.nextTick(function () { + self.emit('error', new Error('Unexpected end of multipart data')) + if (self._part && !self._ignoreData) { + const type = (self._isPreamble ? 'Preamble' : 'Part') + self._part.emit('error', new Error(type + ' terminated early due to unexpected end of multipart data')) + self._part.push(null) + process.nextTick(function () { + self._realFinish = true + self.emit('finish') + self._realFinish = false + }) + return + } + self._realFinish = true + self.emit('finish') + self._realFinish = false + }) + } + } else { WritableStream.prototype.emit.apply(this, arguments) } +} + +Dicer.prototype._write = function (data, encoding, cb) { + // ignore unexpected data (e.g. extra trailer data after finished) + if (!this._hparser && !this._bparser) { return cb() } + + if (this._headerFirst && this._isPreamble) { + if (!this._part) { + this._part = new PartStream(this._partOpts) + if (this._events.preamble) { this.emit('preamble', this._part) } else { this._ignore() } + } + const r = this._hparser.push(data) + if (!this._inHeader && r !== undefined && r < data.length) { data = data.slice(r) } else { return cb() } + } + + // allows for "easier" testing + if (this._firstWrite) { + this._bparser.push(B_CRLF) + this._firstWrite = false + } + + this._bparser.push(data) + + if (this._pause) { this._cb = cb } else { cb() } +} + +Dicer.prototype.reset = function () { + this._part = undefined + this._bparser = undefined + this._hparser = undefined +} + +Dicer.prototype.setBoundary = function (boundary) { + const self = this + this._bparser = new StreamSearch('\r\n--' + boundary) + this._bparser.on('info', function (isMatch, data, start, end) { + self._oninfo(isMatch, data, start, end) + }) +} + +Dicer.prototype._ignore = function () { + if (this._part && !this._ignoreData) { + this._ignoreData = true + this._part.on('error', EMPTY_FN) + // we must perform some kind of read on the stream even though we are + // ignoring the data, otherwise node's Readable stream will not emit 'end' + // after pushing null to the stream + this._part.resume() + } +} + +Dicer.prototype._oninfo = function (isMatch, data, start, end) { + let buf; const self = this; let i = 0; let r; let shouldWriteMore = true + + if (!this._part && this._justMatched && data) { + while (this._dashes < 2 && (start + i) < end) { + if (data[start + i] === DASH) { + ++i + ++this._dashes + } else { + if (this._dashes) { buf = B_ONEDASH } + this._dashes = 0 + break + } + } + if (this._dashes === 2) { + if ((start + i) < end && this._events.trailer) { this.emit('trailer', data.slice(start + i, end)) } + this.reset() + this._finished = true + // no more parts will be added + if (self._parts === 0) { + self._realFinish = true + self.emit('finish') + self._realFinish = false + } + } + if (this._dashes) { return } + } + if (this._justMatched) { this._justMatched = false } + if (!this._part) { + this._part = new PartStream(this._partOpts) + this._part._read = function (n) { + self._unpause() + } + if (this._isPreamble && this._events.preamble) { this.emit('preamble', this._part) } else if (this._isPreamble !== true && this._events.part) { this.emit('part', this._part) } else { this._ignore() } + if (!this._isPreamble) { this._inHeader = true } + } + if (data && start < end && !this._ignoreData) { + if (this._isPreamble || !this._inHeader) { + if (buf) { shouldWriteMore = this._part.push(buf) } + shouldWriteMore = this._part.push(data.slice(start, end)) + if (!shouldWriteMore) { this._pause = true } + } else if (!this._isPreamble && this._inHeader) { + if (buf) { this._hparser.push(buf) } + r = this._hparser.push(data.slice(start, end)) + if (!this._inHeader && r !== undefined && r < end) { this._oninfo(false, data, start + r, end) } + } + } + if (isMatch) { + this._hparser.reset() + if (this._isPreamble) { this._isPreamble = false } else { + if (start !== end) { + ++this._parts + this._part.on('end', function () { + if (--self._parts === 0) { + if (self._finished) { + self._realFinish = true + self.emit('finish') + self._realFinish = false + } else { + self._unpause() + } + } + }) + } + } + this._part.push(null) + this._part = undefined + this._ignoreData = false + this._justMatched = true + this._dashes = 0 + } +} + +Dicer.prototype._unpause = function () { + if (!this._pause) { return } + + this._pause = false + if (this._cb) { + const cb = this._cb + this._cb = undefined + cb() + } +} + +module.exports = Dicer diff --git a/fastify-busboy/deps/dicer/lib/HeaderParser.js b/fastify-busboy/deps/dicer/lib/HeaderParser.js new file mode 100644 index 0000000..65f667b --- /dev/null +++ b/fastify-busboy/deps/dicer/lib/HeaderParser.js @@ -0,0 +1,100 @@ +'use strict' + +const EventEmitter = require('node:events').EventEmitter +const inherits = require('node:util').inherits +const getLimit = require('../../../lib/utils/getLimit') + +const StreamSearch = require('../../streamsearch/sbmh') + +const B_DCRLF = Buffer.from('\r\n\r\n') +const RE_CRLF = /\r\n/g +const RE_HDR = /^([^:]+):[ \t]?([\x00-\xFF]+)?$/ // eslint-disable-line no-control-regex + +function HeaderParser (cfg) { + EventEmitter.call(this) + + cfg = cfg || {} + const self = this + this.nread = 0 + this.maxed = false + this.npairs = 0 + this.maxHeaderPairs = getLimit(cfg, 'maxHeaderPairs', 2000) + this.maxHeaderSize = getLimit(cfg, 'maxHeaderSize', 80 * 1024) + this.buffer = '' + this.header = {} + this.finished = false + this.ss = new StreamSearch(B_DCRLF) + this.ss.on('info', function (isMatch, data, start, end) { + if (data && !self.maxed) { + if (self.nread + end - start >= self.maxHeaderSize) { + end = self.maxHeaderSize - self.nread + start + self.nread = self.maxHeaderSize + self.maxed = true + } else { self.nread += (end - start) } + + self.buffer += data.toString('binary', start, end) + } + if (isMatch) { self._finish() } + }) +} +inherits(HeaderParser, EventEmitter) + +HeaderParser.prototype.push = function (data) { + const r = this.ss.push(data) + if (this.finished) { return r } +} + +HeaderParser.prototype.reset = function () { + this.finished = false + this.buffer = '' + this.header = {} + this.ss.reset() +} + +HeaderParser.prototype._finish = function () { + if (this.buffer) { this._parseHeader() } + this.ss.matches = this.ss.maxMatches + const header = this.header + this.header = {} + this.buffer = '' + this.finished = true + this.nread = this.npairs = 0 + this.maxed = false + this.emit('header', header) +} + +HeaderParser.prototype._parseHeader = function () { + if (this.npairs === this.maxHeaderPairs) { return } + + const lines = this.buffer.split(RE_CRLF) + const len = lines.length + let m, h + + for (var i = 0; i < len; ++i) { // eslint-disable-line no-var + if (lines[i].length === 0) { continue } + if (lines[i][0] === '\t' || lines[i][0] === ' ') { + // folded header content + // RFC2822 says to just remove the CRLF and not the whitespace following + // it, so we follow the RFC and include the leading whitespace ... + if (h) { + this.header[h][this.header[h].length - 1] += lines[i] + continue + } + } + + const posColon = lines[i].indexOf(':') + if ( + posColon === -1 || + posColon === 0 + ) { + return + } + m = RE_HDR.exec(lines[i]) + h = m[1].toLowerCase() + this.header[h] = this.header[h] || [] + this.header[h].push((m[2] || '')) + if (++this.npairs === this.maxHeaderPairs) { break } + } +} + +module.exports = HeaderParser diff --git a/fastify-busboy/deps/dicer/lib/PartStream.js b/fastify-busboy/deps/dicer/lib/PartStream.js new file mode 100644 index 0000000..c91da1c --- /dev/null +++ b/fastify-busboy/deps/dicer/lib/PartStream.js @@ -0,0 +1,13 @@ +'use strict' + +const inherits = require('node:util').inherits +const ReadableStream = require('node:stream').Readable + +function PartStream (opts) { + ReadableStream.call(this, opts) +} +inherits(PartStream, ReadableStream) + +PartStream.prototype._read = function (n) {} + +module.exports = PartStream diff --git a/fastify-busboy/deps/dicer/lib/dicer.d.ts b/fastify-busboy/deps/dicer/lib/dicer.d.ts new file mode 100644 index 0000000..3c5b896 --- /dev/null +++ b/fastify-busboy/deps/dicer/lib/dicer.d.ts @@ -0,0 +1,164 @@ +// Type definitions for dicer 0.2 +// Project: https://github.com/mscdex/dicer +// Definitions by: BendingBender <https://github.com/BendingBender> +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped +// TypeScript Version: 2.2 +/// <reference types="node" /> + +import stream = require("stream"); + +// tslint:disable:unified-signatures + +/** + * A very fast streaming multipart parser for node.js. + * Dicer is a WritableStream + * + * Dicer (special) events: + * - on('finish', ()) - Emitted when all parts have been parsed and the Dicer instance has been ended. + * - on('part', (stream: PartStream)) - Emitted when a new part has been found. + * - on('preamble', (stream: PartStream)) - Emitted for preamble if you should happen to need it (can usually be ignored). + * - on('trailer', (data: Buffer)) - Emitted when trailing data was found after the terminating boundary (as with the preamble, this can usually be ignored too). + */ +export class Dicer extends stream.Writable { + /** + * Creates and returns a new Dicer instance with the following valid config settings: + * + * @param config The configuration to use + */ + constructor(config: Dicer.Config); + /** + * Sets the boundary to use for parsing and performs some initialization needed for parsing. + * You should only need to use this if you set headerFirst to true in the constructor and are parsing the boundary from the preamble header. + * + * @param boundary The boundary to use + */ + setBoundary(boundary: string): void; + addListener(event: "finish", listener: () => void): this; + addListener(event: "part", listener: (stream: Dicer.PartStream) => void): this; + addListener(event: "preamble", listener: (stream: Dicer.PartStream) => void): this; + addListener(event: "trailer", listener: (data: Buffer) => void): this; + addListener(event: "close", listener: () => void): this; + addListener(event: "drain", listener: () => void): this; + addListener(event: "error", listener: (err: Error) => void): this; + addListener(event: "pipe", listener: (src: stream.Readable) => void): this; + addListener(event: "unpipe", listener: (src: stream.Readable) => void): this; + addListener(event: string, listener: (...args: any[]) => void): this; + on(event: "finish", listener: () => void): this; + on(event: "part", listener: (stream: Dicer.PartStream) => void): this; + on(event: "preamble", listener: (stream: Dicer.PartStream) => void): this; + on(event: "trailer", listener: (data: Buffer) => void): this; + on(event: "close", listener: () => void): this; + on(event: "drain", listener: () => void): this; + on(event: "error", listener: (err: Error) => void): this; + on(event: "pipe", listener: (src: stream.Readable) => void): this; + on(event: "unpipe", listener: (src: stream.Readable) => void): this; + on(event: string, listener: (...args: any[]) => void): this; + once(event: "finish", listener: () => void): this; + once(event: "part", listener: (stream: Dicer.PartStream) => void): this; + once(event: "preamble", listener: (stream: Dicer.PartStream) => void): this; + once(event: "trailer", listener: (data: Buffer) => void): this; + once(event: "close", listener: () => void): this; + once(event: "drain", listener: () => void): this; + once(event: "error", listener: (err: Error) => void): this; + once(event: "pipe", listener: (src: stream.Readable) => void): this; + once(event: "unpipe", listener: (src: stream.Readable) => void): this; + once(event: string, listener: (...args: any[]) => void): this; + prependListener(event: "finish", listener: () => void): this; + prependListener(event: "part", listener: (stream: Dicer.PartStream) => void): this; + prependListener(event: "preamble", listener: (stream: Dicer.PartStream) => void): this; + prependListener(event: "trailer", listener: (data: Buffer) => void): this; + prependListener(event: "close", listener: () => void): this; + prependListener(event: "drain", listener: () => void): this; + prependListener(event: "error", listener: (err: Error) => void): this; + prependListener(event: "pipe", listener: (src: stream.Readable) => void): this; + prependListener(event: "unpipe", listener: (src: stream.Readable) => void): this; + prependListener(event: string, listener: (...args: any[]) => void): this; + prependOnceListener(event: "finish", listener: () => void): this; + prependOnceListener(event: "part", listener: (stream: Dicer.PartStream) => void): this; + prependOnceListener(event: "preamble", listener: (stream: Dicer.PartStream) => void): this; + prependOnceListener(event: "trailer", listener: (data: Buffer) => void): this; + prependOnceListener(event: "close", listener: () => void): this; + prependOnceListener(event: "drain", listener: () => void): this; + prependOnceListener(event: "error", listener: (err: Error) => void): this; + prependOnceListener(event: "pipe", listener: (src: stream.Readable) => void): this; + prependOnceListener(event: "unpipe", listener: (src: stream.Readable) => void): this; + prependOnceListener(event: string, listener: (...args: any[]) => void): this; + removeListener(event: "finish", listener: () => void): this; + removeListener(event: "part", listener: (stream: Dicer.PartStream) => void): this; + removeListener(event: "preamble", listener: (stream: Dicer.PartStream) => void): this; + removeListener(event: "trailer", listener: (data: Buffer) => void): this; + removeListener(event: "close", listener: () => void): this; + removeListener(event: "drain", listener: () => void): this; + removeListener(event: "error", listener: (err: Error) => void): this; + removeListener(event: "pipe", listener: (src: stream.Readable) => void): this; + removeListener(event: "unpipe", listener: (src: stream.Readable) => void): this; + removeListener(event: string, listener: (...args: any[]) => void): this; +} + +declare namespace Dicer { + interface Config { + /** + * This is the boundary used to detect the beginning of a new part. + */ + boundary?: string | undefined; + /** + * If true, preamble header parsing will be performed first. + */ + headerFirst?: boolean | undefined; + /** + * The maximum number of header key=>value pairs to parse Default: 2000 (same as node's http). + */ + maxHeaderPairs?: number | undefined; + } + + /** + * PartStream is a _ReadableStream_ + * + * PartStream (special) events: + * - on('header', (header: object)) - An object containing the header for this particular part. Each property value is an array of one or more string values. + */ + interface PartStream extends stream.Readable { + addListener(event: "header", listener: (header: object) => void): this; + addListener(event: "close", listener: () => void): this; + addListener(event: "data", listener: (chunk: Buffer | string) => void): this; + addListener(event: "end", listener: () => void): this; + addListener(event: "readable", listener: () => void): this; + addListener(event: "error", listener: (err: Error) => void): this; + addListener(event: string, listener: (...args: any[]) => void): this; + on(event: "header", listener: (header: object) => void): this; + on(event: "close", listener: () => void): this; + on(event: "data", listener: (chunk: Buffer | string) => void): this; + on(event: "end", listener: () => void): this; + on(event: "readable", listener: () => void): this; + on(event: "error", listener: (err: Error) => void): this; + on(event: string, listener: (...args: any[]) => void): this; + once(event: "header", listener: (header: object) => void): this; + once(event: "close", listener: () => void): this; + once(event: "data", listener: (chunk: Buffer | string) => void): this; + once(event: "end", listener: () => void): this; + once(event: "readable", listener: () => void): this; + once(event: "error", listener: (err: Error) => void): this; + once(event: string, listener: (...args: any[]) => void): this; + prependListener(event: "header", listener: (header: object) => void): this; + prependListener(event: "close", listener: () => void): this; + prependListener(event: "data", listener: (chunk: Buffer | string) => void): this; + prependListener(event: "end", listener: () => void): this; + prependListener(event: "readable", listener: () => void): this; + prependListener(event: "error", listener: (err: Error) => void): this; + prependListener(event: string, listener: (...args: any[]) => void): this; + prependOnceListener(event: "header", listener: (header: object) => void): this; + prependOnceListener(event: "close", listener: () => void): this; + prependOnceListener(event: "data", listener: (chunk: Buffer | string) => void): this; + prependOnceListener(event: "end", listener: () => void): this; + prependOnceListener(event: "readable", listener: () => void): this; + prependOnceListener(event: "error", listener: (err: Error) => void): this; + prependOnceListener(event: string, listener: (...args: any[]) => void): this; + removeListener(event: "header", listener: (header: object) => void): this; + removeListener(event: "close", listener: () => void): this; + removeListener(event: "data", listener: (chunk: Buffer | string) => void): this; + removeListener(event: "end", listener: () => void): this; + removeListener(event: "readable", listener: () => void): this; + removeListener(event: "error", listener: (err: Error) => void): this; + removeListener(event: string, listener: (...args: any[]) => void): this; + } +}
\ No newline at end of file diff --git a/fastify-busboy/deps/streamsearch/sbmh.js b/fastify-busboy/deps/streamsearch/sbmh.js new file mode 100644 index 0000000..b90c0e8 --- /dev/null +++ b/fastify-busboy/deps/streamsearch/sbmh.js @@ -0,0 +1,228 @@ +'use strict' + +/** + * Copyright Brian White. All rights reserved. + * + * @see https://github.com/mscdex/streamsearch + * + * 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. + * + * Based heavily on the Streaming Boyer-Moore-Horspool C++ implementation + * by Hongli Lai at: https://github.com/FooBarWidget/boyer-moore-horspool + */ +const EventEmitter = require('node:events').EventEmitter +const inherits = require('node:util').inherits + +function SBMH (needle) { + if (typeof needle === 'string') { + needle = Buffer.from(needle) + } + + if (!Buffer.isBuffer(needle)) { + throw new TypeError('The needle has to be a String or a Buffer.') + } + + const needleLength = needle.length + + if (needleLength === 0) { + throw new Error('The needle cannot be an empty String/Buffer.') + } + + if (needleLength > 256) { + throw new Error('The needle cannot have a length bigger than 256.') + } + + this.maxMatches = Infinity + this.matches = 0 + + this._occ = new Array(256) + .fill(needleLength) // Initialize occurrence table. + this._lookbehind_size = 0 + this._needle = needle + this._bufpos = 0 + + this._lookbehind = Buffer.alloc(needleLength) + + // Populate occurrence table with analysis of the needle, + // ignoring last letter. + for (var i = 0; i < needleLength - 1; ++i) { // eslint-disable-line no-var + this._occ[needle[i]] = needleLength - 1 - i + } +} +inherits(SBMH, EventEmitter) + +SBMH.prototype.reset = function () { + this._lookbehind_size = 0 + this.matches = 0 + this._bufpos = 0 +} + +SBMH.prototype.push = function (chunk, pos) { + if (!Buffer.isBuffer(chunk)) { + chunk = Buffer.from(chunk, 'binary') + } + const chlen = chunk.length + this._bufpos = pos || 0 + let r + while (r !== chlen && this.matches < this.maxMatches) { r = this._sbmh_feed(chunk) } + return r +} + +SBMH.prototype._sbmh_feed = function (data) { + const len = data.length + const needle = this._needle + const needleLength = needle.length + const lastNeedleChar = needle[needleLength - 1] + + // Positive: points to a position in `data` + // pos == 3 points to data[3] + // Negative: points to a position in the lookbehind buffer + // pos == -2 points to lookbehind[lookbehind_size - 2] + let pos = -this._lookbehind_size + let ch + + if (pos < 0) { + // Lookbehind buffer is not empty. Perform Boyer-Moore-Horspool + // search with character lookup code that considers both the + // lookbehind buffer and the current round's haystack data. + // + // Loop until + // there is a match. + // or until + // we've moved past the position that requires the + // lookbehind buffer. In this case we switch to the + // optimized loop. + // or until + // the character to look at lies outside the haystack. + while (pos < 0 && pos <= len - needleLength) { + ch = this._sbmh_lookup_char(data, pos + needleLength - 1) + + if ( + ch === lastNeedleChar && + this._sbmh_memcmp(data, pos, needleLength - 1) + ) { + this._lookbehind_size = 0 + ++this.matches + this.emit('info', true) + + return (this._bufpos = pos + needleLength) + } + pos += this._occ[ch] + } + + // No match. + + if (pos < 0) { + // There's too few data for Boyer-Moore-Horspool to run, + // so let's use a different algorithm to skip as much as + // we can. + // Forward pos until + // the trailing part of lookbehind + data + // looks like the beginning of the needle + // or until + // pos == 0 + while (pos < 0 && !this._sbmh_memcmp(data, pos, len - pos)) { ++pos } + } + + if (pos >= 0) { + // Discard lookbehind buffer. + this.emit('info', false, this._lookbehind, 0, this._lookbehind_size) + this._lookbehind_size = 0 + } else { + // Cut off part of the lookbehind buffer that has + // been processed and append the entire haystack + // into it. + const bytesToCutOff = this._lookbehind_size + pos + if (bytesToCutOff > 0) { + // The cut off data is guaranteed not to contain the needle. + this.emit('info', false, this._lookbehind, 0, bytesToCutOff) + } + + this._lookbehind.copy(this._lookbehind, 0, bytesToCutOff, + this._lookbehind_size - bytesToCutOff) + this._lookbehind_size -= bytesToCutOff + + data.copy(this._lookbehind, this._lookbehind_size) + this._lookbehind_size += len + + this._bufpos = len + return len + } + } + + pos += (pos >= 0) * this._bufpos + + // Lookbehind buffer is now empty. We only need to check if the + // needle is in the haystack. + if (data.indexOf(needle, pos) !== -1) { + pos = data.indexOf(needle, pos) + ++this.matches + if (pos > 0) { this.emit('info', true, data, this._bufpos, pos) } else { this.emit('info', true) } + + return (this._bufpos = pos + needleLength) + } else { + pos = len - needleLength + } + + // There was no match. If there's trailing haystack data that we cannot + // match yet using the Boyer-Moore-Horspool algorithm (because the trailing + // data is less than the needle size) then match using a modified + // algorithm that starts matching from the beginning instead of the end. + // Whatever trailing data is left after running this algorithm is added to + // the lookbehind buffer. + while ( + pos < len && + ( + data[pos] !== needle[0] || + ( + (Buffer.compare( + data.subarray(pos, pos + len - pos), + needle.subarray(0, len - pos) + ) !== 0) + ) + ) + ) { + ++pos + } + if (pos < len) { + data.copy(this._lookbehind, 0, pos, pos + (len - pos)) + this._lookbehind_size = len - pos + } + + // Everything until pos is guaranteed not to contain needle data. + if (pos > 0) { this.emit('info', false, data, this._bufpos, pos < len ? pos : len) } + + this._bufpos = len + return len +} + +SBMH.prototype._sbmh_lookup_char = function (data, pos) { + return (pos < 0) + ? this._lookbehind[this._lookbehind_size + pos] + : data[pos] +} + +SBMH.prototype._sbmh_memcmp = function (data, pos, len) { + for (var i = 0; i < len; ++i) { // eslint-disable-line no-var + if (this._sbmh_lookup_char(data, pos + i) !== this._needle[i]) { return false } + } + return true +} + +module.exports = SBMH diff --git a/fastify-busboy/lib/main.d.ts b/fastify-busboy/lib/main.d.ts new file mode 100644 index 0000000..91b6448 --- /dev/null +++ b/fastify-busboy/lib/main.d.ts @@ -0,0 +1,196 @@ +// Definitions by: Jacob Baskin <https://github.com/jacobbaskin> +// BendingBender <https://github.com/BendingBender> +// Igor Savin <https://github.com/kibertoad> + +/// <reference types="node" /> + +import * as http from 'http'; +import { Readable, Writable } from 'stream'; +export { Dicer } from "../deps/dicer/lib/dicer"; + +export const Busboy: BusboyConstructor; +export default Busboy; + +export interface BusboyConfig { + /** + * These are the HTTP headers of the incoming request, which are used by individual parsers. + */ + headers: BusboyHeaders; + /** + * `highWaterMark` to use for this Busboy instance. + * @default WritableStream default. + */ + highWaterMark?: number | undefined; + /** + * highWaterMark to use for file streams. + * @default ReadableStream default. + */ + fileHwm?: number | undefined; + /** + * Default character set to use when one isn't defined. + * @default 'utf8' + */ + defCharset?: string | undefined; + /** + * Detect if a Part is a file. + * + * By default a file is detected if contentType + * is application/octet-stream or fileName is not + * undefined. + * + * Modify this to handle e.g. Blobs. + */ + isPartAFile?: (fieldName: string | undefined, contentType: string | undefined, fileName: string | undefined) => boolean; + /** + * If paths in the multipart 'filename' field shall be preserved. + * @default false + */ + preservePath?: boolean | undefined; + /** + * Various limits on incoming data. + */ + limits?: + | { + /** + * Max field name size (in bytes) + * @default 100 bytes + */ + fieldNameSize?: number | undefined; + /** + * Max field value size (in bytes) + * @default 1MB + */ + fieldSize?: number | undefined; + /** + * Max number of non-file fields + * @default Infinity + */ + fields?: number | undefined; + /** + * For multipart forms, the max file size (in bytes) + * @default Infinity + */ + fileSize?: number | undefined; + /** + * For multipart forms, the max number of file fields + * @default Infinity + */ + files?: number | undefined; + /** + * For multipart forms, the max number of parts (fields + files) + * @default Infinity + */ + parts?: number | undefined; + /** + * For multipart forms, the max number of header key=>value pairs to parse + * @default 2000 + */ + headerPairs?: number | undefined; + + /** + * For multipart forms, the max size of a header part + * @default 81920 + */ + headerSize?: number | undefined; + } + | undefined; +} + +export type BusboyHeaders = { 'content-type': string } & http.IncomingHttpHeaders; + +export interface BusboyFileStream extends + Readable { + + truncated: boolean; + + /** + * The number of bytes that have been read so far. + */ + bytesRead: number; +} + +export interface Busboy extends Writable { + addListener<Event extends keyof BusboyEvents>(event: Event, listener: BusboyEvents[Event]): this; + + addListener(event: string | symbol, listener: (...args: any[]) => void): this; + + on<Event extends keyof BusboyEvents>(event: Event, listener: BusboyEvents[Event]): this; + + on(event: string | symbol, listener: (...args: any[]) => void): this; + + once<Event extends keyof BusboyEvents>(event: Event, listener: BusboyEvents[Event]): this; + + once(event: string | symbol, listener: (...args: any[]) => void): this; + + removeListener<Event extends keyof BusboyEvents>(event: Event, listener: BusboyEvents[Event]): this; + + removeListener(event: string | symbol, listener: (...args: any[]) => void): this; + + off<Event extends keyof BusboyEvents>(event: Event, listener: BusboyEvents[Event]): this; + + off(event: string | symbol, listener: (...args: any[]) => void): this; + + prependListener<Event extends keyof BusboyEvents>(event: Event, listener: BusboyEvents[Event]): this; + + prependListener(event: string | symbol, listener: (...args: any[]) => void): this; + + prependOnceListener<Event extends keyof BusboyEvents>(event: Event, listener: BusboyEvents[Event]): this; + + prependOnceListener(event: string | symbol, listener: (...args: any[]) => void): this; +} + +export interface BusboyEvents { + /** + * Emitted for each new file form field found. + * + * * Note: if you listen for this event, you should always handle the `stream` no matter if you care about the + * file contents or not (e.g. you can simply just do `stream.resume();` if you want to discard the contents), + * otherwise the 'finish' event will never fire on the Busboy instance. However, if you don't care about **any** + * incoming files, you can simply not listen for the 'file' event at all and any/all files will be automatically + * and safely discarded (these discarded files do still count towards `files` and `parts` limits). + * * If a configured file size limit was reached, `stream` will both have a boolean property `truncated` + * (best checked at the end of the stream) and emit a 'limit' event to notify you when this happens. + * + * @param listener.transferEncoding Contains the 'Content-Transfer-Encoding' value for the file stream. + * @param listener.mimeType Contains the 'Content-Type' value for the file stream. + */ + file: ( + fieldname: string, + stream: BusboyFileStream, + filename: string, + transferEncoding: string, + mimeType: string, + ) => void; + /** + * Emitted for each new non-file field found. + */ + field: ( + fieldname: string, + value: string, + fieldnameTruncated: boolean, + valueTruncated: boolean, + transferEncoding: string, + mimeType: string, + ) => void; + finish: () => void; + /** + * Emitted when specified `parts` limit has been reached. No more 'file' or 'field' events will be emitted. + */ + partsLimit: () => void; + /** + * Emitted when specified `files` limit has been reached. No more 'file' events will be emitted. + */ + filesLimit: () => void; + /** + * Emitted when specified `fields` limit has been reached. No more 'field' events will be emitted. + */ + fieldsLimit: () => void; + error: (error: unknown) => void; +} + +export interface BusboyConstructor { + (options: BusboyConfig): Busboy; + + new(options: BusboyConfig): Busboy; +} + diff --git a/fastify-busboy/lib/main.js b/fastify-busboy/lib/main.js new file mode 100644 index 0000000..8794beb --- /dev/null +++ b/fastify-busboy/lib/main.js @@ -0,0 +1,85 @@ +'use strict' + +const WritableStream = require('node:stream').Writable +const { inherits } = require('node:util') +const Dicer = require('../deps/dicer/lib/Dicer') + +const MultipartParser = require('./types/multipart') +const UrlencodedParser = require('./types/urlencoded') +const parseParams = require('./utils/parseParams') + +function Busboy (opts) { + if (!(this instanceof Busboy)) { return new Busboy(opts) } + + if (typeof opts !== 'object') { + throw new TypeError('Busboy expected an options-Object.') + } + if (typeof opts.headers !== 'object') { + throw new TypeError('Busboy expected an options-Object with headers-attribute.') + } + if (typeof opts.headers['content-type'] !== 'string') { + throw new TypeError('Missing Content-Type-header.') + } + + const { + headers, + ...streamOptions + } = opts + + this.opts = { + autoDestroy: false, + ...streamOptions + } + WritableStream.call(this, this.opts) + + this._done = false + this._parser = this.getParserByHeaders(headers) + this._finished = false +} +inherits(Busboy, WritableStream) + +Busboy.prototype.emit = function (ev) { + if (ev === 'finish') { + if (!this._done) { + this._parser?.end() + return + } else if (this._finished) { + return + } + this._finished = true + } + WritableStream.prototype.emit.apply(this, arguments) +} + +Busboy.prototype.getParserByHeaders = function (headers) { + const parsed = parseParams(headers['content-type']) + + const cfg = { + defCharset: this.opts.defCharset, + fileHwm: this.opts.fileHwm, + headers, + highWaterMark: this.opts.highWaterMark, + isPartAFile: this.opts.isPartAFile, + limits: this.opts.limits, + parsedConType: parsed, + preservePath: this.opts.preservePath + } + + if (MultipartParser.detect.test(parsed[0])) { + return new MultipartParser(this, cfg) + } + if (UrlencodedParser.detect.test(parsed[0])) { + return new UrlencodedParser(this, cfg) + } + throw new Error('Unsupported Content-Type.') +} + +Busboy.prototype._write = function (chunk, encoding, cb) { + this._parser.write(chunk, cb) +} + +module.exports = Busboy +module.exports.default = Busboy +module.exports.Busboy = Busboy + +module.exports.Dicer = Dicer diff --git a/fastify-busboy/lib/types/multipart.js b/fastify-busboy/lib/types/multipart.js new file mode 100644 index 0000000..ad242db --- /dev/null +++ b/fastify-busboy/lib/types/multipart.js @@ -0,0 +1,306 @@ +'use strict' + +// TODO: +// * support 1 nested multipart level +// (see second multipart example here: +// http://www.w3.org/TR/html401/interact/forms.html#didx-multipartform-data) +// * support limits.fieldNameSize +// -- this will require modifications to utils.parseParams + +const { Readable } = require('node:stream') +const { inherits } = require('node:util') + +const Dicer = require('../../deps/dicer/lib/Dicer') + +const parseParams = require('../utils/parseParams') +const decodeText = require('../utils/decodeText') +const basename = require('../utils/basename') +const getLimit = require('../utils/getLimit') + +const RE_BOUNDARY = /^boundary$/i +const RE_FIELD = /^form-data$/i +const RE_CHARSET = /^charset$/i +const RE_FILENAME = /^filename$/i +const RE_NAME = /^name$/i + +Multipart.detect = /^multipart\/form-data/i +function Multipart (boy, cfg) { + let i + let len + const self = this + let boundary + const limits = cfg.limits + const isPartAFile = cfg.isPartAFile || ((fieldName, contentType, fileName) => (contentType === 'application/octet-stream' || fileName !== undefined)) + const parsedConType = cfg.parsedConType || [] + const defCharset = cfg.defCharset || 'utf8' + const preservePath = cfg.preservePath + const fileOpts = { highWaterMark: cfg.fileHwm } + + for (i = 0, len = parsedConType.length; i < len; ++i) { + if (Array.isArray(parsedConType[i]) && + RE_BOUNDARY.test(parsedConType[i][0])) { + boundary = parsedConType[i][1] + break + } + } + + function checkFinished () { + if (nends === 0 && finished && !boy._done) { + finished = false + self.end() + } + } + + if (typeof boundary !== 'string') { throw new Error('Multipart: Boundary not found') } + + const fieldSizeLimit = getLimit(limits, 'fieldSize', 1 * 1024 * 1024) + const fileSizeLimit = getLimit(limits, 'fileSize', Infinity) + const filesLimit = getLimit(limits, 'files', Infinity) + const fieldsLimit = getLimit(limits, 'fields', Infinity) + const partsLimit = getLimit(limits, 'parts', Infinity) + const headerPairsLimit = getLimit(limits, 'headerPairs', 2000) + const headerSizeLimit = getLimit(limits, 'headerSize', 80 * 1024) + + let nfiles = 0 + let nfields = 0 + let nends = 0 + let curFile + let curField + let finished = false + + this._needDrain = false + this._pause = false + this._cb = undefined + this._nparts = 0 + this._boy = boy + + const parserCfg = { + boundary, + maxHeaderPairs: headerPairsLimit, + maxHeaderSize: headerSizeLimit, + partHwm: fileOpts.highWaterMark, + highWaterMark: cfg.highWaterMark + } + + this.parser = new Dicer(parserCfg) + this.parser.on('drain', function () { + self._needDrain = false + if (self._cb && !self._pause) { + const cb = self._cb + self._cb = undefined + cb() + } + }).on('part', function onPart (part) { + if (++self._nparts > partsLimit) { + self.parser.removeListener('part', onPart) + self.parser.on('part', skipPart) + boy.hitPartsLimit = true + boy.emit('partsLimit') + return skipPart(part) + } + + // hack because streams2 _always_ doesn't emit 'end' until nextTick, so let + // us emit 'end' early since we know the part has ended if we are already + // seeing the next part + if (curField) { + const field = curField + field.emit('end') + field.removeAllListeners('end') + } + + part.on('header', function (header) { + let contype + let fieldname + let parsed + let charset + let encoding + let filename + let nsize = 0 + + if (header['content-type']) { + parsed = parseParams(header['content-type'][0]) + if (parsed[0]) { + contype = parsed[0].toLowerCase() + for (i = 0, len = parsed.length; i < len; ++i) { + if (RE_CHARSET.test(parsed[i][0])) { + charset = parsed[i][1].toLowerCase() + break + } + } + } + } + + if (contype === undefined) { contype = 'text/plain' } + if (charset === undefined) { charset = defCharset } + + if (header['content-disposition']) { + parsed = parseParams(header['content-disposition'][0]) + if (!RE_FIELD.test(parsed[0])) { return skipPart(part) } + for (i = 0, len = parsed.length; i < len; ++i) { + if (RE_NAME.test(parsed[i][0])) { + fieldname = parsed[i][1] + } else if (RE_FILENAME.test(parsed[i][0])) { + filename = parsed[i][1] + if (!preservePath) { filename = basename(filename) } + } + } + } else { return skipPart(part) } + + if (header['content-transfer-encoding']) { encoding = header['content-transfer-encoding'][0].toLowerCase() } else { encoding = '7bit' } + + let onData, + onEnd + + if (isPartAFile(fieldname, contype, filename)) { + // file/binary field + if (nfiles === filesLimit) { + if (!boy.hitFilesLimit) { + boy.hitFilesLimit = true + boy.emit('filesLimit') + } + return skipPart(part) + } + + ++nfiles + + if (!boy._events.file) { + self.parser._ignore() + return + } + + ++nends + const file = new FileStream(fileOpts) + curFile = file + file.on('end', function () { + --nends + self._pause = false + checkFinished() + if (self._cb && !self._needDrain) { + const cb = self._cb + self._cb = undefined + cb() + } + }) + file._read = function (n) { + if (!self._pause) { return } + self._pause = false + if (self._cb && !self._needDrain) { + const cb = self._cb + self._cb = undefined + cb() + } + } + boy.emit('file', fieldname, file, filename, encoding, contype) + + onData = function (data) { + if ((nsize += data.length) > fileSizeLimit) { + const extralen = fileSizeLimit - nsize + data.length + if (extralen > 0) { file.push(data.slice(0, extralen)) } + file.truncated = true + file.bytesRead = fileSizeLimit + part.removeAllListeners('data') + file.emit('limit') + return + } else if (!file.push(data)) { self._pause = true } + + file.bytesRead = nsize + } + + onEnd = function () { + curFile = undefined + file.push(null) + } + } else { + // non-file field + if (nfields === fieldsLimit) { + if (!boy.hitFieldsLimit) { + boy.hitFieldsLimit = true + boy.emit('fieldsLimit') + } + return skipPart(part) + } + + ++nfields + ++nends + let buffer = '' + let truncated = false + curField = part + + onData = function (data) { + if ((nsize += data.length) > fieldSizeLimit) { + const extralen = (fieldSizeLimit - (nsize - data.length)) + buffer += data.toString('binary', 0, extralen) + truncated = true + part.removeAllListeners('data') + } else { buffer += data.toString('binary') } + } + + onEnd = function () { + curField = undefined + if (buffer.length) { buffer = decodeText(buffer, 'binary', charset) } + boy.emit('field', fieldname, buffer, false, truncated, encoding, contype) + --nends + checkFinished() + } + } + + /* As of node@2efe4ab761666 (v0.10.29+/v0.11.14+), busboy had become + broken. Streams2/streams3 is a huge black box of confusion, but + somehow overriding the sync state seems to fix things again (and still + seems to work for previous node versions). + */ + part._readableState.sync = false + + part.on('data', onData) + part.on('end', onEnd) + }).on('error', function (err) { + if (curFile) { curFile.emit('error', err) } + }) + }).on('error', function (err) { + boy.emit('error', err) + }).on('finish', function () { + finished = true + checkFinished() + }) +} + +Multipart.prototype.write = function (chunk, cb) { + const r = this.parser.write(chunk) + if (r && !this._pause) { + cb() + } else { + this._needDrain = !r + this._cb = cb + } +} + +Multipart.prototype.end = function () { + const self = this + + if (self.parser.writable) { + self.parser.end() + } else if (!self._boy._done) { + process.nextTick(function () { + self._boy._done = true + self._boy.emit('finish') + }) + } +} + +function skipPart (part) { + part.resume() +} + +function FileStream (opts) { + Readable.call(this, opts) + + this.bytesRead = 0 + + this.truncated = false +} + +inherits(FileStream, Readable) + +FileStream.prototype._read = function (n) {} + +module.exports = Multipart diff --git a/fastify-busboy/lib/types/urlencoded.js b/fastify-busboy/lib/types/urlencoded.js new file mode 100644 index 0000000..6f5f784 --- /dev/null +++ b/fastify-busboy/lib/types/urlencoded.js @@ -0,0 +1,190 @@ +'use strict' + +const Decoder = require('../utils/Decoder') +const decodeText = require('../utils/decodeText') +const getLimit = require('../utils/getLimit') + +const RE_CHARSET = /^charset$/i + +UrlEncoded.detect = /^application\/x-www-form-urlencoded/i +function UrlEncoded (boy, cfg) { + const limits = cfg.limits + const parsedConType = cfg.parsedConType + this.boy = boy + + this.fieldSizeLimit = getLimit(limits, 'fieldSize', 1 * 1024 * 1024) + this.fieldNameSizeLimit = getLimit(limits, 'fieldNameSize', 100) + this.fieldsLimit = getLimit(limits, 'fields', Infinity) + + let charset + for (var i = 0, len = parsedConType.length; i < len; ++i) { // eslint-disable-line no-var + if (Array.isArray(parsedConType[i]) && + RE_CHARSET.test(parsedConType[i][0])) { + charset = parsedConType[i][1].toLowerCase() + break + } + } + + if (charset === undefined) { charset = cfg.defCharset || 'utf8' } + + this.decoder = new Decoder() + this.charset = charset + this._fields = 0 + this._state = 'key' + this._checkingBytes = true + this._bytesKey = 0 + this._bytesVal = 0 + this._key = '' + this._val = '' + this._keyTrunc = false + this._valTrunc = false + this._hitLimit = false +} + +UrlEncoded.prototype.write = function (data, cb) { + if (this._fields === this.fieldsLimit) { + if (!this.boy.hitFieldsLimit) { + this.boy.hitFieldsLimit = true + this.boy.emit('fieldsLimit') + } + return cb() + } + + let idxeq; let idxamp; let i; let p = 0; const len = data.length + + while (p < len) { + if (this._state === 'key') { + idxeq = idxamp = undefined + for (i = p; i < len; ++i) { + if (!this._checkingBytes) { ++p } + if (data[i] === 0x3D/* = */) { + idxeq = i + break + } else if (data[i] === 0x26/* & */) { + idxamp = i + break + } + if (this._checkingBytes && this._bytesKey === this.fieldNameSizeLimit) { + this._hitLimit = true + break + } else if (this._checkingBytes) { ++this._bytesKey } + } + + if (idxeq !== undefined) { + // key with assignment + if (idxeq > p) { this._key += this.decoder.write(data.toString('binary', p, idxeq)) } + this._state = 'val' + + this._hitLimit = false + this._checkingBytes = true + this._val = '' + this._bytesVal = 0 + this._valTrunc = false + this.decoder.reset() + + p = idxeq + 1 + } else if (idxamp !== undefined) { + // key with no assignment + ++this._fields + let key; const keyTrunc = this._keyTrunc + if (idxamp > p) { key = (this._key += this.decoder.write(data.toString('binary', p, idxamp))) } else { key = this._key } + + this._hitLimit = false + this._checkingBytes = true + this._key = '' + this._bytesKey = 0 + this._keyTrunc = false + this.decoder.reset() + + if (key.length) { + this.boy.emit('field', decodeText(key, 'binary', this.charset), + '', + keyTrunc, + false) + } + + p = idxamp + 1 + if (this._fields === this.fieldsLimit) { return cb() } + } else if (this._hitLimit) { + // we may not have hit the actual limit if there are encoded bytes... + if (i > p) { this._key += this.decoder.write(data.toString('binary', p, i)) } + p = i + if ((this._bytesKey = this._key.length) === this.fieldNameSizeLimit) { + // yep, we actually did hit the limit + this._checkingBytes = false + this._keyTrunc = true + } + } else { + if (p < len) { this._key += this.decoder.write(data.toString('binary', p)) } + p = len + } + } else { + idxamp = undefined + for (i = p; i < len; ++i) { + if (!this._checkingBytes) { ++p } + if (data[i] === 0x26/* & */) { + idxamp = i + break + } + if (this._checkingBytes && this._bytesVal === this.fieldSizeLimit) { + this._hitLimit = true + break + } else if (this._checkingBytes) { ++this._bytesVal } + } + + if (idxamp !== undefined) { + ++this._fields + if (idxamp > p) { this._val += this.decoder.write(data.toString('binary', p, idxamp)) } + this.boy.emit('field', decodeText(this._key, 'binary', this.charset), + decodeText(this._val, 'binary', this.charset), + this._keyTrunc, + this._valTrunc) + this._state = 'key' + + this._hitLimit = false + this._checkingBytes = true + this._key = '' + this._bytesKey = 0 + this._keyTrunc = false + this.decoder.reset() + + p = idxamp + 1 + if (this._fields === this.fieldsLimit) { return cb() } + } else if (this._hitLimit) { + // we may not have hit the actual limit if there are encoded bytes... + if (i > p) { this._val += this.decoder.write(data.toString('binary', p, i)) } + p = i + if ((this._val === '' && this.fieldSizeLimit === 0) || + (this._bytesVal = this._val.length) === this.fieldSizeLimit) { + // yep, we actually did hit the limit + this._checkingBytes = false + this._valTrunc = true + } + } else { + if (p < len) { this._val += this.decoder.write(data.toString('binary', p)) } + p = len + } + } + } + cb() +} + +UrlEncoded.prototype.end = function () { + if (this.boy._done) { return } + + if (this._state === 'key' && this._key.length > 0) { + this.boy.emit('field', decodeText(this._key, 'binary', this.charset), + '', + this._keyTrunc, + false) + } else if (this._state === 'val') { + this.boy.emit('field', decodeText(this._key, 'binary', this.charset), + decodeText(this._val, 'binary', this.charset), + this._keyTrunc, + this._valTrunc) + } + this.boy._done = true + this.boy.emit('finish') +} + +module.exports = UrlEncoded diff --git a/fastify-busboy/lib/utils/Decoder.js b/fastify-busboy/lib/utils/Decoder.js new file mode 100644 index 0000000..7917678 --- /dev/null +++ b/fastify-busboy/lib/utils/Decoder.js @@ -0,0 +1,54 @@ +'use strict' + +const RE_PLUS = /\+/g + +const HEX = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 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, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, + 0, 1, 1, 1, 1, 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, 1, 1, 1, 1, 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 +] + +function Decoder () { + this.buffer = undefined +} +Decoder.prototype.write = function (str) { + // Replace '+' with ' ' before decoding + str = str.replace(RE_PLUS, ' ') + let res = '' + let i = 0; let p = 0; const len = str.length + for (; i < len; ++i) { + if (this.buffer !== undefined) { + if (!HEX[str.charCodeAt(i)]) { + res += '%' + this.buffer + this.buffer = undefined + --i // retry character + } else { + this.buffer += str[i] + ++p + if (this.buffer.length === 2) { + res += String.fromCharCode(parseInt(this.buffer, 16)) + this.buffer = undefined + } + } + } else if (str[i] === '%') { + if (i > p) { + res += str.substring(p, i) + p = i + } + this.buffer = '' + ++p + } + } + if (p < len && this.buffer === undefined) { res += str.substring(p) } + return res +} +Decoder.prototype.reset = function () { + this.buffer = undefined +} + +module.exports = Decoder diff --git a/fastify-busboy/lib/utils/basename.js b/fastify-busboy/lib/utils/basename.js new file mode 100644 index 0000000..db58819 --- /dev/null +++ b/fastify-busboy/lib/utils/basename.js @@ -0,0 +1,14 @@ +'use strict' + +module.exports = function basename (path) { + if (typeof path !== 'string') { return '' } + for (var i = path.length - 1; i >= 0; --i) { // eslint-disable-line no-var + switch (path.charCodeAt(i)) { + case 0x2F: // '/' + case 0x5C: // '\' + path = path.slice(i + 1) + return (path === '..' || path === '.' ? '' : path) + } + } + return (path === '..' || path === '.' ? '' : path) +} diff --git a/fastify-busboy/lib/utils/decodeText.js b/fastify-busboy/lib/utils/decodeText.js new file mode 100644 index 0000000..be35d6b --- /dev/null +++ b/fastify-busboy/lib/utils/decodeText.js @@ -0,0 +1,114 @@ +'use strict' + +// Node has always utf-8 +const utf8Decoder = new TextDecoder('utf-8') +const textDecoders = new Map([ + ['utf-8', utf8Decoder], + ['utf8', utf8Decoder] +]) + +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, sourceEncoding) => { + if (data.length === 0) { + return '' + } + if (typeof data === 'string') { + data = Buffer.from(data, sourceEncoding) + } + return data.utf8Slice(0, data.length) + }, + + latin1: (data, sourceEncoding) => { + if (data.length === 0) { + return '' + } + if (typeof data === 'string') { + return data + } + return data.latin1Slice(0, data.length) + }, + + utf16le: (data, sourceEncoding) => { + if (data.length === 0) { + return '' + } + if (typeof data === 'string') { + data = Buffer.from(data, sourceEncoding) + } + return data.ucs2Slice(0, data.length) + }, + + base64: (data, sourceEncoding) => { + if (data.length === 0) { + return '' + } + if (typeof data === 'string') { + data = Buffer.from(data, sourceEncoding) + } + return data.base64Slice(0, data.length) + }, + + other: (data, sourceEncoding) => { + if (data.length === 0) { + return '' + } + if (typeof data === 'string') { + data = Buffer.from(data, sourceEncoding) + } + + if (textDecoders.has(this.toString())) { + try { + return textDecoders.get(this).decode(data) + } catch (e) { } + } + return typeof data === 'string' + ? data + : data.toString() + } +} + +function decodeText (text, sourceEncoding, destEncoding) { + if (text) { + return getDecoder(destEncoding)(text, sourceEncoding) + } + return text +} + +module.exports = decodeText diff --git a/fastify-busboy/lib/utils/getLimit.js b/fastify-busboy/lib/utils/getLimit.js new file mode 100644 index 0000000..cb64fd6 --- /dev/null +++ b/fastify-busboy/lib/utils/getLimit.js @@ -0,0 +1,16 @@ +'use strict' + +module.exports = function getLimit (limits, name, defaultLimit) { + if ( + !limits || + limits[name] === undefined || + limits[name] === null + ) { return defaultLimit } + + if ( + typeof limits[name] !== 'number' || + isNaN(limits[name]) + ) { throw new TypeError('Limit ' + name + ' is not a valid number') } + + return limits[name] +} diff --git a/fastify-busboy/lib/utils/parseParams.js b/fastify-busboy/lib/utils/parseParams.js new file mode 100644 index 0000000..1698e62 --- /dev/null +++ b/fastify-busboy/lib/utils/parseParams.js @@ -0,0 +1,196 @@ +/* eslint-disable object-property-newline */ +'use strict' + +const decodeText = require('./decodeText') + +const RE_ENCODED = /%[a-fA-F0-9][a-fA-F0-9]/g + +const EncodedLookup = { + '%00': '\x00', '%01': '\x01', '%02': '\x02', '%03': '\x03', '%04': '\x04', + '%05': '\x05', '%06': '\x06', '%07': '\x07', '%08': '\x08', '%09': '\x09', + '%0a': '\x0a', '%0A': '\x0a', '%0b': '\x0b', '%0B': '\x0b', '%0c': '\x0c', + '%0C': '\x0c', '%0d': '\x0d', '%0D': '\x0d', '%0e': '\x0e', '%0E': '\x0e', + '%0f': '\x0f', '%0F': '\x0f', '%10': '\x10', '%11': '\x11', '%12': '\x12', + '%13': '\x13', '%14': '\x14', '%15': '\x15', '%16': '\x16', '%17': '\x17', + '%18': '\x18', '%19': '\x19', '%1a': '\x1a', '%1A': '\x1a', '%1b': '\x1b', + '%1B': '\x1b', '%1c': '\x1c', '%1C': '\x1c', '%1d': '\x1d', '%1D': '\x1d', + '%1e': '\x1e', '%1E': '\x1e', '%1f': '\x1f', '%1F': '\x1f', '%20': '\x20', + '%21': '\x21', '%22': '\x22', '%23': '\x23', '%24': '\x24', '%25': '\x25', + '%26': '\x26', '%27': '\x27', '%28': '\x28', '%29': '\x29', '%2a': '\x2a', + '%2A': '\x2a', '%2b': '\x2b', '%2B': '\x2b', '%2c': '\x2c', '%2C': '\x2c', + '%2d': '\x2d', '%2D': '\x2d', '%2e': '\x2e', '%2E': '\x2e', '%2f': '\x2f', + '%2F': '\x2f', '%30': '\x30', '%31': '\x31', '%32': '\x32', '%33': '\x33', + '%34': '\x34', '%35': '\x35', '%36': '\x36', '%37': '\x37', '%38': '\x38', + '%39': '\x39', '%3a': '\x3a', '%3A': '\x3a', '%3b': '\x3b', '%3B': '\x3b', + '%3c': '\x3c', '%3C': '\x3c', '%3d': '\x3d', '%3D': '\x3d', '%3e': '\x3e', + '%3E': '\x3e', '%3f': '\x3f', '%3F': '\x3f', '%40': '\x40', '%41': '\x41', + '%42': '\x42', '%43': '\x43', '%44': '\x44', '%45': '\x45', '%46': '\x46', + '%47': '\x47', '%48': '\x48', '%49': '\x49', '%4a': '\x4a', '%4A': '\x4a', + '%4b': '\x4b', '%4B': '\x4b', '%4c': '\x4c', '%4C': '\x4c', '%4d': '\x4d', + '%4D': '\x4d', '%4e': '\x4e', '%4E': '\x4e', '%4f': '\x4f', '%4F': '\x4f', + '%50': '\x50', '%51': '\x51', '%52': '\x52', '%53': '\x53', '%54': '\x54', + '%55': '\x55', '%56': '\x56', '%57': '\x57', '%58': '\x58', '%59': '\x59', + '%5a': '\x5a', '%5A': '\x5a', '%5b': '\x5b', '%5B': '\x5b', '%5c': '\x5c', + '%5C': '\x5c', '%5d': '\x5d', '%5D': '\x5d', '%5e': '\x5e', '%5E': '\x5e', + '%5f': '\x5f', '%5F': '\x5f', '%60': '\x60', '%61': '\x61', '%62': '\x62', + '%63': '\x63', '%64': '\x64', '%65': '\x65', '%66': '\x66', '%67': '\x67', + '%68': '\x68', '%69': '\x69', '%6a': '\x6a', '%6A': '\x6a', '%6b': '\x6b', + '%6B': '\x6b', '%6c': '\x6c', '%6C': '\x6c', '%6d': '\x6d', '%6D': '\x6d', + '%6e': '\x6e', '%6E': '\x6e', '%6f': '\x6f', '%6F': '\x6f', '%70': '\x70', + '%71': '\x71', '%72': '\x72', '%73': '\x73', '%74': '\x74', '%75': '\x75', + '%76': '\x76', '%77': '\x77', '%78': '\x78', '%79': '\x79', '%7a': '\x7a', + '%7A': '\x7a', '%7b': '\x7b', '%7B': '\x7b', '%7c': '\x7c', '%7C': '\x7c', + '%7d': '\x7d', '%7D': '\x7d', '%7e': '\x7e', '%7E': '\x7e', '%7f': '\x7f', + '%7F': '\x7f', '%80': '\x80', '%81': '\x81', '%82': '\x82', '%83': '\x83', + '%84': '\x84', '%85': '\x85', '%86': '\x86', '%87': '\x87', '%88': '\x88', + '%89': '\x89', '%8a': '\x8a', '%8A': '\x8a', '%8b': '\x8b', '%8B': '\x8b', + '%8c': '\x8c', '%8C': '\x8c', '%8d': '\x8d', '%8D': '\x8d', '%8e': '\x8e', + '%8E': '\x8e', '%8f': '\x8f', '%8F': '\x8f', '%90': '\x90', '%91': '\x91', + '%92': '\x92', '%93': '\x93', '%94': '\x94', '%95': '\x95', '%96': '\x96', + '%97': '\x97', '%98': '\x98', '%99': '\x99', '%9a': '\x9a', '%9A': '\x9a', + '%9b': '\x9b', '%9B': '\x9b', '%9c': '\x9c', '%9C': '\x9c', '%9d': '\x9d', + '%9D': '\x9d', '%9e': '\x9e', '%9E': '\x9e', '%9f': '\x9f', '%9F': '\x9f', + '%a0': '\xa0', '%A0': '\xa0', '%a1': '\xa1', '%A1': '\xa1', '%a2': '\xa2', + '%A2': '\xa2', '%a3': '\xa3', '%A3': '\xa3', '%a4': '\xa4', '%A4': '\xa4', + '%a5': '\xa5', '%A5': '\xa5', '%a6': '\xa6', '%A6': '\xa6', '%a7': '\xa7', + '%A7': '\xa7', '%a8': '\xa8', '%A8': '\xa8', '%a9': '\xa9', '%A9': '\xa9', + '%aa': '\xaa', '%Aa': '\xaa', '%aA': '\xaa', '%AA': '\xaa', '%ab': '\xab', + '%Ab': '\xab', '%aB': '\xab', '%AB': '\xab', '%ac': '\xac', '%Ac': '\xac', + '%aC': '\xac', '%AC': '\xac', '%ad': '\xad', '%Ad': '\xad', '%aD': '\xad', + '%AD': '\xad', '%ae': '\xae', '%Ae': '\xae', '%aE': '\xae', '%AE': '\xae', + '%af': '\xaf', '%Af': '\xaf', '%aF': '\xaf', '%AF': '\xaf', '%b0': '\xb0', + '%B0': '\xb0', '%b1': '\xb1', '%B1': '\xb1', '%b2': '\xb2', '%B2': '\xb2', + '%b3': '\xb3', '%B3': '\xb3', '%b4': '\xb4', '%B4': '\xb4', '%b5': '\xb5', + '%B5': '\xb5', '%b6': '\xb6', '%B6': '\xb6', '%b7': '\xb7', '%B7': '\xb7', + '%b8': '\xb8', '%B8': '\xb8', '%b9': '\xb9', '%B9': '\xb9', '%ba': '\xba', + '%Ba': '\xba', '%bA': '\xba', '%BA': '\xba', '%bb': '\xbb', '%Bb': '\xbb', + '%bB': '\xbb', '%BB': '\xbb', '%bc': '\xbc', '%Bc': '\xbc', '%bC': '\xbc', + '%BC': '\xbc', '%bd': '\xbd', '%Bd': '\xbd', '%bD': '\xbd', '%BD': '\xbd', + '%be': '\xbe', '%Be': '\xbe', '%bE': '\xbe', '%BE': '\xbe', '%bf': '\xbf', + '%Bf': '\xbf', '%bF': '\xbf', '%BF': '\xbf', '%c0': '\xc0', '%C0': '\xc0', + '%c1': '\xc1', '%C1': '\xc1', '%c2': '\xc2', '%C2': '\xc2', '%c3': '\xc3', + '%C3': '\xc3', '%c4': '\xc4', '%C4': '\xc4', '%c5': '\xc5', '%C5': '\xc5', + '%c6': '\xc6', '%C6': '\xc6', '%c7': '\xc7', '%C7': '\xc7', '%c8': '\xc8', + '%C8': '\xc8', '%c9': '\xc9', '%C9': '\xc9', '%ca': '\xca', '%Ca': '\xca', + '%cA': '\xca', '%CA': '\xca', '%cb': '\xcb', '%Cb': '\xcb', '%cB': '\xcb', + '%CB': '\xcb', '%cc': '\xcc', '%Cc': '\xcc', '%cC': '\xcc', '%CC': '\xcc', + '%cd': '\xcd', '%Cd': '\xcd', '%cD': '\xcd', '%CD': '\xcd', '%ce': '\xce', + '%Ce': '\xce', '%cE': '\xce', '%CE': '\xce', '%cf': '\xcf', '%Cf': '\xcf', + '%cF': '\xcf', '%CF': '\xcf', '%d0': '\xd0', '%D0': '\xd0', '%d1': '\xd1', + '%D1': '\xd1', '%d2': '\xd2', '%D2': '\xd2', '%d3': '\xd3', '%D3': '\xd3', + '%d4': '\xd4', '%D4': '\xd4', '%d5': '\xd5', '%D5': '\xd5', '%d6': '\xd6', + '%D6': '\xd6', '%d7': '\xd7', '%D7': '\xd7', '%d8': '\xd8', '%D8': '\xd8', + '%d9': '\xd9', '%D9': '\xd9', '%da': '\xda', '%Da': '\xda', '%dA': '\xda', + '%DA': '\xda', '%db': '\xdb', '%Db': '\xdb', '%dB': '\xdb', '%DB': '\xdb', + '%dc': '\xdc', '%Dc': '\xdc', '%dC': '\xdc', '%DC': '\xdc', '%dd': '\xdd', + '%Dd': '\xdd', '%dD': '\xdd', '%DD': '\xdd', '%de': '\xde', '%De': '\xde', + '%dE': '\xde', '%DE': '\xde', '%df': '\xdf', '%Df': '\xdf', '%dF': '\xdf', + '%DF': '\xdf', '%e0': '\xe0', '%E0': '\xe0', '%e1': '\xe1', '%E1': '\xe1', + '%e2': '\xe2', '%E2': '\xe2', '%e3': '\xe3', '%E3': '\xe3', '%e4': '\xe4', + '%E4': '\xe4', '%e5': '\xe5', '%E5': '\xe5', '%e6': '\xe6', '%E6': '\xe6', + '%e7': '\xe7', '%E7': '\xe7', '%e8': '\xe8', '%E8': '\xe8', '%e9': '\xe9', + '%E9': '\xe9', '%ea': '\xea', '%Ea': '\xea', '%eA': '\xea', '%EA': '\xea', + '%eb': '\xeb', '%Eb': '\xeb', '%eB': '\xeb', '%EB': '\xeb', '%ec': '\xec', + '%Ec': '\xec', '%eC': '\xec', '%EC': '\xec', '%ed': '\xed', '%Ed': '\xed', + '%eD': '\xed', '%ED': '\xed', '%ee': '\xee', '%Ee': '\xee', '%eE': '\xee', + '%EE': '\xee', '%ef': '\xef', '%Ef': '\xef', '%eF': '\xef', '%EF': '\xef', + '%f0': '\xf0', '%F0': '\xf0', '%f1': '\xf1', '%F1': '\xf1', '%f2': '\xf2', + '%F2': '\xf2', '%f3': '\xf3', '%F3': '\xf3', '%f4': '\xf4', '%F4': '\xf4', + '%f5': '\xf5', '%F5': '\xf5', '%f6': '\xf6', '%F6': '\xf6', '%f7': '\xf7', + '%F7': '\xf7', '%f8': '\xf8', '%F8': '\xf8', '%f9': '\xf9', '%F9': '\xf9', + '%fa': '\xfa', '%Fa': '\xfa', '%fA': '\xfa', '%FA': '\xfa', '%fb': '\xfb', + '%Fb': '\xfb', '%fB': '\xfb', '%FB': '\xfb', '%fc': '\xfc', '%Fc': '\xfc', + '%fC': '\xfc', '%FC': '\xfc', '%fd': '\xfd', '%Fd': '\xfd', '%fD': '\xfd', + '%FD': '\xfd', '%fe': '\xfe', '%Fe': '\xfe', '%fE': '\xfe', '%FE': '\xfe', + '%ff': '\xff', '%Ff': '\xff', '%fF': '\xff', '%FF': '\xff' +} + +function encodedReplacer (match) { + return EncodedLookup[match] +} + +const STATE_KEY = 0 +const STATE_VALUE = 1 +const STATE_CHARSET = 2 +const STATE_LANG = 3 + +function parseParams (str) { + const res = [] + let state = STATE_KEY + let charset = '' + let inquote = false + let escaping = false + let p = 0 + let tmp = '' + const len = str.length + + for (var i = 0; i < len; ++i) { // eslint-disable-line no-var + const char = str[i] + if (char === '\\' && inquote) { + if (escaping) { escaping = false } else { + escaping = true + continue + } + } else if (char === '"') { + if (!escaping) { + if (inquote) { + inquote = false + state = STATE_KEY + } else { inquote = true } + continue + } else { escaping = false } + } else { + if (escaping && inquote) { tmp += '\\' } + escaping = false + if ((state === STATE_CHARSET || state === STATE_LANG) && char === "'") { + if (state === STATE_CHARSET) { + state = STATE_LANG + charset = tmp.substring(1) + } else { state = STATE_VALUE } + tmp = '' + continue + } else if (state === STATE_KEY && + (char === '*' || char === '=') && + res.length) { + state = char === '*' + ? STATE_CHARSET + : STATE_VALUE + res[p] = [tmp, undefined] + tmp = '' + continue + } else if (!inquote && char === ';') { + state = STATE_KEY + if (charset) { + if (tmp.length) { + tmp = decodeText(tmp.replace(RE_ENCODED, encodedReplacer), + 'binary', + charset) + } + charset = '' + } else if (tmp.length) { + tmp = decodeText(tmp, 'binary', 'utf8') + } + if (res[p] === undefined) { res[p] = tmp } else { res[p][1] = tmp } + tmp = '' + ++p + continue + } else if (!inquote && (char === ' ' || char === '\t')) { continue } + } + tmp += char + } + if (charset && tmp.length) { + tmp = decodeText(tmp.replace(RE_ENCODED, encodedReplacer), + 'binary', + charset) + } else if (tmp) { + tmp = decodeText(tmp, 'binary', 'utf8') + } + + if (res[p] === undefined) { + if (tmp) { res[p] = tmp } + } else { res[p][1] = tmp } + + return res +} + +module.exports = parseParams diff --git a/fastify-busboy/package.json b/fastify-busboy/package.json new file mode 100644 index 0000000..4be895c --- /dev/null +++ b/fastify-busboy/package.json @@ -0,0 +1,86 @@ +{ + "name": "@fastify/busboy", + "version": "2.1.0", + "private": false, + "author": "Brian White <mscdex@mscdex.net>", + "contributors": [ + { + "name": "Igor Savin", + "email": "kibertoad@gmail.com", + "url": "https://github.com/kibertoad" + }, + { + "name": "Aras Abbasi", + "email": "aras.abbasi@gmail.com", + "url": "https://github.com/uzlopak" + } + ], + "description": "A streaming parser for HTML form data for node.js", + "main": "lib/main", + "type": "commonjs", + "types": "lib/main.d.ts", + "scripts": { + "bench:busboy": "cd benchmarks && npm install && npm run benchmark-fastify", + "bench:dicer": "node bench/dicer/dicer-bench-multipart-parser.js", + "coveralls": "nyc report --reporter=lcov", + "lint": "npm run lint:standard", + "lint:everything": "npm run lint && npm run test:types", + "lint:fix": "standard --fix", + "lint:standard": "standard --verbose | snazzy", + "test:mocha": "tap", + "test:types": "tsd", + "test:coverage": "nyc npm run test", + "test": "npm run test:mocha" + }, + "engines": { + "node": ">=14" + }, + "devDependencies": { + "@types/node": "^20.1.0", + "busboy": "^1.0.0", + "photofinish": "^1.8.0", + "snazzy": "^9.0.0", + "standard": "^17.0.0", + "tap": "^16.3.8", + "tinybench": "^2.5.1", + "tsd": "^0.29.0", + "typescript": "^5.0.2" + }, + "keywords": [ + "uploads", + "forms", + "multipart", + "form-data" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/fastify/busboy.git" + }, + "tsd": { + "directory": "test/types", + "compilerOptions": { + "esModuleInterop": false, + "module": "commonjs", + "target": "ES2017" + } + }, + "standard": { + "globals": [ + "describe", + "it" + ], + "ignore": [ + "bench" + ] + }, + "files": [ + "README.md", + "LICENSE", + "lib/*", + "deps/encoding/*", + "deps/dicer/lib", + "deps/streamsearch/", + "deps/dicer/LICENSE" + ] +} diff --git a/fastify-busboy/test/busboy-constructor.test.js b/fastify-busboy/test/busboy-constructor.test.js new file mode 100644 index 0000000..8607789 --- /dev/null +++ b/fastify-busboy/test/busboy-constructor.test.js @@ -0,0 +1,75 @@ +'use strict' + +const Busboy = require('../lib/main') +const { test } = require('tap') + +test('busboy-constructor - should throw an Error if no options are provided', t => { + t.plan(1) + + t.throws(() => new Busboy(), new Error('Busboy expected an options-Object.')) +}) + +test('busboy-constructor - should throw an Error if options does not contain headers', t => { + t.plan(1) + + t.throws(() => new Busboy({}), new Error('Busboy expected an options-Object with headers-attribute.')) +}) + +test('busboy-constructor - if busboy is called without new-operator, still creates a busboy instance', t => { + t.plan(1) + + const busboyInstance = Busboy({ headers: { 'content-type': 'application/x-www-form-urlencoded' } }) + t.type(busboyInstance, Busboy) +}) + +test('busboy-constructor - should throw an Error if content-type is not set', t => { + t.plan(1) + + t.throws(() => new Busboy({ headers: {} }), new Error('Missing Content-Type-header.')) +}) + +test('busboy-constructor - should throw an Error if content-type is unsupported', t => { + t.plan(1) + + t.throws(() => new Busboy({ headers: { 'content-type': 'unsupported' } }), new Error('Unsupported Content-Type.')) +}) + +test('busboy-constructor - should not throw an Error if content-type is urlencoded', t => { + t.plan(1) + + t.doesNotThrow(() => new Busboy({ headers: { 'content-type': 'application/x-www-form-urlencoded' } })) +}) + +test('busboy-constructor - if busboy is called without stream options autoDestroy is set to false', t => { + t.plan(1) + + const busboyInstance = Busboy({ headers: { 'content-type': 'application/x-www-form-urlencoded' } }) + t.equal(busboyInstance._writableState.autoDestroy, false) +}) + +test('busboy-constructor - if busboy is called with invalid value for stream option highWaterMark we should throw', t => { + t.plan(1) + + t.throws(() => Busboy({ highWaterMark: 'not_allowed_value_for_highWaterMark', headers: { 'content-type': 'application/x-www-form-urlencoded' } }), new Error('not_allowed_value_for_highWaterMark')) +}) + +test('busboy-constructor - if busboy is called with stream options and autoDestroy:true, autoDestroy should be set to true', t => { + t.plan(1) + + const busboyInstance = Busboy({ autoDestroy: true, headers: { 'content-type': 'application/x-www-form-urlencoded' } }) + t.equal(busboyInstance._writableState.autoDestroy, true) +}) + +test('busboy-constructor - busboy should be initialized with private attribute _done set as false', t => { + t.plan(1) + + const busboyInstance = Busboy({ headers: { 'content-type': 'application/x-www-form-urlencoded' } }) + t.equal(busboyInstance._done, false) +}) + +test('busboy-constructor - busboy should be initialized with private attribute _finished set as false', t => { + t.plan(1) + + const busboyInstance = Busboy({ headers: { 'content-type': 'application/x-www-form-urlencoded' } }) + t.equal(busboyInstance._finished, false) +}) diff --git a/fastify-busboy/test/decoder.test.js b/fastify-busboy/test/decoder.test.js new file mode 100644 index 0000000..fa4ce69 --- /dev/null +++ b/fastify-busboy/test/decoder.test.js @@ -0,0 +1,98 @@ +'use strict' + +const { test } = require('tap') +const Decoder = require('../lib/utils/Decoder') + +test('Decoder', t => { + const tests = + [ + { + source: ['Hello world'], + expected: 'Hello world', + what: 'No encoded bytes' + }, + { + source: ['Hello%20world'], + expected: 'Hello world', + what: 'One full encoded byte' + }, + { + source: ['Hello%20world%21'], + expected: 'Hello world!', + what: 'Two full encoded bytes' + }, + { + source: ['Hello%', '20world'], + expected: 'Hello world', + what: 'One full encoded byte split #1' + }, + { + source: ['Hello%2', '0world'], + expected: 'Hello world', + what: 'One full encoded byte split #2' + }, + { + source: ['Hello%20', 'world'], + expected: 'Hello world', + what: 'One full encoded byte (concat)' + }, + { + source: ['Hello%2Qworld'], + expected: 'Hello%2Qworld', + what: 'Malformed encoded byte #1' + }, + { + source: ['Hello%world'], + expected: 'Hello%world', + what: 'Malformed encoded byte #2' + }, + { + source: ['Hello+world'], + expected: 'Hello world', + what: 'Plus to space' + }, + { + source: ['Hello+world%21'], + expected: 'Hello world!', + what: 'Plus and encoded byte' + }, + { + source: ['5%2B5%3D10'], + expected: '5+5=10', + what: 'Encoded plus' + }, + { + source: ['5+%2B+5+%3D+10'], + expected: '5 + 5 = 10', + what: 'Spaces and encoded plus' + } + ] + t.plan(tests.length + 1) + + tests.forEach((v) => { + t.test(v.what, t => { + t.plan(1) + + const dec = new Decoder() + let result = '' + v.source.forEach(function (s) { + result += dec.write(s) + }) + const msg = 'Decoded string mismatch.\n' + + 'Saw: ' + result + '\n' + + 'Expected: ' + v.expected + t.strictSame(result, v.expected, msg) + }) + }) + + t.test('reset sets internal buffer to undefined', t => { + t.plan(2) + + const dec = new Decoder() + dec.write('Hello+world%2') + + t.notSame(dec.buffer, undefined) + dec.reset() + t.equal(dec.buffer, undefined) + }) +}) diff --git a/fastify-busboy/test/dicer-constructor.test.js b/fastify-busboy/test/dicer-constructor.test.js new file mode 100644 index 0000000..e0e6a6c --- /dev/null +++ b/fastify-busboy/test/dicer-constructor.test.js @@ -0,0 +1,22 @@ +'use strict' + +const { test } = require('tap') +const Dicer = require('../deps/dicer/lib/Dicer') + +test('dicer-constructor', t => { + t.plan(2) + + t.test('should throw an Error when no options parameter is supplied to Dicer', t => { + t.plan(1) + + t.throws(() => new Dicer(), new Error('Boundary required')) + }) + + t.test('without new operator a new dicer instance will be initialized', t => { + t.plan(1) + + t.type(Dicer({ + boundary: '----boundary' + }), Dicer) + }) +}) diff --git a/fastify-busboy/test/dicer-endfinish.test.js b/fastify-busboy/test/dicer-endfinish.test.js new file mode 100644 index 0000000..4718076 --- /dev/null +++ b/fastify-busboy/test/dicer-endfinish.test.js @@ -0,0 +1,96 @@ +'use strict' + +const Dicer = require('../deps/dicer/lib/Dicer') +const { test } = require('tap') + +test('dicer-endfinish', t => { + t.plan(1) + + t.test('should properly handle finish', t => { + t.plan(4) + + const CRLF = '\r\n' + const boundary = 'boundary' + + const writeSep = '--' + boundary + + const writePart = [ + writeSep, + 'Content-Type: text/plain', + 'Content-Length: 0' + ].join(CRLF) + + CRLF + CRLF + + 'some data' + CRLF + + const writeEnd = '--' + CRLF + + let firedEnd = false + let firedFinish = false + + const dicer = new Dicer({ boundary }) + dicer.on('part', partListener) + dicer.on('finish', finishListener) + dicer.write(writePart + writeSep) + + function partListener (partReadStream) { + partReadStream.on('data', function () { }) + partReadStream.on('end', partEndListener) + } + function partEndListener () { + firedEnd = true + setImmediate(afterEnd) + } + function afterEnd () { + dicer.end(writeEnd) + setImmediate(afterWrite) + } + function finishListener () { + t.ok(firedEnd, 'end before finishing') + firedFinish = true + test2() + } + function afterWrite () { + t.ok(firedFinish, 'Failed to finish') + } + + let isPausePush = true + + let firedPauseCallback = false + let firedPauseFinish = false + + let dicer2 = null + + function test2 () { + dicer2 = new Dicer({ boundary }) + dicer2.on('part', pausePartListener) + dicer2.on('finish', pauseFinish) + dicer2.write(writePart + writeSep, 'utf8', pausePartCallback) + setImmediate(pauseAfterWrite) + } + function pausePartListener (partReadStream) { + partReadStream.on('data', function () { }) + partReadStream.on('end', function () { }) + const realPush = partReadStream.push + partReadStream.push = function fakePush () { + realPush.apply(partReadStream, arguments) + if (!isPausePush) { return true } + isPausePush = false + return false + } + } + function pauseAfterWrite () { + dicer2.end(writeEnd) + setImmediate(pauseAfterEnd) + } + function pauseAfterEnd () { + t.ok(firedPauseCallback, 'Called callback after pause') + t.ok(firedPauseFinish, 'Finish after pause') + } + function pauseFinish () { + firedPauseFinish = true + } + function pausePartCallback () { + firedPauseCallback = true + } + }) +}) diff --git a/fastify-busboy/test/dicer-export.test.js b/fastify-busboy/test/dicer-export.test.js new file mode 100644 index 0000000..05df4e6 --- /dev/null +++ b/fastify-busboy/test/dicer-export.test.js @@ -0,0 +1,24 @@ +'use strict' + +const { test } = require('tap') +const { Dicer } = require('../lib/main') + +test('dicer-export', t => { + t.plan(2) + + t.test('without new operator a new dicer instance will be initialized', t => { + t.plan(1) + + t.type(Dicer({ + boundary: '----boundary' + }), Dicer) + }) + + t.test('with new operator a new dicer instance will be initialized', t => { + t.plan(1) + + t.type(new Dicer({ + boundary: '----boundary' + }), Dicer) + }) +}) diff --git a/fastify-busboy/test/dicer-headerparser.test.js b/fastify-busboy/test/dicer-headerparser.test.js new file mode 100644 index 0000000..73da283 --- /dev/null +++ b/fastify-busboy/test/dicer-headerparser.test.js @@ -0,0 +1,192 @@ +'use strict' + +const { test } = require('tap') +const HeaderParser = require('../deps/dicer/lib/HeaderParser') + +test('dicer-headerparser', t => { + const DCRLF = '\r\n\r\n' + const MAXED_BUFFER = Buffer.allocUnsafe(128 * 1024) + MAXED_BUFFER.fill(0x41) // 'A' + + const tests = [ + { + source: DCRLF, + expected: {}, + what: 'No header' + }, + { + source: ['Content-Type:\t text/plain', + 'Content-Length:0' + ].join('\r\n') + DCRLF, + expected: { 'content-type': [' text/plain'], 'content-length': ['0'] }, + what: 'Value spacing' + }, + { + source: ['Content-Type:\t text/plain', + 'Content-Length:0' + ].join('\r\n') + DCRLF, + cfg: { + maxHeaderPairs: 0 + }, + expected: {}, + what: 'should enforce maxHeaderPairs of 0' + }, + { + source: ['Content-Type:\t text/plain', + 'Content-Length:0' + ].join('\r\n') + DCRLF, + cfg: { + maxHeaderPairs: 1 + }, + expected: { 'content-type': [' text/plain'] }, + what: 'should enforce maxHeaderPairs of 1' + }, + { + source: ['Content-Type:\r\n text/plain', + 'Foo:\r\n bar\r\n baz' + ].join('\r\n') + DCRLF, + expected: {}, + cfg: { + maxHeaderSize: 0 + }, + what: 'should enforce maxHeaderSize of 0' + }, + { + source: ['Content-Type:\r\n text/plain', + 'Foo:\r\n bar\r\n baz' + ].join('\r\n') + DCRLF, + expected: { 'content-type': [' text/plai'] }, + cfg: { + maxHeaderSize: 25 + }, + what: 'should enforce maxHeaderSize of 25' + }, + { + source: ['Content-Type:\r\n text/plain', + 'Foo:\r\n bar\r\n baz' + ].join('\r\n') + DCRLF, + expected: { 'content-type': [' text/plain'] }, + cfg: { + maxHeaderSize: 31 + }, + what: 'should enforce maxHeaderSize of 31 and ignore the second header' + }, + { + source: ['Content-Type:\r\n text/plain', + 'Foo:\r\n bar\r\n baz' + ].join('\r\n') + DCRLF, + expected: { 'content-type': [' text/plain'], foo: [''] }, + cfg: { + maxHeaderSize: 32 + }, + what: 'should enforce maxHeaderSize of 32 and only add key of second header' + }, + { + source: ['Content-Type:\r\n text/plain', + 'Foo:\r\n bar\r\n baz' + ].join('\r\n') + DCRLF, + expected: { 'content-type': [' text/plain'], foo: ['\r'] }, + cfg: { + maxHeaderSize: 33 + }, + what: 'should enforce maxHeaderSize of 32 and get only first character of second pair' + }, + { + source: ['Content-Type:\r\n text/plain', + ' : ' + ].join('\r\n') + DCRLF, + expected: { 'content-type': [' text/plain : '] }, + what: 'should not break if invalid header pair (colon exists but empty key and value) is provided' + }, + { + source: ['Content-Type:\r\n text/plain', + 'FoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobaz' + ].join('\r\n') + DCRLF, + expected: { 'content-type': [' text/plain'] }, + what: 'should not break if invalid header pair (no distinctive colon) is provided' + }, + { + source: ['Content-Type:\r\n text/plain', + ':FoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobaz' + ].join('\r\n') + DCRLF, + expected: { 'content-type': [' text/plain'] }, + what: 'should not break if invalid header pair (no key) is provided' + }, + { + source: ['Content-Type:\t text/plain', + 'Content-Length:0' + ].join('\r\n') + DCRLF, + cfg: { + maxHeaderPairs: 2 + }, + expected: { 'content-type': [' text/plain'], 'content-length': ['0'] }, + what: 'should enforce maxHeaderPairs of 2' + }, + { + source: ['Content-Type:\r\n text/plain', + 'Foo:\r\n bar\r\n baz' + ].join('\r\n') + DCRLF, + expected: { 'content-type': [' text/plain'], foo: [' bar baz'] }, + what: 'Folded values' + }, + { + source: [ + 'Foo: bar', + 'Foo: baz' + ].join('\r\n') + DCRLF, + expected: { foo: ['bar', 'baz'] }, + what: 'Folded values' + }, + { + source: ['Content-Type:', + 'Foo: ' + ].join('\r\n') + DCRLF, + expected: { 'content-type': [''], foo: [''] }, + what: 'Empty values' + }, + { + source: MAXED_BUFFER.toString('ascii') + DCRLF, + expected: {}, + what: 'Max header size (single chunk)' + }, + { + source: ['ABCDEFGHIJ', MAXED_BUFFER.toString('ascii'), DCRLF], + expected: {}, + what: 'Max header size (multiple chunks #1)' + }, + { + source: [MAXED_BUFFER.toString('ascii'), MAXED_BUFFER.toString('ascii'), DCRLF], + expected: {}, + what: 'Max header size (multiple chunk #2)' + } + ] + + t.plan(tests.length) + + tests.forEach(function (v) { + t.test(v.what, t => { + t.plan(4) + + const cfg = { + ...v.cfg + } + + const parser = Object.keys(cfg).length ? new HeaderParser(cfg) : new HeaderParser() + let fired = false + + parser.on('header', function (header) { + t.ok(!fired, `${v.what}: Header event fired more than once`) + fired = true + t.strictSame(header, + v.expected, + `${v.what}: Parsed result mismatch`) + }) + if (!Array.isArray(v.source)) { v.source = [v.source] } + v.source.forEach(function (s) { + parser.push(s) + }) + t.ok(fired, `${v.what}: Did not receive header from parser`) + t.pass() + }) + }) +}) diff --git a/fastify-busboy/test/dicer-malformed-header.test.js b/fastify-busboy/test/dicer-malformed-header.test.js new file mode 100644 index 0000000..c25ccdd --- /dev/null +++ b/fastify-busboy/test/dicer-malformed-header.test.js @@ -0,0 +1,29 @@ +'use strict' + +const { test } = require('tap') +const Dicer = require('../deps/dicer/lib/Dicer') + +test('dicer-malformed-header', t => { + t.plan(1) + + t.test('should gracefully handle headers with leading whitespace', t => { + t.plan(3) + const d = new Dicer({ boundary: '----WebKitFormBoundaryoo6vortfDzBsDiro' }) + + d.on('part', function (p) { + p.on('header', function (header) { + t.hasProp(header, ' content-disposition') + t.strictSame(header[' content-disposition'], ['form-data; name="bildbeschreibung"']) + }) + p.on('data', function (data) { + }) + p.on('end', function () { + }) + }) + d.on('finish', function () { + t.pass() + }) + + d.write(Buffer.from('------WebKitFormBoundaryoo6vortfDzBsDiro\r\n Content-Disposition: form-data; name="bildbeschreibung"\r\n\r\n\r\n------WebKitFormBoundaryoo6vortfDzBsDiro--')) + }) +}) diff --git a/fastify-busboy/test/dicer-multipart-extra-trailer.test.js b/fastify-busboy/test/dicer-multipart-extra-trailer.test.js new file mode 100644 index 0000000..335605a --- /dev/null +++ b/fastify-busboy/test/dicer-multipart-extra-trailer.test.js @@ -0,0 +1,82 @@ +'use strict' + +const { test } = require('tap') +const Dicer = require('../deps/dicer/lib/Dicer') +const fs = require('fs') +const path = require('path') + +const FIXTURES_ROOT = path.join(__dirname, 'fixtures/') + +test('dicer-multipart-extra-trailer', t => { + t.plan(1) + + t.test('Extra trailer data pushed after finished', t => { + t.plan(5) + const fixtureBase = FIXTURES_ROOT + 'many' + let n = 0 + const buffer = Buffer.allocUnsafe(16) + const state = { parts: [] } + + const fd = fs.openSync(fixtureBase + '/original', 'r') + + const dicer = new Dicer({ boundary: '----WebKitFormBoundaryWLHCs9qmcJJoyjKR' }) + let error + let finishes = 0 + let trailerEmitted = false + + dicer.on('part', function (p) { + const part = { + body: undefined, + bodylen: 0, + error: undefined, + header: undefined + } + + p.on('header', function (h) { + part.header = h + }).on('data', function (data) { + // make a copy because we are using readSync which re-uses a buffer ... + const copy = Buffer.allocUnsafe(data.length) + data.copy(copy) + data = copy + if (!part.body) { part.body = [data] } else { part.body.push(data) } + part.bodylen += data.length + }).on('error', function (err) { + part.error = err + t.fail() + }).on('end', function () { + if (part.body) { part.body = Buffer.concat(part.body, part.bodylen) } + state.parts.push(part) + }) + }).on('error', function (err) { + error = err + }).on('trailer', function (data) { + trailerEmitted = true + t.equal(data.toString(), 'Extra', 'trailer should contain the extra data') + }).on('finish', function () { + t.ok(finishes++ === 0, makeMsg('Extra trailer data pushed after finished', 'finish emitted multiple times')) + t.ok(trailerEmitted, makeMsg('Extra trailer data pushed after finished', 'should have emitted trailer')) + + t.ok(error === undefined, makeMsg('Extra trailer data pushed after finished', 'Unexpected error')) + + t.pass() + }) + + while (true) { + n = fs.readSync(fd, buffer, 0, buffer.length, null) + if (n === 0) { + setTimeout(function () { + dicer.write('\r\n\r\n\r\n') + dicer.end() + }, 50) + break + } + dicer.write(n === buffer.length ? buffer : buffer.slice(0, n)) + } + fs.closeSync(fd) + }) +}) + +function makeMsg (what, msg) { + return what + ': ' + msg +} diff --git a/fastify-busboy/test/dicer-multipart-nolisteners.test.js b/fastify-busboy/test/dicer-multipart-nolisteners.test.js new file mode 100644 index 0000000..1e311ba --- /dev/null +++ b/fastify-busboy/test/dicer-multipart-nolisteners.test.js @@ -0,0 +1,44 @@ +'use strict' + +const Dicer = require('../deps/dicer/lib/Dicer') +const { test } = require('tap') +const fs = require('fs') +const path = require('path') + +const FIXTURES_ROOT = path.join(__dirname, 'fixtures/') + +test('dicer-multipart-nolisteners', t => { + t.plan(1) + + t.test('No preamble or part listeners', t => { + t.plan(3) + const fixtureBase = path.resolve(FIXTURES_ROOT, 'many') + let n = 0 + const buffer = Buffer.allocUnsafe(16) + + const fd = fs.openSync(fixtureBase + '/original', 'r') + + const dicer = new Dicer({ boundary: '----WebKitFormBoundaryWLHCs9qmcJJoyjKR' }) + let error + let finishes = 0 + + dicer.on('error', function (err) { + error = err + }).on('finish', function () { + t.ok(finishes++ === 0, 'finish emitted multiple times') + + t.ok(error === undefined, `Unexpected error: ${error}`) + t.pass() + }) + + while (true) { + n = fs.readSync(fd, buffer, 0, buffer.length, null) + if (n === 0) { + dicer.end() + break + } + dicer.write(n === buffer.length ? buffer : buffer.slice(0, n)) + } + fs.closeSync(fd) + }) +}) diff --git a/fastify-busboy/test/dicer-multipart.test.js b/fastify-busboy/test/dicer-multipart.test.js new file mode 100644 index 0000000..c35c4d0 --- /dev/null +++ b/fastify-busboy/test/dicer-multipart.test.js @@ -0,0 +1,223 @@ +'use strict' + +const Dicer = require('../deps/dicer/lib/Dicer') +const assert = require('node:assert') +const fs = require('node:fs') +const path = require('node:path') +const inspect = require('node:util').inspect +const { test } = require('tap') + +const FIXTURES_ROOT = path.join(__dirname, 'fixtures/') + +test('dicer-multipart', t => { + const tests = + [ + { + source: 'nested', + opts: { boundary: 'AaB03x' }, + chsize: 32, + nparts: 2, + what: 'One nested multipart' + }, + { + source: 'many', + opts: { boundary: '----WebKitFormBoundaryWLHCs9qmcJJoyjKR' }, + chsize: 16, + nparts: 7, + what: 'Many parts' + }, + { + source: 'many-wrongboundary', + opts: { boundary: 'LOLOLOL' }, + chsize: 8, + nparts: 0, + dicerError: true, + what: 'Many parts, wrong boundary' + }, + { + source: 'many-noend', + opts: { boundary: '----WebKitFormBoundaryWLHCs9qmcJJoyjKR' }, + chsize: 16, + nparts: 7, + npartErrors: 1, + dicerError: true, + what: 'Many parts, end boundary missing, 1 file open' + }, + { + source: 'nested-full', + opts: { boundary: 'AaB03x', headerFirst: true }, + chsize: 32, + nparts: 2, + what: 'One nested multipart with preceding header' + }, + { + source: 'nested-full', + opts: { headerFirst: true }, + chsize: 32, + nparts: 2, + setBoundary: 'AaB03x', + what: 'One nested multipart with preceding header, using setBoundary' + } + ] + + t.plan(tests.length) + + tests.forEach(function (v) { + t.test(v.what, t => { + t.plan(1) + const fixtureBase = FIXTURES_ROOT + v.source + const state = { parts: [], preamble: undefined } + + const dicer = new Dicer(v.opts) + let error + let partErrors = 0 + let finishes = 0 + + dicer.on('preamble', function (p) { + const preamble = { + body: undefined, + bodylen: 0, + error: undefined, + header: undefined + } + + p.on('header', function (h) { + preamble.header = h + if (v.setBoundary) { dicer.setBoundary(v.setBoundary) } + }).on('data', function (data) { + // make a copy because we are using readSync which re-uses a buffer ... + const copy = Buffer.allocUnsafe(data.length) + data.copy(copy) + data = copy + if (!preamble.body) { preamble.body = [data] } else { preamble.body.push(data) } + preamble.bodylen += data.length + }).on('error', function (err) { + preamble.error = err + }).on('end', function () { + if (preamble.body) { preamble.body = Buffer.concat(preamble.body, preamble.bodylen) } + if (preamble.body || preamble.header) { state.preamble = preamble } + }) + }) + dicer.on('part', function (p) { + const part = { + body: undefined, + bodylen: 0, + error: undefined, + header: undefined + } + + p.on('header', function (h) { + part.header = h + }).on('data', function (data) { + if (!part.body) { part.body = [data] } else { part.body.push(data) } + part.bodylen += data.length + }).on('error', function (err) { + part.error = err + ++partErrors + }).on('end', function () { + if (part.body) { part.body = Buffer.concat(part.body, part.bodylen) } + state.parts.push(part) + }) + }).on('error', function (err) { + error = err + }).on('finish', function () { + assert(finishes++ === 0, makeMsg(v.what, 'finish emitted multiple times')) + + if (v.dicerError) { assert(error !== undefined, makeMsg(v.what, 'Expected error')) } else { assert(error === undefined, makeMsg(v.what, 'Unexpected error: ' + error)) } + + let preamble + if (fs.existsSync(fixtureBase + '/preamble')) { + const prebody = fs.readFileSync(fixtureBase + '/preamble') + if (prebody.length) { + preamble = { + body: prebody, + bodylen: prebody.length, + error: undefined, + header: undefined + } + } + } + if (fs.existsSync(fixtureBase + '/preamble.header')) { + const prehead = JSON.parse(fs.readFileSync(fixtureBase + + '/preamble.header', 'binary')) + if (!preamble) { + preamble = { + body: undefined, + bodylen: 0, + error: undefined, + header: prehead + } + } else { preamble.header = prehead } + } + if (fs.existsSync(fixtureBase + '/preamble.error')) { + const err = new Error(fs.readFileSync(fixtureBase + + '/preamble.error', 'binary')) + if (!preamble) { + preamble = { + body: undefined, + bodylen: 0, + error: err, + header: undefined + } + } else { preamble.error = err } + } + + assert.deepEqual(state.preamble, + preamble, + makeMsg(v.what, + 'Preamble mismatch:\nActual:' + + inspect(state.preamble) + + '\nExpected: ' + + inspect(preamble))) + + assert.equal(state.parts.length, + v.nparts, + makeMsg(v.what, + 'Part count mismatch:\nActual: ' + + state.parts.length + + '\nExpected: ' + + v.nparts)) + + if (!v.npartErrors) { v.npartErrors = 0 } + assert.equal(partErrors, + v.npartErrors, + makeMsg(v.what, + 'Part errors mismatch:\nActual: ' + + partErrors + + '\nExpected: ' + + v.npartErrors)) + + for (let i = 0, header, body; i < v.nparts; ++i) { + if (fs.existsSync(fixtureBase + '/part' + (i + 1))) { + body = fs.readFileSync(fixtureBase + '/part' + (i + 1)) + if (body.length === 0) { body = undefined } + } else { body = undefined } + assert.deepEqual(state.parts[i].body, + body, + makeMsg(v.what, + 'Part #' + (i + 1) + ' body mismatch')) + if (fs.existsSync(fixtureBase + '/part' + (i + 1) + '.header')) { + header = fs.readFileSync(fixtureBase + + '/part' + (i + 1) + '.header', 'binary') + header = JSON.parse(header) + } else { header = undefined } + assert.deepEqual(state.parts[i].header, + header, + makeMsg(v.what, + 'Part #' + (i + 1) + + ' parsed header mismatch:\nActual: ' + + inspect(state.parts[i].header) + + '\nExpected: ' + + inspect(header))) + } + t.pass() + }) + + fs.createReadStream(fixtureBase + '/original').pipe(dicer) + }) + }) +}) + +function makeMsg (what, msg) { + return what + ': ' + msg +} diff --git a/fastify-busboy/test/fixtures/many-noend/original b/fastify-busboy/test/fixtures/many-noend/original new file mode 100644 index 0000000..ad9f0cc --- /dev/null +++ b/fastify-busboy/test/fixtures/many-noend/original @@ -0,0 +1,31 @@ +------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="_method"
+
+put
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="profile[blog]"
+
+
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="profile[public_email]"
+
+
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="profile[interests]"
+
+
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="profile[bio]"
+
+hello
+
+"quote"
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="commit"
+
+Save
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="media"; filename=""
+Content-Type: application/octet-stream
+
+
diff --git a/fastify-busboy/test/fixtures/many-noend/part1 b/fastify-busboy/test/fixtures/many-noend/part1 new file mode 100644 index 0000000..a232311 --- /dev/null +++ b/fastify-busboy/test/fixtures/many-noend/part1 @@ -0,0 +1 @@ +put
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/many-noend/part1.header b/fastify-busboy/test/fixtures/many-noend/part1.header new file mode 100644 index 0000000..5e6bbe5 --- /dev/null +++ b/fastify-busboy/test/fixtures/many-noend/part1.header @@ -0,0 +1 @@ +{"content-disposition": ["form-data; name=\"_method\""]}
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/many-noend/part2 b/fastify-busboy/test/fixtures/many-noend/part2 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/fastify-busboy/test/fixtures/many-noend/part2 diff --git a/fastify-busboy/test/fixtures/many-noend/part2.header b/fastify-busboy/test/fixtures/many-noend/part2.header new file mode 100644 index 0000000..5b53966 --- /dev/null +++ b/fastify-busboy/test/fixtures/many-noend/part2.header @@ -0,0 +1 @@ +{"content-disposition": ["form-data; name=\"profile[blog]\""]}
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/many-noend/part3 b/fastify-busboy/test/fixtures/many-noend/part3 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/fastify-busboy/test/fixtures/many-noend/part3 diff --git a/fastify-busboy/test/fixtures/many-noend/part3.header b/fastify-busboy/test/fixtures/many-noend/part3.header new file mode 100644 index 0000000..579e16e --- /dev/null +++ b/fastify-busboy/test/fixtures/many-noend/part3.header @@ -0,0 +1 @@ +{"content-disposition": ["form-data; name=\"profile[public_email]\""]}
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/many-noend/part4 b/fastify-busboy/test/fixtures/many-noend/part4 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/fastify-busboy/test/fixtures/many-noend/part4 diff --git a/fastify-busboy/test/fixtures/many-noend/part4.header b/fastify-busboy/test/fixtures/many-noend/part4.header new file mode 100644 index 0000000..b41be09 --- /dev/null +++ b/fastify-busboy/test/fixtures/many-noend/part4.header @@ -0,0 +1 @@ +{"content-disposition": ["form-data; name=\"profile[interests]\""]}
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/many-noend/part5 b/fastify-busboy/test/fixtures/many-noend/part5 new file mode 100644 index 0000000..f2bb979 --- /dev/null +++ b/fastify-busboy/test/fixtures/many-noend/part5 @@ -0,0 +1,3 @@ +hello
+
+"quote"
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/many-noend/part5.header b/fastify-busboy/test/fixtures/many-noend/part5.header new file mode 100644 index 0000000..92e417f --- /dev/null +++ b/fastify-busboy/test/fixtures/many-noend/part5.header @@ -0,0 +1 @@ +{"content-disposition": ["form-data; name=\"profile[bio]\""]}
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/many-noend/part6 b/fastify-busboy/test/fixtures/many-noend/part6 new file mode 100644 index 0000000..f0f5479 --- /dev/null +++ b/fastify-busboy/test/fixtures/many-noend/part6 @@ -0,0 +1 @@ +Save
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/many-noend/part6.header b/fastify-busboy/test/fixtures/many-noend/part6.header new file mode 100644 index 0000000..65a68a9 --- /dev/null +++ b/fastify-busboy/test/fixtures/many-noend/part6.header @@ -0,0 +1 @@ +{"content-disposition": ["form-data; name=\"commit\""]}
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/many-noend/part7.header b/fastify-busboy/test/fixtures/many-noend/part7.header new file mode 100644 index 0000000..25171e8 --- /dev/null +++ b/fastify-busboy/test/fixtures/many-noend/part7.header @@ -0,0 +1,2 @@ +{"content-disposition": ["form-data; name=\"media\"; filename=\"\""],
+ "content-type": ["application/octet-stream"]}
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/many-wrongboundary/original b/fastify-busboy/test/fixtures/many-wrongboundary/original new file mode 100644 index 0000000..859770c --- /dev/null +++ b/fastify-busboy/test/fixtures/many-wrongboundary/original @@ -0,0 +1,32 @@ +------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="_method"
+
+put
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="profile[blog]"
+
+
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="profile[public_email]"
+
+
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="profile[interests]"
+
+
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="profile[bio]"
+
+hello
+
+"quote"
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="media"; filename=""
+Content-Type: application/octet-stream
+
+
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="commit"
+
+Save
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR--
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/many-wrongboundary/preamble b/fastify-busboy/test/fixtures/many-wrongboundary/preamble new file mode 100644 index 0000000..6e4bcc6 --- /dev/null +++ b/fastify-busboy/test/fixtures/many-wrongboundary/preamble @@ -0,0 +1,33 @@ +
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="_method"
+
+put
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="profile[blog]"
+
+
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="profile[public_email]"
+
+
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="profile[interests]"
+
+
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="profile[bio]"
+
+hello
+
+"quote"
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="media"; filename=""
+Content-Type: application/octet-stream
+
+
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="commit"
+
+Save
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR--
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/many-wrongboundary/preamble.error b/fastify-busboy/test/fixtures/many-wrongboundary/preamble.error new file mode 100644 index 0000000..15f4c89 --- /dev/null +++ b/fastify-busboy/test/fixtures/many-wrongboundary/preamble.error @@ -0,0 +1 @@ +Preamble terminated early due to unexpected end of multipart data
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/many/original b/fastify-busboy/test/fixtures/many/original new file mode 100644 index 0000000..779c5cb --- /dev/null +++ b/fastify-busboy/test/fixtures/many/original @@ -0,0 +1,32 @@ +------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="_method"
+
+put
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="profile[blog]"
+
+
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="profile[public_email]"
+
+
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="profile[interests]"
+
+
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="profile[bio]"
+
+hello
+
+"quote"
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="media"; filename=""
+Content-Type: application/octet-stream
+
+
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR
+Content-Disposition: form-data; name="commit"
+
+Save
+------WebKitFormBoundaryWLHCs9qmcJJoyjKR--Extra
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/many/part1 b/fastify-busboy/test/fixtures/many/part1 new file mode 100644 index 0000000..a232311 --- /dev/null +++ b/fastify-busboy/test/fixtures/many/part1 @@ -0,0 +1 @@ +put
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/many/part1.header b/fastify-busboy/test/fixtures/many/part1.header new file mode 100644 index 0000000..5e6bbe5 --- /dev/null +++ b/fastify-busboy/test/fixtures/many/part1.header @@ -0,0 +1 @@ +{"content-disposition": ["form-data; name=\"_method\""]}
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/many/part2 b/fastify-busboy/test/fixtures/many/part2 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/fastify-busboy/test/fixtures/many/part2 diff --git a/fastify-busboy/test/fixtures/many/part2.header b/fastify-busboy/test/fixtures/many/part2.header new file mode 100644 index 0000000..5b53966 --- /dev/null +++ b/fastify-busboy/test/fixtures/many/part2.header @@ -0,0 +1 @@ +{"content-disposition": ["form-data; name=\"profile[blog]\""]}
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/many/part3 b/fastify-busboy/test/fixtures/many/part3 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/fastify-busboy/test/fixtures/many/part3 diff --git a/fastify-busboy/test/fixtures/many/part3.header b/fastify-busboy/test/fixtures/many/part3.header new file mode 100644 index 0000000..579e16e --- /dev/null +++ b/fastify-busboy/test/fixtures/many/part3.header @@ -0,0 +1 @@ +{"content-disposition": ["form-data; name=\"profile[public_email]\""]}
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/many/part4 b/fastify-busboy/test/fixtures/many/part4 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/fastify-busboy/test/fixtures/many/part4 diff --git a/fastify-busboy/test/fixtures/many/part4.header b/fastify-busboy/test/fixtures/many/part4.header new file mode 100644 index 0000000..b41be09 --- /dev/null +++ b/fastify-busboy/test/fixtures/many/part4.header @@ -0,0 +1 @@ +{"content-disposition": ["form-data; name=\"profile[interests]\""]}
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/many/part5 b/fastify-busboy/test/fixtures/many/part5 new file mode 100644 index 0000000..f2bb979 --- /dev/null +++ b/fastify-busboy/test/fixtures/many/part5 @@ -0,0 +1,3 @@ +hello
+
+"quote"
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/many/part5.header b/fastify-busboy/test/fixtures/many/part5.header new file mode 100644 index 0000000..92e417f --- /dev/null +++ b/fastify-busboy/test/fixtures/many/part5.header @@ -0,0 +1 @@ +{"content-disposition": ["form-data; name=\"profile[bio]\""]}
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/many/part6 b/fastify-busboy/test/fixtures/many/part6 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/fastify-busboy/test/fixtures/many/part6 diff --git a/fastify-busboy/test/fixtures/many/part6.header b/fastify-busboy/test/fixtures/many/part6.header new file mode 100644 index 0000000..25171e8 --- /dev/null +++ b/fastify-busboy/test/fixtures/many/part6.header @@ -0,0 +1,2 @@ +{"content-disposition": ["form-data; name=\"media\"; filename=\"\""],
+ "content-type": ["application/octet-stream"]}
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/many/part7 b/fastify-busboy/test/fixtures/many/part7 new file mode 100644 index 0000000..f0f5479 --- /dev/null +++ b/fastify-busboy/test/fixtures/many/part7 @@ -0,0 +1 @@ +Save
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/many/part7.header b/fastify-busboy/test/fixtures/many/part7.header new file mode 100644 index 0000000..65a68a9 --- /dev/null +++ b/fastify-busboy/test/fixtures/many/part7.header @@ -0,0 +1 @@ +{"content-disposition": ["form-data; name=\"commit\""]}
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/nested-full/original b/fastify-busboy/test/fixtures/nested-full/original new file mode 100644 index 0000000..3044550 --- /dev/null +++ b/fastify-busboy/test/fixtures/nested-full/original @@ -0,0 +1,24 @@ +User-Agent: foo bar baz
+Content-Type: multipart/form-data; boundary=AaB03x
+
+--AaB03x
+Content-Disposition: form-data; name="foo"
+
+bar
+--AaB03x
+Content-Disposition: form-data; name="files"
+Content-Type: multipart/mixed, boundary=BbC04y
+
+--BbC04y
+Content-Disposition: attachment; filename="file.txt"
+Content-Type: text/plain
+
+contents
+--BbC04y
+Content-Disposition: attachment; filename="flowers.jpg"
+Content-Type: image/jpeg
+Content-Transfer-Encoding: binary
+
+contents
+--BbC04y--
+--AaB03x--
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/nested-full/part1 b/fastify-busboy/test/fixtures/nested-full/part1 new file mode 100644 index 0000000..ba0e162 --- /dev/null +++ b/fastify-busboy/test/fixtures/nested-full/part1 @@ -0,0 +1 @@ +bar
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/nested-full/part1.header b/fastify-busboy/test/fixtures/nested-full/part1.header new file mode 100644 index 0000000..03bd093 --- /dev/null +++ b/fastify-busboy/test/fixtures/nested-full/part1.header @@ -0,0 +1 @@ +{"content-disposition": ["form-data; name=\"foo\""]}
diff --git a/fastify-busboy/test/fixtures/nested-full/part2 b/fastify-busboy/test/fixtures/nested-full/part2 new file mode 100644 index 0000000..2d4deb5 --- /dev/null +++ b/fastify-busboy/test/fixtures/nested-full/part2 @@ -0,0 +1,12 @@ +--BbC04y
+Content-Disposition: attachment; filename="file.txt"
+Content-Type: text/plain
+
+contents
+--BbC04y
+Content-Disposition: attachment; filename="flowers.jpg"
+Content-Type: image/jpeg
+Content-Transfer-Encoding: binary
+
+contents
+--BbC04y--
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/nested-full/part2.header b/fastify-busboy/test/fixtures/nested-full/part2.header new file mode 100644 index 0000000..bbe4513 --- /dev/null +++ b/fastify-busboy/test/fixtures/nested-full/part2.header @@ -0,0 +1,2 @@ +{"content-disposition": ["form-data; name=\"files\""],
+ "content-type": ["multipart/mixed, boundary=BbC04y"]}
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/nested-full/preamble.header b/fastify-busboy/test/fixtures/nested-full/preamble.header new file mode 100644 index 0000000..2815341 --- /dev/null +++ b/fastify-busboy/test/fixtures/nested-full/preamble.header @@ -0,0 +1,2 @@ +{"user-agent": ["foo bar baz"],
+ "content-type": ["multipart/form-data; boundary=AaB03x"]}
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/nested/original b/fastify-busboy/test/fixtures/nested/original new file mode 100644 index 0000000..380f451 --- /dev/null +++ b/fastify-busboy/test/fixtures/nested/original @@ -0,0 +1,21 @@ +--AaB03x
+Content-Disposition: form-data; name="foo"
+
+bar
+--AaB03x
+Content-Disposition: form-data; name="files"
+Content-Type: multipart/mixed, boundary=BbC04y
+
+--BbC04y
+Content-Disposition: attachment; filename="file.txt"
+Content-Type: text/plain
+
+contents
+--BbC04y
+Content-Disposition: attachment; filename="flowers.jpg"
+Content-Type: image/jpeg
+Content-Transfer-Encoding: binary
+
+contents
+--BbC04y--
+--AaB03x--
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/nested/part1 b/fastify-busboy/test/fixtures/nested/part1 new file mode 100644 index 0000000..ba0e162 --- /dev/null +++ b/fastify-busboy/test/fixtures/nested/part1 @@ -0,0 +1 @@ +bar
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/nested/part1.header b/fastify-busboy/test/fixtures/nested/part1.header new file mode 100644 index 0000000..03bd093 --- /dev/null +++ b/fastify-busboy/test/fixtures/nested/part1.header @@ -0,0 +1 @@ +{"content-disposition": ["form-data; name=\"foo\""]}
diff --git a/fastify-busboy/test/fixtures/nested/part2 b/fastify-busboy/test/fixtures/nested/part2 new file mode 100644 index 0000000..2d4deb5 --- /dev/null +++ b/fastify-busboy/test/fixtures/nested/part2 @@ -0,0 +1,12 @@ +--BbC04y
+Content-Disposition: attachment; filename="file.txt"
+Content-Type: text/plain
+
+contents
+--BbC04y
+Content-Disposition: attachment; filename="flowers.jpg"
+Content-Type: image/jpeg
+Content-Transfer-Encoding: binary
+
+contents
+--BbC04y--
\ No newline at end of file diff --git a/fastify-busboy/test/fixtures/nested/part2.header b/fastify-busboy/test/fixtures/nested/part2.header new file mode 100644 index 0000000..bbe4513 --- /dev/null +++ b/fastify-busboy/test/fixtures/nested/part2.header @@ -0,0 +1,2 @@ +{"content-disposition": ["form-data; name=\"files\""],
+ "content-type": ["multipart/mixed, boundary=BbC04y"]}
\ No newline at end of file diff --git a/fastify-busboy/test/get-limit.test.js b/fastify-busboy/test/get-limit.test.js new file mode 100644 index 0000000..76a2997 --- /dev/null +++ b/fastify-busboy/test/get-limit.test.js @@ -0,0 +1,34 @@ +'use strict' + +const getLimit = require('../lib/utils/getLimit') +const { test } = require('tap') + +test('Get limit', t => { + t.plan(2) + + t.test('Correctly resolves limits', t => { + t.plan(8) + t.strictSame(getLimit(undefined, 'fieldSize', 1), 1) + t.strictSame(getLimit(undefined, 'fileSize', Infinity), Infinity) + + t.strictSame(getLimit({}, 'fieldSize', 1), 1) + t.strictSame(getLimit({}, 'fileSize', Infinity), Infinity) + t.strictSame(getLimit({ fieldSize: null }, 'fieldSize', 1), 1) + t.strictSame(getLimit({ fileSize: null }, 'fileSize', Infinity), Infinity) + + t.strictSame(getLimit({ fieldSize: 0 }, 'fieldSize', 1), 0) + t.strictSame(getLimit({ fileSize: 2 }, 'fileSize', 1), 2) + }) + + t.test('Throws an error on incorrect limits', t => { + t.plan(2) + + t.throws(function () { + getLimit({ fieldSize: '1' }, 'fieldSize', 1) + }, new Error('Limit fieldSize is not a valid number')) + + t.throws(function () { + getLimit({ fieldSize: NaN }, 'fieldSize', 1) + }, new Error('Limit fieldSize is not a valid number')) + }) +}) diff --git a/fastify-busboy/test/multipart-stream-pause.test.js b/fastify-busboy/test/multipart-stream-pause.test.js new file mode 100644 index 0000000..856cf71 --- /dev/null +++ b/fastify-busboy/test/multipart-stream-pause.test.js @@ -0,0 +1,82 @@ +'use strict' + +const { inspect } = require('util') +const { test } = require('tap') + +const Busboy = require('..') + +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) { + 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'), + Buffer.allocUnsafe(100000) + ]) +} + +test('multipart-stream-pause - processes stream correctly', t => { + t.plan(6) + 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 busboy = new Busboy({ + headers: { + 'content-type': 'multipart/form-data; boundary=' + BOUNDARY + } + }) + let finishes = 0 + const results = [] + const expected = [ + ['file', 'file', 'file.bin', '7bit', 'application/octet-stream'], + ['field', 'foo', 'foo value', false, false, '7bit', 'text/plain'], + ['field', 'bar', 'bar value', false, false, '7bit', 'text/plain'] + ] + + busboy.on('field', function (key, val, keyTrunc, valTrunc, encoding, contype) { + results.push(['field', key, val, keyTrunc, valTrunc, encoding, contype]) + }) + busboy.on('file', function (fieldname, stream, filename, encoding, mimeType) { + results.push(['file', fieldname, filename, encoding, mimeType]) + // Simulate a pipe where the destination is pausing (perhaps due to waiting + // for file system write to finish) + setTimeout(function () { + stream.resume() + }, 10) + }) + busboy.on('finish', function () { + t.ok(finishes++ === 0, 'finish emitted multiple times') + t.strictSame(results.length, + expected.length, + 'Parsed result count mismatch. Saw ' + + results.length + + '. Expected: ' + expected.length) + + results.forEach(function (result, i) { + t.strictSame(result, + expected[i], + 'Result mismatch:\nParsed: ' + inspect(result) + + '\nExpected: ' + inspect(expected[i])) + }) + t.pass() + }).on('error', function (err) { + t.error(err) + }) + + reqChunks.forEach(function (buf) { + busboy.write(buf) + }) + busboy.end() +}) diff --git a/fastify-busboy/test/parse-params.test.js b/fastify-busboy/test/parse-params.test.js new file mode 100644 index 0000000..eea4768 --- /dev/null +++ b/fastify-busboy/test/parse-params.test.js @@ -0,0 +1,124 @@ +'use strict' + +const { inspect } = require('node:util') +const { test } = require('tap') +const parseParams = require('../lib/utils/parseParams') + +test('parse-params', t => { + const tests = [ + { + source: 'video/ogg', + expected: ['video/ogg'], + what: 'No parameters' + }, + { + source: 'video/ogg;', + expected: ['video/ogg'], + what: 'No parameters (with separator)' + }, + { + source: 'video/ogg; ', + expected: ['video/ogg'], + what: 'No parameters (with separator followed by whitespace)' + }, + { + source: ';video/ogg', + expected: ['', 'video/ogg'], + what: 'Empty parameter' + }, + { + source: 'video/*', + expected: ['video/*'], + what: 'Subtype with asterisk' + }, + { + source: 'text/plain; encoding=utf8', + expected: ['text/plain', ['encoding', 'utf8']], + what: 'Unquoted' + }, + { + source: 'text/plain; encoding=', + expected: ['text/plain', ['encoding', '']], + what: 'Unquoted empty string' + }, + { + source: 'text/plain; encoding="utf8"', + expected: ['text/plain', ['encoding', 'utf8']], + what: 'Quoted' + }, + { + source: 'text/plain; greeting="hello \\"world\\""', + expected: ['text/plain', ['greeting', 'hello "world"']], + what: 'Quotes within quoted' + }, + { + source: 'text/plain; encoding=""', + expected: ['text/plain', ['encoding', '']], + what: 'Quoted empty string' + }, + { + source: 'text/plain; encoding="utf8";\t foo=bar;test', + expected: ['text/plain', ['encoding', 'utf8'], ['foo', 'bar'], 'test'], + what: 'Multiple params with various spacing' + }, + { + source: "text/plain; filename*=iso-8859-1'en'%A3%20rates", + expected: ['text/plain', ['filename', '£ rates']], + what: 'Extended parameter (RFC 5987) with language' + }, + { + source: "text/plain; filename*=utf-8''%c2%a3%20and%20%e2%82%ac%20rates", + expected: ['text/plain', ['filename', '£ and € rates']], + what: 'Extended parameter (RFC 5987) without language' + }, + { + source: "text/plain; filename*=utf-8''%E6%B5%8B%E8%AF%95%E6%96%87%E6%A1%A3", + expected: ['text/plain', ['filename', '测试文档']], + what: 'Extended parameter (RFC 5987) without language #2' + }, + { + source: "text/plain; filename*=iso-8859-1'en'%A3%20rates; altfilename*=utf-8''%c2%a3%20and%20%e2%82%ac%20rates", + expected: ['text/plain', ['filename', '£ rates'], ['altfilename', '£ and € rates']], + what: 'Multiple extended parameters (RFC 5987) with mixed charsets' + }, + { + source: "text/plain; filename*=iso-8859-1'en'%A3%20rates; altfilename=\"foobarbaz\"", + expected: ['text/plain', ['filename', '£ rates'], ['altfilename', 'foobarbaz']], + what: 'Mixed regular and extended parameters (RFC 5987)' + }, + { + source: "text/plain; filename=\"foobarbaz\"; altfilename*=iso-8859-1'en'%A3%20rates", + expected: ['text/plain', ['filename', 'foobarbaz'], ['altfilename', '£ rates']], + what: 'Mixed regular and extended parameters (RFC 5987) #2' + }, + { + source: 'text/plain; filename="C:\\folder\\test.png"', + expected: ['text/plain', ['filename', 'C:\\folder\\test.png']], + what: 'Unescaped backslashes should be considered backslashes' + }, + { + source: 'text/plain; filename="John \\"Magic\\" Smith.png"', + expected: ['text/plain', ['filename', 'John "Magic" Smith.png']], + what: 'Escaped double-quotes should be considered double-quotes' + }, + { + source: 'multipart/form-data; charset=utf-8; boundary=0xKhTmLbOuNdArY', + expected: ['multipart/form-data', ['charset', 'utf-8'], ['boundary', '0xKhTmLbOuNdArY']], + what: 'Multiple non-quoted parameters' + } + ] + + t.plan(tests.length) + + tests.forEach((v) => { + t.test(v.what, t => { + t.plan(1) + + const result = parseParams(v.source) + t.strictSame( + result, + v.expected, + `parsed parameters match.\nSaw: ${inspect(result)}\nExpected: ${inspect(v.expected)}`) + }) + }) +}) diff --git a/fastify-busboy/test/streamsearch.test.js b/fastify-busboy/test/streamsearch.test.js new file mode 100644 index 0000000..968c7de --- /dev/null +++ b/fastify-busboy/test/streamsearch.test.js @@ -0,0 +1,396 @@ +'use strict' + +const { test } = require('tap') +const Streamsearch = require('../deps/streamsearch/sbmh') + +test('streamsearch', t => { + t.plan(17) + + t.test('should throw an error if the needle is not a String or Buffer', t => { + t.plan(1) + + t.throws(() => new Streamsearch(2), new Error('The needle has to be a String or a Buffer.')) + }) + t.test('should throw an error if the needle is an empty String', t => { + t.plan(1) + + t.throws(() => new Streamsearch(''), new Error('The needle cannot be an empty String/Buffer.')) + }) + t.test('should throw an error if the needle is an empty Buffer', t => { + t.plan(1) + + t.throws(() => new Streamsearch(Buffer.from('')), new Error('The needle cannot be an empty String/Buffer.')) + }) + t.test('should throw an error if the needle is bigger than 256 characters', t => { + t.plan(1) + + t.throws(() => new Streamsearch(Buffer.from(Array(257).fill('a').join(''))), new Error('The needle cannot have a length bigger than 256.')) + }) + + t.test('should process a Buffer without a needle', t => { + t.plan(5) + const expected = [ + [false, Buffer.from('bar hello'), 0, 9] + ] + const needle = '\r\n' + const s = new Streamsearch(needle) + const chunks = [ + Buffer.from('bar hello') + ] + let i = 0 + s.on('info', (isMatched, data, start, end) => { + t.strictSame(isMatched, expected[i][0]) + t.strictSame(data, expected[i][1]) + t.strictSame(start, expected[i][2]) + t.strictSame(end, expected[i][3]) + i++ + if (i >= 1) { + t.pass() + } + }) + + s.push(chunks[0]) + }) + + t.test('should cast a string without a needle', t => { + t.plan(5) + + const expected = [ + [false, Buffer.from('bar hello'), 0, 9] + ] + const needle = '\r\n' + const s = new Streamsearch(needle) + const chunks = [ + 'bar hello' + ] + let i = 0 + s.on('info', (isMatched, data, start, end) => { + t.strictSame(isMatched, expected[i][0]) + t.strictSame(data, expected[i][1]) + t.strictSame(start, expected[i][2]) + t.strictSame(end, expected[i][3]) + i++ + if (i >= 1) { + t.pass() + } + }) + + s.push(chunks[0]) + }) + + t.test('should process a chunk with a needle at the beginning', t => { + t.plan(9) + + const expected = [ + [true, undefined, undefined, undefined], + [false, Buffer.from('\r\nbar hello'), 2, 11] + ] + const needle = '\r\n' + const s = new Streamsearch(needle) + const chunks = [ + Buffer.from('\r\nbar hello') + ] + let i = 0 + s.on('info', (isMatched, data, start, end) => { + t.strictSame(isMatched, expected[i][0]) + t.strictSame(data, expected[i][1]) + t.strictSame(start, expected[i][2]) + t.strictSame(end, expected[i][3]) + i++ + if (i >= 2) { + t.pass() + } + }) + + s.push(chunks[0]) + }) + + t.test('should process a chunk with a needle in the middle', t => { + t.plan(9) + const expected = [ + [true, Buffer.from('bar\r\n hello'), 0, 3], + [false, Buffer.from('bar\r\n hello'), 5, 11] + ] + const needle = '\r\n' + const s = new Streamsearch(needle) + const chunks = [ + Buffer.from('bar\r\n hello') + ] + let i = 0 + s.on('info', (isMatched, data, start, end) => { + t.strictSame(isMatched, expected[i][0]) + t.strictSame(data, expected[i][1]) + t.strictSame(start, expected[i][2]) + t.strictSame(end, expected[i][3]) + i++ + if (i >= 2) { + t.pass() + } + }) + + s.push(chunks[0]) + }) + + t.test('should process a chunk with a needle at the end', t => { + t.plan(5) + const expected = [ + [true, Buffer.from('bar hello\r\n'), 0, 9] + ] + const needle = '\r\n' + const s = new Streamsearch(needle) + const chunks = [ + Buffer.from('bar hello\r\n') + ] + let i = 0 + s.on('info', (isMatched, data, start, end) => { + t.strictSame(isMatched, expected[i][0]) + t.strictSame(data, expected[i][1]) + t.strictSame(start, expected[i][2]) + t.strictSame(end, expected[i][3]) + i++ + if (i >= 1) { + t.pass() + } + }) + + s.push(chunks[0]) + }) + + t.test('should process a chunk with multiple needle at the end', t => { + t.plan(9) + const expected = [ + [true, Buffer.from('bar hello\r\n\r\n'), 0, 9], + [true, Buffer.from('bar hello\r\n\r\n'), 11, 11] + ] + const needle = '\r\n' + const s = new Streamsearch(needle) + const chunks = [ + Buffer.from('bar hello\r\n\r\n') + ] + let i = 0 + s.on('info', (isMatched, data, start, end) => { + t.strictSame(isMatched, expected[i][0]) + t.strictSame(data, expected[i][1]) + t.strictSame(start, expected[i][2]) + t.strictSame(end, expected[i][3]) + i++ + if (i >= 2) { + t.pass() + } + }) + + s.push(chunks[0]) + }) + + t.test('should process two chunks without a needle', t => { + t.plan(9) + const expected = [ + [false, Buffer.from('bar'), 0, 3], + [false, Buffer.from('hello'), 0, 5] + ] + const needle = '\r\n' + const s = new Streamsearch(needle) + const chunks = [ + Buffer.from('bar'), + Buffer.from('hello') + ] + let i = 0 + s.on('info', (isMatched, data, start, end) => { + t.strictSame(isMatched, expected[i][0]) + t.strictSame(data, expected[i][1]) + t.strictSame(start, expected[i][2]) + t.strictSame(end, expected[i][3]) + i++ + if (i >= 2) { + t.pass() + } + }) + + s.push(chunks[0]) + s.push(chunks[1]) + }) + + t.test('should process two chunks with an overflowing needle', t => { + t.plan(13) + const expected = [ + [false, Buffer.from('bar\r'), 0, 3], + [true, undefined, undefined, undefined], + [false, Buffer.from('\nhello'), 1, 6] + ] + const needle = '\r\n' + const s = new Streamsearch(needle) + const chunks = [ + Buffer.from('bar\r'), + Buffer.from('\nhello') + ] + let i = 0 + s.on('info', (isMatched, data, start, end) => { + t.strictSame(isMatched, expected[i][0]) + t.strictSame(data, expected[i][1]) + t.strictSame(start, expected[i][2]) + t.strictSame(end, expected[i][3]) + i++ + if (i >= 3) { + t.pass() + } + }) + + s.push(chunks[0]) + s.push(chunks[1]) + }) + + t.test('should process two chunks with a potentially overflowing needle', t => { + t.plan(13) + + const expected = [ + [false, Buffer.from('bar\r'), 0, 3], + [false, Buffer.from('\r\0\0'), 0, 1], + [false, Buffer.from('\n\r\nhello'), 0, 8] + ] + const needle = '\r\n\n' + const s = new Streamsearch(needle) + const chunks = [ + Buffer.from('bar\r'), + Buffer.from('\n\r\nhello') + ] + let i = 0 + s.on('info', (isMatched, data, start, end) => { + t.strictSame(isMatched, expected[i][0]) + t.strictSame(data, expected[i][1]) + t.strictSame(start, expected[i][2]) + t.strictSame(end, expected[i][3]) + i++ + if (i >= 3) { + t.pass() + } + }) + + s.push(chunks[0]) + s.push(chunks[1]) + }) + + t.test('should process three chunks with a overflowing needle', t => { + t.plan(13) + + const expected = [ + [false, Buffer.from('bar\r'), 0, 3], + [true, undefined, undefined, undefined], + [false, Buffer.from('\nhello'), 1, 6] + ] + const needle = '\r\n\n' + const s = new Streamsearch(needle) + const chunks = [ + Buffer.from('bar\r'), + Buffer.from('\n'), + Buffer.from('\nhello') + ] + let i = 0 + s.on('info', (isMatched, data, start, end) => { + t.strictSame(isMatched, expected[i][0]) + t.strictSame(data, expected[i][1]) + t.strictSame(start, expected[i][2]) + t.strictSame(end, expected[i][3]) + i++ + if (i >= 3) { + t.pass() + } + }) + + s.push(chunks[0]) + s.push(chunks[1]) + s.push(chunks[2]) + }) + + t.test('should process four chunks with a overflowing needle', t => { + t.plan(13) + + const expected = [ + [false, Buffer.from('bar\r'), 0, 3], + [true, undefined, undefined, undefined], + [false, Buffer.from('hello'), 0, 5] + ] + const needle = '\r\n\n' + const s = new Streamsearch(needle) + const chunks = [ + Buffer.from('bar\r'), + Buffer.from('\n'), + Buffer.from('\n'), + Buffer.from('hello') + ] + let i = 0 + s.on('info', (isMatched, data, start, end) => { + t.strictSame(isMatched, expected[i][0]) + t.strictSame(data, expected[i][1]) + t.strictSame(start, expected[i][2]) + t.strictSame(end, expected[i][3]) + i++ + if (i >= 3) { + t.pass() + } + }) + + s.push(chunks[0]) + s.push(chunks[1]) + s.push(chunks[2]) + s.push(chunks[3]) + }) + + t.test('should process four chunks with a potentially overflowing needle', t => { + t.plan(17) + + const expected = [ + [false, Buffer.from('bar\r'), 0, 3], + [false, Buffer.from('\r\n\0'), 0, 2], + [false, Buffer.from('\r\n\0'), 0, 1], + [false, Buffer.from('hello'), 0, 5] + ] + const needle = '\r\n\n' + const s = new Streamsearch(needle) + const chunks = [ + Buffer.from('bar\r'), + Buffer.from('\n'), + Buffer.from('\r'), + Buffer.from('hello') + ] + let i = 0 + s.on('info', (isMatched, data, start, end) => { + t.strictSame(isMatched, expected[i][0]) + t.strictSame(data, expected[i][1]) + t.strictSame(start, expected[i][2]) + t.strictSame(end, expected[i][3]) + i++ + if (i >= 4) { + t.pass() + } + }) + + s.push(chunks[0]) + s.push(chunks[1]) + s.push(chunks[2]) + s.push(chunks[3]) + }) + + t.test('should reset the internal values if .reset() is called', t => { + t.plan(9) + + const s = new Streamsearch('test') + + t.strictSame(s._lookbehind_size, 0) + t.strictSame(s.matches, 0) + t.strictSame(s._bufpos, 0) + + s._lookbehind_size = 1 + s._bufpos = 1 + s.matches = 1 + + t.strictSame(s._lookbehind_size, 1) + t.strictSame(s.matches, 1) + t.strictSame(s._bufpos, 1) + + s.reset() + + t.strictSame(s._lookbehind_size, 0) + t.strictSame(s.matches, 0) + t.strictSame(s._bufpos, 0) + }) +}) diff --git a/fastify-busboy/test/types-multipart.test.js b/fastify-busboy/test/types-multipart.test.js new file mode 100644 index 0000000..dc7ae88 --- /dev/null +++ b/fastify-busboy/test/types-multipart.test.js @@ -0,0 +1,678 @@ +'use strict' + +const Busboy = require('..') + +const { test } = require('tap') +const { inspect } = require('util') + +const EMPTY_FN = function () { +} + +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', + '', + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="upload_file_1"; filename="1k_b.dat"', + 'Content-Type: application/octet-stream', + '', + 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--' + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + expected: [ + ['field', 'file_name_0', 'super alpha file', false, false, '7bit', 'text/plain'], + ['field', 'file_name_1', 'super beta file', false, false, '7bit', 'text/plain'], + ['file', 'upload_file_0', 1023, 0, '1k_a.dat', '7bit', 'application/octet-stream'], + ['file', 'upload_file_1', 1023, 0, '1k_b.dat', '7bit', 'application/octet-stream'] + ], + what: 'Fields and files', + plan: 11 + }, + { + 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: [ + ['field', 'cont', 'some random content', false, false, '7bit', 'text/plain'], + ['field', 'pass', 'some random pass', false, false, '7bit', 'text/plain'], + ['field', 'bit', '2', false, false, '7bit', 'text/plain'] + ], + what: 'Fields only', + plan: 6 + }, + { + source: [ + '' + ], + boundary: '----WebKitFormBoundaryTB2MiQ36fnSJlrhY', + expected: [], + shouldError: 'Unexpected end of multipart data', + what: 'No fields and no files', + plan: 3 + }, + { + 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: [ + ['field', 'file_name_0', 'super', false, true, '7bit', 'text/plain'], + ['file', 'upload_file_0', 13, 2, '1k_a.dat', '7bit', 'application/octet-stream'] + ], + what: 'Fields and files (limits)', + plan: 7 + }, + { + source: [ + ['-----------------------------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: { + fields: 0 + }, + events: ['file'], + expected: [ + ['file', 'upload_file_0', 26, 0, '1k_a.dat', '7bit', 'application/octet-stream'] + ], + what: 'should not emit fieldsLimit if no field was sent', + plan: 6 + }, + { + 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: { + fields: 0 + }, + events: ['file', 'fieldsLimit'], + expected: [ + ['file', 'upload_file_0', 26, 0, '1k_a.dat', '7bit', 'application/octet-stream'] + ], + what: 'should respect fields limit of 0', + plan: 6 + }, + { + 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', + '', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--' + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + limits: { + fields: 1 + }, + events: ['field', 'file', 'fieldsLimit'], + expected: [ + ['field', 'file_name_0', 'super alpha file', false, false, '7bit', 'text/plain'], + ['file', 'upload_file_0', 26, 0, '1k_a.dat', '7bit', 'application/octet-stream'] + ], + what: 'should respect fields limit of 7', + plan: 7 + }, + { + source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="file_name_0"', + '', + 'super alpha file', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--' + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + limits: { + files: 0 + }, + events: ['field'], + expected: [ + ['field', 'file_name_0', 'super alpha file', false, false, '7bit', 'text/plain'] + ], + what: 'should not emit filesLimit if no file was sent', + plan: 4 + }, + { + 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 + }, + events: ['field', 'filesLimit'], + expected: [ + ['field', 'file_name_0', 'super alpha file', false, false, '7bit', 'text/plain'] + ], + what: 'should respect fields limit of 0', + plan: 4 + }, + { + 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', + 'Content-Disposition: form-data; name="upload_file_b"; filename="1k_b.dat"', + 'Content-Type: application/octet-stream', + '', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--' + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + limits: { + files: 1 + }, + events: ['field', 'file', 'filesLimit'], + expected: [ + ['field', 'file_name_0', 'super alpha file', false, false, '7bit', 'text/plain'], + ['file', 'upload_file_0', 26, 0, '1k_a.dat', '7bit', 'application/octet-stream'] + ], + what: 'should respect fields limit of 1', + plan: 7 + }, + { + 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', + '', + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="upload_file_1"; filename="1k_b.dat"', + 'Content-Type: application/octet-stream', + '', + 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--' + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + expected: [ + ['field', 'file_name_0', 'super alpha file', false, false, '7bit', 'text/plain'], + ['field', 'file_name_1', 'super beta file', false, false, '7bit', 'text/plain'] + ], + events: ['field'], + what: 'Fields and (ignored) files', + plan: 5 + }, + { + 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: [ + ['file', 'upload_file_0', 26, 0, '1k_a.dat', '7bit', 'application/octet-stream'], + ['file', 'upload_file_1', 26, 0, '1k_b.dat', '7bit', 'application/octet-stream'], + ['file', 'upload_file_2', 26, 0, '1k_c.dat', '7bit', 'application/octet-stream'] + ], + what: 'Files with filenames containing paths', + plan: 12 + }, + { + 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: [ + ['file', 'upload_file_0', 26, 0, '/absolute/1k_a.dat', '7bit', 'application/octet-stream'], + ['file', 'upload_file_1', 26, 0, 'C:\\absolute\\1k_b.dat', '7bit', 'application/octet-stream'], + ['file', 'upload_file_2', 26, 0, 'relative/1k_c.dat', '7bit', 'application/octet-stream'] + ], + what: 'Paths to be preserved through the preservePath option', + plan: 12 + }, + { + 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: [ + ['field', 'cont', 'some random content', false, false, '7bit', 'text/plain'] + ], + what: 'Empty content-type and empty content-disposition', + plan: 4 + }, + { + config: { + isPartAFile: (fieldName) => (fieldName !== 'upload_file_0') + }, + source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="upload_file_0"; filename="blob"', + 'Content-Type: application/json', + '', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--' + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + expected: [ + ['field', 'upload_file_0', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', false, false, '7bit', 'application/json'] + ], + what: 'Blob uploads should be handled as fields if isPartAFile is provided.', + plan: 4 + }, + { + config: { + isPartAFile: (fieldName) => (fieldName !== 'upload_file_0') + }, + source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="upload_file_0"; filename="blob"', + 'Content-Type: application/json', + '', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + '-----------------------------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: [ + ['field', 'upload_file_0', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', false, false, '7bit', 'application/json'], + ['file', 'file', 26, 0, 'näme.txt', '7bit', 'application/octet-stream'] + ], + what: 'Blob uploads should be handled as fields if isPartAFile is provided. Other parts should be files.', + plan: 7 + }, + { + config: { + isPartAFile: (fieldName) => (fieldName === 'upload_file_0') + }, + source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="upload_file_0"; filename="blob"', + 'Content-Type: application/json', + '', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + '-----------------------------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: [ + ['file', 'upload_file_0', 26, 0, 'blob', '7bit', 'application/json'], + ['field', 'file', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', false, false, '7bit', 'application/octet-stream'] + ], + what: 'Blob uploads sould be handled as files if corresponding isPartAFile is provided. Other parts should be fields.', + plan: 7 + }, + { + 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: [ + ['file', 'file', 26, 0, 'näme.txt', '7bit', 'application/octet-stream'] + ], + what: 'Unicode filenames', + plan: 6 + }, + { + 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: [], + shouldError: 'Unexpected end of multipart data', + what: 'Stopped mid-header', + plan: 3 + }, + { + source: [ + ['------WebKitFormBoundaryTB2MiQ36fnSJlrhY', + 'Content-Disposition: form-data; name="cont"', + 'Content-Type: application/json', + '', + '{}', + '------WebKitFormBoundaryTB2MiQ36fnSJlrhY--' + ].join('\r\n') + ], + boundary: '----WebKitFormBoundaryTB2MiQ36fnSJlrhY', + expected: [ + ['field', 'cont', '{}', false, false, '7bit', 'application/json'] + ], + what: 'content-type for fields', + plan: 4 + }, + { + source: [ + '------WebKitFormBoundaryTB2MiQ36fnSJlrhY--\r\n' + ], + boundary: '----WebKitFormBoundaryTB2MiQ36fnSJlrhY', + expected: [], + what: 'empty form', + plan: 3 + }, + { + source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="field1"', + 'content-type: text/plain; charset=utf-8', + '', + 'Aufklärung ist der Ausgang des Menschen aus seiner selbstverschuldeten Unmündigkeit.', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="field2"', + 'content-type: text/plain; charset=iso-8859-1', + '', + 'sapere aude!', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--' + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + expected: [ + ['field', 'field1', 'Aufklärung ist der Ausgang des Menschen aus seiner selbstverschuldeten Unmündigkeit.', false, false, '7bit', 'text/plain'], + ['field', 'field2', 'sapere aude!', false, false, '7bit', 'text/plain'] + ], + what: 'Fields and files', + plan: 5 + }, + { + source: [[ + '------WebKitFormBoundaryzca7IDMnT6QwqBp7', + 'Content-Disposition: form-data; name="regsubmit"', + '', + 'yes', + '------WebKitFormBoundaryzca7IDMnT6QwqBp7', + '------WebKitFormBoundaryzca7IDMnT6QwqBp7', + 'Content-Disposition: form-data; name="referer"', + '', + 'http://domainExample/./', + '------WebKitFormBoundaryzca7IDMnT6QwqBp7', + 'Content-Disposition: form-data; name="activationauth"', + '', + '', + '------WebKitFormBoundaryzca7IDMnT6QwqBp7', + 'Content-Disposition: form-data; name="seccodemodid"', + '', + 'member::register', + '------WebKitFormBoundaryzca7IDMnT6QwqBp7--'].join('\r\n') + ], + boundary: '----WebKitFormBoundaryzca7IDMnT6QwqBp7', + expected: [ + ['field', 'regsubmit', 'yes', false, false, '7bit', 'text/plain'], + ['field', 'referer', 'http://domainExample/./', false, false, '7bit', 'text/plain'], + ['field', 'activationauth', '', false, false, '7bit', 'text/plain'], + ['field', 'seccodemodid', 'member::register', false, false, '7bit', 'text/plain'] + ], + what: 'one empty part should get ignored', + plan: 7 + }, + { + source: [ + ' ------WebKitFormBoundaryTB2MiQ36fnSJlrhY--\r\n' + ], + boundary: '----WebKitFormBoundaryTB2MiQ36fnSJlrhY', + expected: [], + shouldError: 'Unexpected end of multipart data', + what: 'empty form with preceding whitespace', + plan: 3 + }, + { + source: [ + '------WebKitFormBoundaryTB2MiQ36fnSJlrhY--\r\n' + ], + boundary: '----WebKitFormBoundaryTB2MiQ36fnSJlrhYY', + expected: [], + shouldError: 'Unexpected end of multipart data', + what: 'empty form with wrong boundary (extra Y)', + plan: 3 + }, + { + source: [[ + '------WebKitFormBoundaryzca7IDMnT6QwqBp7', + 'Content-Disposition: form-data; name="regsubmit"', + '', + 'yes', + '------WebKitFormBoundaryzca7IDMnT6QwqBp7', + '------WebKitFormBoundaryzca7IDMnT6QwqBp7', + '------WebKitFormBoundaryzca7IDMnT6QwqBp7', + '------WebKitFormBoundaryzca7IDMnT6QwqBp7', + 'Content-Disposition: form-data; name="referer"', + '', + 'http://domainExample/./', + '------WebKitFormBoundaryzca7IDMnT6QwqBp7', + 'Content-Disposition: form-data; name="activationauth"', + '', + '', + '------WebKitFormBoundaryzca7IDMnT6QwqBp7', + 'Content-Disposition: form-data; name="seccodemodid"', + '', + 'member::register', + '------WebKitFormBoundaryzca7IDMnT6QwqBp7--'].join('\r\n') + ], + boundary: '----WebKitFormBoundaryzca7IDMnT6QwqBp7', + expected: [ + ['field', 'regsubmit', 'yes', false, false, '7bit', 'text/plain'], + ['field', 'referer', 'http://domainExample/./', false, false, '7bit', 'text/plain'], + ['field', 'activationauth', '', false, false, '7bit', 'text/plain'], + ['field', 'seccodemodid', 'member::register', false, false, '7bit', 'text/plain'] + ], + what: 'multiple empty parts should get ignored', + plan: 7 + } +] + +tests.forEach((v) => { + test(v.what, t => { + t.plan(v.plan) + const busboy = new Busboy({ + ...v.config, + limits: v.limits, + preservePath: v.preservePath, + headers: { + 'content-type': 'multipart/form-data; boundary=' + v.boundary + } + }) + let finishes = 0 + const results = [] + + if (v.events === undefined || v.events.indexOf('field') > -1) { + busboy.on('field', function (key, val, keyTrunc, valTrunc, encoding, contype) { + results.push(['field', key, val, keyTrunc, valTrunc, encoding, contype]) + }) + } + if (v.events === undefined || v.events.indexOf('file') > -1) { + busboy.on('file', function (fieldname, stream, filename, encoding, mimeType) { + let nb = 0 + const info = ['file', + fieldname, + nb, + 0, + filename, + encoding, + mimeType] + results.push(info) + stream.on('data', function (d) { + nb += d.length + }).on('limit', function () { + ++info[3] + }).on('end', function () { + info[2] = nb + t.ok(typeof (stream.bytesRead) === 'number', 'file.bytesRead is missing') + t.ok(stream.bytesRead === nb, 'file.bytesRead is not equal to filesize') + if (stream.truncated) { ++info[3] } + }) + }) + } + busboy.on('finish', function () { + t.ok(finishes++ === 0, 'finish emitted multiple times') + t.equal(results.length, + v.expected.length, + 'Parsed result count mismatch. Saw ' + + results.length + + '. Expected: ' + v.expected.length) + + results.forEach(function (result, i) { + t.strictSame(result, + v.expected[i], + 'Result mismatch:\nParsed: ' + inspect(result) + + '\nExpected: ' + inspect(v.expected[i]) + ) + }) + t.pass() + }).on('error', function (err) { + if (!v.shouldError || v.shouldError !== err.message) { t.error(err) } + }) + + v.source.forEach(function (s) { + busboy.write(Buffer.from(s, 'utf8'), EMPTY_FN) + }) + busboy.end() + }) +}) diff --git a/fastify-busboy/test/types-urlencoded.test.js b/fastify-busboy/test/types-urlencoded.test.js new file mode 100644 index 0000000..73cc286 --- /dev/null +++ b/fastify-busboy/test/types-urlencoded.test.js @@ -0,0 +1,210 @@ +'use strict' + +const { inspect } = require('util') +const Busboy = require('..') +const { test } = require('tap') + +const EMPTY_FN = function () { +} + +const tests = [ + { + source: ['foo'], + expected: [['foo', '', false, false]], + what: 'Unassigned value', + plan: 4 + }, + { + source: ['foo=bar'], + expected: [['foo', 'bar', false, false]], + what: 'Assigned value', + plan: 4 + }, + { + source: ['foo&bar=baz'], + expected: [['foo', '', false, false], + ['bar', 'baz', false, false]], + what: 'Unassigned and assigned value', + plan: 5 + }, + { + source: ['foo=bar&baz'], + expected: [['foo', 'bar', false, false], + ['baz', '', false, false]], + what: 'Assigned and unassigned value', + plan: 5 + }, + { + source: ['foo=bar&baz=bla'], + expected: [['foo', 'bar', false, false], + ['baz', 'bla', false, false]], + what: 'Two assigned values', + plan: 5 + }, + { + source: ['foo&bar'], + expected: [['foo', '', false, false], + ['bar', '', false, false]], + what: 'Two unassigned values', + plan: 5 + }, + { + source: ['foo&bar&'], + expected: [['foo', '', false, false], + ['bar', '', false, false]], + what: 'Two unassigned values and ampersand', + plan: 5 + }, + { + source: ['foo=bar+baz%2Bquux'], + expected: [['foo', 'bar baz+quux', false, false]], + what: 'Assigned value with (plus) space', + plan: 4 + }, + { + source: ['foo=bar%20baz%21'], + expected: [['foo', 'bar baz!', false, false]], + what: 'Assigned value with encoded bytes', + plan: 4 + }, + { + source: ['foo%20bar=baz%20bla%21'], + expected: [['foo bar', 'baz bla!', false, false]], + what: 'Assigned value with encoded bytes #2', + plan: 4 + }, + { + source: ['foo=bar%20baz%21&num=1000'], + expected: [['foo', 'bar baz!', false, false], + ['num', '1000', false, false]], + what: 'Two assigned values, one with encoded bytes', + plan: 5 + }, + { + source: ['foo=bar&baz=bla'], + expected: [], + what: 'Limits: zero fields', + limits: { fields: 0 }, + plan: 3 + }, + { + source: ['foo=bar&baz=bla'], + expected: [['foo', 'bar', false, false]], + what: 'Limits: one field', + limits: { fields: 1 }, + plan: 4 + }, + { + source: ['foo=bar&baz=bla'], + expected: [['foo', 'bar', false, false], + ['baz', 'bla', false, false]], + what: 'Limits: field part lengths match limits', + limits: { fieldNameSize: 3, fieldSize: 3 }, + plan: 5 + }, + { + source: ['foo=bar&baz=bla'], + expected: [['fo', 'bar', true, false], + ['ba', 'bla', true, false]], + what: 'Limits: truncated field name', + limits: { fieldNameSize: 2 }, + plan: 5 + }, + { + source: ['foo=bar&baz=bla'], + expected: [['foo', 'ba', false, true], + ['baz', 'bl', false, true]], + what: 'Limits: truncated field value', + limits: { fieldSize: 2 }, + plan: 5 + }, + { + source: ['foo=bar&baz=bla'], + expected: [['fo', 'ba', true, true], + ['ba', 'bl', true, true]], + what: 'Limits: truncated field name and value', + limits: { fieldNameSize: 2, fieldSize: 2 }, + plan: 5 + }, + { + source: ['foo=bar&baz=bla'], + expected: [['fo', '', true, true], + ['ba', '', true, true]], + what: 'Limits: truncated field name and zero value limit', + limits: { fieldNameSize: 2, fieldSize: 0 }, + plan: 5 + }, + { + source: ['foo=bar&baz=bla'], + expected: [['', '', true, true], + ['', '', true, true]], + what: 'Limits: truncated zero field name and zero value limit', + limits: { fieldNameSize: 0, fieldSize: 0 }, + plan: 5 + }, + { + source: ['&'], + expected: [], + what: 'Ampersand', + plan: 3 + }, + { + source: ['&&&&&'], + expected: [], + what: 'Many ampersands', + plan: 3 + }, + { + source: ['='], + expected: [['', '', false, false]], + what: 'Assigned value, empty name and value', + plan: 4 + }, + { + source: [''], + expected: [], + what: 'Nothing', + plan: 3 + } +] + +tests.forEach((v) => { + test(v.what, t => { + t.plan(v.plan || 20) + const busboy = new Busboy({ + limits: v.limits, + headers: { + 'content-type': 'application/x-www-form-urlencoded; charset=utf-8' + } + }) + let finishes = 0 + const results = [] + + busboy.on('field', function (key, val, keyTrunc, valTrunc) { + results.push([key, val, keyTrunc, valTrunc]) + }) + busboy.on('file', function () { + throw new Error('Unexpected file') + }) + busboy.on('finish', function () { + t.ok(finishes++ === 0, 'finish emitted multiple times') + t.equal(results.length, v.expected.length) + + let i = 0 + results.forEach(function (result) { + t.strictSame(result, + v.expected[i], + 'Result mismatch:\nParsed: ' + inspect(result) + + '\nExpected: ' + inspect(v.expected[i]) + ) + ++i + }) + t.pass() + }) + + v.source.forEach(function (s) { + busboy.write(Buffer.from(s, 'utf8'), EMPTY_FN) + }) + busboy.end() + }) +}) diff --git a/fastify-busboy/test/types/dicer.test-d.ts b/fastify-busboy/test/types/dicer.test-d.ts new file mode 100644 index 0000000..466c1e1 --- /dev/null +++ b/fastify-busboy/test/types/dicer.test-d.ts @@ -0,0 +1,81 @@ +import { Dicer } from "../../lib/main"; +import * as fs from "fs"; +import * as stream from "stream"; + +function testDicerSyntax() { + const opts: Dicer.Config = { + boundary: "testing", + }; + const dicer = new Dicer(opts); + const opts2: Dicer.Config = { + headerFirst: true, + maxHeaderPairs: 1, + }; + const opts3: Dicer.Config = { + boundary: "more-testing", + headerFirst: false, + maxHeaderPairs: 8, + }; + dicer.setBoundary("new-testing-boundary"); + dicer.on("part", handleDicerPartStream); + dicer.on("finish", () => { + console.log("dicer parsing finished"); + }); + dicer.on("preamble", part => { + console.log("dicer preamble to new part"); + }); + dicer.on("trailer", data => { + console.log(`dicer trailing data found: ${data.length} bytes`); + }); + dicer.on("close", () => { + console.log("dicer close"); + }); + dicer.on("drain", () => { + console.log("dicer drain"); + }); + dicer.on("error", err => { + console.error(`dicer error: ${err.message || JSON.stringify(err)}`); + }); + dicer.on("finish", () => { + console.log("dicer finish"); + }); + dicer.on("pipe", (src: stream.Readable) => { + console.log("dicer pipe"); + }); + dicer.on("unpipe", (src: stream.Readable) => { + console.log("dicer unpipe"); + }); + const inputFileStream = fs.createReadStream("in-test-file.txt"); + inputFileStream.pipe(dicer); +} +/** + * Handle a part found by a Dicer parser + * + * @param part Part found + */ +function handleDicerPartStream(part: Dicer.PartStream) { + console.log("dicer part found"); + const outputFileStream = fs.createWriteStream("out-test-file.txt"); + part.on("readable", () => { + console.log("part readable"); + }); + part.on("header", header => { + console.log(`part header found:\n${JSON.stringify(header)}`); + }); + part.on("data", () => { + console.log("part data"); + }); + part.on("finish", () => { + console.log("part finished"); + }); + part.on("error", err => { + console.error(`part error: ${err.message || JSON.stringify(err)}`); + }); + part.on("end", () => { + console.log("part ended"); + }); + part.on("close", () => { + console.log("part closed"); + }); + part.pipe(outputFileStream); +}
\ No newline at end of file diff --git a/fastify-busboy/test/types/main.test-d.ts b/fastify-busboy/test/types/main.test-d.ts new file mode 100644 index 0000000..fb58b3f --- /dev/null +++ b/fastify-busboy/test/types/main.test-d.ts @@ -0,0 +1,241 @@ +import BusboyDefault, { BusboyConstructor, BusboyConfig, BusboyHeaders, Busboy, BusboyEvents, BusboyFileStream } from '../..'; +import {expectError, expectType} from "tsd"; +import BusboyESM from "../.."; + +// test type exports +type Constructor = BusboyConstructor; +type Config = BusboyConfig; +type Headers = BusboyHeaders; +type Events = BusboyEvents; +type BB = Busboy; + +expectType<Busboy>(new BusboyESM({ headers: { 'content-type': 'foo' } })); +expectType<Busboy>(new Busboy({ headers: { 'content-type': 'foo' } })); + +expectError(new BusboyDefault({})); +const busboy = BusboyDefault({ headers: { 'content-type': 'foo' } }); // $ExpectType Busboy +new BusboyDefault({ headers: { 'content-type': 'foo' } }); // $ExpectType Busboy +new BusboyDefault({ headers: { 'content-type': 'foo' }, highWaterMark: 1000 }); // $ExpectType Busboy +new BusboyDefault({ headers: { 'content-type': 'foo' }, fileHwm: 1000 }); // $ExpectType Busboy +new BusboyDefault({ headers: { 'content-type': 'foo' }, defCharset: 'utf8' }); // $ExpectType Busboy +new BusboyDefault({ headers: { 'content-type': 'foo' }, preservePath: true }); // $ExpectType Busboy +new BusboyDefault({ headers: { 'content-type': 'foo' }, limits: { fieldNameSize: 200 } }); // $ExpectType Busboy +new BusboyDefault({ headers: { 'content-type': 'foo' }, limits: { fieldSize: 200 } }); // $ExpectType Busboy +new BusboyDefault({ headers: { 'content-type': 'foo' }, limits: { fields: 200 } }); // $ExpectType Busboy +new BusboyDefault({ headers: { 'content-type': 'foo' }, limits: { fileSize: 200 } }); // $ExpectType Busboy +new BusboyDefault({ headers: { 'content-type': 'foo' }, limits: { files: 200 } }); // $ExpectType Busboy +new BusboyDefault({ headers: { 'content-type': 'foo' }, limits: { parts: 200 } }); // $ExpectType Busboy +new BusboyDefault({ headers: { 'content-type': 'foo' }, limits: { headerPairs: 200 } }); // $ExpectType Busboy +new BusboyDefault({ headers: { 'content-type': 'foo' }, limits: { headerSize: 200 } }); // $ExpectType Busboy +new BusboyDefault({ headers: { 'content-type': 'foo' }, isPartAFile: (fieldName, contentType, fileName) => fieldName === 'my-special-field' || fileName !== 'not-so-special.txt' }); // $ExpectType Busboy +new BusboyDefault({ headers: { 'content-type': 'foo' }, isPartAFile: (fieldName, contentType, fileName) => fileName !== undefined }); // $ExpectType Busboy + +busboy.addListener('file', (fieldname, file, filename, encoding, mimetype) => { + expectType<string> (fieldname) + expectType<BusboyFileStream>(file); + expectType<string>(filename); + expectType<string>(encoding); + expectType<string>(mimetype); +}); +busboy.addListener('field', (fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) => { + expectType<string> (fieldname); + expectType<string> (val); + expectType<boolean> (fieldnameTruncated); + expectType<boolean> (valTruncated); + expectType<string> (encoding); + expectType<string> (mimetype); +}); +busboy.addListener('partsLimit', () => {}); +busboy.addListener('filesLimit', () => {}); +busboy.addListener('fieldsLimit', () => {}); +busboy.addListener('error', e => { + expectType<unknown> (e); +}); +busboy.addListener('finish', () => {}); +// test fallback +busboy.on('foo', foo => { + expectType<any> (foo); +}); +busboy.on(Symbol('foo'), foo => { + expectType<any>(foo); +}); + +busboy.on('file', (fieldname, file, filename, encoding, mimetype) => { + expectType<string> (fieldname); + expectType<BusboyFileStream> (file); + expectType<string> (filename); + expectType<string> (encoding); + expectType<string> (mimetype); +}); +busboy.on('field', (fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) => { + expectType<string> (fieldname); + expectType<string> (val); + expectType<boolean> (fieldnameTruncated); + expectType<boolean> (valTruncated); + expectType<string> (encoding); + expectType<string> (mimetype); +}); +busboy.on('partsLimit', () => {}); +busboy.on('filesLimit', () => {}); +busboy.on('fieldsLimit', () => {}); +busboy.on('error', e => { + expectType<unknown> (e); +}); +busboy.on('finish', () => {}); +// test fallback +busboy.on('foo', foo => { + expectType<any> (foo); +}); +busboy.on(Symbol('foo'), foo => { + expectType<any> (foo); +}); + +busboy.once('file', (fieldname, file, filename, encoding, mimetype) => { + expectType<string> (fieldname); + expectType<BusboyFileStream> (file); + expectType<string> (filename); + expectType<string> (encoding); + expectType<string> (mimetype); +}); +busboy.once('field', (fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) => { + expectType<string> (fieldname); + expectType<string> (val); + expectType<boolean> (fieldnameTruncated); + expectType<boolean> (valTruncated); + expectType<string> (encoding); + expectType<string> (mimetype); +}); +busboy.once('partsLimit', () => {}); +busboy.once('filesLimit', () => {}); +busboy.once('fieldsLimit', () => {}); +busboy.once('error', e => { + expectType<unknown> (e); +}); +busboy.once('finish', () => {}); +// test fallback +busboy.once('foo', foo => { + expectType<any> (foo); +}); +busboy.once(Symbol('foo'), foo => { + expectType<any> (foo); +}); + +busboy.removeListener('file', (fieldname, file, filename, encoding, mimetype) => { + expectType<string> (fieldname); + expectType<BusboyFileStream> (file); + expectType<string> (filename); + expectType<string> (encoding); + expectType<string> (mimetype); +}); +busboy.removeListener('field', (fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) => { + expectType<string> (fieldname); + expectType<string> (val); + expectType<boolean> (fieldnameTruncated); + expectType<boolean> (valTruncated); + expectType<string> (encoding); + expectType<string> (mimetype); +}); +busboy.removeListener('partsLimit', () => {}); +busboy.removeListener('filesLimit', () => {}); +busboy.removeListener('fieldsLimit', () => {}); +busboy.removeListener('error', e => { + expectType<unknown> (e); +}); +busboy.removeListener('finish', () => {}); +// test fallback +busboy.removeListener('foo', foo => { + expectType<any> (foo); +}); +busboy.removeListener(Symbol('foo'), foo => { + expectType<any> (foo); +}); + +busboy.off('file', (fieldname, file, filename, encoding, mimetype) => { + expectType<string> (fieldname); + expectType<BusboyFileStream> (file); + expectType<string> (filename); + expectType<string> (encoding); + expectType<string> (mimetype); +}); +busboy.off('field', (fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) => { + expectType<string> (fieldname); + expectType<string> (val); + expectType<boolean> (fieldnameTruncated); + expectType<boolean> (valTruncated); + expectType<string> (encoding); + expectType<string> (mimetype); +}); +busboy.off('partsLimit', () => {}); +busboy.off('filesLimit', () => {}); +busboy.off('fieldsLimit', () => {}); +busboy.off('error', e => { + expectType<unknown> (e); +}); +busboy.off('finish', () => {}); +// test fallback +busboy.off('foo', foo => { + expectType<any> (foo); +}); +busboy.off(Symbol('foo'), foo => { + expectType<any> (foo); +}); + +busboy.prependListener('file', (fieldname, file, filename, encoding, mimetype) => { + expectType<string> (fieldname); + expectType<BusboyFileStream> (file); + expectType<string> (filename); + expectType<string> (encoding); + expectType<string> (mimetype); +}); +busboy.prependListener('field', (fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) => { + expectType<string> (fieldname); + expectType<string> (val); + expectType<boolean> (fieldnameTruncated); + expectType<boolean> (valTruncated); + expectType<string> (encoding); + expectType<string> (mimetype); +}); +busboy.prependListener('partsLimit', () => {}); +busboy.prependListener('filesLimit', () => {}); +busboy.prependListener('fieldsLimit', () => {}); +busboy.prependListener('error', e => { + expectType<unknown> (e); +}); +busboy.prependListener('finish', () => {}); +// test fallback +busboy.prependListener('foo', foo => { + expectType<any> (foo); +}); +busboy.prependListener(Symbol('foo'), foo => { + expectType<any> (foo); +}); + +busboy.prependOnceListener('file', (fieldname, file, filename, encoding, mimetype) => { + expectType<string> (fieldname); + expectType<BusboyFileStream> (file); + expectType<string> (filename); + expectType<string> (encoding); + expectType<string> (mimetype); +}); +busboy.prependOnceListener('field', (fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) => { + expectType<string> (fieldname); + expectType<string> (val); + expectType<boolean> (fieldnameTruncated); + expectType<boolean> (valTruncated); + expectType<string> (encoding); + expectType<string> (mimetype); +}); +busboy.prependOnceListener('partsLimit', () => {}); +busboy.prependOnceListener('filesLimit', () => {}); +busboy.prependOnceListener('fieldsLimit', () => {}); +busboy.prependOnceListener('error', e => { + expectType<unknown> (e); +}); +busboy.prependOnceListener('finish', () => {}); +// test fallback +busboy.prependOnceListener('foo', foo => { + expectType<any> (foo); +}); +busboy.prependOnceListener(Symbol('foo'), foo => { + expectType<any> (foo); +}); diff --git a/fastify-busboy/tsconfig.json b/fastify-busboy/tsconfig.json new file mode 100644 index 0000000..eec9314 --- /dev/null +++ b/fastify-busboy/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "outDir": "dist", + "module": "commonjs", + "target": "es2015", + "sourceMap": false, + "declaration": true, + "declarationMap": false, + "types": ["node"], + "strict": true, + "moduleResolution": "node", + "noUnusedLocals": false, + "noUnusedParameters": false, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "noImplicitAny": true, + "noImplicitThis": true, + "strictNullChecks": true, + "importHelpers": true, + "baseUrl": ".", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true + }, + "exclude": [ + "node_modules", + "test", + "dist" + ] +} |