|
|
/** * Parted (https://github.com/chjj/parted)
* A streaming multipart state parser. * Copyright (c) 2011, Christopher Jeffrey. (MIT Licensed) */
var fs = require('fs') , path = require('path') , EventEmitter = require('events').EventEmitter , StringDecoder = require('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.'); } break; 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(';')[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;
|