Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial commit

  • Loading branch information...
commit db0036ec7c58c59d100d49741f235504e5f5f176 0 parents
@tj tj authored
Showing with 1,920 additions and 0 deletions.
  1. +3 −0  .gitmodules
  2. 0  History.md
  3. +5 −0 Makefile
  4. 0  Readme.md
  5. +12 −0 index.js
  6. +1 −0  support/expresso
  7. +1 −0  support/formidable/.gitignore
  8. +4 −0 support/formidable/Makefile
  9. +157 −0 support/formidable/Readme.md
  10. +66 −0 support/formidable/benchmark/bench-multipart-parser.js
  11. +43 −0 support/formidable/example/post.js
  12. +48 −0 support/formidable/example/upload.js
  13. +1 −0  support/formidable/index.js
  14. +1 −0  support/formidable/lib/formidable/formidable.js
  15. +326 −0 support/formidable/lib/formidable/incoming_form.js
  16. +1 −0  support/formidable/lib/formidable/index.js
  17. +311 −0 support/formidable/lib/formidable/multipart_parser.js
  18. +25 −0 support/formidable/lib/formidable/querystring_parser.js
  19. +6 −0 support/formidable/package.json
  20. +61 −0 support/formidable/test/common.js
  21. +47 −0 support/formidable/test/fixture/multipart.js
  22. +86 −0 support/formidable/test/integration/test-multipart-parser.js
  23. +614 −0 support/formidable/test/simple/test-incoming-form.js
  24. +50 −0 support/formidable/test/simple/test-multipart-parser.js
  25. +45 −0 support/formidable/test/simple/test-querystring-parser.js
  26. +6 −0 test/multipart.test.js
