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) !== '/') {
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('/') || '/'));
}
}

View File

@ -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) {

View File

@ -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;
};

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."), ".");
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");