diff --git a/lib/fs.js b/lib/fs.js index 39bb3777bf9035..d8bb700ba2af02 100644 --- a/lib/fs.js +++ b/lib/fs.js @@ -1562,6 +1562,133 @@ 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 ? + /(.*?)(?:[\/\\]+|$)/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; +} + +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) @@ -1571,9 +1698,139 @@ 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) { + 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) { @@ -1586,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..a67cde7a3cafda --- /dev/null +++ b/test/parallel/test-fs-realpath-subst-drive.js @@ -0,0 +1,60 @@ +'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'); + 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)); +});