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/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/doc/api.txt b/doc/api.txt index 12e61b02a6..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 @@ -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. @@ -205,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 @@ -653,6 +656,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). @@ -1176,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/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/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/lib/sys.js b/lib/sys.js index 18b6359722..461a12c8a1 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) { + // 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. + var keys = showHidden ? Object.getOwnPropertyNames(value).map(function (key) { + return '' + key; + }) : Object.keys(value); + var 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]'; + } + } + + var base, type, braces; + // 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,3 @@ 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; -} diff --git a/src/node.cc b/src/node.cc index e2e0b819d7..a4fd638f6c 100644 --- a/src/node.cc +++ b/src/node.cc @@ -9,6 +9,8 @@ #include #include #include /* dlopen(), dlsym() */ +#include +#include /* setuid, getuid */ #include #include @@ -468,12 +470,37 @@ 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) { - 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(); } @@ -958,6 +985,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); @@ -1170,6 +1199,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.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 216f995416..93f70b18a5 100644 --- a/src/node_file.cc +++ b/src/node_file.cc @@ -51,6 +51,7 @@ static int After(eio_req *req) { case EIO_UNLINK: case EIO_RMDIR: case EIO_MKDIR: + case EIO_FTRUNCATE: argc = 0; break; @@ -193,6 +194,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; @@ -403,6 +423,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); 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(); } diff --git a/src/node_stdio.cc b/src/node_stdio.cc index 2b1da6c055..2b5b30612c 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,19 @@ 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) { + if (errno == EAGAIN || errno == EIO) { + usleep(100); + continue; + } + return ThrowException(errno_exception(errno)); + } + written += (size_t)r; + } return Undefined(); } @@ -197,6 +219,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/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/fixtures/print-chars.js b/test/mjsunit/fixtures/print-chars.js new file mode 100644 index 0000000000..ba539ffb76 --- /dev/null +++ b/test/mjsunit/fixtures/print-chars.js @@ -0,0 +1,10 @@ +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. 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); 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 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..58c945bea9 --- /dev/null +++ b/test/mjsunit/test-stdout-flush.js @@ -0,0 +1,28 @@ +process.mixin(require("./common")); + +var sub = path.join(fixturesDir, 'print-chars.js'); + +n = 100000; + +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; + puts(count); + } +}); + +child.addListener("exit", function (data) { + assert.equal(n, count); + puts("okay"); +}); 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) +); 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 diff --git a/wscript b/wscript index 2208bb7215..31169e3db8 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 @@ -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))