'use strict' /* eslint-disable */ /** * Parted (https://github.com/chjj/parted) * A streaming multipart state parser. * Copyright (c) 2011, Christopher Jeffrey. (MIT Licensed) */ const fs = require('node:fs') const path = require('node:path') const EventEmitter = require('node:events').EventEmitter const StringDecoder = require('node:string_decoder').StringDecoder const set = require('qs').set const each = Array.prototype.forEach /** * Character Constants */ const DASH = '-'.charCodeAt(0) const CR = '\r'.charCodeAt(0) const LF = '\n'.charCodeAt(0) const COLON = ':'.charCodeAt(0) const SPACE = ' '.charCodeAt(0) /** * Parser */ const 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 || {} const 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) { let i = 0 let len = data.length const buff = this.buff const key = this.key let ch let val let 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 () { const self = this const field = this.field const data = this.data const file = this.file let 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 let 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) { const parser = new Parser(req.headers['content-type'], options) const diskLimit = options.diskLimit const limit = options.limit const parts = {} const 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, 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 */ const isWindows = process.platform === 'win32' const stream = function (name, dir) { const ext = path.extname(name) || '' var name = path.basename(name, ext) || '' var dir = dir || Parser.root let 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 const rx = new RegExp('\\b' + name + '\\s*=\\s*("[^"]+"|\'[^\']+\'|[^;,]+)', 'i') const cap = rx.exec(str) if (cap) { return cap[1].trim().replace(/^['"]|['"]$/g, '') } } /** * Expose */ module.exports = Parser