Skip to content

Commit

Permalink
path: fix win32 volume-relative paths
Browse files Browse the repository at this point in the history
`path.resolve()` and `path.join()` are left alone in this commit due to
the lack of clear semantics.

Backport-PR-URL: #14787
PR-URL: #14440
Fixes: #14405
Reviewed-By: Refael Ackermann <[email protected]>
Reviewed-By: Tobias Nießen <[email protected]>
  • Loading branch information
TimothyGu authored and MylesBorins committed Aug 16, 2017
1 parent c2a0a93 commit eefd322
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 58 deletions.
45 changes: 25 additions & 20 deletions lib/path.js
Original file line number Diff line number Diff line change
Expand Up @@ -885,14 +885,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
Expand Down Expand Up @@ -956,15 +970,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/*\*/) {
Expand Down Expand Up @@ -1023,7 +1034,6 @@ const win32 = {
ret.root = ret.dir = path.slice(0, 3);
return ret;
}
isAbsolute = true;
rootEnd = 3;
}
} else {
Expand All @@ -1045,7 +1055,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;
Expand Down Expand Up @@ -1094,26 +1104,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;
},
Expand Down
82 changes: 45 additions & 37 deletions test/parallel/test-path-parse-format.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,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:\\' ],
['', ''],

// 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 = [
Expand All @@ -39,26 +44,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 = [
Expand Down Expand Up @@ -169,14 +175,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));
Expand Down
28 changes: 27 additions & 1 deletion test/parallel/test-path.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,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');
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'),
Expand Down Expand Up @@ -99,6 +109,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'),
Expand Down Expand Up @@ -166,6 +178,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;
Expand All @@ -176,12 +189,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(''));

Expand Down Expand Up @@ -385,6 +405,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');
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');
Expand Down

0 comments on commit eefd322

Please sign in to comment.