Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

path: fix win32 volume-relative paths #14440

Closed
wants to merge 1 commit into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 25 additions & 20 deletions lib/path.js
Original file line number Diff line number Diff line change
@@ -908,14 +908,28 @@ const win32 = {

extname: function extname(path) {
assertPath(path);
var start = 0;
var startDot = -1;
var startPart = 0;
var end = -1;
var matchedSlash = true;
// Track the state of characters (if any) we see before our first dot and
// after any path separator we find
var preDotState = 0;
for (var i = path.length - 1; i >= 0; --i) {

// Check for a drive letter prefix so as not to mistake the following
// path separator as an extra separator at the end of the path that can be
// disregarded
if (path.length >= 2) {
const code = path.charCodeAt(0);
if (path.charCodeAt(1) === 58/*:*/ &&
((code >= 65/*A*/ && code <= 90/*Z*/) ||
(code >= 97/*a*/ && code <= 122/*z*/))) {
start = startPart = 2;
}
}

for (var i = path.length - 1; i >= start; --i) {
const code = path.charCodeAt(i);
if (code === 47/*/*/ || code === 92/*\*/) {
// If we reached a path separator that was not part of a set of path
@@ -978,15 +992,12 @@ const win32 = {
var len = path.length;
var rootEnd = 0;
var code = path.charCodeAt(0);
var isAbsolute = false;

// Try to match a root
if (len > 1) {
if (code === 47/*/*/ || code === 92/*\*/) {
// Possible UNC root

isAbsolute = true;

code = path.charCodeAt(1);
rootEnd = 1;
if (code === 47/*/*/ || code === 92/*\*/) {
@@ -1045,7 +1056,6 @@ const win32 = {
ret.root = ret.dir = path;
return ret;
}
isAbsolute = true;
rootEnd = 3;
}
} else {
@@ -1067,7 +1077,7 @@ const win32 = {
ret.root = path.slice(0, rootEnd);

var startDot = -1;
var startPart = 0;
var startPart = rootEnd;
var end = -1;
var matchedSlash = true;
var i = path.length - 1;
@@ -1116,26 +1126,21 @@ const win32 = {
startDot === end - 1 &&
startDot === startPart + 1)) {
if (end !== -1) {
if (startPart === 0 && isAbsolute)
ret.base = ret.name = path.slice(rootEnd, end);
else
ret.base = ret.name = path.slice(startPart, end);
ret.base = ret.name = path.slice(startPart, end);
}
} else {
if (startPart === 0 && isAbsolute) {
ret.name = path.slice(rootEnd, startDot);
ret.base = path.slice(rootEnd, end);
} else {
ret.name = path.slice(startPart, startDot);
ret.base = path.slice(startPart, end);
}
ret.name = path.slice(startPart, startDot);
ret.base = path.slice(startPart, end);
ret.ext = path.slice(startDot, end);
}

if (startPart > 0)
// If the directory is the root, use the entire root as the `dir` including
// the trailing slash if any (`C:\abc` -> `C:\`). Otherwise, strip out the
// trailing slash (`C:\abc\def` -> `C:\abc`).
if (startPart > 0 && startPart !== rootEnd)
ret.dir = path.slice(0, startPart - 1);
else if (isAbsolute)
ret.dir = path.slice(0, rootEnd);
else
ret.dir = ret.root;

return ret;
},
82 changes: 45 additions & 37 deletions test/parallel/test-path-parse-format.js
Original file line number Diff line number Diff line change
@@ -25,28 +25,33 @@ const assert = require('assert');
const path = require('path');

const winPaths = [
'C:\\path\\dir\\index.html',
'C:\\another_path\\DIR\\1\\2\\33\\\\index',
'another_path\\DIR with spaces\\1\\2\\33\\index',
'\\foo\\C:',
'file',
'.\\file',
'C:\\',
'C:',
'\\',
'',
// [path, root]
['C:\\path\\dir\\index.html', 'C:\\'],
['C:\\another_path\\DIR\\1\\2\\33\\\\index', 'C:\\'],
['another_path\\DIR with spaces\\1\\2\\33\\index', ''],
['\\', '\\'],
['\\foo\\C:', '\\'],
['file', ''],
['file:stream', ''],
['.\\file', ''],
['C:', 'C:'],
['C:.', 'C:'],
['C:..', 'C:'],
['C:abc', 'C:'],
['C:\\', 'C:\\'],
['C:\\abc', 'C:\\' ],
['', ''],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add tests for NTFS alterate streams? e.g. ['cd:foo', '']. Not just to test root, but also the other properties used in checkParseFormat.


// unc
'\\\\server\\share\\file_path',
'\\\\server two\\shared folder\\file path.zip',
'\\\\teela\\admin$\\system32',
'\\\\?\\UNC\\server\\share'
['\\\\server\\share\\file_path', '\\\\server\\share\\'],
['\\\\server two\\shared folder\\file path.zip',
'\\\\server two\\shared folder\\'],
['\\\\teela\\admin$\\system32', '\\\\teela\\admin$\\'],
['\\\\?\\UNC\\server\\share', '\\\\?\\UNC\\']
];

const winSpecialCaseParseTests = [
['/foo/bar', { root: '/' }],
['C:', { root: 'C:', dir: 'C:', base: '' }],
['C:\\', { root: 'C:\\', dir: 'C:\\', base: '' }]
];

const winSpecialCaseFormatTests = [
@@ -60,26 +65,27 @@ const winSpecialCaseFormatTests = [
];

const unixPaths = [
'/home/user/dir/file.txt',
'/home/user/a dir/another File.zip',
'/home/user/a dir//another&File.',
'/home/user/a$$$dir//another File.zip',
'user/dir/another File.zip',
'file',
'.\\file',
'./file',
'C:\\foo',
'/',
'',
'.',
'..',
'/foo',
'/foo.',
'/foo.bar',
'/.',
'/.foo',
'/.foo.bar',
'/foo/bar.baz',
// [path, root]
['/home/user/dir/file.txt', '/'],
['/home/user/a dir/another File.zip', '/'],
['/home/user/a dir//another&File.', '/'],
['/home/user/a$$$dir//another File.zip', '/'],
['user/dir/another File.zip', ''],
['file', ''],
['.\\file', ''],
['./file', ''],
['C:\\foo', ''],
['/', '/'],
['', ''],
['.', ''],
['..', ''],
['/foo', '/'],
['/foo.', '/'],
['/foo.bar', '/'],
['/.', '/'],
['/.foo', '/'],
['/.foo.bar', '/'],
['/foo/bar.baz', '/']
];

const unixSpecialCaseFormatTests = [
@@ -182,14 +188,16 @@ function checkErrors(path) {
}

function checkParseFormat(path, paths) {
paths.forEach(function(element) {
paths.forEach(function([element, root]) {
const output = path.parse(element);
assert.strictEqual(typeof output.root, 'string');
assert.strictEqual(typeof output.dir, 'string');
assert.strictEqual(typeof output.base, 'string');
assert.strictEqual(typeof output.ext, 'string');
assert.strictEqual(typeof output.name, 'string');
assert.strictEqual(path.format(output), element);
assert.strictEqual(output.root, root);
assert(output.dir.startsWith(output.root));
assert.strictEqual(output.dir, output.dir ? path.dirname(element) : '');
assert.strictEqual(output.base, path.basename(element));
assert.strictEqual(output.ext, path.extname(element));
28 changes: 27 additions & 1 deletion test/parallel/test-path.js
Original file line number Diff line number Diff line change
@@ -72,6 +72,16 @@ assert.strictEqual(path.win32.basename('aaa\\bbb', 'bbb'), 'bbb');
assert.strictEqual(path.win32.basename('aaa\\bbb\\\\\\\\', 'bbb'), 'bbb');
assert.strictEqual(path.win32.basename('aaa\\bbb', 'bb'), 'b');
assert.strictEqual(path.win32.basename('aaa\\bbb', 'b'), 'bb');
assert.strictEqual(path.win32.basename('C:'), '');
assert.strictEqual(path.win32.basename('C:.'), '.');
assert.strictEqual(path.win32.basename('C:\\'), '');
assert.strictEqual(path.win32.basename('C:\\dir\\base.ext'), 'base.ext');
assert.strictEqual(path.win32.basename('C:\\basename.ext'), 'basename.ext');
assert.strictEqual(path.win32.basename('C:basename.ext'), 'basename.ext');
assert.strictEqual(path.win32.basename('C:basename.ext\\'), 'basename.ext');
assert.strictEqual(path.win32.basename('C:basename.ext\\\\'), 'basename.ext');
assert.strictEqual(path.win32.basename('C:foo'), 'foo');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, a test for NTFS alternate streams would be nice: basename('cd:foo') === 'cd:foo'

assert.strictEqual(path.win32.basename('file:stream'), 'file:stream');

// On unix a backslash is just treated as any other character.
assert.strictEqual(path.posix.basename('\\dir\\basename.ext'),
@@ -120,6 +130,8 @@ assert.strictEqual(path.win32.dirname('c:foo\\'), 'c:');
assert.strictEqual(path.win32.dirname('c:foo\\bar'), 'c:foo');
assert.strictEqual(path.win32.dirname('c:foo\\bar\\'), 'c:foo');
assert.strictEqual(path.win32.dirname('c:foo\\bar\\baz'), 'c:foo\\bar');
assert.strictEqual(path.win32.dirname('file:stream'), '.');
assert.strictEqual(path.win32.dirname('dir\\file:stream'), 'dir');
assert.strictEqual(path.win32.dirname('\\\\unc\\share'),
'\\\\unc\\share');
assert.strictEqual(path.win32.dirname('\\\\unc\\share\\foo'),
@@ -187,6 +199,7 @@ assert.strictEqual(path.win32.dirname('foo'), '.');
['file./', '.'],
['file.//', '.'],
].forEach((test) => {
const expected = test[1];
[path.posix.extname, path.win32.extname].forEach((extname) => {
let input = test[0];
let os;
@@ -197,12 +210,19 @@ assert.strictEqual(path.win32.dirname('foo'), '.');
os = 'posix';
}
const actual = extname(input);
const expected = test[1];
const message = `path.${os}.extname(${JSON.stringify(input)})\n expect=${
JSON.stringify(expected)}\n actual=${JSON.stringify(actual)}`;
if (actual !== expected)
failures.push(`\n${message}`);
});
{
const input = `C:${test[0].replace(slashRE, '\\')}`;
const actual = path.win32.extname(input);
const message = `path.win32.extname(${JSON.stringify(input)})\n expect=${
JSON.stringify(expected)}\n actual=${JSON.stringify(actual)}`;
if (actual !== expected)
failures.push(`\n${message}`);
}
});
assert.strictEqual(failures.length, 0, failures.join(''));

@@ -406,6 +426,12 @@ assert.strictEqual(path.win32.normalize('a//b//.'), 'a\\b');
assert.strictEqual(path.win32.normalize('//server/share/dir/file.ext'),
'\\\\server\\share\\dir\\file.ext');
assert.strictEqual(path.win32.normalize('/a/b/c/../../../x/y/z'), '\\x\\y\\z');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional:

Could you parameterize this part like the others are:

[[a,b]].forEach(([tested, expected]) => ...

(not the scope of this PR but still would be nice)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather leave that to a dedicated PR, as there are many other tests that do not use this pattern in the file.

assert.strictEqual(path.win32.normalize('C:'), 'C:.');
assert.strictEqual(path.win32.normalize('C:..\\abc'), 'C:..\\abc');
assert.strictEqual(path.win32.normalize('C:..\\..\\abc\\..\\def'),
'C:..\\..\\def');
assert.strictEqual(path.win32.normalize('C:\\.'), 'C:\\');
assert.strictEqual(path.win32.normalize('file:stream'), 'file:stream');

assert.strictEqual(path.posix.normalize('./fixtures///b/../b/c.js'),
'fixtures/b/c.js');