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 // /tmp/node-test-realpath-f1 -> $tmpDir/node-test-realpath-d1/foo 302 // /tmp/node-test-realpath-d1 -> $tmpDir/node-test-realpath-d2 303 // /tmp/node-test-realpath-d2/foo -> $tmpDir/node-test-realpath-f2 304 // /tmp/node-test-realpath-f2 305 // -> $tmpDir/targets/nested-index/one/realpath-c 306 // $tmpDir/targets/nested-index/one/realpath-c 307 // -> $tmpDir/targets/nested-index/two/realpath-c 308 // $tmpDir/targets/nested-index/two/realpath-c -> $tmpDir/cycles/root.js 309 // $tmpDir/targets/cycles/root.js (hard) 310 311 const entry = tmp('node-test-realpath-f1'); 312 try { fs.unlinkSync(tmp('node-test-realpath-d2/foo')); } catch {} 313 try { fs.rmdirSync(tmp('node-test-realpath-d2')); } catch {} 314 fs.mkdirSync(tmp('node-test-realpath-d2'), 0o700); 315 try { 316 [ 317 [entry, `${tmpDir}/node-test-realpath-d1/foo`], 318 [tmp('node-test-realpath-d1'), 319 `${tmpDir}/node-test-realpath-d2`], 320 [tmp('node-test-realpath-d2/foo'), '../node-test-realpath-f2'], 321 [tmp('node-test-realpath-f2'), 322 `${targetsAbsDir}/nested-index/one/realpath-c`], 323 [`${targetsAbsDir}/nested-index/one/realpath-c`, 324 `${targetsAbsDir}/nested-index/two/realpath-c`], 325 [`${targetsAbsDir}/nested-index/two/realpath-c`, 326 `${tmpDir}/cycles/root.js`], 327 ].forEach(function(t) { 328 try { fs.unlinkSync(t[0]); } catch {} 329 fs.symlinkSync(t[1], t[0]); 330 unlink.push(t[0]); 331 }); 332 } finally { 333 unlink.push(tmp('node-test-realpath-d2')); 334 } 335 const expected = `${tmpAbsDir}/cycles/root.js`; 336 assertEqualPath(realpathSync(entry), path.resolve(expected)); 337 asynctest(realpath, [entry], callback, function(err, result) { 338 assertEqualPath(result, path.resolve(expected)); 339 return true; 340 }); 341} 342 343function test_non_symlinks(realpath, realpathSync, callback) { 344 console.log('test_non_symlinks'); 345 const entrydir = path.dirname(tmpAbsDir); 346 const entry = `${tmpAbsDir.substr(entrydir.length + 1)}/cycles/root.js`; 347 const expected = `${tmpAbsDir}/cycles/root.js`; 348 const origcwd = process.cwd(); 349 process.chdir(entrydir); 350 assertEqualPath(realpathSync(entry), path.resolve(expected)); 351 asynctest(realpath, [entry], callback, function(err, result) { 352 process.chdir(origcwd); 353 assertEqualPath(result, path.resolve(expected)); 354 return true; 355 }); 356} 357 358const upone = path.join(process.cwd(), '..'); 359function test_escape_cwd(realpath, realpathSync, cb) { 360 console.log('test_escape_cwd'); 361 asynctest(realpath, ['..'], cb, function(er, uponeActual) { 362 assertEqualPath( 363 upone, uponeActual, 364 `realpath("..") expected: ${path.resolve(upone)} actual:${uponeActual}`); 365 }); 366} 367 368function test_upone_actual(realpath, realpathSync, cb) { 369 console.log('test_upone_actual'); 370 const uponeActual = realpathSync('..'); 371 assertEqualPath(upone, uponeActual); 372 cb(); 373} 374 375// Going up with .. multiple times 376// . 377// `-- a/ 378// |-- b/ 379// | `-- e -> .. 380// `-- d -> .. 381// realpath(a/b/e/d/a/b/e/d/a) ==> a 382function test_up_multiple(realpath, realpathSync, cb) { 383 console.error('test_up_multiple'); 384 if (skipSymlinks) { 385 common.printSkipMessage('symlink test (no privs)'); 386 return cb(); 387 } 388 const tmpdir = require('../common/tmpdir'); 389 tmpdir.refresh(); 390 fs.mkdirSync(tmp('a'), 0o755); 391 fs.mkdirSync(tmp('a/b'), 0o755); 392 fs.symlinkSync('..', tmp('a/d'), 'dir'); 393 unlink.push(tmp('a/d')); 394 fs.symlinkSync('..', tmp('a/b/e'), 'dir'); 395 unlink.push(tmp('a/b/e')); 396 397 const abedabed = tmp('abedabed'.split('').join('/')); 398 const abedabed_real = tmp(''); 399 400 const abedabeda = tmp('abedabeda'.split('').join('/')); 401 const abedabeda_real = tmp('a'); 402 403 assertEqualPath(realpathSync(abedabeda), abedabeda_real); 404 assertEqualPath(realpathSync(abedabed), abedabed_real); 405 406 realpath(abedabeda, function(er, real) { 407 assert.ifError(er); 408 assertEqualPath(abedabeda_real, real); 409 realpath(abedabed, function(er, real) { 410 assert.ifError(er); 411 assertEqualPath(abedabed_real, real); 412 cb(); 413 }); 414 }); 415} 416 417 418// Going up with .. multiple times with options = null 419// . 420// `-- a/ 421// |-- b/ 422// | `-- e -> .. 423// `-- d -> .. 424// realpath(a/b/e/d/a/b/e/d/a) ==> a 425function test_up_multiple_with_null_options(realpath, realpathSync, cb) { 426 console.error('test_up_multiple'); 427 if (skipSymlinks) { 428 common.printSkipMessage('symlink test (no privs)'); 429 return cb(); 430 } 431 const tmpdir = require('../common/tmpdir'); 432 tmpdir.refresh(); 433 fs.mkdirSync(tmp('a'), 0o755); 434 fs.mkdirSync(tmp('a/b'), 0o755); 435 fs.symlinkSync('..', tmp('a/d'), 'dir'); 436 unlink.push(tmp('a/d')); 437 fs.symlinkSync('..', tmp('a/b/e'), 'dir'); 438 unlink.push(tmp('a/b/e')); 439 440 const abedabed = tmp('abedabed'.split('').join('/')); 441 const abedabed_real = tmp(''); 442 443 const abedabeda = tmp('abedabeda'.split('').join('/')); 444 const abedabeda_real = tmp('a'); 445 446 assertEqualPath(realpathSync(abedabeda), abedabeda_real); 447 assertEqualPath(realpathSync(abedabed), abedabed_real); 448 449 realpath(abedabeda, null, function(er, real) { 450 assert.ifError(er); 451 assertEqualPath(abedabeda_real, real); 452 realpath(abedabed, null, function(er, real) { 453 assert.ifError(er); 454 assertEqualPath(abedabed_real, real); 455 cb(); 456 }); 457 }); 458} 459 460// Absolute symlinks with children. 461// . 462// `-- a/ 463// |-- b/ 464// | `-- c/ 465// | `-- x.txt 466// `-- link -> /tmp/node-test-realpath-abs-kids/a/b/ 467// realpath(root+'/a/link/c/x.txt') ==> root+'/a/b/c/x.txt' 468function test_abs_with_kids(realpath, realpathSync, cb) { 469 console.log('test_abs_with_kids'); 470 471 // This one should still run, even if skipSymlinks is set, 472 // because it uses a junction. 473 const type = skipSymlinks ? 'junction' : 'dir'; 474 475 console.log('using type=%s', type); 476 477 const root = `${tmpAbsDir}/node-test-realpath-abs-kids`; 478 function cleanup() { 479 ['/a/b/c/x.txt', 480 '/a/link', 481 ].forEach(function(file) { 482 try { fs.unlinkSync(root + file); } catch {} 483 }); 484 ['/a/b/c', 485 '/a/b', 486 '/a', 487 '', 488 ].forEach(function(folder) { 489 try { fs.rmdirSync(root + folder); } catch {} 490 }); 491 } 492 493 function setup() { 494 cleanup(); 495 ['', 496 '/a', 497 '/a/b', 498 '/a/b/c', 499 ].forEach(function(folder) { 500 console.log(`mkdir ${root}${folder}`); 501 fs.mkdirSync(root + folder, 0o700); 502 }); 503 fs.writeFileSync(`${root}/a/b/c/x.txt`, 'foo'); 504 fs.symlinkSync(`${root}/a/b`, `${root}/a/link`, type); 505 } 506 setup(); 507 const linkPath = `${root}/a/link/c/x.txt`; 508 const expectPath = `${root}/a/b/c/x.txt`; 509 const actual = realpathSync(linkPath); 510 // console.log({link:linkPath,expect:expectPath,actual:actual},'sync'); 511 assertEqualPath(actual, path.resolve(expectPath)); 512 asynctest(realpath, [linkPath], cb, function(er, actual) { 513 // console.log({link:linkPath,expect:expectPath,actual:actual},'async'); 514 assertEqualPath(actual, path.resolve(expectPath)); 515 cleanup(); 516 }); 517} 518 519function test_root(realpath, realpathSync, cb) { 520 assertEqualPath(root, realpathSync('/')); 521 realpath('/', function(err, result) { 522 assert.ifError(err); 523 assertEqualPath(root, result); 524 cb(); 525 }); 526} 527 528function test_root_with_null_options(realpath, realpathSync, cb) { 529 realpath('/', null, function(err, result) { 530 assert.ifError(err); 531 assertEqualPath(root, result); 532 cb(); 533 }); 534} 535 536// ---------------------------------------------------------------------------- 537 538const tests = [ 539 test_simple_error_callback, 540 test_simple_error_cb_with_null_options, 541 test_simple_relative_symlink, 542 test_simple_absolute_symlink, 543 test_deep_relative_file_symlink, 544 test_deep_relative_dir_symlink, 545 test_cyclic_link_protection, 546 test_cyclic_link_overprotection, 547 test_relative_input_cwd, 548 test_deep_symlink_mix, 549 test_non_symlinks, 550 test_escape_cwd, 551 test_upone_actual, 552 test_abs_with_kids, 553 test_up_multiple, 554 test_up_multiple_with_null_options, 555 test_root, 556 test_root_with_null_options, 557]; 558const numtests = tests.length; 559let testsRun = 0; 560function runNextTest(err) { 561 assert.ifError(err); 562 const test = tests.shift(); 563 if (!test) { 564 return console.log(`${numtests} subtests completed OK for fs.realpath`); 565 } 566 testsRun++; 567 test(fs.realpath, fs.realpathSync, common.mustSucceed(() => { 568 testsRun++; 569 test(fs.realpath.native, 570 fs.realpathSync.native, 571 common.mustCall(runNextTest)); 572 })); 573} 574 575function runTest() { 576 const tmpDirs = ['cycles', 'cycles/folder']; 577 tmpDirs.forEach(function(t) { 578 t = tmp(t); 579 fs.mkdirSync(t, 0o700); 580 }); 581 fs.writeFileSync(tmp('cycles/root.js'), "console.error('roooot!');"); 582 console.error('start tests'); 583 runNextTest(); 584} 585 586 587process.on('exit', function() { 588 assert.strictEqual(2 * numtests, testsRun); 589 assert.strictEqual(async_completed, async_expected); 590}); 591