Skip to content

Commit

Permalink
fs: restore JS implementation of realpath
Browse files Browse the repository at this point in the history
This reverts parts of b488b19
restoring javascript implementation of realpath and realpathSync.

Fixes: #7175
Fixes: #6861
Fixes: #7294
Fixes: #7192
Fixes: #7044
Fixes: #6624
Fixes: #6978
PR-URL: #7899
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
  • Loading branch information
bzoz authored and cjihrig committed Aug 15, 2016
1 parent 60d6e04 commit 18a3064
Show file tree
Hide file tree
Showing 4 changed files with 360 additions and 14 deletions.
8 changes: 6 additions & 2 deletions doc/api/fs.md
Original file line number Diff line number Diff line change
Expand Up @@ -1222,6 +1222,8 @@ added: v0.1.31
Asynchronous realpath(3). The `callback` gets two arguments `(err,
resolvedPath)`. May use `process.cwd` to resolve relative paths.

Only paths that can be converted to UTF8 strings are supported.

The optional `options` argument can be a string specifying an encoding, or an
object with an `encoding` property specifying the character encoding to use for
the path passed to the callback. If the `encoding` is set to `'buffer'`,
Expand All @@ -1238,10 +1240,12 @@ added: v0.1.31

Synchronous realpath(3). Returns the resolved path.

Only paths that can be converted to UTF8 strings are supported.

The optional `options` argument can be a string specifying an encoding, or an
object with an `encoding` property specifying the character encoding to use for
the path passed to the callback. If the `encoding` is set to `'buffer'`,
the path returned will be passed as a `Buffer` object.
the returned value. If the `encoding` is set to `'buffer'`, the path returned
will be passed as a `Buffer` object.

## fs.rename(oldPath, newPath, callback)
<!-- YAML
Expand Down
225 changes: 213 additions & 12 deletions lib/fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -1563,38 +1563,239 @@ fs.unwatchFile = function(filename, listener) {
};


fs.realpathSync = function realpathSync(path, options) {
// Regexp that finds the next portion 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 encodeRealpathResult(result, options, err) {
if (!options || !options.encoding || options.encoding === 'utf8' || err)
return result;
const asBuffer = Buffer.from(result);
if (options.encoding === 'buffer') {
return asBuffer;
} else {
return asBuffer.toString(options.encoding);
}
}

fs.realpathSync = function realpathSync(p, options) {
if (!options)
options = {};
else if (typeof options === 'string')
options = {encoding: 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);
nullCheck(p);

p = p.toString('utf8');
p = pathModule.resolve(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]) {
continue;
}

var resolvedLink;
var stat = fs.lstatSync(base);
if (!stat.isSymbolicLink()) {
knownHard[base] = true;
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);

if (!isWindows) seenLinks[id] = linkTarget;

// resolve the link, then start over
p = pathModule.resolve(resolvedLink, p.slice(pos));
start();
}

return encodeRealpathResult(p, options);
};


fs.realpath = function realpath(path, options, callback) {
fs.realpath = function realpath(p, options, callback) {
if (typeof callback !== 'function') {
callback = maybeCallback(options);
options = {};
}

if (!options) {
options = {};
} else if (typeof options === 'function') {
callback = options;
options = {};
} else if (typeof options === 'string') {
options = {encoding: options};
} else if (typeof options !== 'object') {
throw new TypeError('"options" must be a string or an object');
}
callback = makeCallback(callback);
if (!nullCheck(path, callback))
if (!nullCheck(p, callback))
return;
var req = new FSReqWrap();
req.oncomplete = callback;
binding.realpath(pathModule._makeLong(path), options.encoding, req);
return;
};

p = p.toString('utf8');
p = pathModule.resolve(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 callback(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) {
return callback(null, encodeRealpathResult(p, options));
}

// 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]) {
return process.nextTick(LOOP);
}

return fs.lstat(base, gotStat);
}

function gotStat(err, stat) {
if (err) return callback(err);

// if not a symlink, skip to the next path part
if (!stat.isSymbolicLink()) {
knownHard[base] = true;
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 callback(err);

fs.readlink(base, function(err, target) {
if (!isWindows) seenLinks[id] = target;
gotTarget(err, target);
});
});
}

function gotTarget(err, target, base) {
if (err) return callback(err);

var resolvedLink = pathModule.resolve(previous, target);
gotResolvedLink(resolvedLink);
}

function gotResolvedLink(resolvedLink) {
// resolve the link, then start over
p = pathModule.resolve(resolvedLink, p.slice(pos));
start();
}
};

fs.mkdtemp = function(prefix, options, callback) {
if (!prefix || typeof prefix !== 'string')
Expand Down
88 changes: 88 additions & 0 deletions test/parallel/test-fs-realpath-buffer-encoding.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const fs = require('fs');

const string_dir = fs.realpathSync(common.fixturesDir);
const buffer_dir = Buffer.from(string_dir);

const encodings = ['ascii', 'utf8', 'utf16le', 'ucs2',
'base64', 'binary', 'hex'];
var expected = {};
encodings.forEach((encoding) => {
expected[encoding] = buffer_dir.toString(encoding);
});


// test sync version
for (var encoding in expected) {
const expected_value = expected[encoding];
let result;

result = fs.realpathSync(string_dir, {encoding: encoding});
assert.strictEqual(result, expected_value);

result = fs.realpathSync(string_dir, encoding);
assert.strictEqual(result, expected_value);

result = fs.realpathSync(buffer_dir, {encoding: encoding});
assert.strictEqual(result, expected_value);

result = fs.realpathSync(buffer_dir, encoding);
assert.strictEqual(result, expected_value);
}

let buffer_result;
buffer_result = fs.realpathSync(string_dir, {encoding: 'buffer'});
assert.deepStrictEqual(buffer_result, buffer_dir);

buffer_result = fs.realpathSync(string_dir, 'buffer');
assert.deepStrictEqual(buffer_result, buffer_dir);

buffer_result = fs.realpathSync(buffer_dir, {encoding: 'buffer'});
assert.deepStrictEqual(buffer_result, buffer_dir);

buffer_result = fs.realpathSync(buffer_dir, 'buffer');
assert.deepStrictEqual(buffer_result, buffer_dir);

// test async version
for (encoding in expected) {
const expected_value = expected[encoding];

fs.realpath(string_dir, {encoding: encoding}, common.mustCall((err, res) => {
assert(!err);
assert.strictEqual(res, expected_value);
}));
fs.realpath(string_dir, encoding, common.mustCall((err, res) => {
assert(!err);
assert.strictEqual(res, expected_value);
}));
fs.realpath(buffer_dir, {encoding: encoding}, common.mustCall((err, res) => {
assert(!err);
assert.strictEqual(res, expected_value);
}));
fs.realpath(buffer_dir, encoding, common.mustCall((err, res) => {
assert(!err);
assert.strictEqual(res, expected_value);
}));
}

fs.realpath(string_dir, {encoding: 'buffer'}, common.mustCall((err, res) => {
assert(!err);
assert.deepStrictEqual(res, buffer_dir);
}));

fs.realpath(string_dir, 'buffer', common.mustCall((err, res) => {
assert(!err);
assert.deepStrictEqual(res, buffer_dir);
}));

fs.realpath(buffer_dir, {encoding: 'buffer'}, common.mustCall((err, res) => {
assert(!err);
assert.deepStrictEqual(res, buffer_dir);
}));

fs.realpath(buffer_dir, 'buffer', common.mustCall((err, res) => {
assert(!err);
assert.deepStrictEqual(res, buffer_dir);
}));
Loading

0 comments on commit 18a3064

Please sign in to comment.