Fix issue #262. Allow fs.realpath to traverse above the current working directory.
authorisaacs <i@izs.me>
Thu, 9 Sep 2010 00:25:59 +0000 (17:25 -0700)
committerRyan Dahl <ry@tinyclouds.org>
Thu, 9 Sep 2010 00:35:58 +0000 (17:35 -0700)
lib/fs.js
test/simple/test-fs-realpath.js

index f164a45..5cdbb1c 100644 (file)
--- a/lib/fs.js
+++ b/lib/fs.js
@@ -469,132 +469,119 @@ var path = require('path');
 var normalize = path.normalize;
 var normalizeArray = path.normalizeArray;
 
-fs.realpathSync = function (path) {
-  var seen_links = {}, knownHards = {}, buf, i = 0, part, x, stats;
-  if (path.charAt(0) !== '/') {
-    var cwd = process.cwd().split('/');
-    path = cwd.concat(path.split('/'));
-    path = normalizeArray(path);
-    i = cwd.length;
-    buf = [].concat(cwd);
-  } else {
-    path = normalizeArray(path.split('/'));
-    buf = [''];
-  }
-  for (; i<path.length; i++) {
-    part = path.slice(0, i+1).join('/');
-    if (part.length !== 0) {
-      if (part in knownHards) {
-        buf.push(path[i]);
-      } else {
-        stats = fs.lstatSync(part);
-        if (stats.isSymbolicLink()) {
-          x = stats.dev.toString(32)+":"+stats.ino.toString(32);
-          if (x in seen_links)
-            throw new Error("cyclic link at "+part);
-          seen_links[x] = true;
-          part = fs.readlinkSync(part);
-          if (part.charAt(0) === '/') {
-            // absolute
-            path = normalizeArray(part.split('/'));
-            buf = [''];
-            i = 0;
-          } else {
-            // relative
-            Array.prototype.splice.apply(path, [i, 1].concat(part.split('/')));
-            part = normalizeArray(path);
-            var y = 0, L = Math.max(path.length, part.length), delta;
-            for (; y<L && path[y] === part[y]; y++);
-            if (y !== L) {
-              path = part;
-              delta = i-y;
-              i = y-1;
-              if (delta > 0) buf.splice(y, delta);
-            } else {
-              i--;
-            }
-          }
-        } else {
-          buf.push(path[i]);
-          knownHards[buf.join('/')] = true;
-        }
-      }
+// realpath
+// Not using realpath(2) because it's bad.
+// See: http://insanecoding.blogspot.com/2007/11/pathmax-simply-isnt.html
+fs.realpathSync = realpathSync;
+fs.realpath = realpath;
+function realpathSync (p) {
+  if (p.charAt(0) !== '/') {
+    p = path.join(process.cwd(), p);
+  }
+  p = p.split('/');
+  var buf = [ '' ];
+  var seenLinks = {};
+  var knownHard = {};
+  // walk down the path, swapping out linked pathparts for their real
+  // values, and pushing non-link path bits onto the buffer.
+  // then return the buffer.
+  // NB: path.length changes.
+  for (var i = 0; i < p.length; i ++) {
+    // skip over empty path parts.
+    if (p[i] === '') continue;
+    var part = buf.join('/')+'/'+p[i];
+    if (knownHard[part]) {
+      buf.push( p[i] );
+      continue;
+    }
+    var stat = fs.lstatSync(part);
+    if (!stat.isSymbolicLink()) {
+      // not a symlink. easy.
+      knownHard[ part ] = true;
+      buf.push(p[i]);
+      continue;
     }
+    var id = stat.dev.toString(32)+':'+stat.ino.toString(32);
+    if (seenLinks[id]) throw new Error("cyclic link at "+part);
+    seenLinks[id] = true;
+    var target = fs.readlinkSync(part);
+    if (target.charAt(0) === '/') {
+      // absolute. Start over.
+      buf = [''];
+      p = path.normalizeArray(target.split('/'));
+      i = 0;
+      continue;
+    }
+    // not absolute.  join and splice.
+    target = target.split('/');
+    Array.prototype.splice.apply(p, [i, 1].concat(target));
+    p = path.normalizeArray(p);
+    i = 0;
+    buf = [''];
   }
   return buf.join('/');
 }
-
-
-fs.realpath = function (path, callback) {
-  var seen_links = {}, knownHards = {}, buf = [''], i = 0, part, x;
-  if (path.charAt(0) !== '/') {
-    // assumes cwd is canonical
-    var cwd = process.cwd().split('/');
-    path = cwd.concat(path.split('/'));
-    path = normalizeArray(path);
-    i = cwd.length-1;
-    buf = [].concat(cwd);
-  } else {
-    path = normalizeArray(path.split('/'));
-  }
-  function done(err) {
-    if (callback) {
-      if (!err) callback(err, buf.join('/'));
-      else callback(err);
+function realpath (p, cb) {
+  if (p.charAt(0) !== '/') {
+    p = path.join(process.cwd(), p);
+  }
+  p = p.split('/');
+  var buf = [ '' ];
+  var seenLinks = {};
+  var knownHard = {};
+  // walk down the path, swapping out linked pathparts for their real
+  // values, and pushing non-link path bits onto the buffer.
+  // then return the buffer.
+  // NB: path.length changes.
+  var i = 0;
+  var part;
+  LOOP();
+  function LOOP () {
+    i ++;
+    if (!(i < p.length)) return exit();
+    // skip over empty path parts.
+    if (p[i] === '') return process.nextTick(LOOP);
+    part = buf.join('/')+'/'+p[i];
+    if (knownHard[part]) {
+      buf.push( p[i] );
+      return process.nextTick(LOOP);
     }
-  }
-  function next() {
-    if (++i === path.length) return done();
-    part = path.slice(0, i+1).join('/');
-    if (part.length === 0) return next();
-    if (part in knownHards) {
-      buf.push(path[i]);
-      next();
-    } else {
-      fs.lstat(part, function(err, stats){
-        if (err) return done(err);
-        if (stats.isSymbolicLink()) {
-          x = stats.dev.toString(32)+":"+stats.ino.toString(32);
-          if (x in seen_links)
-            return done(new Error("cyclic link at "+part));
-          seen_links[x] = true;
-          fs.readlink(part, function(err, npart){
-            if (err) return done(err);
-            part = npart;
-            if (part.charAt(0) === '/') {
-              // absolute
-              path = normalizeArray(part.split('/'));
-              buf = [''];
-              i = 0;
-            } else {
-              // relative
-              Array.prototype.splice.apply(path, [i, 1].concat(part.split('/')));
-              part = normalizeArray(path);
-              var y = 0, L = Math.max(path.length, part.length), delta;
-              for (; y<L && path[y] === part[y]; y++);
-              if (y !== L) {
-                path = part;
-                delta = i-y;
-                i = y-1; // resolve new node if needed
-                if (delta > 0) buf.splice(y, delta);
-              }
-              else {
-                i--; // resolve new node if needed
-              }
-            }
-            next();
-          }); // binding.readlink
-        }
-        else {
-          buf.push(path[i]);
-          knownHards[buf.join('/')] = true;
-          next();
-        }
-      }); // binding.lstat
+    return fs.lstat(part, gotStat);
+  }
+  function gotStat (er, stat) {
+    if (er) return cb(er);
+    if (!stat.isSymbolicLink()) {
+      // not a symlink. easy.
+      knownHard[ part ] = true;
+      buf.push(p[i]);
+      return process.nextTick(LOOP);
+    }
+    var id = stat.dev.toString(32)+':'+stat.ino.toString(32);
+    if (seenLinks[id]) return cb(new Error("cyclic link at "+part));
+    seenLinks[id] = true;
+    fs.readlink(part, gotTarget);
+  }
+  function gotTarget (er, target) {
+    if (er) return cb(er);
+    if (target.charAt(0) === '/') {
+      // absolute. Start over.
+      buf = [''];
+      p = path.normalizeArray(target.split('/'));
+      i = 0;
+      return process.nextTick(LOOP);
     }
+    // not absolute.  join and splice.
+    target = target.split('/');
+    Array.prototype.splice.apply(p, [i, 1].concat(target));
+    p = path.normalizeArray(p);
+    i = 0;
+    buf = [''];
+    return process.nextTick(LOOP);
   }
-  next();
-};
+  function exit () {
+    cb(null, buf.join('/') || '/');
+  }
+}
 
 var pool;
 function allocNewPool () {
index 7ff4ea5..c845e8c 100644 (file)
@@ -6,12 +6,11 @@ var async_completed = 0, async_expected = 0, unlink = [];
 
 function asynctest(testBlock, args, callback, assertBlock) {
   async_expected++;
-  testBlock.apply(testBlock, args.concat([function(err){
+  testBlock.apply(testBlock, args.concat(function(err){
     var ignoreError = false;
     if (assertBlock) {
       try {
-        ignoreError = assertBlock.apply(assertBlock,
-          Array.prototype.slice.call(arguments));
+        ignoreError = assertBlock.apply(assertBlock, arguments);
       }
       catch (e) {
         err = e;
@@ -19,7 +18,7 @@ function asynctest(testBlock, args, callback, assertBlock) {
     }
     async_completed++;
     callback(ignoreError ? null : err);
-  }]));
+  }));
 }
 
 function bashRealpath(path, callback) {
@@ -227,6 +226,18 @@ function test_non_symlinks(callback) {
   });
 }
 
+var upone = path.join(process.cwd(), "..");
+function test_escape_cwd (cb) {
+  asynctest(fs.realpath, [".."], cb, function(er, uponeActual){
+    assert.equal(upone, uponeActual,
+      "realpath('..') expected: "+upone+" actual:"+uponeActual);
+  })
+}
+var uponeActual = fs.realpathSync("..");
+assert.equal(upone, uponeActual,
+  "realpathSync('..') expected: "+upone+" actual:"+uponeActual);
+
+
 // ----------------------------------------------------------------------------
 
 var tests = [
@@ -238,6 +249,7 @@ var tests = [
   test_relative_input_cwd,
   test_deep_symlink_mix,
   test_non_symlinks,
+  test_escape_cwd
 ];
 var numtests = tests.length;
 function runNextTest(err) {