From 86d4470a1553976ba68816e8c62dbce0e533322d Mon Sep 17 00:00:00 2001 From: Bartosz Sosnowski Date: Wed, 6 Jul 2016 12:59:07 +0200 Subject: [PATCH 1/3] Restore realpath javascript implementation This is a partial revert of https://github.com/nodejs/node/commit/b488b19eaf2b2e7a3ca5eccd2445e245847a5f76 It restores old javascript implementation of realpath and realpathSync. The names have been changed: added JS and not exported in fs module. --- lib/fs.js | 227 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 227 insertions(+) diff --git a/lib/fs.js b/lib/fs.js index 39bb3777bf9035..7516f19e15cba4 100644 --- a/lib/fs.js +++ b/lib/fs.js @@ -1562,6 +1562,112 @@ fs.unwatchFile = function(filename, listener) { } }; +// Regexp that finds the next partion of a (partial) path +// result is [base_with_slash, base], e.g. ['somedir/', 'somedir'] +const nextPartRe = isWindows ? + /(.*?)(?:[\/\\]+|$)/g : + /(.*?)(?:[\/]+|$)/g; + +// Regex to find the device root, including trailing slash. E.g. 'c:\\'. +const splitRootRe = isWindows ? + /^(?:[a-zA-Z]:|[\\\/]{2}[^\\\/]+[\\\/][^\\\/]+)?[\\\/]*/ : + /^[\/]*/; + +function realpathSyncJS(p, cache) { + // make p is absolute + p = pathModule.resolve(p); + + if (cache && Object.prototype.hasOwnProperty.call(cache, p)) { + return cache[p]; + } + + const original = p; + const seenLinks = {}; + const knownHard = {}; + + // current character position in p + var pos; + // the partial path so far, including a trailing slash if any + var current; + // the partial path without a trailing slash (except when pointing at a root) + var base; + // the partial path scanned in the previous round, with slash + var previous; + + start(); + + function start() { + // Skip over roots + var m = splitRootRe.exec(p); + pos = m[0].length; + current = m[0]; + base = m[0]; + previous = ''; + + // On windows, check that the root exists. On unix there is no need. + if (isWindows && !knownHard[base]) { + fs.lstatSync(base); + knownHard[base] = true; + } + } + + // walk down the path, swapping out linked pathparts for their real + // values + // NB: p.length changes. + while (pos < p.length) { + // find the next part + nextPartRe.lastIndex = pos; + var result = nextPartRe.exec(p); + previous = current; + current += result[0]; + base = previous + result[1]; + pos = nextPartRe.lastIndex; + + // continue if not a symlink + if (knownHard[base] || (cache && cache[base] === base)) { + continue; + } + + var resolvedLink; + if (cache && Object.prototype.hasOwnProperty.call(cache, base)) { + // some known symbolic link. no need to stat again. + resolvedLink = cache[base]; + } else { + var stat = fs.lstatSync(base); + if (!stat.isSymbolicLink()) { + knownHard[base] = true; + if (cache) cache[base] = base; + continue; + } + + // read the link if it wasn't read before + // dev/ino always return 0 on windows, so skip the check. + var linkTarget = null; + if (!isWindows) { + var id = stat.dev.toString(32) + ':' + stat.ino.toString(32); + if (seenLinks.hasOwnProperty(id)) { + linkTarget = seenLinks[id]; + } + } + if (linkTarget === null) { + fs.statSync(base); + linkTarget = fs.readlinkSync(base); + } + resolvedLink = pathModule.resolve(previous, linkTarget); + // track this, if given a cache. + if (cache) cache[base] = resolvedLink; + if (!isWindows) seenLinks[id] = linkTarget; + } + + // resolve the link, then start over + p = pathModule.resolve(resolvedLink, p.slice(pos)); + start(); + } + + if (cache) cache[original] = p; + + return p; +}; fs.realpathSync = function realpathSync(path, options) { if (!options) @@ -1574,6 +1680,127 @@ fs.realpathSync = function realpathSync(path, options) { return binding.realpath(pathModule._makeLong(path), options.encoding); }; +function realpathJS(p, cache, cb) { + if (typeof cb !== 'function') { + cb = maybeCallback(cache); + cache = null; + } + + // make p is absolute + p = pathModule.resolve(p); + + if (cache && Object.prototype.hasOwnProperty.call(cache, p)) { + return process.nextTick(cb.bind(null, null, cache[p])); + } + + const original = p; + const seenLinks = {}; + const knownHard = {}; + + // current character position in p + var pos; + // the partial path so far, including a trailing slash if any + var current; + // the partial path without a trailing slash (except when pointing at a root) + var base; + // the partial path scanned in the previous round, with slash + var previous; + + start(); + + function start() { + // Skip over roots + var m = splitRootRe.exec(p); + pos = m[0].length; + current = m[0]; + base = m[0]; + previous = ''; + + // On windows, check that the root exists. On unix there is no need. + if (isWindows && !knownHard[base]) { + fs.lstat(base, function(err) { + if (err) return cb(err); + knownHard[base] = true; + LOOP(); + }); + } else { + process.nextTick(LOOP); + } + } + + // walk down the path, swapping out linked pathparts for their real + // values + function LOOP() { + // stop if scanned past end of path + if (pos >= p.length) { + if (cache) cache[original] = p; + return cb(null, p); + } + + // find the next part + nextPartRe.lastIndex = pos; + var result = nextPartRe.exec(p); + previous = current; + current += result[0]; + base = previous + result[1]; + pos = nextPartRe.lastIndex; + + // continue if not a symlink + if (knownHard[base] || (cache && cache[base] === base)) { + return process.nextTick(LOOP); + } + + if (cache && Object.prototype.hasOwnProperty.call(cache, base)) { + // known symbolic link. no need to stat again. + return gotResolvedLink(cache[base]); + } + + return fs.lstat(base, gotStat); + } + + function gotStat(err, stat) { + if (err) return cb(err); + + // if not a symlink, skip to the next path part + if (!stat.isSymbolicLink()) { + knownHard[base] = true; + if (cache) cache[base] = base; + return process.nextTick(LOOP); + } + + // stat & read the link if not read before + // call gotTarget as soon as the link target is known + // dev/ino always return 0 on windows, so skip the check. + if (!isWindows) { + var id = stat.dev.toString(32) + ':' + stat.ino.toString(32); + if (seenLinks.hasOwnProperty(id)) { + return gotTarget(null, seenLinks[id], base); + } + } + fs.stat(base, function(err) { + if (err) return cb(err); + + fs.readlink(base, function(err, target) { + if (!isWindows) seenLinks[id] = target; + gotTarget(err, target); + }); + }); + } + + function gotTarget(err, target, base) { + if (err) return cb(err); + + var resolvedLink = pathModule.resolve(previous, target); + if (cache) cache[base] = resolvedLink; + gotResolvedLink(resolvedLink); + } + + function gotResolvedLink(resolvedLink) { + // resolve the link, then start over + p = pathModule.resolve(resolvedLink, p.slice(pos)); + start(); + } +}; fs.realpath = function realpath(path, options, callback) { if (!options) { From 288cd95c63e8565e9c4c76040687c47f808b12a9 Mon Sep 17 00:00:00 2001 From: Bartosz Sosnowski Date: Wed, 6 Jul 2016 13:00:37 +0200 Subject: [PATCH 2/3] win, fs: fix realpath behavior on substed drives Before v6 on drives created with subst and network mapped drives realpath used to return filename on the mapped drive. With v6 it now returns filename on the original device or on the network share. This restores the old behavior by using old javascript implementation when path returned by new implementation is on a different device than the original path. Fixes: 7294 --- lib/fs.js | 50 +++++++++++++++- test/parallel/test-fs-realpath-subst-drive.js | 58 +++++++++++++++++++ 2 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 test/parallel/test-fs-realpath-subst-drive.js diff --git a/lib/fs.js b/lib/fs.js index 7516f19e15cba4..72527e937b05be 100644 --- a/lib/fs.js +++ b/lib/fs.js @@ -1562,6 +1562,8 @@ fs.unwatchFile = function(filename, listener) { } }; +// Cache for JS real path +var realpathCache = {}; // Regexp that finds the next partion of a (partial) path // result is [base_with_slash, base], e.g. ['somedir/', 'somedir'] const nextPartRe = isWindows ? @@ -1669,6 +1671,25 @@ function realpathSyncJS(p, cache) { return p; }; +function realpathJSNeeded(resolvedPath, result, err) { + if (!isWindows || err) + return false; + const resultStr = result.toString('utf8'); + return resultStr.length > 0 && resolvedPath.length > 0 && + resultStr[0].toUpperCase() !== resolvedPath[0].toUpperCase(); +}; + +function convertRealpathJSResult(result, encoding, err) { + if (!encoding || encoding === 'utf8' || err) + return result; + const asBuffer = Buffer.from(result); + if (encoding === 'buffer') { + return asBuffer; + } else { + return asBuffer.toString(encoding); + } +}; + fs.realpathSync = function realpathSync(path, options) { if (!options) options = {}; @@ -1677,7 +1698,16 @@ fs.realpathSync = function realpathSync(path, options) { else if (typeof options !== 'object') throw new TypeError('"options" must be a string or an object'); nullCheck(path); - return binding.realpath(pathModule._makeLong(path), options.encoding); + const resolvedPath = pathModule.resolve(path.toString('utf8')); + const uvResult = binding.realpath(pathModule._makeLong(resolvedPath), + options.encoding); + if (realpathJSNeeded(resolvedPath, uvResult)) + { + const jsResult = realpathSyncJS(resolvedPath, realpathCache); + return convertRealpathJSResult(jsResult, options.encoding); + } else { + return uvResult; + } }; function realpathJS(p, cache, cb) { @@ -1813,11 +1843,25 @@ fs.realpath = function realpath(path, options, callback) { } else if (typeof options !== 'object') { throw new TypeError('"options" must be a string or an object'); } - callback = makeCallback(callback); if (!nullCheck(path, callback)) return; + + const resolvedPath = pathModule.resolve(path.toString('utf8')); + const win32callback = function(err, result) { + if (realpathJSNeeded(resolvedPath, result, err)) { + realpathJS(resolvedPath, realpathCache, function(err, result) { + result = convertRealpathJSResult(result, options.encoding, err); + return callback(err, result); + }); + } else { + return callback(err, result); + } + }; + + const use_callback = makeCallback(isWindows ? win32callback : callback); + var req = new FSReqWrap(); - req.oncomplete = callback; + req.oncomplete = use_callback; binding.realpath(pathModule._makeLong(path), options.encoding, req); return; }; diff --git a/test/parallel/test-fs-realpath-subst-drive.js b/test/parallel/test-fs-realpath-subst-drive.js new file mode 100644 index 00000000000000..293a17c82b0467 --- /dev/null +++ b/test/parallel/test-fs-realpath-subst-drive.js @@ -0,0 +1,58 @@ +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const spawnSync = require('child_process').spawnSync; + +if (!common.isWindows) { + common.skip("Test for Windows only"); + return; +} +let result; + +// create a subst drive +const driveLetters = "ABCDEFGHIJKLMNOPQRSTUWXYZ"; +let driveLetter; +for (var i = 0; i < driveLetters.length; ++i) { + driveLetter = `${driveLetters[i]}:` + result = spawnSync('subst', [driveLetter, common.fixturesDir]); + if (result.status === 0) + break; +} +if (i === driveLetters.length) { + common.skip("Cannot create subst drive"); + return; +} + +var asyncCompleted = 0; +// schedule cleanup (and check if all callbacks where called) +process.on('exit', function() { + spawnSync('subst', ['/d', driveLetter]); + assert.equal(asyncCompleted, 2); +}); + + +// test: +const filename = `${driveLetter}\\empty.js`; +const filenameBuffer = Buffer.from(filename); + +result = fs.realpathSync(filename); +assert.equal(typeof result, 'string'); +assert.equal(result, filename); + +result = fs.realpathSync(filename, 'buffer'); +assert(Buffer.isBuffer(result)); +assert(result.equals(filenameBuffer)); + +fs.realpath(filename, function(err, result) { + ++asyncCompleted; + assert(!err); + assert.equal(typeof result, 'string'); + assert.equal(result, filename); +}); + +fs.realpath(filename, 'buffer', function(err, result) { + ++asyncCompleted; + assert(!err); + assert(Buffer.isBuffer(result)); + assert(result.equals(filenameBuffer)); +}); From 91cde7749e9ea8ebb93064628552805a10797922 Mon Sep 17 00:00:00 2001 From: Bartosz Sosnowski Date: Wed, 6 Jul 2016 18:47:34 +0200 Subject: [PATCH 3/3] fixup: style --- lib/fs.js | 10 +++++----- test/parallel/test-fs-realpath-subst-drive.js | 16 +++++++++------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/lib/fs.js b/lib/fs.js index 72527e937b05be..d8bb700ba2af02 100644 --- a/lib/fs.js +++ b/lib/fs.js @@ -1562,7 +1562,7 @@ fs.unwatchFile = function(filename, listener) { } }; -// Cache for JS real path +// Cache for JS real path var realpathCache = {}; // Regexp that finds the next partion of a (partial) path // result is [base_with_slash, base], e.g. ['somedir/', 'somedir'] @@ -1669,7 +1669,7 @@ function realpathSyncJS(p, cache) { if (cache) cache[original] = p; return p; -}; +} function realpathJSNeeded(resolvedPath, result, err) { if (!isWindows || err) @@ -1677,7 +1677,7 @@ function realpathJSNeeded(resolvedPath, result, err) { const resultStr = result.toString('utf8'); return resultStr.length > 0 && resolvedPath.length > 0 && resultStr[0].toUpperCase() !== resolvedPath[0].toUpperCase(); -}; +} function convertRealpathJSResult(result, encoding, err) { if (!encoding || encoding === 'utf8' || err) @@ -1688,7 +1688,7 @@ function convertRealpathJSResult(result, encoding, err) { } else { return asBuffer.toString(encoding); } -}; +} fs.realpathSync = function realpathSync(path, options) { if (!options) @@ -1830,7 +1830,7 @@ function realpathJS(p, cache, cb) { p = pathModule.resolve(resolvedLink, p.slice(pos)); start(); } -}; +} fs.realpath = function realpath(path, options, callback) { if (!options) { diff --git a/test/parallel/test-fs-realpath-subst-drive.js b/test/parallel/test-fs-realpath-subst-drive.js index 293a17c82b0467..a67cde7a3cafda 100644 --- a/test/parallel/test-fs-realpath-subst-drive.js +++ b/test/parallel/test-fs-realpath-subst-drive.js @@ -1,33 +1,35 @@ +'use strict'; + const common = require('../common'); const assert = require('assert'); const fs = require('fs'); const spawnSync = require('child_process').spawnSync; if (!common.isWindows) { - common.skip("Test for Windows only"); + common.skip('Test for Windows only'); return; } let result; // create a subst drive -const driveLetters = "ABCDEFGHIJKLMNOPQRSTUWXYZ"; +const driveLetters = 'ABCDEFGHIJKLMNOPQRSTUWXYZ'; let driveLetter; for (var i = 0; i < driveLetters.length; ++i) { - driveLetter = `${driveLetters[i]}:` - result = spawnSync('subst', [driveLetter, common.fixturesDir]); + driveLetter = `${driveLetters[i]}:`; + result = spawnSync('subst', [driveLetter, common.fixturesDir]); if (result.status === 0) break; } if (i === driveLetters.length) { - common.skip("Cannot create subst drive"); + common.skip('Cannot create subst drive'); return; } var asyncCompleted = 0; // schedule cleanup (and check if all callbacks where called) process.on('exit', function() { - spawnSync('subst', ['/d', driveLetter]); - assert.equal(asyncCompleted, 2); + spawnSync('subst', ['/d', driveLetter]); + assert.equal(asyncCompleted, 2); });