1// Copyright Joyent, Inc. and other Node contributors. 2// 3// Permission is hereby granted, free of charge, to any person obtaining a 4// copy of this software and associated documentation files (the 5// "Software"), to deal in the Software without restriction, including 6// without limitation the rights to use, copy, modify, merge, publish, 7// distribute, sublicense, and/or sell copies of the Software, and to permit 8// persons to whom the Software is furnished to do so, subject to the 9// following conditions: 10// 11// The above copyright notice and this permission notice shall be included 12// in all copies or substantial portions of the Software. 13// 14// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 17// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 19// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 20// USE OR OTHER DEALINGS IN THE SOFTWARE. 21 22'use strict'; 23const common = require('../common'); 24const fixtures = require('../common/fixtures'); 25const tmpdir = require('../common/tmpdir'); 26 27if (!common.isMainThread) 28 common.skip('process.chdir is not available in Workers'); 29 30const assert = require('assert'); 31const fs = require('fs'); 32const path = require('path'); 33let async_completed = 0; 34let async_expected = 0; 35const unlink = []; 36const skipSymlinks = !common.canCreateSymLink(); 37const tmpDir = tmpdir.path; 38 39tmpdir.refresh(); 40 41let root = '/'; 42let assertEqualPath = assert.strictEqual; 43if (common.isWindows) { 44 // Something like "C:\\" 45 root = process.cwd().substr(0, 3); 46 assertEqualPath = function(path_left, path_right, message) { 47 assert 48 .strictEqual(path_left.toLowerCase(), path_right.toLowerCase(), message); 49 }; 50} 51 52process.nextTick(runTest); 53 54function tmp(p) { 55 return path.join(tmpDir, p); 56} 57 58const targetsAbsDir = path.join(tmpDir, 'targets'); 59const tmpAbsDir = tmpDir; 60 61// Set up targetsAbsDir and expected subdirectories 62fs.mkdirSync(targetsAbsDir); 63fs.mkdirSync(path.join(targetsAbsDir, 'nested-index')); 64fs.mkdirSync(path.join(targetsAbsDir, 'nested-index', 'one')); 65fs.mkdirSync(path.join(targetsAbsDir, 'nested-index', 'two')); 66 67function asynctest(testBlock, args, callback, assertBlock) { 68 async_expected++; 69 testBlock.apply(testBlock, args.concat(function(err) { 70 let ignoreError = false; 71 if (assertBlock) { 72 try { 73 ignoreError = assertBlock.apply(assertBlock, arguments); 74 } catch (e) { 75 err = e; 76 } 77 } 78 async_completed++; 79 callback(ignoreError ? null : err); 80 })); 81} 82 83// sub-tests: 84function test_simple_error_callback(realpath, realpathSync, cb) { 85 realpath('/this/path/does/not/exist', common.mustCall(function(err, s) { 86 assert(err); 87 assert(!s); 88 cb(); 89 })); 90} 91 92function test_simple_error_cb_with_null_options(realpath, realpathSync, cb) { 93 realpath('/this/path/does/not/exist', null, common.mustCall(function(err, s) { 94 assert(err); 95 assert(!s); 96 cb(); 97 })); 98} 99 100function test_simple_relative_symlink(realpath, realpathSync, callback) { 101 console.log('test_simple_relative_symlink'); 102 if (skipSymlinks) { 103 common.printSkipMessage('symlink test (no privs)'); 104 return callback(); 105 } 106 const entry = `${tmpDir}/symlink`; 107 const expected = `${tmpDir}/cycles/root.js`; 108 [ 109 [entry, `../${path.basename(tmpDir)}/cycles/root.js`] 110 ].forEach(function(t) { 111 try { fs.unlinkSync(t[0]); } catch {} 112 console.log('fs.symlinkSync(%j, %j, %j)', t[1], t[0], 'file'); 113 fs.symlinkSync(t[1], t[0], 'file'); 114 unlink.push(t[0]); 115 }); 116 const result = realpathSync(entry); 117 assertEqualPath(result, path.resolve(expected)); 118 asynctest(realpath, [entry], callback, function(err, result) { 119 assertEqualPath(result, path.resolve(expected)); 120 }); 121} 122 123function test_simple_absolute_symlink(realpath, realpathSync, callback) { 124 console.log('test_simple_absolute_symlink'); 125 126 // This one should still run, even if skipSymlinks is set, 127 // because it uses a junction. 128 const type = skipSymlinks ? 'junction' : 'dir'; 129 130 console.log('using type=%s', type); 131 132 const entry = `${tmpAbsDir}/symlink`; 133 const expected = fixtures.path('nested-index', 'one'); 134 [ 135 [entry, expected] 136 ].forEach(function(t) { 137 try { fs.unlinkSync(t[0]); } catch {} 138 console.error('fs.symlinkSync(%j, %j, %j)', t[1], t[0], type); 139 fs.symlinkSync(t[1], t[0], type); 140 unlink.push(t[0]); 141 }); 142 const result = realpathSync(entry); 143 assertEqualPath(result, path.resolve(expected)); 144 asynctest(realpath, [entry], callback, function(err, result) { 145 assertEqualPath(result, path.resolve(expected)); 146 }); 147} 148 149function test_deep_relative_file_symlink(realpath, realpathSync, callback) { 150 console.log('test_deep_relative_file_symlink'); 151 if (skipSymlinks) { 152 common.printSkipMessage('symlink test (no privs)'); 153 return callback(); 154 } 155 156 const expected = fixtures.path('cycles', 'root.js'); 157 const linkData1 = path 158 .relative(path.join(targetsAbsDir, 'nested-index', 'one'), 159 expected); 160 const linkPath1 = path.join(targetsAbsDir, 161 'nested-index', 'one', 'symlink1.js'); 162 try { fs.unlinkSync(linkPath1); } catch {} 163 fs.symlinkSync(linkData1, linkPath1, 'file'); 164 165 const linkData2 = '../one/symlink1.js'; 166 const entry = path.join(targetsAbsDir, 167 'nested-index', 'two', 'symlink1-b.js'); 168 try { fs.unlinkSync(entry); } catch {} 169 fs.symlinkSync(linkData2, entry, 'file'); 170 unlink.push(linkPath1); 171 unlink.push(entry); 172 173 assertEqualPath(realpathSync(entry), path.resolve(expected)); 174 asynctest(realpath, [entry], callback, function(err, result) { 175 assertEqualPath(result, path.resolve(expected)); 176 }); 177} 178 179function test_deep_relative_dir_symlink(realpath, realpathSync, callback) { 180 console.log('test_deep_relative_dir_symlink'); 181 if (skipSymlinks) { 182 common.printSkipMessage('symlink test (no privs)'); 183 return callback(); 184 } 185 const expected = fixtures.path('cycles', 'folder'); 186 const path1b = path.join(targetsAbsDir, 'nested-index', 'one'); 187 const linkPath1b = path.join(path1b, 'symlink1-dir'); 188 const linkData1b = path.relative(path1b, expected); 189 try { fs.unlinkSync(linkPath1b); } catch {} 190 fs.symlinkSync(linkData1b, linkPath1b, 'dir'); 191 192 const linkData2b = '../one/symlink1-dir'; 193 const entry = path.join(targetsAbsDir, 194 'nested-index', 'two', 'symlink12-dir'); 195 try { fs.unlinkSync(entry); } catch {} 196 fs.symlinkSync(linkData2b, entry, 'dir'); 197 unlink.push(linkPath1b); 198 unlink.push(entry); 199 200 assertEqualPath(realpathSync(entry), path.resolve(expected)); 201 202 asynctest(realpath, [entry], callback, function(err, result) { 203 assertEqualPath(result, path.resolve(expected)); 204 }); 205} 206 207function test_cyclic_link_protection(realpath, realpathSync, callback) { 208 console.log('test_cyclic_link_protection'); 209 if (skipSymlinks) { 210 common.printSkipMessage('symlink test (no privs)'); 211 return callback(); 212 } 213 const entry = path.join(tmpDir, '/cycles/realpath-3a'); 214 [ 215 [entry, '../cycles/realpath-3b'], 216 [path.join(tmpDir, '/cycles/realpath-3b'), '../cycles/realpath-3c'], 217 [path.join(tmpDir, '/cycles/realpath-3c'), '../cycles/realpath-3a'] 218 ].forEach(function(t) { 219 try { fs.unlinkSync(t[0]); } catch {} 220 fs.symlinkSync(t[1], t[0], 'dir'); 221 unlink.push(t[0]); 222 }); 223 assert.throws(() => { 224 realpathSync(entry); 225 }, { code: 'ELOOP', name: 'Error' }); 226 asynctest( 227 realpath, [entry], callback, common.mustCall(function(err, result) { 228 assert.strictEqual(err.path, entry); 229 assert.strictEqual(result, undefined); 230 return true; 231 })); 232} 233 234function test_cyclic_link_overprotection(realpath, realpathSync, callback) { 235 console.log('test_cyclic_link_overprotection'); 236 if (skipSymlinks) { 237 common.printSkipMessage('symlink test (no privs)'); 238 return callback(); 239 } 240 const cycles = `${tmpDir}/cycles`; 241 const expected = realpathSync(cycles); 242 const folder = `${cycles}/folder`; 243 const link = `${folder}/cycles`; 244 let testPath = cycles; 245 testPath += '/folder/cycles'.repeat(10); 246 try { fs.unlinkSync(link); } catch {} 247 fs.symlinkSync(cycles, link, 'dir'); 248 unlink.push(link); 249 assertEqualPath(realpathSync(testPath), path.resolve(expected)); 250 asynctest(realpath, [testPath], callback, function(er, res) { 251 assertEqualPath(res, path.resolve(expected)); 252 }); 253} 254 255function test_relative_input_cwd(realpath, realpathSync, callback) { 256 console.log('test_relative_input_cwd'); 257 if (skipSymlinks) { 258 common.printSkipMessage('symlink test (no privs)'); 259 return callback(); 260 } 261 262 // We need to calculate the relative path to the tmp dir from cwd 263 const entrydir = process.cwd(); 264 const entry = path.relative(entrydir, 265 path.join(`${tmpDir}/cycles/realpath-3a`)); 266 const expected = `${tmpDir}/cycles/root.js`; 267 [ 268 [entry, '../cycles/realpath-3b'], 269 [`${tmpDir}/cycles/realpath-3b`, '../cycles/realpath-3c'], 270 [`${tmpDir}/cycles/realpath-3c`, 'root.js'] 271 ].forEach(function(t) { 272 const fn = t[0]; 273 console.error('fn=%j', fn); 274 try { fs.unlinkSync(fn); } catch {} 275 const b = path.basename(t[1]); 276 const type = (b === 'root.js' ? 'file' : 'dir'); 277 console.log('fs.symlinkSync(%j, %j, %j)', t[1], fn, type); 278 fs.symlinkSync(t[1], fn, 'file'); 279 unlink.push(fn); 280 }); 281 282 const origcwd = process.cwd(); 283 process.chdir(entrydir); 284 assertEqualPath(realpathSync(entry), path.resolve(expected)); 285 asynctest(realpath, [entry], callback, function(err, result) { 286 process.chdir(origcwd); 287 assertEqualPath(result, path.resolve(expected)); 288 return true; 289 }); 290} 291 292function test_deep_symlink_mix(realpath, realpathSync, callback) { 293 console.log('test_deep_symlink_mix'); 294 if (common.isWindows) { 295 // This one is a mix of files and directories, and it's quite tricky 296 // to get the file/dir links sorted out correctly. 297 common.printSkipMessage('symlink test (no privs)'); 298 return callback(); 299 } 300 301 /* 302 /tmp/node-test-realpath-f1 -> $tmpDir/node-test-realpath-d1/foo 303 /tmp/node-test-realpath-d1 -> $tmpDir/node-test-realpath-d2 304 /tmp/node-test-realpath-d2/foo -> $tmpDir/node-test-realpath-f2 305 /tmp/node-test-realpath-f2 306 -> $tmpDir/targets/nested-index/one/realpath-c 307 $tmpDir/targets/nested-index/one/realpath-c 308 -> $tmpDir/targets/nested-index/two/realpath-c 309 $tmpDir/targets/nested-index/two/realpath-c -> $tmpDir/cycles/root.js 310 $tmpDir/targets/cycles/root.js (hard) 311 */ 312 const entry = tmp('node-test-realpath-f1'); 313 try { fs.unlinkSync(tmp('node-test-realpath-d2/foo')); } catch {} 314 try { fs.rmdirSync(tmp('node-test-realpath-d2')); } catch {} 315 fs.mkdirSync(tmp('node-test-realpath-d2'), 0o700); 316 try { 317 [ 318 [entry, `${tmpDir}/node-test-realpath-d1/foo`], 319 [tmp('node-test-realpath-d1'), 320 `${tmpDir}/node-test-realpath-d2`], 321 [tmp('node-test-realpath-d2/foo'), '../node-test-realpath-f2'], 322 [tmp('node-test-realpath-f2'), 323 `${targetsAbsDir}/nested-index/one/realpath-c`], 324 [`${targetsAbsDir}/nested-index/one/realpath-c`, 325 `${targetsAbsDir}/nested-index/two/realpath-c`], 326 [`${targetsAbsDir}/nested-index/two/realpath-c`, 327 `${tmpDir}/cycles/root.js`] 328 ].forEach(function(t) { 329 try { fs.unlinkSync(t[0]); } catch {} 330 fs.symlinkSync(t[1], t[0]); 331 unlink.push(t[0]); 332 }); 333 } finally { 334 unlink.push(tmp('node-test-realpath-d2')); 335 } 336 const expected = `${tmpAbsDir}/cycles/root.js`; 337 assertEqualPath(realpathSync(entry), path.resolve(expected)); 338 asynctest(realpath, [entry], callback, function(err, result) { 339 assertEqualPath(result, path.resolve(expected)); 340 return true; 341 }); 342} 343 344function test_non_symlinks(realpath, realpathSync, callback) { 345 console.log('test_non_symlinks'); 346 const entrydir = path.dirname(tmpAbsDir); 347 const entry = `${tmpAbsDir.substr(entrydir.length + 1)}/cycles/root.js`; 348 const expected = `${tmpAbsDir}/cycles/root.js`; 349 const origcwd = process.cwd(); 350 process.chdir(entrydir); 351 assertEqualPath(realpathSync(entry), path.resolve(expected)); 352 asynctest(realpath, [entry], callback, function(err, result) { 353 process.chdir(origcwd); 354 assertEqualPath(result, path.resolve(expected)); 355 return true; 356 }); 357} 358 359const upone = path.join(process.cwd(), '..'); 360function test_escape_cwd(realpath, realpathSync, cb) { 361 console.log('test_escape_cwd'); 362 asynctest(realpath, ['..'], cb, function(er, uponeActual) { 363 assertEqualPath( 364 upone, uponeActual, 365 `realpath("..") expected: ${path.resolve(upone)} actual:${uponeActual}`); 366 }); 367} 368 369function test_upone_actual(realpath, realpathSync, cb) { 370 console.log('test_upone_actual'); 371 const uponeActual = realpathSync('..'); 372 assertEqualPath(upone, uponeActual); 373 cb(); 374} 375 376// Going up with .. multiple times 377// . 378// `-- a/ 379// |-- b/ 380// | `-- e -> .. 381// `-- d -> .. 382// realpath(a/b/e/d/a/b/e/d/a) ==> a 383function test_up_multiple(realpath, realpathSync, cb) { 384 console.error('test_up_multiple'); 385 if (skipSymlinks) { 386 common.printSkipMessage('symlink test (no privs)'); 387 return cb(); 388 } 389 const tmpdir = require('../common/tmpdir'); 390 tmpdir.refresh(); 391 fs.mkdirSync(tmp('a'), 0o755); 392 fs.mkdirSync(tmp('a/b'), 0o755); 393 fs.symlinkSync('..', tmp('a/d'), 'dir'); 394 unlink.push(tmp('a/d')); 395 fs.symlinkSync('..', tmp('a/b/e'), 'dir'); 396 unlink.push(tmp('a/b/e')); 397 398 const abedabed = tmp('abedabed'.split('').join('/')); 399 const abedabed_real = tmp(''); 400 401 const abedabeda = tmp('abedabeda'.split('').join('/')); 402 const abedabeda_real = tmp('a'); 403 404 assertEqualPath(realpathSync(abedabeda), abedabeda_real); 405 assertEqualPath(realpathSync(abedabed), abedabed_real); 406 407 realpath(abedabeda, function(er, real) { 408 assert.ifError(er); 409 assertEqualPath(abedabeda_real, real); 410 realpath(abedabed, function(er, real) { 411 assert.ifError(er); 412 assertEqualPath(abedabed_real, real); 413 cb(); 414 }); 415 }); 416} 417 418 419// Going up with .. multiple times with options = null 420// . 421// `-- a/ 422// |-- b/ 423// | `-- e -> .. 424// `-- d -> .. 425// realpath(a/b/e/d/a/b/e/d/a) ==> a 426function test_up_multiple_with_null_options(realpath, realpathSync, cb) { 427 console.error('test_up_multiple'); 428 if (skipSymlinks) { 429 common.printSkipMessage('symlink test (no privs)'); 430 return cb(); 431 } 432 const tmpdir = require('../common/tmpdir'); 433 tmpdir.refresh(); 434 fs.mkdirSync(tmp('a'), 0o755); 435 fs.mkdirSync(tmp('a/b'), 0o755); 436 fs.symlinkSync('..', tmp('a/d'), 'dir'); 437 unlink.push(tmp('a/d')); 438 fs.symlinkSync('..', tmp('a/b/e'), 'dir'); 439 unlink.push(tmp('a/b/e')); 440 441 const abedabed = tmp('abedabed'.split('').join('/')); 442 const abedabed_real = tmp(''); 443 444 const abedabeda = tmp('abedabeda'.split('').join('/')); 445 const abedabeda_real = tmp('a'); 446 447 assertEqualPath(realpathSync(abedabeda), abedabeda_real); 448 assertEqualPath(realpathSync(abedabed), abedabed_real); 449 450 realpath(abedabeda, null, function(er, real) { 451 assert.ifError(er); 452 assertEqualPath(abedabeda_real, real); 453 realpath(abedabed, null, function(er, real) { 454 assert.ifError(er); 455 assertEqualPath(abedabed_real, real); 456 cb(); 457 }); 458 }); 459} 460 461// Absolute symlinks with children. 462// . 463// `-- a/ 464// |-- b/ 465// | `-- c/ 466// | `-- x.txt 467// `-- link -> /tmp/node-test-realpath-abs-kids/a/b/ 468// realpath(root+'/a/link/c/x.txt') ==> root+'/a/b/c/x.txt' 469function test_abs_with_kids(realpath, realpathSync, cb) { 470 console.log('test_abs_with_kids'); 471 472 // This one should still run, even if skipSymlinks is set, 473 // because it uses a junction. 474 const type = skipSymlinks ? 'junction' : 'dir'; 475 476 console.log('using type=%s', type); 477 478 const root = `${tmpAbsDir}/node-test-realpath-abs-kids`; 479 function cleanup() { 480 ['/a/b/c/x.txt', 481 '/a/link' 482 ].forEach(function(file) { 483 try { fs.unlinkSync(root + file); } catch {} 484 }); 485 ['/a/b/c', 486 '/a/b', 487 '/a', 488 '' 489 ].forEach(function(folder) { 490 try { fs.rmdirSync(root + folder); } catch {} 491 }); 492 } 493 494 function setup() { 495 cleanup(); 496 ['', 497 '/a', 498 '/a/b', 499 '/a/b/c' 500 ].forEach(function(folder) { 501 console.log(`mkdir ${root}${folder}`); 502 fs.mkdirSync(root + folder, 0o700); 503 }); 504 fs.writeFileSync(`${root}/a/b/c/x.txt`, 'foo'); 505 fs.symlinkSync(`${root}/a/b`, `${root}/a/link`, type); 506 } 507 setup(); 508 const linkPath = `${root}/a/link/c/x.txt`; 509 const expectPath = `${root}/a/b/c/x.txt`; 510 const actual = realpathSync(linkPath); 511 // console.log({link:linkPath,expect:expectPath,actual:actual},'sync'); 512 assertEqualPath(actual, path.resolve(expectPath)); 513 asynctest(realpath, [linkPath], cb, function(er, actual) { 514 // console.log({link:linkPath,expect:expectPath,actual:actual},'async'); 515 assertEqualPath(actual, path.resolve(expectPath)); 516 cleanup(); 517 }); 518} 519 520function test_root(realpath, realpathSync, cb) { 521 assertEqualPath(root, realpathSync('/')); 522 realpath('/', function(err, result) { 523 assert.ifError(err); 524 assertEqualPath(root, result); 525 cb(); 526 }); 527} 528 529function test_root_with_null_options(realpath, realpathSync, cb) { 530 realpath('/', null, function(err, result) { 531 assert.ifError(err); 532 assertEqualPath(root, result); 533 cb(); 534 }); 535} 536 537// ---------------------------------------------------------------------------- 538 539const tests = [ 540 test_simple_error_callback, 541 test_simple_error_cb_with_null_options, 542 test_simple_relative_symlink, 543 test_simple_absolute_symlink, 544 test_deep_relative_file_symlink, 545 test_deep_relative_dir_symlink, 546 test_cyclic_link_protection, 547 test_cyclic_link_overprotection, 548 test_relative_input_cwd, 549 test_deep_symlink_mix, 550 test_non_symlinks, 551 test_escape_cwd, 552 test_upone_actual, 553 test_abs_with_kids, 554 test_up_multiple, 555 test_up_multiple_with_null_options, 556 test_root, 557 test_root_with_null_options 558]; 559const numtests = tests.length; 560let testsRun = 0; 561function runNextTest(err) { 562 assert.ifError(err); 563 const test = tests.shift(); 564 if (!test) { 565 return console.log(`${numtests} subtests completed OK for fs.realpath`); 566 } 567 testsRun++; 568 test(fs.realpath, fs.realpathSync, common.mustCall((err) => { 569 assert.ifError(err); 570 testsRun++; 571 test(fs.realpath.native, 572 fs.realpathSync.native, 573 common.mustCall(runNextTest)); 574 })); 575} 576 577function runTest() { 578 const tmpDirs = ['cycles', 'cycles/folder']; 579 tmpDirs.forEach(function(t) { 580 t = tmp(t); 581 fs.mkdirSync(t, 0o700); 582 }); 583 fs.writeFileSync(tmp('cycles/root.js'), "console.error('roooot!');"); 584 console.error('start tests'); 585 runNextTest(); 586} 587 588 589process.on('exit', function() { 590 assert.strictEqual(2 * numtests, testsRun); 591 assert.strictEqual(async_completed, async_expected); 592}); 593