From 77145ba22945892a38be53109ed95e04709bfe8e Mon Sep 17 00:00:00 2001 From: Ryan Dahl Date: Sun, 7 Feb 2010 16:51:20 -0800 Subject: [PATCH 01/15] Throw errors from server.listen() --- src/node_net.cc | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/node_net.cc b/src/node_net.cc index 5b39ae23bd..dad2906d64 100644 --- a/src/node_net.cc +++ b/src/node_net.cc @@ -821,6 +821,14 @@ Handle Server::Listen(const Arguments& args) { if (address_list) freeaddrinfo(address_list); + if (server->server_.errorno) { + Local e = Exception::Error( + String::NewSymbol(strerror(server->server_.errorno))); + Local obj = e->ToObject(); + obj->Set(String::NewSymbol("errno"), Integer::New(server->server_.errorno)); + return ThrowException(e); + } + return Undefined(); } From 76c1805c679f7abb65e58c4ffe7fb52ecf666239 Mon Sep 17 00:00:00 2001 From: Ryan Dahl Date: Sun, 7 Feb 2010 17:18:28 -0800 Subject: [PATCH 02/15] Add posix.truncate() --- doc/api.txt | 5 +++++ src/node.js | 10 ++++++++++ src/node_file.cc | 21 +++++++++++++++++++++ 3 files changed, 36 insertions(+) diff --git a/doc/api.txt b/doc/api.txt index 12e61b02a6..757ca9c8fd 100644 --- a/doc/api.txt +++ b/doc/api.txt @@ -653,6 +653,11 @@ sys.puts("stats: " + JSON.stringify(stats)); - on success: no parameters. - on error: no parameters. ++posix.truncate(fd, len)+ :: + See ftruncate(2). + - on success: no parameters. + - on error: no parameters. + +posix.stat(path)+ :: See stat(2). diff --git a/src/node.js b/src/node.js index 76ac289860..d65b8be1c7 100644 --- a/src/node.js +++ b/src/node.js @@ -571,6 +571,16 @@ var posixModule = createInternalModule("posix", function (exports) { return process.fs.rename(oldPath, newPath); }; + exports.truncate = function (fd, len) { + var promise = new events.Promise(); + process.fs.truncate(fd, len, callback(promise)); + return promise; + }; + + exports.truncateSync = function (fd, len) { + return process.fs.truncate(fd, len); + }; + exports.rmdir = function (path) { var promise = new events.Promise(); process.fs.rmdir(path, callback(promise)); diff --git a/src/node_file.cc b/src/node_file.cc index 522903fd1a..153de7516d 100644 --- a/src/node_file.cc +++ b/src/node_file.cc @@ -53,6 +53,7 @@ static int After(eio_req *req) { case EIO_UNLINK: case EIO_RMDIR: case EIO_MKDIR: + case EIO_FTRUNCATE: argc = 0; break; @@ -202,6 +203,25 @@ static Handle Rename(const Arguments& args) { } } +static Handle Truncate(const Arguments& args) { + HandleScope scope; + + if (args.Length() < 2 || !args[0]->IsInt32()) { + return THROW_BAD_ARGS; + } + + int fd = args[0]->Int32Value(); + off_t len = args[1]->Uint32Value(); + + if (args[2]->IsFunction()) { + ASYNC_CALL(ftruncate, args[2], fd, len) + } else { + int ret = ftruncate(fd, len); + if (ret != 0) return ThrowException(errno_exception(errno)); + return Undefined(); + } +} + static Handle Unlink(const Arguments& args) { HandleScope scope; @@ -412,6 +432,7 @@ void File::Initialize(Handle target) { NODE_SET_METHOD(target, "open", Open); NODE_SET_METHOD(target, "read", Read); NODE_SET_METHOD(target, "rename", Rename); + NODE_SET_METHOD(target, "truncate", Truncate); NODE_SET_METHOD(target, "rmdir", RMDir); NODE_SET_METHOD(target, "mkdir", MKDir); NODE_SET_METHOD(target, "sendfile", SendFile); From a695065305b710044569e0ee24b15b932fd2bd67 Mon Sep 17 00:00:00 2001 From: Ryan Dahl Date: Sun, 7 Feb 2010 18:05:58 -0800 Subject: [PATCH 03/15] Add failing test for stdout flush on exit --- test/mjsunit/fixtures/print-chars.js | 12 ++++++++ test/mjsunit/test-readdir.js | 1 + test/mjsunit/test-stdout-flush.js | 44 ++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+) create mode 100644 test/mjsunit/fixtures/print-chars.js create mode 100644 test/mjsunit/test-stdout-flush.js diff --git a/test/mjsunit/fixtures/print-chars.js b/test/mjsunit/fixtures/print-chars.js new file mode 100644 index 0000000000..c4202b6036 --- /dev/null +++ b/test/mjsunit/fixtures/print-chars.js @@ -0,0 +1,12 @@ +process.mixin(require("../common")); + +var n = parseInt(process.argv[2]); + +var s = ""; +for (var i = 0; i < n-1; i++) { + s += 'c'; +} + +puts(s); // \n is the nth char. + +process.exit(0); diff --git a/test/mjsunit/test-readdir.js b/test/mjsunit/test-readdir.js index 766dc6ef46..8cc0ee8c93 100644 --- a/test/mjsunit/test-readdir.js +++ b/test/mjsunit/test-readdir.js @@ -13,6 +13,7 @@ promise.addCallback(function (files) { , 'echo.js' , 'multipart.js' , 'nested-index' + , 'print-chars.js' , 'test_ca.pem' , 'test_cert.pem' , 'test_key.pem' diff --git a/test/mjsunit/test-stdout-flush.js b/test/mjsunit/test-stdout-flush.js new file mode 100644 index 0000000000..52d9633641 --- /dev/null +++ b/test/mjsunit/test-stdout-flush.js @@ -0,0 +1,44 @@ +process.mixin(require("./common")); + +var sub = path.join(fixturesDir, 'print-chars.js'); + +completedTests = 0; + +function test (n, cb) { + var child = process.createChildProcess(process.argv[0], [sub, n]); + + var count = 0; + + child.addListener("error", function (data){ + if (data) { + puts("parent stderr: " + data); + assert.ok(false); + } + }); + + child.addListener("output", function (data){ + if (data) { + count += data.length; + } + }); + + child.addListener("exit", function (data) { + assert.equal(n, count); + puts(n + " okay"); + completedTests++; + if (cb) cb(); + }); +} + + + +test(5000, function () { + test(50000, function () { + test(500000); + }); +}); + + +process.addListener('exit', function () { + assert.equal(3, completedTests); +}); From ae1fc497bbf026007c9d44ea425f1f060ade609f Mon Sep 17 00:00:00 2001 From: Ryan Dahl Date: Sun, 7 Feb 2010 21:56:36 -0800 Subject: [PATCH 04/15] Fix stdout flush issue http://github.com/ry/node/issues#issue/53 --- src/node.cc | 2 ++ src/node_stdio.cc | 13 ++++++++ src/node_stdio.h | 1 + test/mjsunit/fixtures/print-chars.js | 2 -- test/mjsunit/test-stdout-flush.js | 50 ++++++++++------------------ 5 files changed, 33 insertions(+), 35 deletions(-) diff --git a/src/node.cc b/src/node.cc index c7dac8e2e8..5726578fdb 100644 --- a/src/node.cc +++ b/src/node.cc @@ -1160,6 +1160,8 @@ int main(int argc, char *argv[]) { // so your next reading stop should be node::Load()! node::Load(argc, argv); + node::Stdio::Flush(); + #ifndef NDEBUG // Clean up. context.Dispose(); diff --git a/src/node_stdio.cc b/src/node_stdio.cc index 2b1da6c055..adcaff2557 100644 --- a/src/node_stdio.cc +++ b/src/node_stdio.cc @@ -197,6 +197,19 @@ Close (const Arguments& args) return Undefined(); } +void Stdio::Flush() { + if (stdout_fd >= 0) { + close(stdout_fd); + stdout_fd = -1; + } + + if (stdout_coupling) { + coupling_join(stdout_coupling); + coupling_destroy(stdout_coupling); + stdout_coupling = NULL; + } +} + void Stdio::Initialize (v8::Handle target) { diff --git a/src/node_stdio.h b/src/node_stdio.h index 11dd5aeddc..60fa2912fd 100644 --- a/src/node_stdio.h +++ b/src/node_stdio.h @@ -11,6 +11,7 @@ namespace node { class Stdio { public: static void Initialize (v8::Handle target); + static void Flush (); }; } // namespace node diff --git a/test/mjsunit/fixtures/print-chars.js b/test/mjsunit/fixtures/print-chars.js index c4202b6036..ba539ffb76 100644 --- a/test/mjsunit/fixtures/print-chars.js +++ b/test/mjsunit/fixtures/print-chars.js @@ -8,5 +8,3 @@ for (var i = 0; i < n-1; i++) { } puts(s); // \n is the nth char. - -process.exit(0); diff --git a/test/mjsunit/test-stdout-flush.js b/test/mjsunit/test-stdout-flush.js index 52d9633641..58c945bea9 100644 --- a/test/mjsunit/test-stdout-flush.js +++ b/test/mjsunit/test-stdout-flush.js @@ -2,43 +2,27 @@ process.mixin(require("./common")); var sub = path.join(fixturesDir, 'print-chars.js'); -completedTests = 0; +n = 100000; -function test (n, cb) { - var child = process.createChildProcess(process.argv[0], [sub, n]); +var child = process.createChildProcess(process.argv[0], [sub, n]); - var count = 0; +var count = 0; - child.addListener("error", function (data){ - if (data) { - puts("parent stderr: " + data); - assert.ok(false); - } - }); - - child.addListener("output", function (data){ - if (data) { - count += data.length; - } - }); - - child.addListener("exit", function (data) { - assert.equal(n, count); - puts(n + " okay"); - completedTests++; - if (cb) cb(); - }); -} - - - -test(5000, function () { - test(50000, function () { - test(500000); - }); +child.addListener("error", function (data){ + if (data) { + puts("parent stderr: " + data); + assert.ok(false); + } }); +child.addListener("output", function (data){ + if (data) { + count += data.length; + puts(count); + } +}); -process.addListener('exit', function () { - assert.equal(3, completedTests); +child.addListener("exit", function (data) { + assert.equal(n, count); + puts("okay"); }); From 6f92d8f3b078e93a65686655b05141ab456a9293 Mon Sep 17 00:00:00 2001 From: Ryan Dahl Date: Sun, 7 Feb 2010 21:59:56 -0800 Subject: [PATCH 05/15] Add handlescope and flush to process.reallyExit() --- src/node.cc | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/node.cc b/src/node.cc index 5726578fdb..493fad1066 100644 --- a/src/node.cc +++ b/src/node.cc @@ -465,11 +465,10 @@ static Handle Umask(const Arguments& args){ } v8::Handle Exit(const v8::Arguments& args) { - int r = 0; - if (args.Length() > 0) - r = args[0]->IntegerValue(); + HandleScope scope; fflush(stderr); - exit(r); + Stdio::Flush(); + exit(args[0]->IntegerValue()); return Undefined(); } From a38607605c084b6ecb38ee1315aa3f1ae40902ab Mon Sep 17 00:00:00 2001 From: Michael Carter Date: Sun, 7 Feb 2010 22:13:10 -0800 Subject: [PATCH 06/15] Add process.setuid(), getuid() --- doc/api.txt | 3 +++ src/node.cc | 30 ++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/doc/api.txt b/doc/api.txt index 757ca9c8fd..66befaab1f 100644 --- a/doc/api.txt +++ b/doc/api.txt @@ -136,6 +136,9 @@ success code 0. +process.cwd()+:: Returns the current working directory of the process. ++process.getuid(), process.setuid(id)+:: +Gets/sets the user identity of the process. (See setuid(2).) + +process.chdir(directory)+:: Changes the current working directory of the process. diff --git a/src/node.cc b/src/node.cc index 493fad1066..77b9700da5 100644 --- a/src/node.cc +++ b/src/node.cc @@ -9,6 +9,8 @@ #include #include #include /* dlopen(), dlsym() */ +#include +#include /* setuid, getuid */ #include #include @@ -464,6 +466,32 @@ static Handle Umask(const Arguments& args){ return scope.Close(Uint32::New(old)); } + +static Handle GetUid(const Arguments& args) { + HandleScope scope; + int uid = getuid(); + return scope.Close(Integer::New(uid)); +} + + +static Handle SetUid(const Arguments& args) { + HandleScope scope; + + if (args.Length() < 1) { + return ThrowException(Exception::Error( + String::New("setuid requires 1 argument"))); + } + + Local given_uid = args[0]->ToInteger(); + int uid = given_uid->Int32Value(); + int result; + if ((result = setuid(uid)) != 0) { + return ThrowException(Exception::Error(String::New(strerror(errno)))); + } + return Undefined(); +} + + v8::Handle Exit(const v8::Arguments& args) { HandleScope scope; fflush(stderr); @@ -953,6 +981,8 @@ static void Load(int argc, char *argv[]) { NODE_SET_METHOD(process, "reallyExit", Exit); NODE_SET_METHOD(process, "chdir", Chdir); NODE_SET_METHOD(process, "cwd", Cwd); + NODE_SET_METHOD(process, "getuid", GetUid); + NODE_SET_METHOD(process, "setuid", SetUid); NODE_SET_METHOD(process, "umask", Umask); NODE_SET_METHOD(process, "dlopen", DLOpen); NODE_SET_METHOD(process, "kill", Kill); From 4234ac53e26675492d39504a73a5672c24bbda97 Mon Sep 17 00:00:00 2001 From: Ryan Dahl Date: Mon, 8 Feb 2010 09:47:01 -0800 Subject: [PATCH 07/15] Use Google's jsmin.py so that Node can be used for evil --- LICENSE | 3 - tools/js2c.py | 3 +- tools/jsmin.py | 219 +------------------------------------------------ 3 files changed, 3 insertions(+), 222 deletions(-) mode change 100644 => 120000 tools/jsmin.py diff --git a/LICENSE b/LICENSE index db7a42b1f6..dfd91e7c4c 100644 --- a/LICENSE +++ b/LICENSE @@ -9,9 +9,6 @@ are: This code is copyrighted by Marc Alexander Lehmann. Both are dually licensed under MIT and GPL2. - - JSMin JavaScript minifier, located at tools/jsmin.py. This code is - copyrighted by Douglas Crockford and Baruch Even and has an MIT license. - - WAF build system, located at tools/waf. Copyrighted Thomas Nagy. Released under an MIT license. diff --git a/tools/js2c.py b/tools/js2c.py index 312626101b..fb38ece99b 100755 --- a/tools/js2c.py +++ b/tools/js2c.py @@ -49,7 +49,8 @@ def CompressScript(lines, do_jsmin): # If we're not expecting this code to be user visible, we can run it through # a more aggressive minifier. if do_jsmin: - return jsmin.jsmin(lines) + minifier = JavaScriptMinifier() + return minifier.JSMinify(lines) # Remove stuff from the source that we don't want to appear when # people print the source code using Function.prototype.toString(). diff --git a/tools/jsmin.py b/tools/jsmin.py deleted file mode 100644 index ae7581413a..0000000000 --- a/tools/jsmin.py +++ /dev/null @@ -1,218 +0,0 @@ -#!/usr/bin/python - -# This code is original from jsmin by Douglas Crockford, it was translated to -# Python by Baruch Even. The original code had the following copyright and -# license. -# -# /* jsmin.c -# 2007-05-22 -# -# Copyright (c) 2002 Douglas Crockford (www.crockford.com) -# -# 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 shall be used for Good, not Evil. -# -# 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. -# */ - -from StringIO import StringIO - -def jsmin(js): - ins = StringIO(js) - outs = StringIO() - JavascriptMinify().minify(ins, outs) - str = outs.getvalue() - if len(str) > 0 and str[0] == '\n': - str = str[1:] - return str - -def isAlphanum(c): - """return true if the character is a letter, digit, underscore, - dollar sign, or non-ASCII character. - """ - return ((c >= 'a' and c <= 'z') or (c >= '0' and c <= '9') or - (c >= 'A' and c <= 'Z') or c == '_' or c == '$' or c == '\\' or (c is not None and ord(c) > 126)); - -class UnterminatedComment(Exception): - pass - -class UnterminatedStringLiteral(Exception): - pass - -class UnterminatedRegularExpression(Exception): - pass - -class JavascriptMinify(object): - - def _outA(self): - self.outstream.write(self.theA) - def _outB(self): - self.outstream.write(self.theB) - - def _get(self): - """return the next character from stdin. Watch out for lookahead. If - the character is a control character, translate it to a space or - linefeed. - """ - c = self.theLookahead - self.theLookahead = None - if c == None: - c = self.instream.read(1) - if c >= ' ' or c == '\n': - return c - if c == '': # EOF - return '\000' - if c == '\r': - return '\n' - return ' ' - - def _peek(self): - self.theLookahead = self._get() - return self.theLookahead - - def _next(self): - """get the next character, excluding comments. peek() is used to see - if an unescaped '/' is followed by a '/' or '*'. - """ - c = self._get() - if c == '/' and self.theA != '\\': - p = self._peek() - if p == '/': - c = self._get() - while c > '\n': - c = self._get() - return c - if p == '*': - c = self._get() - while 1: - c = self._get() - if c == '*': - if self._peek() == '/': - self._get() - return ' ' - if c == '\000': - raise UnterminatedComment() - - return c - - def _action(self, action): - """do something! What you do is determined by the argument: - 1 Output A. Copy B to A. Get the next B. - 2 Copy B to A. Get the next B. (Delete A). - 3 Get the next B. (Delete B). - action treats a string as a single character. Wow! - action recognizes a regular expression if it is preceded by ( or , or =. - """ - if action <= 1: - self._outA() - - if action <= 2: - self.theA = self.theB - if self.theA == "'" or self.theA == '"': - while 1: - self._outA() - self.theA = self._get() - if self.theA == self.theB: - break - if self.theA <= '\n': - raise UnterminatedStringLiteral() - if self.theA == '\\': - self._outA() - self.theA = self._get() - - - if action <= 3: - self.theB = self._next() - if self.theB == '/' and (self.theA == '(' or self.theA == ',' or - self.theA == '=' or self.theA == ':' or - self.theA == '[' or self.theA == '?' or - self.theA == '!' or self.theA == '&' or - self.theA == '|' or self.theA == ';' or - self.theA == '{' or self.theA == '}' or - self.theA == '\n'): - self._outA() - self._outB() - while 1: - self.theA = self._get() - if self.theA == '/': - break - elif self.theA == '\\': - self._outA() - self.theA = self._get() - elif self.theA <= '\n': - raise UnterminatedRegularExpression() - self._outA() - self.theB = self._next() - - - def _jsmin(self): - """Copy the input to the output, deleting the characters which are - insignificant to JavaScript. Comments will be removed. Tabs will be - replaced with spaces. Carriage returns will be replaced with linefeeds. - Most spaces and linefeeds will be removed. - """ - self.theA = '\n' - self._action(3) - - while self.theA != '\000': - if self.theA == ' ': - if isAlphanum(self.theB): - self._action(1) - else: - self._action(2) - elif self.theA == '\n': - if self.theB in ['{', '[', '(', '+', '-']: - self._action(1) - elif self.theB == ' ': - self._action(3) - else: - if isAlphanum(self.theB): - self._action(1) - else: - self._action(2) - else: - if self.theB == ' ': - if isAlphanum(self.theA): - self._action(1) - else: - self._action(3) - elif self.theB == '\n': - if self.theA in ['}', ']', ')', '+', '-', '"', '\'']: - self._action(1) - else: - if isAlphanum(self.theA): - self._action(1) - else: - self._action(3) - else: - self._action(1) - - def minify(self, instream, outstream): - self.instream = instream - self.outstream = outstream - self.theA = '\n' - self.theB = None - self.theLookahead = None - - self._jsmin() - self.instream.close() - -if __name__ == '__main__': - import sys - jsm = JavascriptMinify() - jsm.minify(sys.stdin, sys.stdout) diff --git a/tools/jsmin.py b/tools/jsmin.py new file mode 120000 index 0000000000..76e4845a60 --- /dev/null +++ b/tools/jsmin.py @@ -0,0 +1 @@ +../deps/v8/tools/jsmin.py \ No newline at end of file From 3adcdfc2e1db7ce0adb017a92864d44424e99012 Mon Sep 17 00:00:00 2001 From: isaacs Date: Fri, 5 Feb 2010 14:26:44 -0800 Subject: [PATCH 08/15] A rewrite of multipart library. 1. Support streaming in the simple case. Put huge warnings around the buffering API. 2. Support nested multipart messages. 3. Support various kinds of multipart messages, including all the various absurdities of multipart email messages. 4. Error out on various invalid types of data that might come through. 5. Gracefully several edge cases that occur when the chunk size is smaller than the boundary length, or cuts into the \r\n, etc. 6. Provide both positive and negative tests. --- doc/api.txt | 181 +++++-- lib/multipart.js | 552 +++++++++++++++------- test/mjsunit/fixtures/multipart.js | 732 ++++++++++++++++++++++++++++- test/mjsunit/test-multipart.js | 218 +++++---- 4 files changed, 1345 insertions(+), 338 deletions(-) diff --git a/doc/api.txt b/doc/api.txt index 66befaab1f..89995e9449 100644 --- a/doc/api.txt +++ b/doc/api.txt @@ -1184,73 +1184,158 @@ After emitted no other events will be emitted on the response. === Multipart Parsing -A library to parse HTTP requests with +multipart/form-data+ is included with +A library to parse +multipart+ internet messages is included with Node. To use it, +require("multipart")+. -+multipart.parse(options)+ :: - - on success: Returns an object where each key holds the value of one part of - the stream. +options+ can either be an instance of - +http.ServerRequest+ or an object containing a "boundary" and - an optional "data" key. - - on error: Returns an instanceof Error object. Right now only the request - content-type / boundary option is checked. The stream data itself - is not validated. ++multipart.parse(message)+ :: + Returns a multipart.Stream wrapper around a streaming message. + The message must contain a `headers` member, and may be either an + HTTP request object or a JSGI-style request object with either a + forEachable or String body. + + + See the Stream class below. + ++multipart.cat(message)+ :: + Returns a promise. + - on success: Returns a multipart.Stream object representing the completed + message. The body of each part is saved on the `body` member. + - on error: Returns an instanceof Error object. This indicates + that the message was malformed in some way. + + + *Note*: This function saves the *entire* message into memory. As such, + it is ill-suited to parsing actual incoming messages from an HTTP request! + If a user uploads a very large file, then it may cause serious problems. + No checking is done to ensure that the file does not overload the memory. + Only use multipart.cat with known and trusted input! ==== +multipart.Stream+ -Here is an example for parsing a +multipart/form-data+ request: - ----------------------------------------- -var multipart = require("multipart"); -var stream = new multipart.Stream(options); -var parts = {}; - -stream.addListener("part", function (part) { - var buffer = ""; - - part.addListener("body", function(chunk) { - buffer = buffer + chunk; - }); - - part.addListener("complete", function() { - parts[part.name] = buffer; - }); -}); - -stream.addListener("complete", function() { - // The parts object now contains all parts and data -}); ----------------------------------------- +The multipart.Stream class is a streaming parser wrapped around a message. +The Stream also contains the properties described for the +part+ objects below, +and is a reference to the top-level message. +===== Events [cols="1,2,10",options="header"] |========================================================= |Event | Parameters | Notes -|+"part"+ | +part+ | Emitted when a new part is found in the stream. - +part+ is an instance of +multipart.Part+. +|+"partBegin"+ | +part+ | Emitted when a new part is found in the stream. + +part+ is a +part object+, described below. +|+"partEnd"+ | +part+ | Emitted when a part is done. +|+"body"+ | +chunk+ | Emitted when a chunk of the body is read. |+"complete"+ | | Emitted when the end of the stream is reached. +|+"error"+ | +error+ | Emitted when a parse error is encountered. This + indicates that the message is malformed. |========================================================= -+stream.bytesTotal+:: -The amount of bytes this stream is expected to have. +===== Properties -+stream.bytesReceived+:: -The amount of bytes received by this stream so far. ++stream.part+:: +The current part being processed. This is important, for instance, when responding +to the +body+ event. -==== +multipart.Part+ ++stream.isMultiPart+:: +True if the stream is a multipart message. Generally this will be true, but non-multipart +messages will behave the same as a multipart message with a single part, and +isMultiPart+ +will be set to +false+. -[cols="1,2,10",options="header"] -|========================================================= -|Event | Parameters | Notes -|+"body"+ | +chunk+ | Emitted when a chunk of body is read. -|+"complete"+ | | Emitted when the end of the part is reached. -|========================================================= ++stream.parts+:: +An array of the parts contained within the message. Each is a +part+ object. -+part.name+:: -The field name of this part. +===== Methods + ++stream.pause+:: +If the underlying message supports pause and resume, then this will pause the stream. + ++stream.resume+:: +If the underlying message supports pause and resume, then this will resume the paused stream. + +==== Part Objects + +As it parses the message, the Stream object will create +Part+ objects. + +===== Properties + ++part.parent+:: +The message that contains this part. + ++part.headers+:: +The headers object for this message. +part.filename+:: -The filename of this part. Only set for file uploads. +The filename, if specified in the +content-disposition+ or +content-type+ header. +For uploads, downloads, and attachments, this is the intended filename for the +attached file. + ++part.name+:: +The name, if specified in the +content-disposition+ or +content-type+ header. For ++multipart/form-data+ messages, this is the name of the field that was posted, and the +body specifies the value. + ++part.isMultiPart+:: +True if this part is a multipart message. + ++part.parts+:: +Array of children contained within a multipart message, or falsey. + ++part.boundary+:: +For multipart messages, this is the boundary that separates subparts. + ++part.type+:: +For multipart messages, this is the multipart type specified in the +content-type+ header. +For example, a message with +content-type: multipart/form-data+ will have a +type+ +property of +form-data+. + +==== Example + +Here is an example for parsing a +multipart/form-data+ request: + +---------------------------------------- +var multipart = require("multipart"), + sys = require("sys"), + http = require("http"); +http.createServer(function (req, res) { + var mp = multipart.parse(req), + fields = {}, + name, filename; + mp.addListener("error", function (er) { + res.sendHeader(400, {"content-type":"text/plain"}); + res.sendBody("You sent a bad message!\n"+er.message); + res.finish(); + }); + mp.addListener("partBegin", function (part) { + name = part.name; + filename = part.filename; + if (name) fields[name] = ""; + }); + mp.addListener("body", function (chunk) { + if (name) { + // just a demo. in reality, you'd probably + // want to sniff for base64 encoding, decode, + // and write the bytes to a file or something. + if (fields[name].length > 1024) return; + fields[name] += chunk; + } + }); + mp.addListener("complete", function () { + var response = "You posted: \n" + sys.inspect(fields); + res.sendHeader(200, { + "content-type" : "text/plain", + "content-length" : response.length + }); + res.sendBody(response); + res.finish(); + }) +}); +---------------------------------------- + +==== Nested Multipart Messages + +Nested multipart parsing is supported. The +stream.part+ object always refers +to the current part. If +part.isMultiPart+ is set, then that part is a +multipart message, which contains other parts. You can inspect its +parts+ +array to see the list of sub-parts, which may also be multipart, and contain +sub-parts. === TCP diff --git a/lib/multipart.js b/lib/multipart.js index dc2a8cea88..15fb0681b4 100644 --- a/lib/multipart.js +++ b/lib/multipart.js @@ -1,194 +1,408 @@ -var sys = require("sys"); -var events = require('events'); - -exports.parse = function(options) { - var promise = new events.Promise(); - - try { - var stream = new exports.Stream(options); - } catch (e) { - process.nextTick(function() { - promise.emitError(e); - }); - return promise; - } - - var parts = {}; - stream.addListener('part', function(part) { - var name = part.name; - var buffer = ''; - part.addListener('body', function(chunk) { - buffer = buffer + chunk; - }); +var sys = require("sys"), + events = require("events"), + wrapExpression = /^[ \t]+/, + multipartExpression = new RegExp( + "^multipart\/(" + + "mixed|rfc822|message|digest|alternative|" + + "related|report|signed|encrypted|form-data|" + + "x-mixed-replace|byteranges)", "i"), + boundaryExpression = /boundary=([^;]+)/i, + CR = "\r", + LF = "\n", + CRLF = CR+LF, + MAX_BUFFER_LENGTH = 16 * 1024, + + // parser states. + s = 0, + S_NEW_PART = s++, + S_HEADER = s++, + S_BODY = s++; + +exports.parse = parse; +exports.cat = cat; +exports.Stream = Stream; + +// Parse a streaming message to a stream. +// If the message has a "body" and no "addListener", then +// just take it in and write() the body. +function parse (message) { + return new Stream(message); +}; - part.addListener('complete', function() { - parts[name] = buffer; - }); +// WARNING: DONT EVER USE THE CAT FUNCTION IN PRODUCTION WEBSITES!! +// It works pretty great, and it's a nice test function. But if +// you use this function to parse an HTTP request from a live web +// site, then you're essentially giving the world permission to +// rack up as much memory usage as they can manage. This function +// buffers the whole message, which is very convenient, but also +// very much the wrong thing to do in most cases. +function cat (message) { + var p = new (events.Promise), + stream = parse(message); + stream.files = {}; + stream.fields = {}; + stream.addListener("partBegin", function (part) { + if (part.filename) stream.files[part.filename] = part; + if (part.name) stream.fields[part.name] = part; }); - - stream.addListener('complete', function() { - promise.emitSuccess(parts); + stream.addListener("body", function (chunk) { + stream.part.body = (stream.part.body || "") + chunk; }); - - return promise; -}; - -exports.Stream = function(options) { - events.EventEmitter.call(this); - - this.init(options); + stream.addListener("error", function (e) { p.emitError(e) }); + stream.addListener("complete", function () { p.emitSuccess(stream) }); + return p; }; -sys.inherits(exports.Stream, events.EventEmitter); - - -var proto = exports.Stream.prototype; - -proto.init = function(options) { - this.buffer = ''; - this.bytesReceived = 0; - this.bytesTotal = 0; - this.part = null; - if ('headers' in options) { - var req = options, contentType = req.headers['content-type']; - if (!contentType) { - throw new Error('Content-Type header not set'); +// events: +// "partBegin", "partEnd", "body", "complete" +// everything emits on the Stream directly. +// the stream's "parts" object is a nested collection of the header objects +// check the stream's "part" member to know what it's currently chewin on. +// this.part.parent refers to that part's containing message (which may be +// the stream itself) +// child messages inherit their parent's headers +// A non-multipart message looks just like a multipart message with a +// single part. +function Stream (message) { + var isMultiPart = multipartHeaders(message, this), + w = isMultiPart ? writer(this) : simpleWriter(this), + e = ender(this); + if (message.addListener) { + message.addListener("body", w); + message.addListener("complete", e); + if (message.pause && message.resume) { + this._pause = message; } - - if (!contentType.match(/^multipart\/form-data/i)) { - throw new Error('Content-Type is not multipart: "'+contentType+'"'); + } else if (message.body) { + var self = this; + if (message.body.pause && message.body.resume) { + this._pause = message.body; } - - var boundary = contentType.match(/boundary=([^;]+)/i) - if (!boundary) { - throw new Error('No boundary in Content-Type header: "'+contentType+'"'); + if (message.body.addListener) { + message.body.addListener("data", w); + message.body.addListener("end", e); + } if (message.body.forEach) { + var p = message.body.forEach(w); + if (p && p.addCallback) p.addCallback(e); + else e(); + } else { + // just write a string. + w(message.body); + e(); } + } +}; +Stream.prototype = { + __proto__ : events.EventEmitter.prototype, + error : function (ex) { + this._error = ex; + this.emit("error", ex); + }, + pause : function () { + if (this._pause) return this._pause.pause(); + throw new Error("Unsupported"); + }, + resume : function () { + if (this._pause) return this._pause.resume(); + throw new Error("Unsupported"); + } +}; - this.boundary = '--'+boundary[1]; - this.bytesTotal = req.headers['content-length']; - - var self = this; - req - .addListener('body', function(chunk) { - self.write(chunk); - }) - .addListener('complete', function() { - self.emit('complete'); - }); - } else { - if (!options.boundary) { - throw new Error('No boundary option given'); +// check the headers of the message. If it wants to be multipart, +// then we'll be returning true. Regardless, if supplied, then +// stream will get a headers object that inherits from message's. +// If no stream object is supplied, then this function just inspects +// the message's headers for multipartness, and modifies the message +// directly. This divergence is so that we can avoid modifying +// the original message when we want a wrapper, but still have the +// info available when it's one of our own objects. +function multipartHeaders (message, stream) { + var field, val, contentType, contentDisposition = ""; + if (stream) stream.headers = {}; + for (var h in message.headers) if (message.headers.hasOwnProperty(h)) { + val = message.headers[h]; + field = h.toLowerCase(); + if (stream) stream.headers[field] = val; + if (field === "content-type") { + contentType = val; + } else if (field === "content-disposition") { + contentDisposition = val; } - - this.boundary = options.boundary; - this.write(options.data || ''); } -}; -proto.write = function(chunk) { - this.bytesReceived = this.bytesReceived + chunk.length; - this.buffer = this.buffer + chunk; - - while (this.buffer.length) { - var offset = this.buffer.indexOf(this.boundary); - - if (offset === 0) { - this.buffer = this.buffer.substr(offset + this.boundary.length + 2); - } else if (offset == -1) { - if (this.buffer === "\r\n") { - this.buffer = ''; - } else { - this.part = (this.part || new Part(this)); - this.part.write(this.buffer); - this.buffer = []; + if (!Array.isArray(contentDisposition)) { + contentDisposition = contentDisposition.split(","); + } + contentDisposition = contentDisposition[contentDisposition.length - 1]; + + var mutate = (stream || message); + + // Name and filename can come along with either content-disposition + // or content-type. Well-behaved agents use CD rather than CT, + // but sadly not all agents are well-behaved. + [contentDisposition, contentType].forEach(function (h) { + if (!h) return; + var cd = h.split(/; */); + cd.shift(); + for (var i = 0, l = cd.length; i < l; i ++) { + var bit = cd[i].split("="), + name = bit.shift(), + val = stripQuotes(bit.join("=")); + if (name === "filename" || name === "name") { + mutate[name] = val; } - } else if (offset > 0) { - this.part = (this.part || new Part(this)); - this.part.write(this.buffer.substr(0, offset - 2)); + } + }); - this.part.emit('complete'); + if (!contentType) { + return false; + } - this.part = new Part(this); - this.buffer = this.buffer.substr(offset + this.boundary.length + 2); - } + // legacy + // TODO: Update this when/if jsgi-style headers are supported. + // this will keep working, but is less efficient than it could be. + if (!Array.isArray(contentType)) { + contentType = contentType.split(","); } -}; + contentType = contentType[contentType.length-1]; -function Part(stream) { - events.EventEmitter.call(this); + // make sure it's actually multipart. + var mpType = multipartExpression.exec(contentType); + if (!mpType) { + return false; + } - this.headers = {}; - this.name = null; - this.filename = null; - this.buffer = ''; - this.bytesReceived = 0; + // make sure we have a boundary. + var boundary = boundaryExpression.exec(contentType); + if (!boundary) { + return false; + } - // Avoids turning Part into a circular JSON object - this.getStream = function() { - return stream; - }; + mutate.type = mpType[1]; + mutate.boundary = "--" + boundary[1]; + mutate.isMultiPart = true; - this._headersComplete = false; + return true; +}; +function simpleWriter (stream) { + stream.part = stream; + stream.type = false; + var started = false; + return function (chunk) { + if (!started) { + stream.emit("partBegin", stream); + started = true; + } + stream.emit("body", chunk); + }; } -sys.inherits(Part, events.EventEmitter); - -Part.prototype.parsedHeaders = function() { - for (var header in this.headers) { - var parts = this.headers[header].split(/; ?/), parsedHeader = {}; - for (var i = 0; i < parts.length; i++) { - var pair = parts[i].split('='); - if (pair.length < 2) { +function writer (stream) { + var buffer = "", + state = S_NEW_PART, + part = stream.part = stream; + stream.parts = []; + stream.parent = stream; + return function (chunk) { + if (stream._error) return; + // write to the buffer, and then process the buffer. + buffer += chunk; + while (buffer.length > 0) { + while (buffer.substr(0, 2) === CRLF) buffer = buffer.substr(2); + switch (state) { + case S_NEW_PART: + // part is a multipart message. + // we're either going to start reading a new part, or we're going to + // end the current part, depending on whether the boundary has -- at + // the end. either way, we expect --boundary right away. + var boundary = part.boundary, + len = boundary.length, + offset = buffer.indexOf(boundary); + if (offset === -1) { + if (buffer.length > MAX_BUFFER_LENGTH) { + return stream.error(new Error( + "Malformed: boundary not found at start of message")); + } + // keep waiting for it. + return; + } + if (offset > 0) { + return stream.error(Error("Malformed: data before the boundary")); + } + if (buffer.length < (len + 2)) { + // we'll need to see either -- or CRLF after the boundary. + // get it on the next pass. + return; + } + if (buffer.substr(len, 2) === "--") { + // this message is done. + // chomp off the boundary and crlf and move up + if (part !== stream) { + // wait to see the crlf, unless this is the top-level message. + if (buffer.length < (len + 4)) { + return; + } + if (buffer.substr(len+2, 2) !== CRLF) { + return stream.error(new Error( + "Malformed: CRLF not found after boundary")); + } + } + buffer = buffer.substr(len + 4); + stream.emit("partEnd", part); + stream.part = part = part.parent; + state = S_NEW_PART; + continue; + } + if (part !== stream) { + // wait to see the crlf, unless this is the top-level message. + if (buffer.length < (len + 2)) { + return; + } + if (buffer.substr(len, 2) !== CRLF) { + return stream.error(new Error( + "Malformed: CRLF not found after boundary")); + } + } + // walk past the crlf + buffer = buffer.substr(len + 2); + // mint a new child part, and start parsing headers. + stream.part = part = startPart(part); + state = S_HEADER; + continue; + case S_HEADER: + // just grab everything to the double crlf. + var headerEnd = buffer.indexOf(CRLF+CRLF); + if (headerEnd === -1) { + if (buffer.length > MAX_BUFFER_LENGTH) { + return stream.error(new Error( + "Malformed: header unreasonably long.")); + } + return; + } + var headerString = buffer.substr(0, headerEnd); + // chomp off the header and the empty line. + buffer = buffer.substr(headerEnd + 4); + try { + parseHeaderString(part.headers, headerString); + } catch (ex) { + return stream.error(ex); + } + multipartHeaders(part); + + // let the world know + stream.emit("partBegin", part); + + if (part.isMultiPart) { + // it has a boundary and we're ready to grab parts out. + state = S_NEW_PART; + } else { + // it doesn't have a boundary, and is about to + // start spitting out body bits. + state = S_BODY; + } + continue; + case S_BODY: + // look for part.parent.boundary + var boundary = part.parent.boundary, + offset = buffer.indexOf(boundary); + if (offset === -1) { + // emit and wait for more data, but be careful, because + // we might only have half of the boundary so far. + // make sure to leave behind the boundary's length, so that we'll + // definitely get it next time if it's on its way. + var emittable = buffer.length - boundary.length; + if (buffer.substr(-1) === CR) emittable -= 1; + if (buffer.substr(-2) === CRLF) emittable -= 2; + + if (emittable > 0) { + stream.emit("body", buffer.substr(0, emittable)); + buffer = buffer.substr(emittable); + } + // haven't seen the boundary, so wait for more bytes. + return; + } + if (offset > 0) { + var emit = buffer.substr(0, offset); + if (emit.substr(-2) === CRLF) emit = emit.substr(0, emit.length-2); + if (emit) stream.emit("body", emit); + buffer = buffer.substr(offset); + } + + // let em know we're done. + stream.emit("partEnd", part); + + // now buffer starts with boundary. + if (buffer.substr(boundary.length, 2) === "--") { + // message end. + // parent ends, look for a new part in the grandparent. + stream.part = part = part.parent; + stream.emit("partEnd", part); + stream.part = part = part.parent; + state = S_NEW_PART; + buffer = buffer.substr(boundary.length + 4); + } else { + // another part coming for the parent message. + stream.part = part = part.parent; + state = S_NEW_PART; + } continue; } - - var key = pair[0].toLowerCase(), val = pair[1] || ''; - val = stripslashes(val).substr(1); - val = val.substr(0, val.length - 1); - - parsedHeader[key] = val; } + }; +}; - if (header == 'content-disposition') { - this.name = parsedHeader.name || null; - this.filename = parsedHeader.filename || null; +function parseHeaderString (headers, string) { + var lines = string.split(CRLF), + field, value, line; + for (var i = 0, l = lines.length; i < l; i ++) { + line = lines[i]; + if (line.match(wrapExpression)) { + if (!field) { + throw new Error("Malformed. First header starts with whitespace."); + } + value += line.replace(wrapExpression, " "); + continue; + } else if (field) { + // now that we know it's not wrapping, put it on the headers obj. + affixHeader(headers, field, value); } - - this.headers[header] = parsedHeader; + line = line.split(":"); + field = line.shift().toLowerCase(); + if (!field) { + throw new Error("Malformed: improper field name."); + } + value = line.join(":").replace(/^\s+/, ""); } + // now affix the last field. + affixHeader(headers, field, value); }; -Part.prototype.write = function(chunk) { - if (this._headersComplete) { - this.bytesReceived = this.bytesReceived + chunk.length; - this.emit('body', chunk); - return; +function affixHeader (headers, field, value) { + if (!headers.hasOwnProperty(field)) { + headers[field] = value; + } else if (Array.isArray(headers[field])) { + headers[field].push(value); + } else { + headers[field] = [headers[field], value]; } +}; - this.buffer = this.buffer + chunk; - while (this.buffer.length) { - var offset = this.buffer.indexOf("\r\n"); - - if (offset === 0) { - this._headersComplete = true; - this.parsedHeaders(); - this.getStream().emit('part', this); - - this.buffer = this.buffer.substr(2); - this.bytesReceived = this.bytesReceived + this.buffer.length; - this.emit('body', this.buffer); - this.buffer = ''; - return; - } else if (offset > 0) { - var header = this.buffer.substr(0, offset).split(/: ?/); - this.headers[header[0].toLowerCase()] = header[1]; - this.buffer = this.buffer.substr(offset+2); - } else if (offset === -1) { - return; - } - } +function startPart (parent) { + var part = { + headers : {}, + parent : parent + }; + parent.parts = parent.parts || []; + parent.parts.push(part); + return part; }; +function ender (stream) { return function () { + if (stream._error) return; + if (!stream.isMultiPart) stream.emit("partEnd", stream); + stream.emit("complete"); +}}; + function stripslashes(str) { // + original by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) // + improved by: Ates Goral (http://magnetiq.com) @@ -198,20 +412,24 @@ function stripslashes(str) { // + improved by: rezna // + input by: Rick Waldron // + reimplemented by: Brett Zamir (http://brett-zamir.me) - // * example 1: stripslashes('Kevin\'s code'); + // * example 1: stripslashes("Kevin\'s code"); // * returns 1: "Kevin's code" - // * example 2: stripslashes('Kevin\\\'s code'); + // * example 2: stripslashes("Kevin\\\'s code"); // * returns 2: "Kevin\'s code" - return (str+'').replace(/\\(.?)/g, function (s, n1) { + return (str+"").replace(/\\(.?)/g, function (s, n1) { switch(n1) { - case '\\': - return '\\'; - case '0': - return '\0'; - case '': - return ''; + case "\\": + return "\\"; + case "0": + return "\0"; + case "": + return ""; default: return n1; } }); -} +}; +function stripQuotes (str) { + str = stripslashes(str); + return str.substr(1, str.length - 2); +}; diff --git a/test/mjsunit/fixtures/multipart.js b/test/mjsunit/fixtures/multipart.js index 51212ba4de..726fec7e5a 100644 --- a/test/mjsunit/fixtures/multipart.js +++ b/test/mjsunit/fixtures/multipart.js @@ -1,11 +1,721 @@ -exports.reply = ["--AaB03x\r", -"content-disposition: form-data; name=\"reply\"\r", -"\r", -"yes\r", -"--AaB03x\r", -"content-disposition: form-data; name=\"fileupload\"; filename=\"dj.jpg\"\r", -"Content-Type: image/jpeg\r", -"Content-Transfer-Encoding: base64\r", -"\r", -"/9j/4AAQSkZJRgABAQAAAQABAAD//gA+Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcg\r", -"--AaB03x--\r\n"].join("\n"); \ No newline at end of file + +// each message contains a header, body, and a list of the parts that are +// expected. Any properties in the expected objects will be matched against +// the parsed parts. + +var messages = exports.messages = []; + +var bad = exports.badMessages = []; + +var longString = ""; +for (var i = 0; i < (16*1024); i ++) longString += Math.random(); + +// content before the first boundary +bad.push({ + headers : { "Content-Type":"multipart/mixed; boundary=boundary" }, + body : "blerg\r\n--boundary\r\nblarggghhh" +}); +// no boundary +bad.push({ + headers : { "Content-Type":"multipart/mixed; boundary=boundary" }, + body : longString +}); +// header unreasonably long. +bad.push({ + headers : { "Content-Type":"multipart/mixed; boundary=boundary" }, + body : "--boundary\r\ncontent-type: "+longString+"\r\n"+longString +}); +// CRLF not found after boundary +bad.push({ + headers : { "Content-Type":"multipart/mixed; boundary=boundary" }, + body : "--boundary"+longString +}); +// start first header with whitespace. +bad.push({ + headers : { "Content-Type":"multipart/mixed; boundary=boundary" }, + body : "--boundary\r\n fail: blahrg\r\n\r\n"+longString +}); + +// The comments in this first test case tell a story about what the parser is +// doing at each step. If you mean to touch the code, it's best to read through +// this test case first so that you know what you're getting into. +messages.push({ + expect : [ + { type : "mixed", boundary : "--inner1" }, + { type : "mixed", boundary : "--inner2" }, + { filename : "hello.txt" }, + { filename : "hello2.txt" }, + { type : "mixed", boundary : "--inner3" }, + { filename : "hello3.txt" }, + { filename : "hello4.txt" }, + { filename : "hello-outer.txt" } + ], + headers : { + "Content-Type":"multipart/mixed; boundary=outer" + }, body : [ + // s=new part, part = stream, part.boundary=--outer + "--outer",// chomp to here, because it matches the boundary. + // mint a new part without a boundary, parent=old part, set state to header + "Content-Type: multipart/mixed; boundary=inner1",// move along + "", // found the end of the header. chomp to here, parse the headers onto + // the current part. Once we do that, we know that the current part + // is multipart, and has a boundary of --inner1 + // s=new part, part = --inner1 + "--inner1", // chomp to here. + // mint a new part without a boundary, parent=--inner1, s=header + "Content-type: multipart/mixed; boundary=inner2", // move along + "", // again, found the end of the header. chomp to here, parse headers + // onto the newly minted part. Then find out that this part has a + // boundary of --inner2. + // s=new part, part=--inner2 + "--inner2", // chomp to here. + // mint a new part without a boundary, parent=--inner2 + "Content-type: text/plain", // move along + "content-disposition: inline; filename=\"hello.txt\"", // move along + "", // chomp to here. found end of header. parse headers + // then we know that it's not multipart, so we'll be looking for + // the parent's boundary and emitting body bits. + // also, we can set part.filename to "hello.txt" + // s=body, part=hello.txt + "hello, world", // chomp, emit the body, looking for parent-boundary + "--inner2", // found parent.boundary. leave it on the buffer, and + // set part=part.parent, s=new part + // on the next pass, we'll chomp to here, mint a new part + // without a boundary, set s=header + "content-type: text/plain", // header... + "content-disposition: inline; filename=\"hello2.txt\"", // header... + "", // chomp to here, parse header onto the current part. + // since it's not multipart, we're looking for parent.boundary + "hello to the world", // body, looking for parent.boundary=--inner + "--inner2--", // found parent.boundary. In this case, we have the + // trailing --, indicating that no more parts are coming + // for this set. We need to back up to the grandparent, + // and then do the new part bit. Chomp off the --inner2-- + // s=new part, part=part.parent.parent=--inner1 + "--inner1", // chomp to here, because this is part.boundary + // mint a new part without a boundary + // s=header, part = (new) + "Content-type: multipart/mixed; boundary=inner3", // header... + "", // chomp to here, parse headers onto the new part. + // it's multipart, so set the boundary=--inner3, + // s=new part, part = --inner3 + "--inner3", // chomp to here. mint a new part with no boundary, parse headers + "Content-type: text/plain", // header + "content-disposition: inline; filename=\"hello3.txt\"", // header + "", // end of header. parse headers onto part, whereupon we find that it is + // not multipart, and has a filename of hello3.txt. + // s=body, part=hello3.txt, looking for part.parent.boundary=--inner3 + "hello, free the world", // body... + "--inner3", // found parent.boundary, and it's not the end. + // s = new part, part = part.parent + // next pass: + // mint a new part without a boundary, s=header + "content-type: text/plain", // header + "content-disposition: inline; filename=\"hello4.txt\"", // header + "", // chomp to here, parse headers on to boundaryless part. + // s=body, part = hello4.txt + "hello for the world", // body, looking for part.parent.boundary=--inner3 + "--inner3--", // found parent.boundary, and it's the end + // chomp this off the buffer, part = part.parent.parent=--inner1 + // s = new part + "--inner1", // chomp to here, because part.boundary = --inner1 + // mint a new boundariless part, s = header + "Content-type: text/plain", // header... + "content-disposition: inline; filename=\"hello-outer.txt\"", // header... + "", // chomp to here, parse headers onto the current part. + // has no boundary, so we're gonna go into body mode. + // s = body, boundary = parent.boundary = --inner1, part = hello-outer.txt + "hello, outer world", // body, looking for parent.boundary=--inner1 + "--inner1--", // found the parent.boundary, and it's the end. + // chomp off the --inner1--, part = part.parent.parent, s = new part + "--outer--" // we're looking for a new part, but found the ending. + // chomp off the --outer--, part = part.parent, s = new part. + ].join("\r\n") +}); + +messages.push({ + headers : { + "Content-Type": "multipart/form-data; boundary=AaB03x", + }, + body : [ + "--AaB03x", + "content-disposition: form-data; name=\"reply\"", + "", + "yes", + "--AaB03x", + "content-disposition: form-data; name=\"fileupload\"; filename=\"dj.jpg\"", + "Content-Type: image/jpeg", + "Content-Transfer-Encoding: base64", + "", + "/9j/4AAQSkZJRgABAQAAAQABAAD//gA+Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcg", + "--AaB03x--", "" + ].join("\r\n"), + expect : [ + { name : "reply" }, + { name : "fileupload", filename : "dj.jpg" } + ] +}); + +// one that's not multipart, just for kicks. +// verify that it ducks as a multipart message with one part. +messages.push({ + headers: { "content-type" : "text/plain" }, + body : "Hello, world!", + + // not much to say about this one, since it's just + // validating that a part was created, not that it has + // any particular properties. + expect : [{}] +}); + +// An actual email message sent from felixge to isaacs. +// Addresses and signatures obscured, but the unicycle pic is preserved for posterity. +messages.push({ + headers: { + // TODO: When node's parser supports header-wrapping, these should actually be wrapped, + // because that's how they appear in real life. + "Delivered-To":"isaacs...@gmail.com", + "Received":"by 10.142.240.14 with SMTP id n14cs252101wfh; Wed, 3 Feb 2010 14:24:08 -0800 (PST)", + "Received":"by 10.223.4.139 with SMTP id 11mr194455far.61.1265235847416; Wed, 03 Feb 2010 14:24:07 -0800 (PST)", + "Return-Path":"", + "Received":"from mail-fx0-f219.google.com (mail-fx0-f219.google.com [209.85.220.219]) by mx.google.com with ESMTP id d13si118373fka.17.2010.02.03.14.24.05; Wed, 03 Feb 2010 14:24:06 -0800 (PST)", + "Received-SPF":"neutral (google.com: 209.85.220.219 is neither permitted nor denied by best guess record for domain of isaacs+caf_=isaacs...=gmail.com@izs.me) client-ip=209.85.220.219;", + "Authentication-Results":"mx.google.com; spf=neutral (google.com: 209.85.220.219 is neither permitted nor denied by best guess record for domain of isaacs+caf_=isaacs...=gmail.com@izs.me) smtp.mail=isaacs+caf_=isaacs...=gmail.com@izs.me; dkim=pass (test mode) header.i=@gmail.com", + "Received":"by mail-fx0-f219.google.com with SMTP id 19so626487fxm.25 for ; Wed, 03 Feb 2010 14:24:05 -0800 (PST)", + "Received":"by 10.216.91.15 with SMTP id g15mr146196wef.24.1265235845694; Wed, 03 Feb 2010 14:24:05 -0800 (PST)", + "X-Forwarded-To":"isaacs...@gmail.com", + "X-Forwarded-For":"isaacs@izs.me isaacs...@gmail.com", + "Delivered-To":"i@izs.me", + "Received":"by 10.216.12.146 with SMTP id 18cs33122wez; Wed, 3 Feb 2010 14:24:00 -0800 (PST)", + "Received":"by 10.213.97.28 with SMTP id j28mr2627124ebn.82.1265235838786; Wed, 03 Feb 2010 14:23:58 -0800 (PST)", + "Return-Path":"", + "Received":"from ey-out-2122.google.com (ey-out-2122.google.com [74.125.78.25]) by mx.google.com with ESMTP id 4si11869270ewy.8.2010.02.03.14.23.54; Wed, 03 Feb 2010 14:23:57 -0800 (PST)", + "Received-SPF":"pass (google.com: domain of hai...@gmail.com designates 74.125.78.25 as permitted sender) client-ip=74.125.78.25;", + "Received":"by ey-out-2122.google.com with SMTP id d26so431288eyd.17 for ; Wed, 03 Feb 2010 14:23:54 -0800 (PST)", + "DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=gamma; h=domainkey-signature:mime-version:sender:received:from:date :x-google-sender-auth:message-id:subject:to:content-type; bh=JXfvYIRzerOieADuqMPGlnlFbIGyPuTssL5icEtSLWw=; b=QDzgOCEbYk8cEdBe+HYx/MJrTWmZyx4qENADOcnnn9Xuk1Q6e/c7b3UsvLf/sMoYrG z96RQhUVOKi9IAzkQhNnOCWDuF1KNxtFnCGhEXMARXBM3qjXe3QmAqXNhJrI0E9bMeme d5aX5GMrz5mIark462cDsTmrFgaYE6JtwASho=", + "DomainKey-Signature":"a=rsa-sha1; c=nofws; d=gmail.com; s=gamma; h=mime-version:sender:from:date:x-google-sender-auth:message-id :subject:to:content-type; b=VYkN8OeNNJyxAseCAPH8u2aBfGmZaFesmieoWEDymQ1DsWg/aXbaWt4JGQlefIfmMK hOXd4EN2/iEix10aWDzKpuUV9gU9Wykm93t3pxD7BCz50Kagwp7NVyDJQLK0H5JSNEU/ IVRp90kKNBsb3v76vPsQydi9awLh/jYrFQVMY=", + "MIME-Version":"1.0", + "Sender":"hai...@gmail.com", + "Received":"by 10.216.86.201 with SMTP id w51mr154937wee.8.1265235834101; Wed, 03 Feb 2010 14:23:54 -0800 (PST)", + "From":"Felix Geisendoerfer ", + "Date":"Wed, 3 Feb 2010 23:23:34 +0100", + "X-Google-Sender-Auth":"0217977a92fcbed0", + "Message-ID":"<56dbc1211002031423g750ba93fs4a2f22ce22431590@mail.gmail.com>", + "Subject":"Me on my unicycle", + "To":"i@izs.me", + "Content-Type":"multipart/mixed; boundary=0016e6d99d0572dfaf047eb9ac2e", + }, + expect : [ + { type : "alternative", boundary : "--0016e6d99d0572dfa5047eb9ac2c" }, + {}, // the first bit, text/plain + {}, // the second bit, text/html + { name : "unicycle.jpg", filename : "unicycle.jpg" } + ], + body : [ + "--0016e6d99d0572dfaf047eb9ac2e", // beginpart->header + "Content-Type: multipart/alternative; boundary=0016e6d99d0572dfa5047eb9ac2c", // headers. isMultipart + "", // bodybegin->beginpart + "--0016e6d99d0572dfa5047eb9ac2c",//header + "Content-Type: text/plain; charset=ISO-8859-1", + "",//bodybegin->body + "*This was 4 years ago, I miss riding my unicycle !*", + "", + "-- fg", + "", + "--0016e6d99d0572dfa5047eb9ac2c", //partend->partbegin + "Content-Type: text/html; charset=ISO-8859-1", + "", + "This was 4 years ago, I miss riding my unicycle !

-- fg

", + "", + "", + "
", + "", + "--0016e6d99d0572dfa5047eb9ac2c--",//partend, walk up tree-->partbegin + "--0016e6d99d0572dfaf047eb9ac2e",//beginpart->header + "Content-Type: image/jpeg; name=\"unicycle.jpg\"",//header + "Content-Disposition: attachment; filename=\"unicycle.jpg\"",//header + "Content-Transfer-Encoding: base64",//header + "X-Attachment-Id: f_g58opqah0",//header + "",//bodybegin->body + "/9j/4AAQSkZJRgABAQEASABIAAD/4gUoSUNDX1BST0ZJTEUAAQEAAAUYYXBwbAIgAABzY25yUkdC",//bodybodybody + "IFhZWiAH0wAHAAEAAAAAAABhY3NwQVBQTAAAAABhcHBsAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAA",//bodybodybody + "AADTLWFwcGwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAty",//bodybodybody + "WFlaAAABCAAAABRnWFlaAAABHAAAABRiWFlaAAABMAAAABR3dHB0AAABRAAAABRjaGFkAAABWAAA",//bodybodybody + "ACxyVFJDAAABhAAAAA5nVFJDAAABhAAAAA5iVFJDAAABhAAAAA5kZXNjAAABlAAAAD1jcHJ0AAAE", + "1AAAAEFkc2NtAAAB1AAAAv5YWVogAAAAAAAAdEsAAD4dAAADy1hZWiAAAAAAAABacwAArKYAABcm", + "WFlaIAAAAAAAACgYAAAVVwAAuDNYWVogAAAAAAAA81IAAQAAAAEWz3NmMzIAAAAAAAEMQgAABd7/", + "//MmAAAHkgAA/ZH///ui///9owAAA9wAAMBsY3VydgAAAAAAAAABAjMAAGRlc2MAAAAAAAAAE0Nh", + "bWVyYSBSR0IgUHJvZmlsZQAAAAAAAAAAAAAAE0NhbWVyYSBSR0IgUHJvZmlsZQAAAABtbHVjAAAA", + "AAAAAA8AAAAMZW5VUwAAACQAAAKeZXNFUwAAACwAAAFMZGFESwAAADQAAAHaZGVERQAAACwAAAGY", + "ZmlGSQAAACgAAADEZnJGVQAAADwAAALCaXRJVAAAACwAAAJybmxOTAAAACQAAAIObm9OTwAAACAA", + "AAF4cHRCUgAAACgAAAJKc3ZTRQAAACoAAADsamFKUAAAABwAAAEWa29LUgAAABgAAAIyemhUVwAA", + "ABoAAAEyemhDTgAAABYAAAHEAEsAYQBtAGUAcgBhAG4AIABSAEcAQgAtAHAAcgBvAGYAaQBpAGwA", + "aQBSAEcAQgAtAHAAcgBvAGYAaQBsACAAZgD2AHIAIABLAGEAbQBlAHIAYTCrMOEw6QAgAFIARwBC", + "ACAw1zDtMNUwoTCkMOtleE9NdvhqXwAgAFIARwBCACCCcl9pY8+P8ABQAGUAcgBmAGkAbAAgAFIA", + "RwBCACAAcABhAHIAYQAgAEMA4QBtAGEAcgBhAFIARwBCAC0AawBhAG0AZQByAGEAcAByAG8AZgBp", + "AGwAUgBHAEIALQBQAHIAbwBmAGkAbAAgAGYA/AByACAASwBhAG0AZQByAGEAc3b4ZzoAIABSAEcA", + "QgAgY8+P8GWHTvYAUgBHAEIALQBiAGUAcwBrAHIAaQB2AGUAbABzAGUAIAB0AGkAbAAgAEsAYQBt", + "AGUAcgBhAFIARwBCAC0AcAByAG8AZgBpAGUAbAAgAEMAYQBtAGUAcgBhznS6VLd8ACAAUgBHAEIA", + "INUEuFzTDMd8AFAAZQByAGYAaQBsACAAUgBHAEIAIABkAGUAIABDAOIAbQBlAHIAYQBQAHIAbwBm", + "AGkAbABvACAAUgBHAEIAIABGAG8AdABvAGMAYQBtAGUAcgBhAEMAYQBtAGUAcgBhACAAUgBHAEIA", + "IABQAHIAbwBmAGkAbABlAFAAcgBvAGYAaQBsACAAUgBWAEIAIABkAGUAIABsIBkAYQBwAHAAYQBy", + "AGUAaQBsAC0AcABoAG8AdABvAAB0ZXh0AAAAAENvcHlyaWdodCAyMDAzIEFwcGxlIENvbXB1dGVy", + "IEluYy4sIGFsbCByaWdodHMgcmVzZXJ2ZWQuAAAAAP/hAIxFeGlmAABNTQAqAAAACAAGAQYAAwAA", + "AAEAAgAAARIAAwAAAAEAAQAAARoABQAAAAEAAABWARsABQAAAAEAAABeASgAAwAAAAEAAgAAh2kA", + "BAAAAAEAAABmAAAAAAAAAEgAAAABAAAASAAAAAEAAqACAAQAAAABAAAAyKADAAQAAAABAAABWwAA", + "AAD/2wBDAAICAgICAQICAgICAgIDAwYEAwMDAwcFBQQGCAcICAgHCAgJCg0LCQkMCggICw8LDA0O", + "Dg4OCQsQEQ8OEQ0ODg7/2wBDAQICAgMDAwYEBAYOCQgJDg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4O", + "Dg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg7/wAARCAFbAMgDASIAAhEBAxEB/8QAHwAAAQUBAQEB", + "AQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1Fh", + "ByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZ", + "WmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXG", + "x8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAEC", + "AwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHB", + "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", + "CivqJ00meTzNH//Z", + "--0016e6d99d0572dfaf047eb9ac2e--",//partend, then complete + "" + ].join("\r\n") +}); diff --git a/test/mjsunit/test-multipart.js b/test/mjsunit/test-multipart.js index 0af864a959..fb1ac4a32d 100644 --- a/test/mjsunit/test-multipart.js +++ b/test/mjsunit/test-multipart.js @@ -1,129 +1,123 @@ process.mixin(require("./common")); -http = require("http"); -var +var http = require("http"), + multipart = require("multipart"), + sys = require("sys"), PORT = 8222, - - multipart = require('multipart'), - fixture = require('./fixtures/multipart'), - - requests = 0, - badRequests = 0, - partsReceived = 0, - partsComplete = 0, - - respond = function(res, text) { - requests++; - if (requests == 5) { - server.close(); + fixture = require("./fixtures/multipart"), + events = require("events"), + testPart = function (expect, part) { + if (!expect) { + throw new Error("Got more parts than expected: "+ + JSON.stringify(part.headers)); + } + for (var i in expect) { + assert.equal(expect[i], part[i]); } - - res.sendHeader(200, {"Content-Type": "text/plain"}); - res.sendBody(text); - res.finish(); }; -var server = http.createServer(function(req, res) { - if (req.headers['x-use-simple-api']) { - multipart.parse(req) - .addCallback(function() { - respond(res, 'thanks'); - }) - .addErrback(function() { - badRequests++; - respond(res, 'no thanks'); - }); - return; - } - - - try { - var stream = new multipart.Stream(req); - } catch (e) { - badRequests++; - respond(res, 'no thanks'); +var emails = fixture.messages.slice(0), + chunkSize = 1, // set to minimum to forcibly expose boundary conditions. + // in a real scenario, this would be much much bigger. + firstPart = new (events.Promise); + +// test streaming messages through directly, as if they were in a file or something. +(function testEmails () { + var email = emails.pop(), + curr = 0; + if (!email) { + firstPart.emitSuccess(); return; } + var expect = email.expect; - var parts = {}; - stream.addListener('part', function(part) { - partsReceived++; + var message = new (events.EventEmitter); + message.headers = email.headers; - var name = part.name; - - if (partsReceived == 1) { - assert.equal('reply', name); - } else if (partsReceived == 2) { - assert.equal('fileupload', name); + var mp = multipart.parse(message); + mp.addListener("partBegin", function (part) { + testPart(email.expect[curr ++], part); + }); + mp.addListener("complete", function () { + process.nextTick(testEmails); + }); + // stream it through in chunks. + var emailBody = email.body; + process.nextTick(function s () { + if (emailBody) { + message.emit("body", emailBody.substr(0, chunkSize)); + emailBody = emailBody.substr(chunkSize); + process.nextTick(s); + } else { + message.emit("complete"); } - - parts[name] = ''; - part.addListener('body', function(chunk) { - parts[name] += chunk; + }); +})(); + +// run good HTTP messages test after previous test ends. +var secondPart = new (events.Promise), + server = http.createServer(function (req, res) { + var mp = multipart.parse(req), + curr = 0; + req.setBodyEncoding("binary"); + if (req.url !== "/bad") { + mp.addListener("partBegin", function (part) { + testPart(message.expect[curr ++], part); + }); + } + mp.addListener("error", function (er) { + res.sendHeader(400, {}); + res.sendBody("bad"); + res.finish(); }); - part.addListener('complete', function(chunk) { - assert.equal(0, part.buffer.length); - if (partsReceived == 1) { - assert.equal('yes', parts[name]); - } else if (partsReceived == 2) { - assert.equal( - '/9j/4AAQSkZJRgABAQAAAQABAAD//gA+Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcg', - parts[name] - ); - } - partsComplete++; + mp.addListener("complete", function () { + res.sendHeader(200, {}); + res.sendBody("ok"); + res.finish(); }); - }); - - stream.addListener('complete', function() { - respond(res, 'thanks'); - }); -}); + }), + message, + client = http.createClient(PORT); server.listen(PORT); -var client = http.createClient(PORT); - -var request = client.request('POST', '/', { - 'Content-Type': 'multipart/form-data; boundary=AaB03x', - 'Content-Length': fixture.reply.length -}); -request.sendBody(fixture.reply, 'binary'); -request.finish(); - -var simpleRequest = client.request('POST', '/', { - 'X-Use-Simple-Api': 'yes', - 'Content-Type': 'multipart/form-data; boundary=AaB03x', - 'Content-Length': fixture.reply.length -}); -simpleRequest.sendBody(fixture.reply, 'binary'); -simpleRequest.finish(); - -var badRequest = client.request('POST', '/', { - 'Content-Type': 'invalid!', - 'Content-Length': fixture.reply.length -}); -badRequest.sendBody(fixture.reply, 'binary'); -badRequest.finish(); - -var simpleBadRequest = client.request('POST', '/', { - 'X-Use-Simple-Api': 'yes', - 'Content-Type': 'something', - 'Content-Length': fixture.reply.length -}); -simpleBadRequest.sendBody(fixture.reply, 'binary'); -simpleBadRequest.finish(); - -var requestWithCharset = client.request('POST', '/', { - 'X-Use-Simple-Api': 'yes', - 'Content-Type': 'multipart/form-data; charset=utf-8; boundary=AaB03x', - 'Content-Length': fixture.reply.length +// could dry these two up a bit. +firstPart.addCallback(function testGoodMessages () { + var httpMessages = fixture.messages.slice(0); + process.nextTick(function testHTTP () { + message = httpMessages.pop(); + if (!message) { + secondPart.emitSuccess(); + return; + } + var req = client.request("POST", "/", message.headers); + req.sendBody(message.body, "binary"); + req.finish(function (res) { + var buff = ""; + res.addListener("body", function (chunk) { buff += chunk }); + res.addListener("complete", function () { + assert.equal(buff, "ok"); + process.nextTick(testHTTP); + }); + }); + }); }); -requestWithCharset.sendBody(fixture.reply, 'binary'); -requestWithCharset.finish(); - -process.addListener('exit', function() { - puts("done"); - assert.equal(2, partsComplete); - assert.equal(2, partsReceived); - assert.equal(2, badRequests); +secondPart.addCallback(function testBadMessages () { + var httpMessages = fixture.badMessages.slice(0); + process.nextTick(function testHTTP () { + message = httpMessages.pop(); + if (!message) { + server.close() + return; + } + var req = client.request("POST", "/bad", message.headers); + req.sendBody(message.body, "binary"); + req.finish(function (res) { + var buff = ""; + res.addListener("body", function (chunk) { buff += chunk }); + res.addListener("complete", function () { + assert.equal(buff, "bad"); + process.nextTick(testHTTP); + }); + }); + }); }); \ No newline at end of file From e33c66654ab1634fea36de13065d1bfd52fce265 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Tue, 9 Feb 2010 10:50:05 -0600 Subject: [PATCH 09/15] Rewrite sys.inspect to be more reliable and handle crazy edge cases. --- lib/sys.js | 168 ++++++++++++++++++++++----------------- test/mjsunit/test-sys.js | 41 +++++++++- 2 files changed, 131 insertions(+), 78 deletions(-) diff --git a/lib/sys.js b/lib/sys.js index 18b6359722..f39a7cf0ea 100644 --- a/lib/sys.js +++ b/lib/sys.js @@ -21,9 +21,99 @@ exports.error = function (x) { * in the best way possible given the different types. * * @param {Object} value The object to print out + * @param {Boolean} showHidden Flag that shows hidden (not enumerable) properties of objects. */ -exports.inspect = function (value) { - return formatter(value, '', []); +exports.inspect = function (obj, showHidden) { + var seen = []; + function format(value) { + var keys, visible_keys, base, type, braces; + // Primitive types cannot have properties + switch (typeof value) { + case 'undefined': return 'undefined'; + case 'string': return JSON.stringify(value); + case 'number': return '' + value; + case 'boolean': return '' + value; + } + // For some reason typeof null is "object", so special case here. + if (value === null) { + return 'null'; + } + + // Look up the keys of the object. + keys = showHidden ? Object.getOwnPropertyNames(value).map(function (key) { + return '' + key; + }) : Object.keys(value); + visible_keys = Object.keys(value); + + // Functions without properties can be shortcutted. + if (typeof value === 'function' && keys.length === 0) { + if (value instanceof RegExp) { + return '' + value; + } else { + return '[Function]'; + } + } + + // Determine the object type + if (value instanceof Array) { + type = 'Array'; + braces = ["[", "]"]; + } else { + type = 'Object'; + braces = ["{", "}"]; + } + + // Make functions say that they are functions + if (typeof value === 'function') { + base = (value instanceof RegExp) ? ' ' + value : ' [Function]'; + } else { + base = ""; + } + + seen.push(value); + + if (keys.length === 0) { + return braces[0] + base + braces[1]; + } + + return braces[0] + base + "\n" + (keys.map(function (key) { + var name, str; + if (value.__lookupGetter__) { + if (value.__lookupGetter__(key)) { + if (value.__lookupSetter__(key)) { + str = "[Getter/Setter]"; + } else { + str = "[Getter]"; + } + } else { + if (value.__lookupSetter__(key)) { + str = "[Setter]"; + } + } + } + if (visible_keys.indexOf(key) < 0) { + name = "[" + key + "]"; + } + if (!str) { + if (seen.indexOf(value[key]) < 0) { + str = format(value[key]); + } else { + str = '[Circular]'; + } + } + if (typeof name === 'undefined') { + if (type === 'Array' && key.match(/^\d+$/)) { + return str; + } + name = JSON.stringify('' + key); + } + + return name + ": " + str; + }).join(",\n")).split("\n").map(function (line) { + return ' ' + line; + }).join('\n') + "\n" + braces[1]; + } + return format(obj); }; exports.p = function (x) { @@ -70,76 +160,4 @@ exports.exec = function (command) { */ exports.inherits = process.inherits; -/** - * A recursive function to format an object - used by inspect. - * - * @param {Object} value - * the value to format - * @param {String} indent - * the indent level of any nested objects, since they are formatted over - * more than one line - * @param {Array} parents - * contains all objects above the current one in the heirachy, used to - * prevent getting stuck in a loop on circular references - */ -var formatter = function(value, indent, parents) { - switch(typeof(value)) { - case 'string': return JSON.stringify(value); - case 'number': return '' + value; - case 'function': return '[Function]'; - case 'boolean': return '' + value; - case 'undefined': return 'undefined'; - case 'object': - if (value == null) return 'null'; - if (parents.indexOf(value) >= 0) return '[Circular]'; - parents.push(value); - - if (value instanceof Array && Object.keys(value).length === value.length) { - return formatObject(value, indent, parents, '[]', function(x, f) { - return f(value[x]); - }); - } else { - return formatObject(value, indent, parents, '{}', function(x, f) { - var child; - if (value.__lookupGetter__(x)) { - if (value.__lookupSetter__(x)) { - child = "[Getter/Setter]"; - } else { - child = "[Getter]"; - } - } else { - if (value.__lookupSetter__(x)) { - child = "[Setter]"; - } else { - child = f(value[x]); - } - } - return f(x) + ': ' + child; - }); - } - return buffer; - default: - throw('inspect unimplemented for ' + typeof(value)); - } -} - -/** - * Helper function for formatting either an array or an object, used internally by formatter - */ -var formatObject = function(obj, indent, parents, parenthesis, entryFormatter) { - var buffer = parenthesis[0]; - var values = []; - var x; - - var localFormatter = function(value) { - return formatter(value, indent + ' ', parents); - }; - for (x in obj) { - values.push(indent + ' ' + entryFormatter(x, localFormatter)); - } - if (values.length > 0) { - buffer += "\n" + values.join(",\n") + "\n" + indent; - } - buffer += parenthesis[1]; - return buffer; -} +// Object.create(null, {name: {value: "Tim", enumerable: true}}) \ No newline at end of file diff --git a/test/mjsunit/test-sys.js b/test/mjsunit/test-sys.js index 005834c5a0..6ba2157e8e 100644 --- a/test/mjsunit/test-sys.js +++ b/test/mjsunit/test-sys.js @@ -9,6 +9,7 @@ assert.equal('"hello"', inspect("hello")); assert.equal("[Function]", inspect(function() {})); assert.equal('undefined', inspect(undefined)); assert.equal('null', inspect(null)); +assert.equal('/foo(bar\\n)?/gi', inspect(/foo(bar\n)?/gi)); assert.equal("\"\\n\\u0001\"", inspect("\n\u0001")); @@ -23,6 +24,24 @@ assert.equal('{\n "a": [Function]\n}', inspect({a: function() {}})); assert.equal('{\n "a": 1,\n "b": 2\n}', inspect({a: 1, b: 2})); assert.equal('{\n "a": {}\n}', inspect({'a': {}})); assert.equal('{\n "a": {\n "b": 2\n }\n}', inspect({'a': {'b': 2}})); +assert.equal('[\n 1,\n 2,\n 3,\n [length]: 3\n]', inspect([1,2,3], true)); +assert.equal("{\n \"visible\": 1\n}", + inspect(Object.create({}, {visible:{value:1,enumerable:true},hidden:{value:2}})) +); +assert.equal("{\n [hidden]: 2,\n \"visible\": 1\n}", + inspect(Object.create({}, {visible:{value:1,enumerable:true},hidden:{value:2}}), true) +); + +// Objects without prototype +assert.equal( + "{\n [hidden]: \"secret\",\n \"name\": \"Tim\"\n}", + inspect(Object.create(null, {name: {value: "Tim", enumerable: true}, hidden: {value: "secret"}}), true) +); +assert.equal( + "{\n \"name\": \"Tim\"\n}", + inspect(Object.create(null, {name: {value: "Tim", enumerable: true}, hidden: {value: "secret"}})) +); + // Dynamic properties assert.equal( @@ -35,12 +54,28 @@ value['a'] = value; assert.equal('{\n "a": [Circular]\n}', inspect(value)); value = Object.create([]); value.push(1); -assert.equal('{\n "0": 1,\n "length": 1\n}', inspect(value)); +assert.equal("[\n 1,\n \"length\": 1\n]", inspect(value)); // Array with dynamic properties value = [1,2,3]; value.__defineGetter__('growingLength', function () { this.push(true); return this.length; }); assert.equal( - "{\n \"0\": 1,\n \"1\": 2,\n \"2\": 3,\n \"growingLength\": [Getter]\n}", + "[\n 1,\n 2,\n 3,\n \"growingLength\": [Getter]\n]", inspect(value) -); \ No newline at end of file +); + +// Function with properties +value = function () {}; +value.aprop = 42; +assert.equal( + "{ [Function]\n \"aprop\": 42\n}", + inspect(value) +); + +// Regular expressions with properties +value = /123/ig; +value.aprop = 42; +assert.equal( + "{ /123/gi\n \"aprop\": 42\n}", + inspect(value) +); From 3aca908db3be3b7354d409974fb9f84c7d980644 Mon Sep 17 00:00:00 2001 From: Tim Caswell Date: Tue, 9 Feb 2010 11:15:02 -0600 Subject: [PATCH 10/15] Document the changes to sys.inspect's API. It now takes an optional showHidden argument that shows hidden/non-enumerable properties of objects. Also cleanup the lib/sys.js file a bit. --- doc/api.txt | 4 ++-- lib/sys.js | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/doc/api.txt b/doc/api.txt index 89995e9449..2eeea99d99 100644 --- a/doc/api.txt +++ b/doc/api.txt @@ -208,8 +208,8 @@ Like +puts()+ but without the trailing new-line. A synchronous output function. Will block the process and output the string immediately to stdout. -+inspect(object)+ :: -Return a string representation of the +object+. (For debugging.) ++inspect(object, showHidden)+ :: +Return a string representation of the +object+. (For debugging.) If showHidden is true, then the object's non-enumerable properties will be shown too. +exec(command)+:: Executes the command as a child process, buffers the output and returns it diff --git a/lib/sys.js b/lib/sys.js index f39a7cf0ea..461a12c8a1 100644 --- a/lib/sys.js +++ b/lib/sys.js @@ -26,7 +26,6 @@ exports.error = function (x) { exports.inspect = function (obj, showHidden) { var seen = []; function format(value) { - var keys, visible_keys, base, type, braces; // Primitive types cannot have properties switch (typeof value) { case 'undefined': return 'undefined'; @@ -40,10 +39,10 @@ exports.inspect = function (obj, showHidden) { } // Look up the keys of the object. - keys = showHidden ? Object.getOwnPropertyNames(value).map(function (key) { + var keys = showHidden ? Object.getOwnPropertyNames(value).map(function (key) { return '' + key; }) : Object.keys(value); - visible_keys = Object.keys(value); + var visible_keys = Object.keys(value); // Functions without properties can be shortcutted. if (typeof value === 'function' && keys.length === 0) { @@ -54,6 +53,7 @@ exports.inspect = function (obj, showHidden) { } } + var base, type, braces; // Determine the object type if (value instanceof Array) { type = 'Array'; @@ -160,4 +160,3 @@ exports.exec = function (command) { */ exports.inherits = process.inherits; -// Object.create(null, {name: {value: "Tim", enumerable: true}}) \ No newline at end of file From 49de41ef463292988ddacfb01a20543b963d9669 Mon Sep 17 00:00:00 2001 From: Ryan Dahl Date: Tue, 9 Feb 2010 13:20:20 -0800 Subject: [PATCH 11/15] bump version --- ChangeLog | 21 ++++++++++++++++++++- doc/api.txt | 2 +- doc/index.html | 4 ++-- wscript | 2 +- 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/ChangeLog b/ChangeLog index 46318dd4dc..00f11cca2a 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,4 +1,23 @@ -2010.02.03, Version 0.1.27 +2010.02.09, Version 0.1.28 + + * Use Google's jsmin.py which can be used for evil. + + * Add posix.truncate() + + * Throw errors from server.listen() + + * stdio bugfix (test by Mikeal Rogers) + + * Module system refactor (Felix Geisendörfer, Blaine Cook) + + * Add process.setuid(), getuid() (Michael Carter) + + * sys.inspect refactor (Tim Caswell) + + * Multipart library rewrite (isaacs) + + +2010.02.03, Version 0.1.27, 0cfa789cc530848725a8cb5595224e78ae7b9dd0 * Implemented __dirname (Felix Geisendörfer) diff --git a/doc/api.txt b/doc/api.txt index 2eeea99d99..87af8b0f6e 100644 --- a/doc/api.txt +++ b/doc/api.txt @@ -1,7 +1,7 @@ NODE(1) ======= Ryan Dahl -Version, 0.1.27, 2010.02.03 +Version, 0.1.28, 2010.02.09 == NAME diff --git a/doc/index.html b/doc/index.html index de7efdd7e5..79bc44ed23 100644 --- a/doc/index.html +++ b/doc/index.html @@ -97,9 +97,9 @@ server.listen(7000, "localhost"); git repo

- 2010.02.03 + 2010.02.09 node-v0.1.27.tar.gz + href="http://s3.amazonaws.com/four.livejournal/20100209/node-v0.1.28.tar.gz">node-v0.1.28.tar.gz

Build

diff --git a/wscript b/wscript index 92a6c199b9..b7cf73781d 100644 --- a/wscript +++ b/wscript @@ -7,7 +7,7 @@ from os.path import join, dirname, abspath from logging import fatal cwd = os.getcwd() -VERSION="0.1.27" +VERSION="0.1.28" APPNAME="node.js" import js2c From 88b935928419819a6547e30dd2c8fa68fa815291 Mon Sep 17 00:00:00 2001 From: Ryan Dahl Date: Tue, 9 Feb 2010 14:11:58 -0800 Subject: [PATCH 12/15] Fix stderr flushing problem --- src/node_stdio.cc | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/node_stdio.cc b/src/node_stdio.cc index adcaff2557..cda79e3a93 100644 --- a/src/node_stdio.cc +++ b/src/node_stdio.cc @@ -4,6 +4,8 @@ #include #include +#include +#include using namespace v8; using namespace node; @@ -42,6 +44,15 @@ EmitClose (void) emit->Call(stdio, 1, argv); } + +static inline Local errno_exception(int errorno) { + Local e = Exception::Error(String::NewSymbol(strerror(errorno))); + Local obj = e->ToObject(); + obj->Set(String::NewSymbol("errno"), Integer::New(errorno)); + return e; +} + + /* STDERR IS ALWAY SYNC */ static Handle WriteError (const Arguments& args) @@ -53,8 +64,13 @@ WriteError (const Arguments& args) String::Utf8Value msg(args[0]->ToString()); - fprintf(stderr, "%s", *msg); - fflush(stderr); + ssize_t r; + size_t written = 0; + while (written < msg.length()) { + r = write(STDERR_FILENO, (*msg) + written, msg.length() - written); + if (r < 0) return ThrowException(errno_exception(errno)); + written += (size_t)r; + } return Undefined(); } From 3eaaaffadb85fd6187bdfaaffc6c0ab414a89a3a Mon Sep 17 00:00:00 2001 From: Ryan Dahl Date: Tue, 9 Feb 2010 14:42:56 -0800 Subject: [PATCH 13/15] Ignore EAGAIN in stderr dumps. (Going out of the way to be sync) --- src/node_stdio.cc | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/node_stdio.cc b/src/node_stdio.cc index cda79e3a93..2b5b30612c 100644 --- a/src/node_stdio.cc +++ b/src/node_stdio.cc @@ -68,7 +68,13 @@ WriteError (const Arguments& args) size_t written = 0; while (written < msg.length()) { r = write(STDERR_FILENO, (*msg) + written, msg.length() - written); - if (r < 0) return ThrowException(errno_exception(errno)); + if (r < 0) { + if (errno == EAGAIN || errno == EIO) { + usleep(100); + continue; + } + return ThrowException(errno_exception(errno)); + } written += (size_t)r; } From 9f6f19df1bab8bd3bad0ec00ed0489957c7cfacf Mon Sep 17 00:00:00 2001 From: Michael Stillwell Date: Tue, 9 Feb 2010 22:32:52 +0000 Subject: [PATCH 14/15] Add test: modifying event listeners during callback. --- .../test-event-emitter-modify-in-emit.js | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 test/mjsunit/test-event-emitter-modify-in-emit.js diff --git a/test/mjsunit/test-event-emitter-modify-in-emit.js b/test/mjsunit/test-event-emitter-modify-in-emit.js new file mode 100644 index 0000000000..7bb2eb3292 --- /dev/null +++ b/test/mjsunit/test-event-emitter-modify-in-emit.js @@ -0,0 +1,32 @@ +process.mixin(require("./common")); +var events = require('events'); + +var callbacks_called = [ ]; + +var e = new events.EventEmitter(); + +function callback1() { + callbacks_called.push("callback1"); + e.addListener("foo", callback2); + e.removeListener("foo", callback1); +} + +function callback2() { + callbacks_called.push("callback2"); + e.removeListener("foo", callback2); +} + +e.addListener("foo", callback1); +assert.equal(1, e.listeners("foo").length); + +e.emit("foo"); +assert.equal(1, e.listeners("foo").length); +assert.deepEqual(["callback1"], callbacks_called); + +e.emit("foo"); +assert.equal(0, e.listeners("foo").length); +assert.deepEqual(["callback1", "callback2"], callbacks_called); + +e.emit("foo"); +assert.equal(0, e.listeners("foo").length); +assert.deepEqual(["callback1", "callback2"], callbacks_called); From 3337e9da1ae6989c1815b02ad639cf9381a08db0 Mon Sep 17 00:00:00 2001 From: masuidrive Date: Wed, 10 Feb 2010 02:21:54 -0800 Subject: [PATCH 15/15] Build system: support spaces in directory names --- wscript | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/wscript b/wscript index b7cf73781d..5800ff76db 100644 --- a/wscript +++ b/wscript @@ -91,7 +91,7 @@ def conf_subproject (conf, subdir, command=None): copytree(src, default_tgt, True) if command: - if os.system("cd %s && %s" % (default_tgt, command)) != 0: + if os.system("cd \"%s\" && %s" % (default_tgt, command)) != 0: conf.fatal("Configuring %s failed." % (subdir)) debug_tgt = join(conf.blddir, "debug", subdir) @@ -191,7 +191,7 @@ def build_udns(bld): static_lib = bld.env["staticlib_PATTERN"] % "udns" - rule = 'cd %s && make' + rule = 'cd "%s" && make' default = bld.new_task_gen( target= join("deps/udns", static_lib), @@ -234,7 +234,7 @@ def v8_cmd(bld, variant): else: mode = "debug" - cmd_R = 'python %s -C %s -Y %s visibility=default mode=%s %s library=static snapshot=on' + cmd_R = 'python "%s" -C "%s" -Y "%s" visibility=default mode=%s %s library=static snapshot=on' cmd = cmd_R % ( scons , bld.srcnode.abspath(bld.env_of_name(variant))