3  .gitmodules
@@ -0,0 +1,3 @@
+[submodule "support/expresso"]
+ path = support/expresso
+ url = git://github.com/visionmedia/expresso.git
0  History.md
No changes.
5 Makefile
@@ -0,0 +1,5 @@
+
+test:
+ @./support/expresso/bin/expresso
+
+.PHONY: test
0  Readme.md
No changes.
12 index.js
@@ -0,0 +1,12 @@
+
+/*!
+ * Connect - Multipart
+ * Copyright(c) 2010 TJ Holowaychuk <tj@vision-media.ca>
+ * MIT Licensed
+ */
+
+module.exports = function multipart(){
+ return function(req, res, next){
+
+ };
+};
1  support/expresso
@@ -0,0 +1 @@
+Subproject commit 1d0a1b7d1ddd0ff488ef702fb3c507b20b6caef5
1  support/formidable/.gitignore
@@ -0,0 +1 @@
+/test/tmp/*
4 support/formidable/Makefile
@@ -0,0 +1,4 @@
+test:
+ @find test/{simple,integration}/test-*.js | xargs -n 1 -t node
+
+.PHONY: test
157 support/formidable/Readme.md
@@ -0,0 +1,157 @@
+# Formidable
+
+## Purpose
+
+A node.js module for dealing with web forms.
+
+## Features
+
+* Fast (~500mb/sec), non-buffering multipart parser
+* Automatically writing file uploads to disk
+* Low memory footprint
+* Graceful error handling
+* Very high test coverage
+
+### Todo
+
+* Limit buffer size for fields
+* Implement QuerystringParser the same way as MultipartParser
+
+## Installation
+
+Via [npm](http://github.com/isaacs/npm):
+
+ npm install formidable@latest
+
+Manually:
+
+ git clone git://github.com/felixge/node-formidable.git formidable
+ vim my.js
+ # var formidable = require('./formidable');
+
+Note: Formidable requires [gently](http://github.com/felixge/node-gently) to run the unit tests, but you won't need it for just using the library.
+
+## Example
+
+Parse an incoming file upload.
+
+ var formidable = require('formidable')
+ , http = require('http')
+ , sys = require('sys');
+
+ http.createServer(function(req, res) {
+ if (req.url == '/upload' && req.method.toLowerCase() == 'post') {
+ // parse a file upload
+ var form = new formidable.IncomingForm();
+ form.parse(req, function(fields, files) {
+ res.writeHead(200, {'content-type': 'text/plain'});
+ res.write('received upload:\n\n');
+ res.end(sys.inspect({fields: fields, files: files}));
+ });
+ return;
+ }
+
+ // show a file upload form
+ res.writeHead(200, {'content-type': 'text/html'});
+ res.end
+ ( '<form action="/upload" enctype="multipart/form-data" method="post">'
+ + '<input type="text" name="title"><br>'
+ + '<input type="file" name="upload" multiple="multiple"><br>'
+ + '<input type="submit" value="Upload">'
+ + '</form>'
+ );
+ });
+
+## API
+
+### formdiable.IncomingForm
+
+#### new formdiable.IncomingForm()
+
+Creates a new incoming form.
+
+#### incomingForm.encoding = 'utf-8'
+
+The encoding to use for incoming form fields.
+
+#### incomingForm.uploadDir = '/tmp'
+
+The directory for placing file uploads in. You can later on move them using `fs.rename()`.
+
+#### incomingForm.keepExtensions = false
+
+If you want the files written to `incomingForm.uploadDir` to include the extensions of the original files, set this property to `true`.
+
+#### incomingForm.type
+
+Either 'multipart' or 'urlencoded' depending on the incoming request.
+
+#### incomingForm.bytesReceived
+
+The amount of bytes received for this form so far.
+
+#### incomingForm.bytesExpected
+
+The expected number of bytes in this form.
+
+#### incomingForm.parse(request, [cb])
+
+Parses an incoming node.js `request` containing form data. If `cb` is provided, all fields an files are collected and passed to the callback:
+
+ incomingForm.parse(req, function(err, fields, files) {
+ // ...
+ });
+
+#### incomingForm.onPart(part)
+
+You may overwrite this method if you are interested in directly accessing the multipart stream. Doing so will disable any `'field'` / `'file'` events processing which would occur otherwise, making you fully responsible for handling the processing.
+
+ incomingForm.onPart = function(part) {
+ part.addListener('data', function() {
+ // ...
+ });
+ }
+
+If you want to use formidable to only handle certain parts for you, you can do so:
+
+ incomingForm.onPart = function(part) {
+ if (!part.filename) {
+ // let formidable handle all non-file parts
+ incomingForm.handlePart(part);
+ }
+ }
+
+Check the code in this method for further inspiration.
+
+#### Event: 'progress' (bytesReceived, bytesExpected)
+
+Emitted after each incoming chunk of data that has been parsed. Can be used to roll your own progress bar.
+
+#### Event: 'field' (name, value)
+
+Emitted whenever a field / value pair has been received.
+
+#### Event: 'file' (name, file)
+
+Emitted whenever a field / file pair has been received. `file` is a JS object with the following properties:
+
+ { path: 'the path in the uploadDir this file was written to'
+ , filename: 'the name this file had on the computer of the uploader'
+ , mime: 'the mime type specified by the user agent of the uploader'
+ }
+
+#### Event: 'error' (err)
+
+Emitted when there is an error processing the incoming form. A request that experiences an error is automatically paused, you will have to manually call `request.resume()` if you want the request to continue firing `'data'` events.
+
+#### Event: 'end' ()
+
+Emitted when the entire request has been received, and all contained files have finished flushing to disk. This is a great place for you to send your response.
+
+## License
+
+Formidable is licensed under the MIT license.
+
+## Credits
+
+* [Ryan Dahl](http://twitter.com/ryah) for his work on [http-parser](http://github.com/ry/http-parser) which heavily inspired multipart_parser.js
66 support/formidable/benchmark/bench-multipart-parser.js
@@ -0,0 +1,66 @@
+require('../test/common');
+var multipartParser = require('formidable/multipart_parser')
+ , MultipartParser = multipartParser.MultipartParser
+ , parser = new MultipartParser()
+ , Buffer = require('buffer').Buffer
+ , 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);
+
+p(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 = new Buffer(size);
+
+ buffer.write(head, 'ascii', 0);
+ buffer.write(tail, 'ascii', buffer.length - tail.length);
+ return buffer;
+}
+
+assert.callbacks(callbacks);
43 support/formidable/example/post.js
@@ -0,0 +1,43 @@
+require('../test/common');
+var http = require('http')
+ , sys = require('sys')
+ , formidable = require('formidable')
+ , server;
+
+server = http.createServer(function(req, res) {
+ if (req.url == '/') {
+ res.writeHead(200, {'content-type': 'text/html'});
+ res.end
+ ( '<form action="/post" method="post">'
+ + '<input type="text" name="title"><br>'
+ + '<input type="text" name="data[foo][]"><br>'
+ + '<input type="submit" value="Submit">'
+ + '</form>'
+ )
+ } else if (req.url == '/post') {
+ var form = new formidable.IncomingForm()
+ , fields = [];
+
+ form
+ .addListener('error', function(err) {
+ res.writeHead(200, {'content-type': 'text/plain'});
+ res.end('error:\n\n'+sys.inspect(err));
+ })
+ .addListener('field', function(field, value) {
+ p([field, value]);
+ fields.push([field, value]);
+ })
+ .addListener('end', function() {
+ puts('-> post done');
+ res.writeHead(200, {'content-type': 'text/plain'});
+ res.end('received fields:\n\n '+sys.inspect(fields));
+ });
+ form.parse(req);
+ } else {
+ res.writeHead(404, {'content-type': 'text/plain'});
+ res.end('404');
+ }
+});
+server.listen(TEST_PORT);
+
+sys.puts('listening on http://localhost:'+TEST_PORT+'/');
48 support/formidable/example/upload.js
@@ -0,0 +1,48 @@
+require('../test/common');
+var http = require('http')
+ , sys = require('sys')
+ , formidable = require('formidable')
+ , server;
+
+server = http.createServer(function(req, res) {
+ if (req.url == '/') {
+ res.writeHead(200, {'content-type': 'text/html'});
+ res.end
+ ( '<form action="/upload" enctype="multipart/form-data" method="post">'
+ + '<input type="text" name="title"><br>'
+ + '<input type="file" name="upload" multiple="multiple"><br>'
+ + '<input type="submit" value="Upload">'
+ + '</form>'
+ )
+ } else if (req.url == '/upload') {
+ var form = new formidable.IncomingForm()
+ , files = []
+ , fields = [];
+
+ form.uploadDir = TEST_TMP;
+
+ form
+ .addListener('field', function(field, value) {
+ p([field, value]);
+ fields.push([field, value]);
+ })
+ .addListener('file', function(field, file) {
+ p([field, file]);
+ files.push([field, file]);
+ })
+ .addListener('end', function() {
+ puts('-> upload done');
+ res.writeHead(200, {'content-type': 'text/plain'});
+ res.write('received fields:\n\n '+sys.inspect(fields));
+ res.write('\n\n');
+ res.end('received files:\n\n '+sys.inspect(files));
+ });
+ form.parse(req);
+ } else {
+ res.writeHead(404, {'content-type': 'text/plain'});
+ res.end('404');
+ }
+});
+server.listen(TEST_PORT);
+
+sys.puts('listening on http://localhost:'+TEST_PORT+'/');
1  support/formidable/index.js
@@ -0,0 +1 @@
+module.exports = require('./lib/formidable');
1  support/formidable/lib/formidable/formidable.js
@@ -0,0 +1 @@
+exports.IncomingForm = require('./incoming_form').IncomingForm;
326 support/formidable/lib/formidable/incoming_form.js
@@ -0,0 +1,326 @@
+if (global.GENTLY) require = GENTLY.hijack(require);
+
+var sys = require('sys')
+ , path = require('path')
+ , WriteStream = require('fs').WriteStream
+ , MultipartParser = require('./multipart_parser').MultipartParser
+ , QuerystringParser = require('./querystring_parser').QuerystringParser
+ , StringDecoder = require('string_decoder').StringDecoder
+ , EventEmitter = require('events').EventEmitter;
+
+function IncomingForm() {
+ EventEmitter.call(this);
+
+ this.error = null;
+ this.ended = false;
+
+ this.keepExtensions = false;
+ this.uploadDir = '/tmp';
+ this.encoding = 'utf-8';
+ this.headers = null;
+ this.type = null;
+
+ this.bytesReceived = null;
+ this.bytesExpected = null;
+
+ this._parser = null;
+ this._flushing = 0;
+};
+sys.inherits(IncomingForm, EventEmitter);
+exports.IncomingForm = IncomingForm;
+
+IncomingForm.prototype.parse = function(req, cb) {
+ this.pause = function() {
+ req.pause();
+ return true;
+ };
+
+ this.resume = function() {
+ try {
+ req.resume();
+ } catch (err) {
+ this._error(err);
+ return false;
+ }
+
+ return true;
+ };
+
+ this.writeHeaders(req.headers);
+
+ var self = this;
+ req
+ .addListener('error', function(err) {
+ self._error(err);
+ })
+ .addListener('data', function(buffer) {
+ self.write(buffer);
+ })
+ .addListener('end', function() {
+ if (self.error) {
+ return;
+ }
+
+ var err = self._parser.end();
+ if (err) {
+ self._error(err);
+ }
+ });
+
+ if (cb) {
+ var fields = {}, files = {};
+ this
+ .addListener('field', function(name, value) {
+ fields[name] = value;
+ })
+ .addListener('file', function(name, file) {
+ files[name] = file;
+ })
+ .addListener('error', function(err) {
+ cb(err, fields, files);
+ })
+ .addListener('end', function() {
+ cb(null, fields, files);
+ });
+ }
+
+ return this;
+};
+
+IncomingForm.prototype.writeHeaders = function(headers) {
+ this.headers = headers;
+ this._parseContentLength();
+ this._parseContentType();
+};
+
+IncomingForm.prototype.write = function(buffer) {
+ if (!this._parser) {
+ this._error(new Error('unintialized parser'));
+ return;
+ }
+
+ var bytesParsed = this._parser.write(buffer);
+ if (bytesParsed !== buffer.length) {
+ this._error(new Error('parser error, '+bytesParsed+' of '+buffer.length+' bytes parsed'));
+ }
+
+ this.bytesReceived += bytesParsed;
+ this.emit('progress', this.bytesReceived, this.bytesExpected);
+
+ return bytesParsed;
+};
+
+IncomingForm.prototype.pause = function() {
+ // this does nothing, unless overwritten in IncomingForm.parse
+ return false;
+};
+
+IncomingForm.prototype.resume = function() {
+ // this does nothing, unless overwritten in IncomingForm.parse
+ return false;
+};
+
+IncomingForm.prototype.onPart = function(part) {
+ // this method can be overwritten by the user
+ this.handlePart(part);
+};
+
+IncomingForm.prototype.handlePart = function(part) {
+ var self = this;
+
+ if (!part.filename) {
+ var value = ''
+ , decoder = new StringDecoder(this.encoding);
+
+ part.addListener('data', function(buffer) {
+ value += decoder.write(buffer);
+ });
+
+ part.addListener('end', function() {
+ if (value) {
+ self.emit('field', part.name, value);
+ }
+ });
+ return;
+ }
+
+ this._flushing++;
+
+ var file = new WriteStream(this._uploadPath(part.filename));
+ part.addListener('data', function(buffer) {
+ self.pause();
+ file.write(buffer, function() {
+ self.resume();
+ });
+ });
+
+ part.addListener('end', function() {
+ file.end(function() {
+ self._flushing--;
+ self.emit
+ ( 'file'
+ , part.name
+ , { path: file.path
+ , filename: part.filename
+ , mime: part.mime
+ }
+ );
+ self._maybeEnd();
+ });
+ });
+};
+
+IncomingForm.prototype._parseContentType = function() {
+ if (!this.headers['content-type']) {
+ this._error(new Error('bad content-type header, no content-type'));
+ return;
+ }
+
+ if (this.headers['content-type'].match(/urlencoded/i)) {
+ this._initUrlencoded();
+ return;
+ }
+
+ if (this.headers['content-type'].match(/multipart/i)) {
+ var m;
+ if (m = this.headers['content-type'].match(/boundary=([^;]+)/i)) {
+ this._initMultipart(m[1]);
+ } else {
+ this._error(new Error('bad content-type header, no multipart boundary'));
+ }
+ return;
+ }
+
+ this._error(new Error('bad content-type header, unknown content-type: '+this.headers['content-type']));
+};
+
+IncomingForm.prototype._error = function(err) {
+ if (this.error) {
+ return;
+ }
+
+ this.error = err;
+ this.pause();
+ this.emit('error', err);
+};
+
+IncomingForm.prototype._parseContentLength = function() {
+ if (this.headers['content-length']) {
+ this.bytesReceived = 0;
+ this.bytesExpected = parseInt(this.headers['content-length'], 10);
+ }
+};
+
+IncomingForm.prototype._newParser = function() {
+ return new MultipartParser();
+};
+
+IncomingForm.prototype._initMultipart = function(boundary) {
+ this.type = 'multipart';
+
+ var parser = new MultipartParser()
+ , self = this
+ , headerField = ''
+ , headerValue = ''
+ , part
+ , addHeader = function() {
+ headerField = headerField.toLowerCase();
+ part.headers[headerField] = headerValue;
+
+ var m;
+ if (headerField == 'content-disposition') {
+ if (m = headerValue.match(/name="([^"]+)"/i)) {
+ part.name = m[1];
+ }
+
+ if (m = headerValue.match(/filename="([^"]+)"/i)) {
+ part.filename = m[1].substr(m[1].lastIndexOf('\\') + 1);
+ }
+ } else if (headerField == 'content-type') {
+ part.mime = headerValue;
+ }
+
+ headerField = '';
+ headerValue = '';
+ };
+
+ parser.initWithBoundary(boundary);
+
+ parser.onPartBegin = function() {
+ part = new EventEmitter();
+ part.headers = {};
+ part.name = null;
+ part.filename = null;
+ part.mime = null;
+ };
+
+ parser.onHeaderField = function(b, start, end) {
+ if (headerValue) {
+ addHeader();
+ }
+ headerField += b.toString(self.encoding, start, end);
+ };
+
+ parser.onHeaderValue = function(b, start, end) {
+ headerValue += b.toString(self.encoding, start, end);
+ };
+
+ parser.onPartData = function(b, start, end) {
+ if (headerValue) {
+ addHeader();
+ self.onPart(part);
+ }
+
+ part.emit('data', b.slice(start, end));
+ };
+
+ parser.onPartEnd = function() {
+ part.emit('end');
+ };
+
+ parser.onEnd = function() {
+ self.ended = true;
+ self._maybeEnd();
+ };
+
+ this._parser = parser;
+};
+
+IncomingForm.prototype._initUrlencoded = function() {
+ this.type = 'urlencoded';
+
+ var parser = new QuerystringParser()
+ , self = this;
+
+ parser.onField = function(key, val) {
+ self.emit('field', key, val);
+ };
+
+ parser.onEnd = function() {
+ self.ended = true;
+ self._maybeEnd();
+ };
+
+ this._parser = parser;
+};
+
+IncomingForm.prototype._uploadPath = function(filename) {
+ var name = '';
+ for (i = 0; i < 32; i++) {
+ name += Math.floor(Math.random() * 16).toString(16);
+ }
+
+ if (this.keepExtensions) {
+ name += path.extname(filename);
+ }
+
+ return path.join(this.uploadDir, name);
+};
+
+IncomingForm.prototype._maybeEnd = function() {
+ if (!this.ended || this._flushing) {
+ return;
+ }
+
+ this.emit('end');
+};
1  support/formidable/lib/formidable/index.js
@@ -0,0 +1 @@
+module.exports = require('./formidable');
311 support/formidable/lib/formidable/multipart_parser.js
@@ -0,0 +1,311 @@
+var Buffer = require('buffer').Buffer
+ , s = 0
+ , S =
+ { PARSER_UNINITIALIZED: s++
+ , START: s++
+ , START_BOUNDARY: s++
+ , HEADER_FIELD_START: s++
+ , HEADER_FIELD: s++
+ , HEADER_VALUE_START: s++
+ , HEADER_VALUE: s++
+ , HEADER_VALUE_ALMOST_DONE: s++
+ , HEADERS_ALMOST_DONE: s++
+ , PART_DATA_START: s++
+ , PART_DATA: s++
+ , PART_END: s++
+ , END: s++
+ }
+
+ , f = 1
+ , F =
+ { PART_BOUNDARY: f
+ , LAST_BOUNDARY: f *= 2
+ }
+
+ , LF = 10
+ , CR = 13
+ , SPACE = 32
+ , HYPHEN = 45
+ , COLON = 58
+ , A = 97
+ , Z = 122
+
+ , lower = function(c) {
+ return c | 0x20;
+ };
+
+for (var s in S) {
+ exports[s] = S[s];
+}
+
+function MultipartParser() {
+ this.boundary = null;
+ this.boundaryChars = null;
+ this.lookbehind = null;
+ this.state = S.PARSER_UNINITIALIZED;
+
+ this.index = null;
+ this.flags = 0;
+};
+exports.MultipartParser = MultipartParser;
+
+MultipartParser.prototype.initWithBoundary = function(str) {
+ this.boundary = new Buffer(str.length+4);
+ this.boundary.write('\r\n--', 'ascii', 0);
+ this.boundary.write(str, 'ascii', 4);
+ this.lookbehind = new Buffer(this.boundary.length+8);
+ this.state = S.START;
+
+ this.boundaryChars = {};
+ for (var i = 0; i < this.boundary.length; i++) {
+ this.boundaryChars[this.boundary[i]] = true;
+ }
+};
+
+MultipartParser.prototype.write = function(buffer) {
+ var self = this
+ , i = 0
+ , len = buffer.length
+ , prevIndex = this.index
+ , index = this.index
+ , state = this.state
+ , flags = this.flags
+ , lookbehind = this.lookbehind
+ , boundary = this.boundary
+ , boundaryChars = this.boundaryChars
+ , boundaryLength = this.boundary.length
+ , boundaryEnd = boundaryLength - 1
+ , bufferLength = buffer.length
+ , c
+ , cl
+
+ , mark = function(name) {
+ self[name+'Mark'] = i;
+ }
+ , clear = function(name) {
+ delete self[name+'Mark'];
+ }
+ , callback = function(name, buffer, start, end) {
+ if (start !== undefined && start === end) {
+ return;
+ }
+
+ var callbackSymbol = 'on'+name.substr(0, 1).toUpperCase()+name.substr(1);
+ if (callbackSymbol in self) {
+ self[callbackSymbol](buffer, start, end);
+ }
+ }
+ , dataCallback = function(name, clear) {
+ var markSymbol = name+'Mark';
+ if (!(markSymbol in self)) {
+ return;
+ }
+
+ if (!clear) {
+ callback(name, buffer, self[markSymbol], buffer.length);
+ self[markSymbol] = 0;
+ } else {
+ callback(name, buffer, self[markSymbol], i);
+ delete self[markSymbol];
+ }
+ };
+
+ for (i = 0; i < len; i++) {
+ c = buffer[i];
+ switch (state) {
+ case S.PARSER_UNINITIALIZED:
+ return i;
+ case S.START:
+ index = 0;
+ state = S.START_BOUNDARY;
+ case S.START_BOUNDARY:
+ if (index == boundary.length - 2) {
+ if (c != CR) {
+ return i;
+ }
+ index++;
+ break;
+ } else if (index - 1 == boundary.length - 2) {
+ if (c != LF) {
+ return i;
+ }
+ index = 0;
+ callback('partBegin');
+ state = S.HEADER_FIELD_START;
+ break;
+ }
+
+ if (c != boundary[index+2]) {
+ return i;
+ }
+ index++;
+ break;
+ case S.HEADER_FIELD_START:
+ state = S.HEADER_FIELD;
+ mark('headerField');
+ index = 0;
+ case S.HEADER_FIELD:
+ if (c == CR) {
+ clear('headerField');
+ state = S.HEADERS_ALMOST_DONE;
+ break;
+ }
+
+ index++;
+ if (c == HYPHEN) {
+ break;
+ }
+
+ if (c == COLON) {
+ if (index == 1) {
+ // empty header field
+ return i;
+ }
+ dataCallback('headerField', true);
+ state = S.HEADER_VALUE_START;
+ break;
+ }
+
+ cl = lower(c);
+ if (cl < A || cl > Z) {
+ return i;
+ }
+ break;
+ case S.HEADER_VALUE_START:
+ if (c == SPACE) {
+ break;
+ }
+
+ mark('headerValue');
+ state = S.HEADER_VALUE;
+ case S.HEADER_VALUE:
+ if (c == CR) {
+ dataCallback('headerValue', true);
+ state = S.HEADER_VALUE_ALMOST_DONE;
+ }
+ break;
+ case S.HEADER_VALUE_ALMOST_DONE:
+ if (c != LF) {
+ return i;
+ }
+ state = S.HEADER_FIELD_START;
+ break;
+ case S.HEADERS_ALMOST_DONE:
+ if (c != LF) {
+ return i;
+ }
+
+ state = S.PART_DATA_START;
+ break;
+ case S.PART_DATA_START:
+ state = S.PART_DATA
+ mark('partData');
+ case S.PART_DATA:
+ prevIndex = index;
+
+ if (index == 0) {
+ // boyer-moore derrived algorithm to safely skip non-boundary data
+ while (i + boundaryLength <= bufferLength) {
+ if (buffer[i + boundaryEnd] in boundaryChars) {
+ break;
+ }
+
+ i += boundaryLength;
+ }
+ c = buffer[i];
+ }
+
+ if (index < boundary.length) {
+ if (boundary[index] == c) {
+ if (index == 0) {
+ dataCallback('partData', true);
+ }
+ index++;
+ } else {
+ index = 0;
+ }
+ } else if (index == boundary.length) {
+ index++;
+ if (c == CR) {
+ // CR = part boundary
+ flags |= F.PART_BOUNDARY;
+ } else if (c == HYPHEN) {
+ // HYPHEN = end boundary
+ flags |= F.LAST_BOUNDARY;
+ } else {
+ index = 0;
+ }
+ } else if (index - 1 == boundary.length) {
+ if (flags & F.PART_BOUNDARY) {
+ index = 0;
+ if (c == LF) {
+ // unset the PART_BOUNDARY flag
+ flags &= ~F.PART_BOUNDARY;
+ callback('partEnd');
+ callback('partBegin');
+ state = S.HEADER_FIELD_START;
+ break;
+ }
+ } else if (flags & F.LAST_BOUNDARY) {
+ if (c == HYPHEN) {
+ index++;
+ } else {
+ index = 0;
+ }
+ } else {
+ index = 0;
+ }
+ } else if (index - 2 == boundary.length) {
+ if (c == CR) {
+ index++;
+ } else {
+ index = 0;
+ }
+ } else if (index - boundary.length == 3) {
+ index = 0;
+ if (c == LF) {
+ callback('partEnd');
+ callback('end');
+ state = S.END;
+ break;
+ }
+ }
+
+ if (index > 0) {
+ // when matching a possible boundary, keep a lookbehind reference
+ // in case it turns out to be a false lead
+ lookbehind[index-1] = c;
+ } else if (prevIndex > 0) {
+ // if our boundary turned out to be rubbish, the captured lookbehind
+ // belongs to partData
+ callback('partData', lookbehind, 0, prevIndex);
+ prevIndex = 0;
+ mark('partData');
+
+ // reconsider the current character even so it interrupted the sequence
+ // it could be the beginning of a new sequence
+ i--;
+ }
+
+ break;
+ default:
+ return i;
+ }
+ }
+
+ dataCallback('headerField');
+ dataCallback('headerValue');
+ dataCallback('partData');
+
+ this.index = index;
+ this.state = state;
+ this.flags = flags;
+
+ return len;
+};
+
+MultipartParser.prototype.end = function() {
+ if (this.state != S.END) {
+ return new Error('MultipartParser.end(): stream ended unexpectedly');
+ }
+};
25 support/formidable/lib/formidable/querystring_parser.js
@@ -0,0 +1,25 @@
+if (global.GENTLY) require = GENTLY.hijack(require);
+
+// This is a buffering parser, not quite as nice as the multipart one.
+// If I find time I'll rewrite this to be fully streaming as well
+var querystring = require('querystring');
+
+function QuerystringParser() {
+ this.buffer = '';
+};
+exports.QuerystringParser = QuerystringParser;
+
+QuerystringParser.prototype.write = function(buffer) {
+ this.buffer += buffer.toString('ascii');
+ return buffer.length;
+};
+
+QuerystringParser.prototype.end = function() {
+ var fields = querystring.parse(this.buffer);
+ for (var field in fields) {
+ this.onField(field, fields[field]);
+ }
+ this.buffer = '';
+
+ this.onEnd();
+};
6 support/formidable/package.json
@@ -0,0 +1,6 @@
+{ "name" : "formidable"
+, "version": "0.9.3"
+, "dependencies": {"gently": ">=0.7.0"}
+, "directories" : { "lib" : "./lib/formidable" }
+, "main" : "./lib/formidable/index"
+}
61 support/formidable/test/common.js
@@ -0,0 +1,61 @@
+var path = require('path')
+ , fs = require('fs')
+ , sys = require('sys')
+ , timeout;
+
+require.paths.unshift(path.dirname(__dirname)+'/lib');
+
+try {
+ global.Gently = require('gently');
+} catch (e) {
+ throw new Error('this test suite requires node-gently');
+}
+
+global.GENTLY = new Gently();
+
+global.puts = sys.puts;
+global.p = function() {
+ sys.error(sys.inspect.apply(null, arguments));
+};
+global.assert = require('assert');
+global.TEST_PORT = 13532;
+global.TEST_FIXTURES = path.join(__dirname, 'fixture');
+global.TEST_TMP = path.join(__dirname, 'tmp');
+
+assert.timeout = function(ms) {
+ return setTimeout(function() {
+ timeout = 'timeout (after '+ms+' ms): ';
+ process.emit('exit');
+
+ throw new Error('timeout after '+ms+' ms');
+ }, ms);
+};
+
+assert.callbacks = function(callbacks) {
+ process.addListener('exit', function() {
+ for (var k in callbacks) {
+ assert.equal(0, callbacks[k], (timeout || '')+k+' count off by '+callbacks[k]);
+ }
+ });
+};
+
+assert.properties = function(obj, properties) {
+ properties.forEach(function(property) {
+ assert.ok(property in obj, 'has property: '+property);
+ });
+
+ for (var property in obj) {
+ if (!obj.hasOwnProperty(property)) {
+ continue;
+ }
+
+ if (typeof obj[property] == 'function') {
+ continue;
+ }
+
+ assert.ok(
+ properties.indexOf(property) > -1,
+ 'does not have property: '+property
+ );
+ }
+};
47 support/formidable/test/fixture/multipart.js
@@ -0,0 +1,47 @@
+var puts = require('sys').puts;
+var p = require('sys').p;
+
+exports['rfc1867'] =
+ { boundary: 'AaB03x'
+ , raw:
+ '--AaB03x\r\n'+
+ 'content-disposition: form-data; name="field1"\r\n'+
+ '\r\n'+
+ 'Joe Blow\r\nalmost tricked you!\r\n'+
+ '--AaB03x\r\n'+
+ 'content-disposition: form-data; name="pics"; filename="file1.txt"\r\n'+
+ 'Content-Type: text/plain\r\n'+
+ '\r\n'+
+ '... contents of file1.txt ...\r\r\n'+
+ '--AaB03x--\r\n'
+ , parts:
+ [ { headers:
+ { 'content-disposition': 'form-data; name="field1"'
+ }
+ , data: 'Joe Blow\r\nalmost tricked you!'
+ }
+ , { headers:
+ { 'content-disposition': 'form-data; name="pics"; filename="file1.txt"'
+ , 'Content-Type': 'text/plain'
+ }
+ , data: '... contents of file1.txt ...\r'
+ }
+ ]
+ };
+
+exports['emptyHeader'] =
+ { boundary: 'AaB03x'
+ , raw:
+ '--AaB03x\r\n'+
+ 'content-disposition: form-data; name="field1"\r\n'+
+ ': foo\r\n'+
+ '\r\n'+
+ 'Joe Blow\r\nalmost tricked you!\r\n'+
+ '--AaB03x\r\n'+
+ 'content-disposition: form-data; name="pics"; filename="file1.txt"\r\n'+
+ 'Content-Type: text/plain\r\n'+
+ '\r\n'+
+ '... contents of file1.txt ...\r\r\n'+
+ '--AaB03x--\r\n'
+ , expectError: true
+ };
86 support/formidable/test/integration/test-multipart-parser.js
@@ -0,0 +1,86 @@
+require('../common');
+var CHUNK_LENGTH = 10
+ , multipartParser = require('formidable/multipart_parser')
+ , MultipartParser = multipartParser.MultipartParser
+ , parser = new MultipartParser()
+ , fixtures = require('../fixture/multipart')
+ , Buffer = require('buffer').Buffer;
+
+Object.keys(fixtures).forEach(function(name) {
+ var fixture = fixtures[name]
+ , buffer = new Buffer(Buffer.byteLength(fixture.raw, 'binary'))
+ , offset = 0
+ , chunk
+ , nparsed
+
+ , parts = []
+ , part = null
+ , headerField = null
+ , headerValue = null
+ , endCalled = false;
+
+ parser.initWithBoundary(fixture.boundary);
+ parser.onPartBegin = function() {
+ part = {headers: {}, data: ''};
+ parts.push(part);
+ headerField = '';
+ headerValue = '';
+ };
+
+ parser.onHeaderField = function(b, start, end) {
+ var str = b.toString('ascii', start, end);
+ if (headerValue) {
+ part.headers[headerField] = headerValue;
+ headerField = '';
+ headerValue = '';
+ }
+ headerField += str;
+ };
+
+ parser.onHeaderValue = function(b, start, end) {
+ var str = b.toString('ascii', start, end);
+ headerValue += str;
+ }
+
+ parser.onPartData = function(b, start, end) {
+ var str = b.toString('ascii', start, end);
+ if (headerField) {
+ part.headers[headerField] = headerValue;
+ headerValue = '';
+ headerField = '';
+ }
+ part.data += b.binarySlice(start, end);
+ }
+
+ parser.onEnd = function() {
+ endCalled = true;
+ }
+
+ buffer.write(fixture.raw, 'binary', 0);
+
+ while (offset < buffer.length) {
+ if (offset + CHUNK_LENGTH < buffer.length) {
+ chunk = buffer.slice(offset, offset+CHUNK_LENGTH);
+ } else {
+ chunk = buffer.slice(offset, buffer.length);
+ }
+ offset = offset + CHUNK_LENGTH;
+
+ nparsed = parser.write(chunk);
+ if (nparsed != chunk.length) {
+ if (fixture.expectError) {
+ return;
+ }
+ puts('-- ERROR --');
+ p(chunk.toString('ascii'));
+ throw new Error(chunk.length+' bytes written, but only '+nparsed+' bytes parsed!');
+ }
+ }
+
+ if (fixture.expectError) {
+ throw new Error('expected parse error did not happen');
+ }
+
+ assert.ok(endCalled);
+ assert.deepEqual(parts, fixture.parts);
+});
614 support/formidable/test/simple/test-incoming-form.js
@@ -0,0 +1,614 @@
+require('../common');
+var MultipartParserStub = GENTLY.stub('./multipart_parser', 'MultipartParser')
+ , QuerystringParserStub = GENTLY.stub('./querystring_parser', 'QuerystringParser')
+ , EventEmitterStub = GENTLY.stub('events', 'EventEmitter')
+ , WriteStreamStub = GENTLY.stub('fs', 'WriteStream');
+
+var IncomingForm = require('formidable/incoming_form').IncomingForm
+ , events = require('events')
+ , fs = require('fs')
+ , path = require('path')
+ , Buffer = require('buffer').Buffer
+ , fixtures = require('../fixture/multipart')
+ , form
+ , gently;
+
+function test(test) {
+ gently = new Gently();
+ gently.expect(EventEmitterStub, 'call');
+ form = new IncomingForm();
+ test();
+ gently.verify(test.name);
+}
+
+test(function constructor() {
+ assert.strictEqual(form.error, null);
+ assert.strictEqual(form.ended, false);
+ assert.strictEqual(form.type, null);
+ assert.strictEqual(form.headers, null);
+ assert.strictEqual(form.keepExtensions, false);
+ assert.strictEqual(form.uploadDir, '/tmp');
+ assert.strictEqual(form.encoding, 'utf-8');
+ assert.strictEqual(form.bytesReceived, null);
+ assert.strictEqual(form.bytesExpected, null);
+ assert.strictEqual(form._parser, null);
+ assert.strictEqual(form._flushing, 0);
+ assert.ok(form instanceof EventEmitterStub);
+ assert.equal(form.constructor.name, 'IncomingForm');
+});
+
+test(function parse() {
+ var REQ = {headers: {}}
+ , emit = {};
+
+ gently.expect(form, 'writeHeaders', function(headers) {
+ assert.strictEqual(headers, REQ.headers);
+ });
+
+ var events = ['error', 'data', 'end'];
+ gently.expect(REQ, 'addListener', events.length, function(event, fn) {
+ assert.equal(event, events.shift());
+ emit[event] = fn;
+ return this;
+ });
+
+ form.parse(REQ);
+
+ (function testPause() {
+ gently.expect(REQ, 'pause');
+ assert.strictEqual(form.pause(), true);
+ })();
+
+ (function testResume() {
+ gently.expect(REQ, 'resume');
+ assert.strictEqual(form.resume(), true);
+ })();
+
+ (function testResumeException() {
+ var ERR = new Error('dasdsa');
+ gently.expect(REQ, 'resume', function() {
+ throw ERR;
+ });
+
+ gently.expect(form, '_error', function(err) {
+ assert.strictEqual(err, ERR);
+ });
+
+ assert.strictEqual(form.resume(), false);
+ })();
+
+ (function testEmitError() {
+ var ERR = new Error('something bad happened');
+ gently.expect(form, '_error',function(err) {
+ assert.strictEqual(err, ERR);
+ });
+ emit.error(ERR);
+ })();
+
+ (function testEmitData() {
+ var BUFFER = [1, 2, 3];
+ gently.expect(form, 'write', function(buffer) {
+ assert.strictEqual(buffer, BUFFER);
+ });
+ emit.data(BUFFER);
+ })();
+
+ (function testEmitEnd() {
+ form._parser = {};
+
+ (function testWithError() {
+ var ERR = new Error('haha');
+ gently.expect(form._parser, 'end', function() {
+ return ERR;
+ });
+
+ gently.expect(form, '_error', function(err) {
+ assert.strictEqual(err, ERR);
+ });
+
+ emit.end();
+ })();
+
+ (function testWithoutError() {
+ gently.expect(form._parser, 'end');
+ emit.end();
+ })();
+
+ (function testAfterError() {
+ form.error = true;
+ emit.end();
+ })();
+ })();
+
+
+ (function testWithCallback() {
+ gently.expect(EventEmitterStub, 'call');
+ var form = new IncomingForm()
+ , REQ = {headers: {}}
+ , parseCalled = 0;
+
+ gently.expect(form, 'writeHeaders');
+ gently.expect(REQ, 'addListener', 3, function() {
+ return this;
+ });
+
+ gently.expect(form, 'addListener', 4, function(event, fn) {
+ if (event == 'field') {
+ fn('field1', 'foo');
+ fn('field1', 'bar');
+ fn('field2', 'nice');
+ }
+
+ if (event == 'file') {
+ fn('file1', '1');
+ fn('file1', '2');
+ fn('file2', '3');
+ }
+
+ if (event == 'end') {
+ fn();
+ }
+ return this;
+ });
+
+ form.parse(REQ, gently.expect(function parseCbOk(err, fields, files) {
+ assert.deepEqual(fields, {field1: 'bar', field2: 'nice'});
+ assert.deepEqual(files, {file1: '2', file2: '3'});
+ }));
+
+ gently.expect(form, 'writeHeaders');
+ gently.expect(REQ, 'addListener', 3, function() {
+ return this;
+ });
+
+ var ERR = new Error('test');
+ gently.expect(form, 'addListener', 3, function(event, fn) {
+ if (event == 'field') {
+ fn('foo', 'bar');
+ }
+
+ if (event == 'error') {
+ fn(ERR);
+ gently.expect(form, 'addListener');
+ }
+ return this;
+ });
+
+ form.parse(REQ, gently.expect(function parseCbErr(err, fields, files) {
+ assert.strictEqual(err, ERR);
+ assert.deepEqual(fields, {foo: 'bar'});
+ }));
+ })();
+});
+
+test(function pause() {
+ assert.strictEqual(form.pause(), false);
+});
+
+test(function resume() {
+ assert.strictEqual(form.resume(), false);
+});
+
+
+test(function writeHeaders() {
+ var HEADERS = {};
+ gently.expect(form, '_parseContentLength');
+ gently.expect(form, '_parseContentType');
+
+ form.writeHeaders(HEADERS);
+ assert.strictEqual(form.headers, HEADERS);
+});
+
+test(function write() {
+ var parser = {}
+ , BUFFER = [1, 2, 3];
+
+ form._parser = parser;
+ form.bytesExpected = 523423;
+
+ (function testBasic() {
+ gently.expect(parser, 'write', function(buffer) {
+ assert.strictEqual(buffer, BUFFER);
+ return buffer.length;
+ });
+
+ gently.expect(form, 'emit', function(event, bytesReceived, bytesExpected) {
+ assert.equal(event, 'progress');
+ assert.equal(bytesReceived, BUFFER.length);
+ assert.equal(bytesExpected, form.bytesExpected);
+ });
+
+ assert.equal(form.write(BUFFER), BUFFER.length);
+ assert.equal(form.bytesReceived, BUFFER.length);
+ })();
+
+ (function testParserError() {
+ gently.expect(parser, 'write', function(buffer) {
+ assert.strictEqual(buffer, BUFFER);
+ return buffer.length - 1;
+ });
+
+ gently.expect(form, '_error', function(err) {
+ assert.ok(err.message.match(/parser error/i));
+ });
+
+ gently.expect(form, 'emit');
+
+ assert.equal(form.write(BUFFER), BUFFER.length - 1);
+ assert.equal(form.bytesReceived, BUFFER.length + BUFFER.length - 1);
+ })();
+
+ (function testUninitialized() {
+ delete form._parser;
+
+ gently.expect(form, '_error', function(err) {
+ assert.ok(err.message.match(/unintialized parser/i));
+ });
+ form.write(BUFFER);
+ })();
+});
+
+test(function parseContentType() {
+ var HEADERS = {};
+
+ form.headers = {'content-type': 'application/x-www-form-urlencoded'};
+ gently.expect(form, '_initUrlencoded');
+ form._parseContentType();
+
+ // accept anything that has 'urlencoded' in it
+ form.headers = {'content-type': 'broken-client/urlencoded-stupid'};
+ gently.expect(form, '_initUrlencoded');
+ form._parseContentType();
+
+ var BOUNDARY = '---------------------------57814261102167618332366269';
+ form.headers = {'content-type': 'multipart/form-data; boundary='+BOUNDARY};
+
+ gently.expect(form, '_initMultipart', function(boundary) {
+ assert.equal(boundary, BOUNDARY);
+ });
+ form._parseContentType();
+
+ (function testNoBoundary() {
+ form.headers = {'content-type': 'multipart/form-data'};
+
+ gently.expect(form, '_error', function(err) {
+ assert.ok(err.message.match(/no multipart boundary/i));
+ });
+ form._parseContentType();
+ })();
+
+ (function testNoContentType() {
+ form.headers = {};
+
+ gently.expect(form, '_error', function(err) {
+ assert.ok(err.message.match(/no content-type/i));
+ });
+ form._parseContentType();
+ })();
+
+ (function testUnknownContentType() {
+ form.headers = {'content-type': 'invalid'};
+
+ gently.expect(form, '_error', function(err) {
+ assert.ok(err.message.match(/unknown content-type/i));
+ });
+ form._parseContentType();
+ })();
+});
+
+test(function parseContentLength() {
+ var HEADERS = {};
+
+ form.headers = {};
+ form._parseContentLength();
+ assert.strictEqual(form.bytesExpected, null);
+
+ form.headers['content-length'] = '8';
+ form._parseContentLength();
+ assert.strictEqual(form.bytesReceived, 0);
+ assert.strictEqual(form.bytesExpected, 8);
+
+ // JS can be evil, lets make sure we are not
+ form.headers['content-length'] = '08';
+ form._parseContentLength();
+ assert.strictEqual(form.bytesExpected, 8);
+});
+
+test(function _initMultipart() {
+ var BOUNDARY = '123'
+ , PARSER;
+
+ gently.expect(MultipartParserStub, 'new', function() {
+ PARSER = this;
+ });
+
+ gently.expect(MultipartParserStub.prototype, 'initWithBoundary', function(boundary) {
+ assert.equal(boundary, BOUNDARY);
+ });
+
+ form._initMultipart(BOUNDARY);
+ assert.equal(form.type, 'multipart');
+ assert.strictEqual(form._parser, PARSER);
+
+ (function testRegularField() {
+ var PART;
+ gently.expect(EventEmitterStub, 'new', function() {
+ PART = this;
+ });
+
+ gently.expect(form, 'onPart', function(part) {
+ assert.strictEqual(part, PART);
+ assert.deepEqual
+ ( part.headers
+ , { 'content-disposition': 'form-data; name="field1"'
+ , 'foo': 'bar'
+ }
+ );
+ assert.equal(part.name, 'field1');
+
+ var strings = ['hello', ' world'];
+ gently.expect(part, 'emit', 2, function(event, b) {
+ assert.equal(event, 'data');
+ assert.equal(b.toString(), strings.shift());
+ });
+
+ gently.expect(part, 'emit', function(event, b) {
+ assert.equal(event, 'end');
+ });
+ });
+
+ PARSER.onPartBegin();
+ PARSER.onHeaderField(new Buffer('content-disposition'), 0, 10);
+ PARSER.onHeaderField(new Buffer('content-disposition'), 10, 19);
+ PARSER.onHeaderValue(new Buffer('form-data; name="field1"'), 0, 14);
+ PARSER.onHeaderValue(new Buffer('form-data; name="field1"'), 14, 24);
+ PARSER.onHeaderField(new Buffer('foo'), 0, 3);
+ PARSER.onHeaderValue(new Buffer('bar'), 0, 3);
+ PARSER.onPartData(new Buffer('hello world'), 0, 5);
+ PARSER.onPartData(new Buffer('hello world'), 5, 11);
+ PARSER.onPartEnd();
+ })();
+
+ (function testFileField() {
+ var PART;
+ gently.expect(EventEmitterStub, 'new', function() {
+ PART = this;
+ });
+
+ gently.expect(form, 'onPart', function(part) {
+ assert.deepEqual
+ ( part.headers
+ , { 'content-disposition': 'form-data; name="field2"; filename="C:\\Documents and Settings\\IE\\Must\\Die\\Sunset.jpg"'
+ , 'content-type': 'text/plain'
+ }
+ );
+ assert.equal(part.name, 'field2');
+ assert.equal(part.filename, 'Sunset.jpg');
+ assert.equal(part.mime, 'text/plain');
+
+ gently.expect(part, 'emit', function(event, b) {
+ assert.equal(event, 'data');
+ assert.equal(b.toString(), '... contents of file1.txt ...');
+ });
+
+ gently.expect(part, 'emit', function(event, b) {
+ assert.equal(event, 'end');
+ });
+ });
+
+ PARSER.onPartBegin();
+ PARSER.onHeaderField(new Buffer('content-disposition'), 0, 19);
+ PARSER.onHeaderValue(new Buffer('form-data; name="field2"; filename="C:\\Documents and Settings\\IE\\Must\\Die\\Sunset.jpg"'), 0, 85);
+ PARSER.onHeaderField(new Buffer('Content-Type'), 0, 12);
+ PARSER.onHeaderValue(new Buffer('text/plain'), 0, 10);
+ PARSER.onPartData(new Buffer('... contents of file1.txt ...'), 0, 29);
+ PARSER.onPartEnd();
+ })();
+
+ (function testEnd() {
+ gently.expect(form, '_maybeEnd');
+ PARSER.onEnd();
+ assert.ok(form.ended);
+ })();
+});
+
+test(function _initUrlencoded() {
+ var PARSER;
+
+ gently.expect(QuerystringParserStub, 'new', function() {
+ PARSER = this;
+ });
+
+ form._initUrlencoded();
+ assert.equal(form.type, 'urlencoded');
+ assert.strictEqual(form._parser, PARSER);
+
+ (function testOnField() {
+ var KEY = 'KEY', VAL = 'VAL';
+ gently.expect(form, 'emit', function(field, key, val) {
+ assert.equal(field, 'field');
+ assert.equal(key, KEY);
+ assert.equal(val, VAL);
+ });
+
+ PARSER.onField(KEY, VAL);
+ })();
+
+ (function testOnEnd() {
+ gently.expect(form, '_maybeEnd');
+
+ PARSER.onEnd();
+ assert.equal(form.ended, true);
+ })();
+});
+
+test(function _error() {
+ var ERR = new Error('bla');
+
+ gently.expect(form, 'pause');
+ gently.expect(form, 'emit', function(event, err) {
+ assert.equal(event, 'error');
+ assert.strictEqual(err, ERR);
+ });
+
+ form._error(ERR);
+ assert.strictEqual(form.error, ERR);
+
+ // make sure _error only does its thing once
+ form._error(ERR);
+});
+
+test(function onPart() {
+ var PART = {};
+ gently.expect(form, 'handlePart', function(part) {
+ assert.strictEqual(part, PART);
+ });
+
+ form.onPart(PART);
+});
+
+test(function handlePart() {
+ (function testUtf8Field() {
+ var PART = new events.EventEmitter();
+ PART.name = 'my_field';
+
+ gently.expect(form, 'emit', function(event, field, value) {
+ assert.equal(event, 'field');
+ assert.equal(field, 'my_field');
+ assert.equal(value, 'hello world: €');
+ });
+
+ form.handlePart(PART);
+ PART.emit('data', new Buffer('hello'));
+ PART.emit('data', new Buffer(' world: '));
+ PART.emit('data', new Buffer([0xE2]));
+ PART.emit('data', new Buffer([0x82, 0xAC]));
+ PART.emit('end');
+ })();
+
+ (function testBinaryField() {
+ var PART = new events.EventEmitter();
+ PART.name = 'my_field2';
+
+ gently.expect(form, 'emit', function(event, field, value) {
+ assert.equal(event, 'field');
+ assert.equal(field, 'my_field2');
+ assert.equal(value, 'hello world: '+new Buffer([0xE2, 0x82, 0xAC]).toString('binary'));
+ });
+
+ form.encoding = 'binary';
+ form.handlePart(PART);
+ PART.emit('data', new Buffer('hello'));
+ PART.emit('data', new Buffer(' world: '));
+ PART.emit('data', new Buffer([0xE2]));
+ PART.emit('data', new Buffer([0x82, 0xAC]));
+ PART.emit('end');
+ })();
+
+ (function testFilePart() {
+ var PART = new events.EventEmitter()
+ , FILE = new events.EventEmitter()
+ , PATH = '/foo/bar';
+
+ PART.name = 'my_file';
+ PART.filename = 'sweet.txt';
+ PART.mime = 'sweet.txt';
+
+ gently.expect(form, '_uploadPath', function(filename) {
+ assert.equal(filename, PART.filename);
+ return PATH;
+ });
+
+ gently.expect(WriteStreamStub, 'new', function(path) {
+ assert.equal(path, PATH);
+ FILE = this;
+ });
+
+ form.handlePart(PART);
+ assert.equal(form._flushing, 1);
+
+ var BUFFER;
+ gently.expect(form, 'pause');
+ gently.expect(FILE, 'write', function(buffer, cb) {
+ assert.strictEqual(buffer, BUFFER);
+ gently.expect(form, 'resume');
+ // @todo handle cb(new Err)
+ cb();
+ });
+
+ PART.emit('data', BUFFER = new Buffer('test'));
+
+ gently.expect(FILE, 'end', function(cb) {
+ gently.expect(form, 'emit', function(event, field, file) {
+ assert.equal(event, 'file');
+ assert.deepEqual
+ ( file
+ , { path: FILE.path
+ , filename: PART.filename
+ , mime: PART.mime
+ }
+ );
+ });
+
+ gently.expect(form, '_maybeEnd');
+
+ cb();
+ assert.equal(form._flushing, 0);
+ });
+
+ PART.emit('end');
+ })();
+});
+
+test(function _uploadPath() {
+ (function testUniqueId() {
+ var UUID_A, UUID_B;
+ gently.expect(GENTLY.hijacked.path, 'join', function(uploadDir, uuid) {
+ assert.equal(uploadDir, form.uploadDir);
+ UUID_A = uuid;
+ });
+ form._uploadPath();
+
+ gently.expect(GENTLY.hijacked.path, 'join', function(uploadDir, uuid) {
+ UUID_B = uuid;
+ });
+ form._uploadPath();
+
+ assert.notEqual(UUID_A, UUID_B);
+ })();
+
+ (function testFileExtension() {
+ form.keepExtensions = true;
+ var FILENAME = 'foo.jpg'
+ , EXT = '.bar';
+
+ gently.expect(GENTLY.hijacked.path, 'extname', function(filename) {
+ assert.equal(filename, FILENAME);
+ gently.restore(path, 'extname');
+
+ return EXT;
+ });
+
+ gently.expect(GENTLY.hijacked.path, 'join', function(uploadDir, name) {
+ assert.equal(path.extname(name), EXT);
+ });
+ form._uploadPath(FILENAME);
+ })();
+});
+
+test(function _maybeEnd() {
+ gently.expect(form, 'emit', 0);
+ form._maybeEnd();
+
+ form.ended = true;
+ form._flushing = 1;
+ form._maybeEnd();
+
+ gently.expect(form, 'emit', function(event) {
+ assert.equal(event, 'end');
+ });
+
+ form.ended = true;
+ form._flushing = 0;
+ form._maybeEnd();
+});
50 support/formidable/test/simple/test-multipart-parser.js
@@ -0,0 +1,50 @@
+require('../common');
+var multipartParser = require('formidable/multipart_parser')
+ , MultipartParser = multipartParser.MultipartParser
+ , events = require('events')
+ , Buffer = require('buffer').Buffer
+ , parser;
+
+function test(test) {
+ parser = new MultipartParser();
+ test();
+}
+
+test(function constructor() {
+ assert.equal(parser.boundary, null);
+ assert.equal(parser.state, 0);
+ assert.equal(parser.flags, 0);
+ assert.equal(parser.boundaryChars, null);
+ assert.equal(parser.index, null);
+ assert.equal(parser.lookbehind, null);
+ assert.equal(parser.constructor.name, 'MultipartParser');
+});
+
+test(function initWithBoundary() {
+ var boundary = 'abc';
+ parser.initWithBoundary(boundary);
+ assert.deepEqual(Array.prototype.slice.call(parser.boundary), [13, 10, 45, 45, 97, 98, 99]);
+ assert.equal(parser.state, multipartParser.START);
+
+ assert.deepEqual(parser.boundaryChars, {10: true, 13: true, 45: true, 97: true, 98: true, 99: true});
+});
+
+test(function parserError() {
+ var boundary = 'abc'
+ , buffer = new Buffer(5);
+
+ parser.initWithBoundary(boundary);
+ buffer.write('--ad', 'ascii', 0);
+ assert.equal(parser.write(buffer), 3);
+});
+
+test(function end() {
+ (function testError() {
+ assert.equal(parser.end().message, 'MultipartParser.end(): stream ended unexpectedly');
+ })();
+
+ (function testRegular() {
+ parser.state = multipartParser.END;
+ assert.strictEqual(parser.end(), undefined);
+ })();
+});
45 support/formidable/test/simple/test-querystring-parser.js
@@ -0,0 +1,45 @@
+require('../common');
+var QuerystringParser = require('formidable/querystring_parser').QuerystringParser
+ , Buffer = require('buffer').Buffer
+ , gently
+ , parser;
+
+function test(test) {
+ gently = new Gently();
+ parser = new QuerystringParser();
+ test();
+ gently.verify(test.name);
+}
+
+test(function constructor() {
+ assert.equal(parser.buffer, '');
+ assert.equal(parser.constructor.name, 'QuerystringParser');
+});
+
+test(function write() {
+ var a = new Buffer('a=1');
+ assert.equal(parser.write(a), a.length);
+
+ var b = new Buffer('&b=2');
+ parser.write(b);
+ assert.equal(parser.buffer, a + b);
+});
+
+test(function end() {
+ var FIELDS = {a: ['b', {c: 'd'}], e: 'f'};
+
+ gently.expect(GENTLY.hijacked.querystring, 'parse', function(str) {
+ assert.equal(str, parser.buffer);
+ return FIELDS;
+ });
+
+ gently.expect(parser, 'onField', Object.keys(FIELDS).length, function(key, val) {
+ assert.deepEqual(FIELDS[key], val);
+ });
+
+ gently.expect(parser, 'onEnd');
+
+ parser.buffer = 'my buffer';
+ parser.end();
+ assert.equal(parser.buffer, '');
+});
6 test/multipart.test.js
@@ -0,0 +1,6 @@
+
+/**
+ * Module dependencies.
+ */
+
+var multipart = require('./../index');
Please sign in to comment.
Something went wrong with that request. Please try again.