diff --git a/lib/fs.js b/lib/fs.js index 3afe6e93dc5..972c6edd28e 100644 --- a/lib/fs.js +++ b/lib/fs.js @@ -488,8 +488,8 @@ function realpathSync (p) { if (p.charAt(0) !== '/') { p = path.join(process.cwd(), p); } - p = p.split('/'); - var buf = [ '' ]; + p = path.split(p); + var buf = []; var seenLinks = {}; var knownHard = {}; // walk down the path, swapping out linked pathparts for their real @@ -499,7 +499,7 @@ function realpathSync (p) { for (var i = 0; i < p.length; i ++) { // skip over empty path parts. if (p[i] === '') continue; - var part = buf.join('/')+'/'+p[i]; + var part = path.join.apply(path, buf.concat(p[i])); if (knownHard[part]) { buf.push( p[i] ); continue; @@ -519,19 +519,22 @@ function realpathSync (p) { var target = seenLinks[id]; if (target.charAt(0) === '/') { // absolute. Start over. - buf = ['']; - p = path.normalizeArray(target.split('/').concat(p.slice(i + 1))); - i = 0; + buf = []; + p = path.normalizeArray(path.split(target).concat(p.slice(i + 1))); + i = -1; continue; } // not absolute. join and splice. - target = target.split('/'); + if (i === 0 && p[i].charAt(0) === "/") { + target = "/"+target; + } + target = path.split(target); Array.prototype.splice.apply(p, [i, 1].concat(target)); p = path.normalizeArray(p); - i = 0; - buf = ['']; + i = -1; + buf = []; } - return buf.join('/') || '/'; + return path.join(buf.join('/') || '/'); } @@ -539,15 +542,15 @@ function realpath (p, cb) { if (p.charAt(0) !== '/') { p = path.join(process.cwd(), p); } - p = p.split('/'); - var buf = [ '' ]; + p = path.split(p); + var buf = []; var seenLinks = {}; var knownHard = {}; // walk down the path, swapping out linked pathparts for their real // values, and pushing non-link path bits onto the buffer. // then return the buffer. // NB: path.length changes. - var i = 0; + var i = -1; var part; LOOP(); function LOOP () { @@ -555,7 +558,7 @@ function realpath (p, cb) { if (!(i < p.length)) return exit(); // skip over empty path parts. if (p[i] === '') return process.nextTick(LOOP); - part = buf.join('/')+'/'+p[i]; + part = path.join(buf.join('/')+'/'+p[i]); if (knownHard[part]) { buf.push( p[i] ); return process.nextTick(LOOP); @@ -583,21 +586,24 @@ function realpath (p, cb) { if (er) return cb(er); if (target.charAt(0) === '/') { // absolute. Start over. - buf = ['']; - p = path.normalizeArray(target.split('/').concat(p.slice(i + 1))); - i = 0; + buf = []; + p = path.normalizeArray(path.split(target).concat(p.slice(i + 1))); + i = -1; return process.nextTick(LOOP); } // not absolute. join and splice. - target = target.split('/'); + if (i === 0 && p[i].charAt(0) === "/") { + target = "/"+target; + } + target = path.split(target); Array.prototype.splice.apply(p, [i, 1].concat(target)); p = path.normalizeArray(p); - i = 0; - buf = ['']; + i = -1; + buf = []; return process.nextTick(LOOP); } function exit () { - cb(null, buf.join('/') || '/'); + cb(null, path.join(buf.join('/') || '/')); } } diff --git a/lib/path.js b/lib/path.js index d3a2ea698fc..5eea05f7820 100644 --- a/lib/path.js +++ b/lib/path.js @@ -1,44 +1,107 @@ +function validPathPart (p, keepBlanks) { + return typeof p === "string" && (p || keepBlanks); +} + exports.join = function () { - return exports.normalize(Array.prototype.join.call(arguments, "/")); + var args = Array.prototype.slice.call(arguments); + // edge case flag to switch into url-resolve-mode + var keepBlanks = false; + if (args[ args.length - 1 ] === true) { + keepBlanks = args.pop(); + } + // return exports.split(args.join("/"), keepBlanks).join("/"); + var joined = exports.normalizeArray(args, keepBlanks).join("/"); + return joined; }; +exports.split = function (path, keepBlanks) { + // split based on /, but only if that / is not at the start or end. + return exports.normalizeArray(path.split(/^|\/(?!$)/), keepBlanks); +}; +function cleanArray (parts, keepBlanks) { + var i = 0; + var l = parts.length - 1; + var stripped = false; + // strip leading empty args + while (i < l && !validPathPart(parts[i], keepBlanks)) { + stripped = true; + i ++; + } + // strip tailing empty args + while (l >= i && !validPathPart(parts[l], keepBlanks)) { + stripped = true; + l --; + } + if (stripped) { + // if l chopped all the way back to i, then this is empty + parts = Array.prototype.slice.call(parts, i, l + 1); + } + return parts.filter(function (p) { return validPathPart(p, keepBlanks) }) + .join('/') + .split(/^|\/(?!$)/); +} +exports.normalizeArray = function (original, keepBlanks) { + var parts = cleanArray(original, keepBlanks); + if (!parts.length || (parts.length === 1 && !parts[0])) return ["."]; -exports.normalizeArray = function (parts, keepBlanks) { - var directories = [], prev; - for (var i = 0, l = parts.length - 1; i <= l; i++) { + // now we're fully ready to rock. + // leading/trailing invalids have been stripped off. + // if it comes in starting with a slash, or ending with a slash, + var leadingSlash = (parts[0].charAt(0) === "/"); + if (leadingSlash) parts[0] = parts[0].substr(1); + var last = parts.slice(-1)[0]; + var tailingSlash = (last.substr(-1) === "/"); + if (tailingSlash) parts[parts.length - 1] = last.slice(0, -1); + var directories = []; + var prev; + for (var i = 0, l = parts.length - 1 ; i <= l ; i ++) { var directory = parts[i]; - // if it's blank, but it's not the first thing, and not the last thing, skip it. - if (directory === "" && i !== 0 && i !== l && !keepBlanks) continue; + // if it's blank, and we're not keeping blanks, then skip it. + if (directory === "" && !keepBlanks) continue; - // if it's a dot, and there was some previous dir already, then skip it. - if (directory === "." && prev !== undefined) continue; - - // if it starts with "", and is a . or .., then skip it. - if (directories.length === 1 && directories[0] === "" && ( - directory === "." || directory === "..")) continue; + // if it's a dot, then skip it + if (directory === "." && ( + directories.length + || (i === 0 && !(tailingSlash && i === l)) + || (i === 0 && leadingSlash) + )) continue; + // if we're dealing with an absolute path, then discard ..s that go + // above that the base. + if (leadingSlash && directories.length === 0 && directory === "..") { + continue; + } + // trying to go up a dir if ( directory === ".." && directories.length && prev !== ".." - && prev !== "." && prev !== undefined - && (prev !== "" || keepBlanks) ) { directories.pop(); prev = directories.slice(-1)[0]; } else { - if (prev === ".") directories.pop(); directories.push(directory); prev = directory; } } + if (!directories.length) { + directories = [ leadingSlash || tailingSlash ? "" : "." ]; + } + var last = directories.slice(-1)[0]; + if (tailingSlash && last.substr(-1) !== "/") { + directories[directories.length-1] += "/"; + } + if (leadingSlash && directories[0].charAt(0) !== "/") { + if (directories[0] === ".") directories[0] = ""; + directories[0] = "/" + directories[0]; + } return directories; }; exports.normalize = function (path, keepBlanks) { - return exports.normalizeArray(path.split("/"), keepBlanks).join("/"); + return exports.join(path, keepBlanks || false); }; exports.dirname = function (path) { diff --git a/lib/url.js b/lib/url.js index 2a9a5cfd703..2dcb18e1323 100644 --- a/lib/url.js +++ b/lib/url.js @@ -272,22 +272,31 @@ function urlResolveObject (source, relative) { else if (dir !== ".") dirs.push(dir); }); - if (mustEndAbs && dirs[0] !== "") { + if (mustEndAbs && dirs[0] !== "" && (!dirs[0] || dirs[0].charAt(0) !== "/")) { dirs.unshift(""); } srcPath = dirs; } - if (hasTrailingSlash && (srcPath.length < 2 || srcPath.slice(-1)[0] !== "")) srcPath.push(""); + if (hasTrailingSlash && (srcPath.join("/").substr(-1) !== "/")) { + srcPath.push(""); + } + + var isAbsolute = srcPath[0] === "" || (srcPath[0] && srcPath[0].charAt(0) === "/"); // put the host back - if ( psychotic ) source.host = srcPath[0] === "" ? "" : srcPath.shift(); + if ( psychotic ) { + source.host = isAbsolute ? "" : srcPath.shift(); + } mustEndAbs = mustEndAbs || (source.host && srcPath.length); - if (mustEndAbs && srcPath[0] !== "") srcPath.unshift(""); + if (mustEndAbs && !isAbsolute) { + srcPath.unshift(""); + } source.pathname = srcPath.join("/"); + return source; }; diff --git a/test/simple/test-path.js b/test/simple/test-path.js index 6570732ab32..e91597bbbba 100644 --- a/test/simple/test-path.js +++ b/test/simple/test-path.js @@ -37,8 +37,71 @@ assert.equal(path.extname(".path/file.ext"), ".ext"); assert.equal(path.extname("file.ext.ext"), ".ext"); assert.equal(path.extname("file."), "."); -assert.equal(path.join(".", "fixtures/b", "..", "/b/c.js"), "fixtures/b/c.js"); -assert.equal(path.join("/foo", "../../../bar"), "/bar"); +// path.join tests +var failures = []; +var joinTests = + // arguments result + [[['.', 'x/b', '..', '/b/c.js' ], 'x/b/c.js' ] + ,[['/.', 'x/b', '..', '/b/c.js' ], '/x/b/c.js' ] + ,[['/foo', '../../../bar' ], '/bar' ] + ,[['foo', '../../../bar' ], '../../bar' ] + ,[['foo/', '../../../bar' ], '../../bar' ] + ,[['foo/x', '../../../bar' ], '../bar' ] + ,[['foo/x', './bar' ], 'foo/x/bar' ] + ,[['foo/x/', './bar' ], 'foo/x/bar' ] + ,[['foo/x/', '.', 'bar' ], 'foo/x/bar' ] + ,[['./' ], './' ] + ,[['.', './' ], './' ] + ,[['.', '.', '.' ], '.' ] + ,[['.', './', '.' ], '.' ] + ,[['.', '/./', '.' ], '.' ] + ,[['.', '/////./', '.' ], '.' ] + ,[['.' ], '.' ] + ,[['','.' ], '.' ] + ,[['', 'foo' ], 'foo' ] + ,[['foo', '/bar' ], 'foo/bar' ] + ,[['', '/foo' ], '/foo' ] + ,[['', '', '/foo' ], '/foo' ] + ,[['', '', 'foo' ], 'foo' ] + ,[['foo', '' ], 'foo' ] + ,[['foo/', '' ], 'foo/' ] + ,[['foo', '', '/bar' ], 'foo/bar' ] + ,[['./', '..', '/foo' ], '../foo' ] + ,[['./', '..', '..', '/foo' ], '../../foo' ] + ,[['.', '..', '..', '/foo' ], '../../foo' ] + ,[['', '..', '..', '/foo' ], '../../foo' ] + ,[['/' ], '/' ] + ,[['/', '.' ], '/' ] + ,[['/', '..' ], '/' ] + ,[['/', '..', '..' ], '/' ] + ,[['' ], '.' ] + ,[['', '' ], '.' ] + ,[[' /foo' ], ' /foo' ] + ,[[' ', 'foo' ], ' /foo' ] + ,[[' ', '.' ], ' ' ] + ,[[' ', '/' ], ' /' ] + ,[[' ', '' ], ' ' ] + // preserving empty path parts, for url resolution case + // pass boolean true as LAST argument. + ,[['', '', true ], '/' ] + ,[['foo', '', true ], 'foo/' ] + ,[['foo', '', 'bar', true ], 'foo//bar' ] + ,[['foo/', '', 'bar', true ], 'foo///bar' ] + ,[['', true ], '.' ] + // filtration of non-strings. + ,[['x', true, 7, 'y', null, {} ], 'x/y' ] + ]; +joinTests.forEach(function (test) { + var actual = path.join.apply(path, test[0]); + var expected = test[1]; + var message = "path.join("+test[0].map(JSON.stringify).join(",")+")" + + "\n expect="+JSON.stringify(expected) + + "\n actual="+JSON.stringify(actual); + if (actual !== expected) failures.push("\n"+message); + // assert.equal(actual, expected, message); +}); +assert.equal(failures.length, 0, failures.join("")) + assert.equal(path.normalize("./fixtures///b/../b/c.js"), "fixtures/b/c.js"); assert.equal(path.normalize("./fixtures///b/../b/c.js",true), "fixtures///b/c.js");