Implement new path.join behavior

1. Express desired path.join behavior in tests.
2. Update fs.realpath to reflect new path.join behavior
3. Update url.resolve() to use new path.join behavior.
This commit is contained in:
isaacs 2010-10-26 14:41:06 -07:00 committed by Ryan Dahl
parent 25eecd179b
commit 9996b459e1
4 changed files with 184 additions and 43 deletions

View File

@ -488,8 +488,8 @@ function realpathSync (p) {
if (p.charAt(0) !== '/') { if (p.charAt(0) !== '/') {
p = path.join(process.cwd(), p); p = path.join(process.cwd(), p);
} }
p = p.split('/'); p = path.split(p);
var buf = [ '' ]; var buf = [];
var seenLinks = {}; var seenLinks = {};
var knownHard = {}; var knownHard = {};
// walk down the path, swapping out linked pathparts for their real // 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 ++) { for (var i = 0; i < p.length; i ++) {
// skip over empty path parts. // skip over empty path parts.
if (p[i] === '') continue; if (p[i] === '') continue;
var part = buf.join('/')+'/'+p[i]; var part = path.join.apply(path, buf.concat(p[i]));
if (knownHard[part]) { if (knownHard[part]) {
buf.push( p[i] ); buf.push( p[i] );
continue; continue;
@ -519,19 +519,22 @@ function realpathSync (p) {
var target = seenLinks[id]; var target = seenLinks[id];
if (target.charAt(0) === '/') { if (target.charAt(0) === '/') {
// absolute. Start over. // absolute. Start over.
buf = ['']; buf = [];
p = path.normalizeArray(target.split('/').concat(p.slice(i + 1))); p = path.normalizeArray(path.split(target).concat(p.slice(i + 1)));
i = 0; i = -1;
continue; continue;
} }
// not absolute. join and splice. // 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)); Array.prototype.splice.apply(p, [i, 1].concat(target));
p = path.normalizeArray(p); p = path.normalizeArray(p);
i = 0; i = -1;
buf = ['']; buf = [];
} }
return buf.join('/') || '/'; return path.join(buf.join('/') || '/');
} }
@ -539,15 +542,15 @@ function realpath (p, cb) {
if (p.charAt(0) !== '/') { if (p.charAt(0) !== '/') {
p = path.join(process.cwd(), p); p = path.join(process.cwd(), p);
} }
p = p.split('/'); p = path.split(p);
var buf = [ '' ]; var buf = [];
var seenLinks = {}; var seenLinks = {};
var knownHard = {}; var knownHard = {};
// walk down the path, swapping out linked pathparts for their real // walk down the path, swapping out linked pathparts for their real
// values, and pushing non-link path bits onto the buffer. // values, and pushing non-link path bits onto the buffer.
// then return the buffer. // then return the buffer.
// NB: path.length changes. // NB: path.length changes.
var i = 0; var i = -1;
var part; var part;
LOOP(); LOOP();
function LOOP () { function LOOP () {
@ -555,7 +558,7 @@ function realpath (p, cb) {
if (!(i < p.length)) return exit(); if (!(i < p.length)) return exit();
// skip over empty path parts. // skip over empty path parts.
if (p[i] === '') return process.nextTick(LOOP); if (p[i] === '') return process.nextTick(LOOP);
part = buf.join('/')+'/'+p[i]; part = path.join(buf.join('/')+'/'+p[i]);
if (knownHard[part]) { if (knownHard[part]) {
buf.push( p[i] ); buf.push( p[i] );
return process.nextTick(LOOP); return process.nextTick(LOOP);
@ -583,21 +586,24 @@ function realpath (p, cb) {
if (er) return cb(er); if (er) return cb(er);
if (target.charAt(0) === '/') { if (target.charAt(0) === '/') {
// absolute. Start over. // absolute. Start over.
buf = ['']; buf = [];
p = path.normalizeArray(target.split('/').concat(p.slice(i + 1))); p = path.normalizeArray(path.split(target).concat(p.slice(i + 1)));
i = 0; i = -1;
return process.nextTick(LOOP); return process.nextTick(LOOP);
} }
// not absolute. join and splice. // 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)); Array.prototype.splice.apply(p, [i, 1].concat(target));
p = path.normalizeArray(p); p = path.normalizeArray(p);
i = 0; i = -1;
buf = ['']; buf = [];
return process.nextTick(LOOP); return process.nextTick(LOOP);
} }
function exit () { function exit () {
cb(null, buf.join('/') || '/'); cb(null, path.join(buf.join('/') || '/'));
} }
} }

View File

@ -1,44 +1,107 @@
function validPathPart (p, keepBlanks) {
return typeof p === "string" && (p || keepBlanks);
}
exports.join = function () { 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) { // now we're fully ready to rock.
var directories = [], prev; // leading/trailing invalids have been stripped off.
for (var i = 0, l = parts.length - 1; i <= l; i++) { // 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]; var directory = parts[i];
// if it's blank, but it's not the first thing, and not the last thing, skip it. // if it's blank, and we're not keeping blanks, then skip it.
if (directory === "" && i !== 0 && i !== l && !keepBlanks) continue; if (directory === "" && !keepBlanks) continue;
// if it's a dot, and there was some previous dir already, then skip it. // if it's a dot, then skip it
if (directory === "." && prev !== undefined) continue; if (directory === "." && (
directories.length
// if it starts with "", and is a . or .., then skip it. || (i === 0 && !(tailingSlash && i === l))
if (directories.length === 1 && directories[0] === "" && ( || (i === 0 && leadingSlash)
directory === "." || directory === "..")) continue; )) 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 ( if (
directory === ".." directory === ".."
&& directories.length && directories.length
&& prev !== ".." && prev !== ".."
&& prev !== "."
&& prev !== undefined && prev !== undefined
&& (prev !== "" || keepBlanks)
) { ) {
directories.pop(); directories.pop();
prev = directories.slice(-1)[0]; prev = directories.slice(-1)[0];
} else { } else {
if (prev === ".") directories.pop();
directories.push(directory); directories.push(directory);
prev = 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; return directories;
}; };
exports.normalize = function (path, keepBlanks) { exports.normalize = function (path, keepBlanks) {
return exports.normalizeArray(path.split("/"), keepBlanks).join("/"); return exports.join(path, keepBlanks || false);
}; };
exports.dirname = function (path) { exports.dirname = function (path) {

View File

@ -272,22 +272,31 @@ function urlResolveObject (source, relative) {
else if (dir !== ".") dirs.push(dir); else if (dir !== ".") dirs.push(dir);
}); });
if (mustEndAbs && dirs[0] !== "") { if (mustEndAbs && dirs[0] !== "" && (!dirs[0] || dirs[0].charAt(0) !== "/")) {
dirs.unshift(""); dirs.unshift("");
} }
srcPath = dirs; 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 // 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); mustEndAbs = mustEndAbs || (source.host && srcPath.length);
if (mustEndAbs && srcPath[0] !== "") srcPath.unshift(""); if (mustEndAbs && !isAbsolute) {
srcPath.unshift("");
}
source.pathname = srcPath.join("/"); source.pathname = srcPath.join("/");
return source; return source;
}; };

View File

@ -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.ext.ext"), ".ext");
assert.equal(path.extname("file."), "."); assert.equal(path.extname("file."), ".");
assert.equal(path.join(".", "fixtures/b", "..", "/b/c.js"), "fixtures/b/c.js"); // path.join tests
assert.equal(path.join("/foo", "../../../bar"), "/bar"); 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"), "fixtures/b/c.js");
assert.equal(path.normalize("./fixtures///b/../b/c.js",true), "fixtures///b/c.js"); assert.equal(path.normalize("./fixtures///b/../b/c.js",true), "fixtures///b/c.js");