char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
688e96e
main
1mod common; 2 3use std:: fs; 4use std:: process::{ Command , Output }; 5 6fn run_in ( dir : & std:: path:: Path , args : & [ & str ]) ->Output { 7Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 8. arg ( "run" ) 9. current_dir ( dir) 10. args ( args) 11. output () 12. unwrap () 13} 14 15fn run_main ( dir : & std:: path:: Path , source : & str , args : & [ & str ]) ->Output { 16 fs:: write ( dir. join ( "main.js" ), source). unwrap (); 17run_in ( dir, args) 18} 19 20fn success ( output : Output ) ->String { 21assert! ( 22 output. status . success (), 23"{}" , 24String :: from_utf8_lossy ( & output. stderr ) 25); 26String :: from_utf8 ( output. stdout ). unwrap () 27} 28 29# [ test ] 30fn posix_lexical_vectors () { 31let output = common:: run ( 32r#" 33import * as path from 'avril:path/posix'; 34const cases = [ 35['join', [], '.'], 36['join', ['', ''], '.'], 37['join', ['/a', 'b', 'c'], '/a/b/c'], 38['join', ['/a/', '/b/'], '/a/b/'], 39['join', ['a', '', 'b'], 'a/b'], 40['join', ['a', '..', 'b'], 'b'], 41['join', ['/', '..', 'a'], '/a'], 42['join', ['', '.', ''], '.'], 43['join', ['/a', '..'], '/'], 44['join', ['a//b', 'c'], 'a/b/c'], 45['join', ['/a/./b/'], '/a/b/'], 46['join', ['foo', 'bar', 'baz', '..', 'qux'], 'foo/bar/qux'], 47['join', ['aé', 'b'], 'aé/b'], 48['join', ['/a\\b', 'c'], '/a\\b/c'], 49['join', ['..', '..', 'a'], '../../a'], 50['join', ['/', '/'], '/'], 51['join', ['///a', 'b'], '/a/b'], 52['join', ['a/', '/'], 'a/'], 53['join', ['.', '.', '.'], '.'], 54['resolve', ['/a', 'b', 'c'], '/a/b/c'], 55['resolve', ['/a', '/b', 'c'], '/b/c'], 56['resolve', ['/'], '/'], 57['resolve', ['/a', '..', '..'], '/'], 58['resolve', ['/a/b/', '../c/'], '/a/c'], 59['resolve', ['/foo/bar', './baz'], '/foo/bar/baz'], 60['resolve', ['/foo/bar', '/tmp/file/'], '/tmp/file'], 61['resolve', ['/a', 'b', '', 'c'], '/a/b/c'], 62['normalize', [''], '.'], 63['normalize', ['.'], '.'], 64['normalize', ['/'], '/'], 65['normalize', ['//'], '/'], 66['normalize', ['///'], '/'], 67['normalize', ['/a//b/./c/../d/'], '/a/b/d/'], 68['normalize', ['a/b/../../c'], 'c'], 69['normalize', ['../a'], '../a'], 70['normalize', ['a/'], 'a/'], 71['normalize', ['a//'], 'a/'], 72['normalize', ['/a/'], '/a/'], 73['normalize', ['./a'], 'a'], 74['normalize', ['a/./'], 'a/'], 75['normalize', ['...'], '...'], 76['normalize', ['....'], '....'], 77['normalize', ['/a\\b'], '/a\\b'], 78['normalize', ['a\\b'], 'a\\b'], 79['normalize', ['a/.../b'], 'a/.../b'], 80['normalize', ['/...'], '/...'], 81['normalize', ['a/././b'], 'a/b'], 82['relative', ['/a/b/c', '/a/d/e'], '../../d/e'], 83['relative', ['/a/b', '/a/b'], ''], 84['relative', ['/', '/foo'], 'foo'], 85['relative', ['/foo', '/'], '..'], 86['relative', ['/foo/bar', '/foo/bar/baz'], 'baz'], 87['relative', ['/foo/bar/baz', '/foo/bar'], '..'], 88['relative', ['/a', '/a'], ''], 89['relative', ['/a/b/c/d', '/a/x/y'], '../../../x/y'], 90['relative', ['/foo', '/foobar'], '../foobar'], 91['relative', ['/foobar', '/foo'], '../foo'], 92['relative', ['/a/./b', '/a/b/c'], 'c'], 93['dirname', [''], '.'], 94['dirname', ['.'], '.'], 95['dirname', ['/'], '/'], 96['dirname', ['//'], '/'], 97['dirname', ['///'], '/'], 98['dirname', ['/a'], '/'], 99['dirname', ['/a/'], '/'], 100['dirname', ['/a/b'], '/a'], 101['dirname', ['/a/b/'], '/a'], 102['dirname', ['a'], '.'], 103['dirname', ['a/'], '.'], 104['dirname', ['a/b'], 'a'], 105['dirname', ['a/b/'], 'a'], 106['dirname', ['..'], '.'], 107['dirname', ['../'], '.'], 108['dirname', ['/a/b/c.txt'], '/a/b'], 109['dirname', ['a\\b'], '.'], 110['dirname', ['/a\\b'], '/'], 111['basename', [''], ''], 112['basename', ['/'], ''], 113['basename', ['//'], ''], 114['basename', ['a'], 'a'], 115['basename', ['a/'], 'a'], 116['basename', ['a//'], 'a'], 117['basename', ['/a/b/c'], 'c'], 118['basename', ['/a/b/c/'], 'c'], 119['basename', ['a.txt'], 'a.txt'], 120['basename', ['.txt'], '.txt'], 121['basename', ['..'], '..'], 122['basename', ['...'], '...'], 123['basename', ['.'], '.'], 124['basename', ['a/.txt'], '.txt'], 125['basename', ['a/..txt'], '..txt'], 126['basename', ['a/b.tar.gz', '.gz'], 'b.tar'], 127['basename', ['a/b.tar.gz', '.tar.gz'], 'b'], 128['basename', ['a.txt', '.txt'], 'a'], 129['basename', ['a.txt', 'a.txt'], ''], 130['basename', ['a', 'a'], ''], 131['basename', ['a/', 'a'], 'a'], 132['basename', ['/a', '/a'], ''], 133['basename', ['a.txt', ''], 'a.txt'], 134['basename', ['ab', 'b'], 'a'], 135['basename', ['ab', 'ab'], ''], 136['extname', [''], ''], 137['extname', ['a'], ''], 138['extname', ['a.'], '.'], 139['extname', ['.a'], ''], 140['extname', ['..a'], '.a'], 141['extname', ['a..'], '.'], 142['extname', ['...'], '.'], 143['extname', ['..'], ''], 144['extname', ['.'], ''], 145['extname', ['a.txt'], '.txt'], 146['extname', ['a.tar.gz'], '.gz'], 147['extname', ['.txt'], ''], 148['extname', ['a/.txt'], ''], 149['extname', ['a/..'], ''], 150['extname', ['a/b.'], '.'], 151['extname', ['/a.b/c'], ''], 152['extname', ['/a.b/c.d'], '.d'], 153['extname', ['a/b/c'], ''], 154['extname', ['a\\b.txt'], '.txt'], 155['extname', ['a b.c d'], '.c d'], 156['isAbsolute', [''], false], 157['isAbsolute', ['/'], true], 158['isAbsolute', ['//'], true], 159['isAbsolute', ['a'], false], 160['isAbsolute', ['/a'], true], 161['isAbsolute', ['./a'], false], 162['isAbsolute', ['../a'], false], 163['isAbsolute', ['a/b'], false], 164['isAbsolute', ['\\'], false], 165['isAbsolute', ['C:\\a'], false], 166['isAbsolute', ['/a\\b'], true], 167]; 168for (let i = 0; i < cases.length; i++) { 169const [fn, args, expected] = cases[i]; 170const actual = path[fn](...args); 171if (actual !== expected) { 172throw new Error(`${fn}(${args.map((a) => JSON.stringify(a)).join(', ')}) = ${JSON.stringify(actual)}, expected ${JSON.stringify(expected)}`); 173} 174} 175if (path.sep !== '/' || path.delimiter !== ':') throw new Error('separators'); 176if (path.toNamespacedPath('/a/b') !== '/a/b' || path.toNamespacedPath('a') !== 'a' 177|| path.toNamespacedPath('') !== '' || path.toNamespacedPath(42) !== 42) { 178throw new Error('toNamespacedPath passthrough'); 179} 180const formats = [ 181[{ dir: '/a/b', base: 'c.txt' }, '/a/b/c.txt'], 182[{ root: '/', base: 'c' }, '/c'], 183[{ dir: '/a', name: 'b', ext: 'txt' }, '/a/b.txt'], 184[{ dir: '/a', name: 'b', ext: '.txt' }, '/a/b.txt'], 185[{ name: 'b', ext: '.txt' }, 'b.txt'], 186[{ root: '/a/', base: '' }, '/a/'], 187[{ dir: '', base: '' }, ''], 188[{ root: '', base: 'x', name: 'ignored', ext: '.y' }, 'x'], 189[{ dir: '/a', root: '/b', base: 'c' }, '/a/c'], 190[{ name: '', ext: '' }, ''], 191]; 192for (const [object, expected] of formats) { 193if (path.format(object) !== expected) throw new Error(`format ${JSON.stringify(object)}`); 194} 195const parsed = [ 196['', ['', '', '', '', '']], 197['/', ['/', '/', '', '', '']], 198['//', ['/', '/', '', '', '']], 199['/a', ['/', '/', 'a', '', 'a']], 200['/a/', ['/', '/', 'a', '', 'a']], 201['/a/b', ['/', '/a', 'b', '', 'b']], 202['/a/b/', ['/', '/a', 'b', '', 'b']], 203['/a/b/c.txt', ['/', '/a/b', 'c.txt', '.txt', 'c']], 204['a', ['', '', 'a', '', 'a']], 205['a/', ['', '', 'a', '', 'a']], 206['a.txt', ['', '', 'a.txt', '.txt', 'a']], 207['.txt', ['', '', '.txt', '', '.txt']], 208['..', ['', '', '..', '', '..']], 209['...', ['', '', '...', '.', '..']], 210['a.tar.gz', ['', '', 'a.tar.gz', '.gz', 'a.tar']], 211['a/b.tar.gz', ['', 'a', 'b.tar.gz', '.gz', 'b.tar']], 212['/.txt', ['/', '/', '.txt', '', '.txt']], 213['a/.txt', ['', 'a', '.txt', '', '.txt']], 214['/a/..', ['/', '/a', '..', '', '..']], 215['a\\b', ['', '', 'a\\b', '', 'a\\b']], 216['/a\\b', ['/', '/', 'a\\b', '', 'a\\b']], 217]; 218for (const [input, [root, dir, base, ext, name]] of parsed) { 219const actual = path.parse(input); 220if (actual.root !== root || actual.dir !== dir || actual.base !== base 221|| actual.ext !== ext || actual.name !== name) { 222throw new Error(`parse(${JSON.stringify(input)}) = ${JSON.stringify(actual)}`); 223} 224} 225console.log('posix vectors'); 226"# , 227& [], 228); 229assert_eq! ( output. stdout , b"posix vectors\n" ); 230} 231 232# [ test ] 233fn win32_lexical_vectors () { 234let output = common:: run ( 235r#" 236import * as path from 'avril:path/win32'; 237const cases = [ 238['join', [], '.'], 239['join', ['.'], '.'], 240['join', ['', ''], '.'], 241['join', ['C:\\a', 'b', 'c'], 'C:\\a\\b\\c'], 242['join', ['C:\\a\\', 'b\\'], 'C:\\a\\b\\'], 243['join', ['a', '', 'b'], 'a\\b'], 244['join', ['a', '..', 'b'], 'b'], 245['join', ['C:\\', '..', 'a'], 'C:\\a'], 246['join', ['', '.', ''], '.'], 247['join', ['C:\\a', '..'], 'C:\\'], 248['join', ['a//b', 'c'], 'a\\b\\c'], 249['join', ['//server', 'share'], '\\\\server\\share\\'], 250['join', ['//server/share', 'dir'], '\\\\server\\share\\dir'], 251['join', ['C:/a', 'b'], 'C:\\a\\b'], 252['join', ['a/b', 'c'], 'a\\b\\c'], 253['join', ['\\\\server\\share\\', 'x'], '\\\\server\\share\\x'], 254['join', ['C:', 'a'], 'C:\\a'], 255['join', ['\\', 'a'], '\\a'], 256['join', ['\\\\', 'a'], '\\a'], 257['join', ['\\\\\\', 'a'], '\\a'], 258['join', ['aé', 'b'], 'aé\\b'], 259['join', ['..', '..', 'a'], '..\\..\\a'], 260['join', ['/', '/'], '\\'], 261['join', ['a/', '/'], 'a\\'], 262['resolve', ['C:\\a', 'b', 'c'], 'C:\\a\\b\\c'], 263['resolve', ['C:\\a', 'D:\\b', 'c'], 'D:\\b\\c'], 264['resolve', ['\\\\s\\sh', 'a'], '\\\\s\\sh\\a'], 265['resolve', ['\\a', 'b'], '\\a\\b'], 266['resolve', ['/a', 'b'], '\\a\\b'], 267['resolve', ['C:\\'], 'C:\\'], 268['resolve', ['C:\\a', '..', '..'], 'C:\\'], 269['resolve', ['C:\\a\\b\\', '..\\c\\'], 'C:\\a\\c'], 270['resolve', ['C:/a', './b'], 'C:\\a\\b'], 271['resolve', ['\\\\?\\C:\\a', 'b'], '\\\\?\\C:\\a\\b'], 272['resolve', ['\\\\.\\C:\\a', 'b'], '\\\\.\\C:\\a\\b'], 273['normalize', [''], '.'], 274['normalize', ['.'], '.'], 275['normalize', ['\\'], '\\'], 276['normalize', ['//'], '\\'], 277['normalize', ['\\\\'], '\\'], 278['normalize', ['C:\\'], 'C:\\'], 279['normalize', ['C:'], 'C:.'], 280['normalize', ['C:a'], 'C:a'], 281['normalize', ['C:\\a\\\\b\\.\\c\\..\\d\\'], 'C:\\a\\b\\d\\'], 282['normalize', ['a\\b\\..\\..\\c'], 'c'], 283['normalize', ['..\\a'], '..\\a'], 284['normalize', ['a\\'], 'a\\'], 285['normalize', ['a\\\\'], 'a\\'], 286['normalize', ['\\a\\'], '\\a\\'], 287['normalize', ['.\\a'], 'a'], 288['normalize', ['a\\.\\'], 'a\\'], 289['normalize', ['...'], '...'], 290['normalize', ['....'], '....'], 291['normalize', ['C:..\\..'], 'C:..\\..'], 292['normalize', ['C:..\\a'], 'C:..\\a'], 293['normalize', ['\\a\\b\\'], '\\a\\b\\'], 294['normalize', ['a/.../b'], 'a\\...\\b'], 295['normalize', ['C:a\\b'], 'C:a\\b'], 296['normalize', ['\\\\server\\share'], '\\\\server\\share\\'], 297['normalize', ['\\\\server\\share\\'], '\\\\server\\share\\'], 298['normalize', ['\\\\server\\share\\a\\..\\b'], '\\\\server\\share\\b'], 299['relative', ['C:\\a\\b', 'C:\\a\\b'], ''], 300['relative', ['C:\\a\\b\\c', 'C:\\a\\d\\e'], '..\\..\\d\\e'], 301['relative', ['C:\\', 'C:\\foo'], 'foo'], 302['relative', ['C:\\foo', 'C:\\'], '..'], 303['relative', ['C:\\foo\\bar', 'C:\\foo\\bar\\baz'], 'baz'], 304['relative', ['C:\\foo\\bar\\baz', 'C:\\foo\\bar'], '..'], 305['relative', ['c:\\a', 'C:\\A'], ''], 306['relative', ['C:\\a', 'D:\\b'], 'D:\\b'], 307['relative', ['\\\\s\\sh\\a', '\\\\s\\sh\\b'], '..\\b'], 308['relative', ['\\\\s\\sh\\a', '\\\\o\\sh\\b'], '\\\\o\\sh\\b'], 309['relative', ['C:\\a\\b', 'C:\\a\\b\\'], ''], 310['relative', ['C:\\Foo\\Bar', 'c:\\foo\\bar\\baz'], 'baz'], 311['relative', ['C:\\a', 'C:\\ab'], '..\\ab'], 312['relative', ['C:\\ab', 'C:\\a'], '..\\a'], 313['relative', ['\\a\\b', '\\a\\c'], '..\\c'], 314['dirname', [''], '.'], 315['dirname', ['.'], '.'], 316['dirname', ['\\'], '\\'], 317['dirname', ['\\\\'], '\\'], 318['dirname', ['C:\\'], 'C:\\'], 319['dirname', ['C:'], 'C:'], 320['dirname', ['C:a'], 'C:'], 321['dirname', ['C:\\a'], 'C:\\'], 322['dirname', ['C:\\a\\'], 'C:\\'], 323['dirname', ['C:\\a\\b'], 'C:\\a'], 324['dirname', ['C:\\a\\b\\'], 'C:\\a'], 325['dirname', ['a'], '.'], 326['dirname', ['a\\'], '.'], 327['dirname', ['a\\b'], 'a'], 328['dirname', ['..'], '.'], 329['dirname', ['..\\'], '.'], 330['dirname', ['\\\\s\\sh'], '\\\\s\\sh'], 331['dirname', ['\\\\s\\sh\\'], '\\\\s\\sh\\'], 332['dirname', ['\\\\s\\sh\\a\\b'], '\\\\s\\sh\\a'], 333['dirname', ['C:\\a/b'], 'C:\\a'], 334['dirname', ['/a/b'], '/a'], 335['dirname', ['\\\\?\\C:\\a\\b'], '\\\\?\\C:\\a'], 336['basename', [''], ''], 337['basename', ['\\'], ''], 338['basename', ['\\\\'], ''], 339['basename', ['a'], 'a'], 340['basename', ['a\\'], 'a'], 341['basename', ['a\\\\'], 'a'], 342['basename', ['C:\\a\\b\\c'], 'c'], 343['basename', ['C:\\a\\b\\c\\'], 'c'], 344['basename', ['a.txt'], 'a.txt'], 345['basename', ['.txt'], '.txt'], 346['basename', ['..'], '..'], 347['basename', ['...'], '...'], 348['basename', ['.'], '.'], 349['basename', ['a\\.txt'], '.txt'], 350['basename', ['a\\..txt'], '..txt'], 351['basename', ['C:\\'], ''], 352['basename', ['C:'], ''], 353['basename', ['C:a'], 'a'], 354['basename', ['\\\\s\\sh\\a.txt'], 'a.txt'], 355['basename', ['a\\b.tar.gz', '.gz'], 'b.tar'], 356['basename', ['a.txt', '.txt'], 'a'], 357['basename', ['a.txt', 'a.txt'], ''], 358['basename', ['a', 'a'], ''], 359['basename', ['a.txt', ''], 'a.txt'], 360['basename', ['ab', 'b'], 'a'], 361['basename', ['C:\\a', 'C:\\a'], ''], 362['extname', [''], ''], 363['extname', ['a'], ''], 364['extname', ['a.'], '.'], 365['extname', ['.a'], ''], 366['extname', ['..a'], '.a'], 367['extname', ['a..'], '.'], 368['extname', ['...'], '.'], 369['extname', ['..'], ''], 370['extname', ['.'], ''], 371['extname', ['a.txt'], '.txt'], 372['extname', ['a.tar.gz'], '.gz'], 373['extname', ['.txt'], ''], 374['extname', ['C:\\a\\.txt'], ''], 375['extname', ['C:a.txt'], '.txt'], 376['extname', ['\\\\s\\sh\\a.b\\c'], ''], 377['extname', ['a\\b.'], '.'], 378['extname', ['a b.c d'], '.c d'], 379['extname', ['C:\\'], ''], 380['extname', ['C:'], ''], 381['isAbsolute', [''], false], 382['isAbsolute', ['\\'], true], 383['isAbsolute', ['/'], true], 384['isAbsolute', ['\\\\'], true], 385['isAbsolute', ['C:\\'], true], 386['isAbsolute', ['C:'], false], 387['isAbsolute', ['C:a'], false], 388['isAbsolute', ['C:\\a'], true], 389['isAbsolute', ['C:/a'], true], 390['isAbsolute', ['a'], false], 391['isAbsolute', ['\\a'], true], 392['isAbsolute', ['\\\\s\\sh'], true], 393['isAbsolute', ['a\\b'], false], 394['isAbsolute', ['C'], false], 395['isAbsolute', ['C\\a'], false], 396['isAbsolute', ['\\\\?\\C:\\a'], true], 397]; 398for (let i = 0; i < cases.length; i++) { 399const [fn, args, expected] = cases[i]; 400const actual = path[fn](...args); 401if (actual !== expected) { 402throw new Error(`${fn}(${args.map((a) => JSON.stringify(a)).join(', ')}) = ${JSON.stringify(actual)}, expected ${JSON.stringify(expected)}`); 403} 404} 405if (path.sep !== '\\' || path.delimiter !== ';') throw new Error('separators'); 406const formats = [ 407[{ dir: 'C:\\a\\b', base: 'c.txt' }, 'C:\\a\\b\\c.txt'], 408[{ root: 'C:\\', base: 'c' }, 'C:\\c'], 409[{ dir: 'C:\\a', name: 'b', ext: 'txt' }, 'C:\\a\\b.txt'], 410[{ root: '\\\\s\\sh\\', base: 'a' }, '\\\\s\\sh\\a'], 411[{ dir: '', base: '' }, ''], 412[{ dir: 'C:\\a', root: 'C:\\b', base: 'c' }, 'C:\\a\\c'], 413]; 414for (const [object, expected] of formats) { 415if (path.format(object) !== expected) throw new Error(`format ${JSON.stringify(object)}`); 416} 417const parsed = [ 418['', ['', '', '', '', '']], 419['\\', ['\\', '\\', '', '', '']], 420['C:\\', ['C:\\', 'C:\\', '', '', '']], 421['C:', ['C:', 'C:', '', '', '']], 422['C:a', ['C:', 'C:', 'a', '', 'a']], 423['C:\\a', ['C:\\', 'C:\\', 'a', '', 'a']], 424['C:\\a\\', ['C:\\', 'C:\\', 'a', '', 'a']], 425['C:\\a\\b', ['C:\\', 'C:\\a', 'b', '', 'b']], 426['C:\\a\\b\\', ['C:\\', 'C:\\a', 'b', '', 'b']], 427['C:\\a\\b\\c.txt', ['C:\\', 'C:\\a\\b', 'c.txt', '.txt', 'c']], 428['a', ['', '', 'a', '', 'a']], 429['a\\', ['', '', 'a', '', 'a']], 430['a.txt', ['', '', 'a.txt', '.txt', 'a']], 431['.txt', ['', '', '.txt', '', '.txt']], 432['..', ['', '', '..', '', '..']], 433['...', ['', '', '...', '.', '..']], 434['a.tar.gz', ['', '', 'a.tar.gz', '.gz', 'a.tar']], 435['C:a.txt', ['C:', 'C:', 'a.txt', '.txt', 'a']], 436['\\\\s\\sh', ['\\\\s\\sh', '\\\\s\\sh', '', '', '']], 437['\\\\s\\sh\\', ['\\\\s\\sh\\', '\\\\s\\sh\\', '', '', '']], 438['\\\\s\\sh\\a', ['\\\\s\\sh\\', '\\\\s\\sh\\', 'a', '', 'a']], 439['\\\\s\\sh\\a\\b.txt', ['\\\\s\\sh\\', '\\\\s\\sh\\a', 'b.txt', '.txt', 'b']], 440['C:\\a/b', ['C:\\', 'C:\\a', 'b', '', 'b']], 441]; 442for (const [input, [root, dir, base, ext, name]] of parsed) { 443const actual = path.parse(input); 444if (actual.root !== root || actual.dir !== dir || actual.base !== base 445|| actual.ext !== ext || actual.name !== name) { 446throw new Error(`parse(${JSON.stringify(input)}) = ${JSON.stringify(actual)}`); 447} 448} 449if (path.toNamespacedPath('C:\\a\\b') !== '\\\\?\\C:\\a\\b') throw new Error('device'); 450if (path.toNamespacedPath('\\\\s\\sh\\a') !== '\\\\?\\UNC\\s\\sh\\a') throw new Error('unc'); 451if (path.toNamespacedPath('\\\\?\\C:\\a') !== '\\\\?\\C:\\a') throw new Error('long'); 452if (path.toNamespacedPath('\\a\\b') !== '\\a\\b') throw new Error('rooted'); 453if (path.toNamespacedPath('') !== '') throw new Error('empty'); 454// Drive-relative resolution with a live working directory is covered 455// separately in win32_drive_relative_uses_cwd_drive_or_drive_root. 456console.log('win32 vectors'); 457"# , 458& [], 459); 460assert_eq! ( output. stdout , b"win32 vectors\n" ); 461} 462 463# [ test ] 464fn win32_drive_relative_uses_cwd_drive_or_drive_root () { 465// Drive-relative inputs consult the working directory's drive without 466// ever reading per-drive environment entries: a cwd on the same drive is 467// reused, any other drive falls back to its root. Runs with ambient 468// authority so the cwd is readable; expectations adapt to the host. 469let output = common:: run ( 470r#" 471import * as win32 from 'avril:path/win32'; 472import { process, runtime } from 'avril:std'; 473const cwd = process.cwd(); 474const same = /^[A-Za-z]:[\\/]/.exec(cwd); 475// A drive the cwd cannot be on: the fallback must be that drive's root. 476const other = same && same[0][0].toLowerCase() === 'q' ? 'Z:' : 'Q:'; 477if (win32.resolve(`${other}foo`, 'bar') !== `${other}\\foo\\bar`) { 478throw new Error('drive root fallback'); 479} 480if (win32.resolve(other, 'b') !== `${other}\\b`) throw new Error('bare fallback'); 481if (same) { 482const drive = same[0].slice(0, 2); 483if (win32.resolve(`${drive}foo`) !== win32.join(cwd, 'foo')) { 484throw new Error('same drive reuses cwd'); 485} 486} else if (runtime.os !== 'windows') { 487// POSIX hosts never have a drive-lettered cwd, so C: always falls back. 488if (win32.resolve('C:foo', 'bar') !== 'C:\\foo\\bar') throw new Error('C root'); 489if (win32.resolve('C:', 'b') !== 'C:\\b') throw new Error('C bare root'); 490} 491console.log('drive relative'); 492"# , 493& [], 494); 495assert_eq! ( output. stdout , b"drive relative\n" ); 496} 497 498# [ test ] 499fn native_default_aliases_selected_flavor_and_namespaces_stay_frozen () { 500let output = common:: run ( 501r#" 502import * as path from 'avril:path'; 503import * as explicitPosix from 'avril:path/posix'; 504import * as explicitWin32 from 'avril:path/win32'; 505import { runtime } from 'avril:std'; 506const isWindows = runtime.os === 'windows'; 507const selected = isWindows ? path.win32 : path.posix; 508if (path.sep !== (isWindows ? '\\' : '/')) throw new Error('native sep'); 509if (path.delimiter !== (isWindows ? ';' : ':')) throw new Error('native delimiter'); 510if (path.join !== selected.join || path.resolve !== selected.resolve 511|| path.fromFileUrl !== selected.fromFileUrl || path.toFileUrl !== selected.toFileUrl 512|| path.sep !== selected.sep || path.parse !== selected.parse) { 513throw new Error('native alias'); 514} 515if (path.posix.sep !== '/' || path.win32.sep !== '\\') throw new Error('nested sep'); 516if (path.win32.normalize('C:\\a\\.\\b') !== 'C:\\a\\b') throw new Error('nested win32'); 517if (explicitWin32.isAbsolute('C:\\a') !== true) throw new Error('subpath'); 518if (explicitPosix.join('/a', 'b') !== '/a/b') throw new Error('explicit posix'); 519// All entry modules share one frozen namespace object per flavor. 520if (path.posix !== explicitPosix.posix || path.win32 !== explicitWin32.win32 521|| explicitPosix.win32 !== explicitWin32.win32) { 522throw new Error('namespace identity'); 523} 524for (const ns of [path.posix, path.win32]) { 525if (!Object.isFrozen(ns)) throw new Error('unfrozen namespace'); 526} 527if (path.posix.posix !== undefined || path.posix.win32 !== undefined 528|| path.win32.posix !== undefined || path.win32.win32 !== undefined) { 529throw new Error('recursive namespace fields'); 530} 531const expected = ['basename', 'delimiter', 'dirname', 'extname', 'format', 'fromFileUrl', 532'isAbsolute', 'join', 'normalize', 'parse', 'posix', 'relative', 'resolve', 'sep', 533'toFileUrl', 'toNamespacedPath', 'win32']; 534for (const module of [path, explicitPosix, explicitWin32]) { 535const keys = Object.keys(module).sort(); 536if (JSON.stringify(keys) !== JSON.stringify(expected)) { 537throw new Error(`exports: ${JSON.stringify(keys)}`); 538} 539} 540// Detached calls never consult a receiver; expectations use the native separator. 541const { join, normalize } = path; 542if (join('a', 'b') !== `a${path.sep}b`) throw new Error('detached join'); 543if (normalize('a/./b') !== `a${path.sep}b`) throw new Error('detached normalize'); 544const { parse, format } = explicitPosix; 545if (parse('/a/b').base !== 'b' || format({ dir: '/a', base: 'b' }) !== '/a/b') { 546throw new Error('detached parse/format'); 547} 548const winJoin = path.win32.join; 549if (winJoin('C:\\a', 'b') !== 'C:\\a\\b') throw new Error('detached win32'); 550console.log('native default'); 551"# , 552& [], 553); 554assert_eq! ( output. stdout , b"native default\n" ); 555} 556 557# [ test ] 558fn unicode_strings_survive_lexical_operations_losslessly () { 559let output = common:: run ( 560r#" 561import * as path from 'avril:path/posix'; 562import * as win32 from 'avril:path/win32'; 563if (path.join('aé', 'b') !== 'aé/b') throw new Error('latin'); 564if (path.normalize('/日本語/./x/') !== '/日本語/x/') throw new Error('cjk'); 565if (path.basename('/😀.txt') !== '😀.txt' || path.extname('/😀.txt') !== '.txt') { 566throw new Error('emoji'); 567} 568if (path.parse('/😀.txt').name !== '😀') throw new Error('parse emoji'); 569// Lone surrogates are preserved as-is by purely lexical operations. 570if (path.join('\ud800', 'x') !== '\ud800/x') throw new Error('join surrogate'); 571if (path.normalize('\ud800') !== '\ud800') throw new Error('normalize surrogate'); 572if (path.basename('\ud800.txt') !== '\ud800.txt') throw new Error('basename surrogate'); 573if (win32.normalize('C:\\\ud800\\x') !== 'C:\\\ud800\\x') throw new Error('win32 surrogate'); 574if (path.extname('\udfff') !== '') throw new Error('trail surrogate'); 575console.log('unicode lossless'); 576"# , 577& [], 578); 579assert_eq! ( output. stdout , b"unicode lossless\n" ); 580} 581 582# [ test ] 583fn invalid_arguments_throw_type_errors_without_coercion () { 584let output = common:: run ( 585r#" 586import * as path from 'avril:path'; 587import * as win32 from 'avril:path/win32'; 588const marker = {}; 589const throwing = { toString() { throw marker; } }; 590const cases = [ 591() => path.join(42), () => path.join(throwing), 592() => path.resolve({}), () => path.normalize(null), 593() => path.normalize(undefined), () => path.relative('/a'), 594() => path.relative('/a', 5), () => path.dirname([]), 595() => path.basename('a', null), () => path.basename('a', Symbol('s')), 596() => path.extname(Symbol('e')), () => path.isAbsolute(true), 597() => path.parse(123), () => path.format('x'), () => path.format(null), 598() => path.format([]), () => path.format(42), () => path.format(() => {}), 599() => path.fromFileUrl(42), () => path.fromFileUrl({}), 600() => path.fromFileUrl({ href: 'file:///a' }), 601() => path.toFileUrl(7), () => win32.toFileUrl(null), 602]; 603for (const fn of cases) { 604try { 605fn(); 606throw new Error('accepted invalid input'); 607} catch (e) { 608if (e === marker) throw new Error('coerced throwing input'); 609if (!(e instanceof TypeError)) throw e; 610} 611} 612// Property reads on the format object keep guest exceptions intact. 613const failing = {}; 614Object.defineProperty(failing, 'dir', { get() { throw marker; } }); 615try { 616path.format(failing); 617throw new Error('swallowed getter'); 618} catch (e) { 619if (e !== marker) throw e; 620} 621// Missing arguments fail the same way as explicit undefined. 622for (const fn of [() => path.normalize(), () => path.parse(), () => path.dirname()]) { 623try { 624fn(); 625throw new Error('accepted missing input'); 626} catch (e) { 627if (!(e instanceof TypeError)) throw e; 628} 629} 630// Non-strings pass straight through toNamespacedPath without validation. 631const passthrough = {}; 632if (win32.toNamespacedPath(passthrough) !== passthrough) throw new Error('brand'); 633if (path.toNamespacedPath(42) !== 42) throw new Error('number'); 634console.log('invalid args'); 635"# , 636& [], 637); 638assert_eq! ( output. stdout , b"invalid args\n" ); 639} 640 641# [ test ] 642fn cwd_is_used_only_when_needed_and_rechecked_after_pledge () { 643let dir = tempfile:: tempdir (). unwrap (); 644 fs:: write ( 645 dir. path (). join ( "main.js" ), 646r#" 647import { normalize, relative, resolve, sep, toNamespacedPath } from 'avril:path'; 648import * as win32 from 'avril:path/win32'; 649import { runtime } from 'avril:std'; 650const isWindows = runtime.os === 'windows'; 651const root = isWindows ? 'C:\\abs' : '/abs'; 652const other = isWindows ? 'C:\\y' : '/y'; 653if (normalize('a/./b') !== 'a/b') throw new Error('pure'); 654if (resolve(root, 'x') !== `${root}${sep}x`) throw new Error('absolute resolve'); 655if (relative(isWindows ? 'C:\\x' : '/x', other) !== `..${sep}y`) { 656throw new Error('absolute relative'); 657} 658if (relative(root, root) !== '') throw new Error('early return'); 659if (win32.toNamespacedPath('C:\\a') !== '\\\\?\\C:\\a') throw new Error('namespaced'); 660// POSIX toNamespacedPath is a pure passthrough, so only the win32 661// flavor (which resolves first) needs the directory here. 662if (isWindows) { 663try { 664toNamespacedPath('rel'); 665throw new Error('read cwd without a grant'); 666} catch (e) { 667if (e.name !== 'PermissionDenied') throw e; 668} 669} else if (toNamespacedPath('rel') !== 'rel') { 670throw new Error('passthrough'); 671} 672for (const fn of [() => resolve('rel'), () => relative('a', other), () => resolve(), 673() => win32.toNamespacedPath('rel')]) { 674try { 675fn(); 676throw new Error('read cwd without a grant'); 677} catch (e) { 678if (e.name !== 'PermissionDenied') throw e; 679} 680} 681console.log('no cwd without grants'); 682"# , 683) 684. unwrap (); 685let output =run_in ( dir. path (), & [ "-0" , "main.js" ]); 686assert! ( 687 output. status . success (), 688"{}" , 689String :: from_utf8_lossy ( & output. stderr ) 690); 691assert_eq! ( output. stdout , b"no cwd without grants\n" ); 692 693// With a read grant for the directory, relative resolution matches process.cwd(). 694 fs:: write ( 695 dir. path (). join ( "main.js" ), 696r#" 697import { join, relative, resolve, sep } from 'avril:path'; 698import { process } from 'avril:std'; 699const cwd = process.cwd(); 700if (resolve('a', 'b') !== join(cwd, 'a', 'b')) throw new Error('resolve cwd'); 701if (resolve() !== cwd) throw new Error('bare resolve'); 702if (relative('sub1', 'sub2') !== `..${sep}sub2`) throw new Error('relative cwd'); 703console.log('cwd matches'); 704"# , 705) 706. unwrap (); 707let output =run_in ( dir. path (), & [ "-0" , "--allow-read=." , "main.js" ]); 708assert! ( 709 output. status . success (), 710"{}" , 711String :: from_utf8_lossy ( & output. stderr ) 712); 713assert_eq! ( output. stdout , b"cwd matches\n" ); 714 715// Pledging away read authority is honored on the next call: no cached cwd. 716 fs:: write ( dir. path (). join ( "main.js" ), r#" 717import { pledge, process, runtime } from 'avril:std'; 718import { join, normalize, resolve } from 'avril:path'; 719import * as win32 from 'avril:path/win32'; 720const cwd = process.cwd(); 721const abs = runtime.os === 'windows' ? 'C:\\still\\absolute' : '/still/absolute'; 722if (resolve('a') !== join(cwd, 'a')) throw new Error('before pledge'); 723pledge({ read: false }); 724// Native toNamespacedPath on POSIX hosts is a pure passthrough, so only 725// resolve and the resolving win32 flavor recheck the directory here. 726for (const fn of [() => resolve('b'), () => win32.toNamespacedPath('rel')]) { 727try { 728fn(); 729throw new Error('used cached cwd after pledge'); 730} catch (e) { 731if (e.name !== 'PermissionDenied') throw e; 732} 733} 734if (normalize('x/../y') !== 'y') throw new Error('pure after pledge'); 735if (resolve(abs) !== abs) throw new Error('absolute after pledge'); 736if (win32.toNamespacedPath('C:\\x') !== '\\\\?\\C:\\x') throw new Error('absolute namespaced'); 737console.log('pledge rechecked'); 738"# ). unwrap (); 739let output =run_in ( dir. path (), & [ "-0" , "--allow-read=." , "main.js" ]); 740assert! ( 741 output. status . success (), 742"{}" , 743String :: from_utf8_lossy ( & output. stderr ) 744); 745assert_eq! ( output. stdout , b"pledge rechecked\n" ); 746} 747 748# [ test ] 749fn file_urls_round_trip_and_reject_without_any_grants () { 750let dir = tempfile:: tempdir (). unwrap (); 751let output =run_main ( 752 dir. path (), 753r#" 754import * as path from 'avril:path'; 755import * as win32 from 'avril:path/win32'; 756const fromCharCode = String.fromCharCode; 757const roundTrip = (ns, input) => { 758const back = ns.fromFileUrl(ns.toFileUrl(input)); 759if (back !== input) throw new Error(`round trip ${JSON.stringify(input)} -> ${back}`); 760}; 761for (const input of ['/a b/c', '/a%b', '/a#b?c', '/日本語/😀', '/a\\b', '/a/b/', 762"/a'b", '/a+b', '/a\tb', '/a|b^c', '/a~b', '/a[b]c', '/a;b,c=d:e@f$g&h!i*j(k)l']) { 763roundTrip(path, input); 764} 765// Every C0 control except NUL, plus DEL, survives the round trip encoded. 766for (let code = 1; code < 32; code++) roundTrip(path, `/a${fromCharCode(code)}b`); 767roundTrip(path, `/a${fromCharCode(127)}b`); 768// UTF-8 width boundaries round-trip; lone surrogates never reach the URL. 769for (const point of [0x7f, 0x80, 0x7ff, 0x800, 0xffff, 0x10000, 0x10ffff]) { 770roundTrip(path, `/a${String.fromCodePoint(point)}b`); 771} 772for (const input of ['C:\\a b\\c', '\\\\server\\share\\a b', 'C:\\a\\', 773'C:\\日本語\\x', 'C:\\a#b', 'C:\\a+b', 'C:\\a~b', '\\\\[::1]\\share']) { 774roundTrip(win32, input); 775} 776// Encoding matches Node's pathToFileURL across the ASCII range: only 777// alphanumerics, sub-delims, `@:`, and `;,=`, `-`, `.`, `_` stay literal. 778const kept = "!$&'()*+,-./0123456789:;=@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz"; 779for (let code = 1; code < 128; code++) { 780if (code === 47) continue; 781const href = path.toFileUrl(`/a${fromCharCode(code)}b`).href; 782const literal = `file:///a${fromCharCode(code)}b`; 783if (kept.includes(fromCharCode(code)) !== (href === literal)) { 784throw new Error(`ascii ${code}: ${href}`); 785} 786} 787if (path.fromFileUrl('file://localhost/a') !== '/a') throw new Error('localhost'); 788if (win32.fromFileUrl('file://localhost/C:/a') !== 'C:\\a') throw new Error('win localhost'); 789if (win32.fromFileUrl('file://server/share/a') !== '\\\\server\\share\\a') { 790throw new Error('unc host'); 791} 792if (win32.fromFileUrl('file://server/share/') !== '\\\\server\\share\\') { 793throw new Error('unc trailing'); 794} 795if (win32.fromFileUrl('file://[::1]/share') !== '\\\\[::1]\\share') { 796throw new Error('ipv6'); 797} 798if (win32.fromFileUrl('file:///c:/a') !== 'c:\\a') throw new Error('lower drive'); 799// Non-ASCII servers normalize through IDNA, deterministically. 800if (win32.toFileUrl('\\\\münchen\\share').href !== 'file://xn--mnchen-3ya/share') { 801throw new Error('idna href'); 802} 803if (win32.fromFileUrl('file://münchen/share') !== '\\\\xn--mnchen-3ya\\share') { 804throw new Error('idna path'); 805} 806// DNS names fold ASCII case, so mixed-case servers still round-trip. 807if (win32.toFileUrl('\\\\SERVER\\share').href !== 'file://server/share') { 808throw new Error('server case'); 809} 810if (path.toFileUrl('/a/b/').href !== 'file:///a/b/') throw new Error('trailing slash'); 811if (win32.toFileUrl('C:\\a\\').href !== 'file:///C:/a/') throw new Error('win trailing'); 812// `.`/`..` clamp at the drive and share roots instead of crossing them. 813const clamped = [ 814['C:\\..\\a', 'file:///C:/a', 'C:\\a'], 815['C:\\', 'file:///C:/', 'C:\\'], 816['C:\\a', 'file:///C:/a', 'C:\\a'], 817['\\\\server\\share\\\\a', 'file://server/share/a', '\\\\server\\share\\a'], 818['\\\\server\\share', 'file://server/share', '\\\\server\\share'], 819['\\\\server\\share\\', 'file://server/share/', '\\\\server\\share\\'], 820['\\\\server\\share\\..\\other', 'file://server/share/other', '\\\\server\\share\\other'], 821['\\\\server\\share\\..', 'file://server/share', '\\\\server\\share'], 822['\\\\server\\share\\a\\.\\b\\..\\c', 'file://server/share/a/c', '\\\\server\\share\\a\\c'], 823['\\\\server\\share\\a', 'file://server/share/a', '\\\\server\\share\\a'], 824]; 825for (const [input, href, back] of clamped) { 826const url = win32.toFileUrl(input); 827if (url.href !== href) throw new Error(`href ${JSON.stringify(input)} -> ${url.href}`); 828if (win32.fromFileUrl(url) !== back) throw new Error(`back ${JSON.stringify(input)}`); 829} 830// A URL instance and its href string convert identically. 831const url = path.toFileUrl('/a b'); 832if (!(url instanceof URL)) throw new Error('returns URL'); 833if (path.fromFileUrl(url) !== path.fromFileUrl(url.href)) throw new Error('instance'); 834const rejections = [ 835() => path.fromFileUrl('https://example.com/a'), 836() => path.fromFileUrl('/a/b'), 837() => path.fromFileUrl('file:///a?b=c'), 838() => path.fromFileUrl('file:///a#x'), 839() => path.fromFileUrl('file:///a%2Fb'), 840() => path.fromFileUrl('file:///a%2fb'), 841() => win32.fromFileUrl('file:///C:/a%5Cb'), 842() => win32.fromFileUrl('file:///C:/a%5cb'), 843() => path.fromFileUrl('file:///a%2'), 844() => path.fromFileUrl('file:///a%2G'), 845() => path.fromFileUrl('file:///a%'), 846() => path.fromFileUrl('file:///a%00b'), 847() => path.fromFileUrl('file:///a%ED%A0%80b'), 848() => path.fromFileUrl('file:///a%ED%BF%BFb'), 849() => path.fromFileUrl('file:///a%C0%AFb'), 850() => path.fromFileUrl('file:///a%E0%80%AFb'), 851() => path.fromFileUrl('file:///a%F0%80%80%AFb'), 852() => path.fromFileUrl('file:///a%E2%82b'), 853() => path.fromFileUrl('file:///a%F0%9F%98b'), 854() => path.fromFileUrl('file:///a%80b'), 855() => path.fromFileUrl('file:///a%BFb'), 856() => path.fromFileUrl('file:///a%F4%90%80%80b'), 857() => path.fromFileUrl('file:///a%FEb'), 858() => path.fromFileUrl('file:///a%FFb'), 859() => path.fromFileUrl('file://other/a'), 860() => path.fromFileUrl(42), 861() => path.fromFileUrl({ href: 'file:///a' }), 862() => win32.fromFileUrl('file:///no-drive/a'), 863() => win32.fromFileUrl('file:///'), 864() => win32.fromFileUrl('file:///C:'), 865() => win32.fromFileUrl('file:///C:foo'), 866() => win32.fromFileUrl('file://server/'), 867() => win32.fromFileUrl('file://server'), 868() => win32.fromFileUrl('file://server//'), 869() => path.fromFileUrl(`file:///${fromCharCode(0xd800)}`), 870() => path.fromFileUrl(`file:///a${fromCharCode(0)}b`), 871() => path.fromFileUrl('file:///a\tb'), 872() => path.fromFileUrl('file:///a\nb'), 873() => path.toFileUrl('a/b'), 874() => path.toFileUrl(''), 875() => win32.toFileUrl('a\\b'), 876() => win32.toFileUrl('C:foo'), 877() => win32.toFileUrl('C:'), 878() => win32.toFileUrl('\\foo'), 879() => win32.toFileUrl('/foo'), 880() => win32.toFileUrl('\\\\?\\C:\\a'), 881() => win32.toFileUrl('\\\\.\\C:\\a'), 882() => win32.toFileUrl('\\\\localhost\\share'), 883() => win32.toFileUrl('\\\\LOCALHOST\\share'), 884() => win32.toFileUrl('\\\\localhost\\\\share\\\\a'), 885() => win32.toFileUrl('\\\\localhost\\\\share\\\\a'), 886() => win32.toFileUrl('\\\\server\\\\.\\\\x'), 887() => win32.toFileUrl('\\\\server\\\\..\\\\x'), 888() => win32.toFileUrl('\\\\server'), 889() => win32.toFileUrl('\\\\server\\'), 890() => win32.toFileUrl('\\\\a\tb\\share'), 891() => win32.toFileUrl('\\\\a\nb\\share'), 892() => win32.toFileUrl('\\\\a b\\share'), 893() => win32.toFileUrl('\\\\a#b\\share'), 894() => win32.toFileUrl('\\\\a?b\\share'), 895() => win32.toFileUrl('\\\\a@b\\share'), 896() => win32.toFileUrl('\\\\a%b\\share'), 897() => win32.toFileUrl('\\\\[::1\\share'), 898() => path.toFileUrl('/a\0b'), 899() => path.toFileUrl('/a\ud800b'), 900() => win32.toFileUrl(3), 901]; 902for (const fn of rejections) { 903try { 904fn(); 905throw new Error('accepted bad file URL input'); 906} catch (e) { 907if (!(e instanceof TypeError)) throw e; 908} 909} 910if (path.fromFileUrl('file:///a?') !== '/a') throw new Error('empty query'); 911if (path.fromFileUrl('file:///a#') !== '/a') throw new Error('empty fragment'); 912console.log('file urls'); 913"# , 914& [ "-0" , "main.js" ], 915); 916let output =success ( output); 917assert_eq! ( output, "file urls\n" ); 918} 919 920# [ test ] 921fn hardened_calls_survive_primordial_and_url_pollution () { 922let dir = tempfile:: tempdir (). unwrap (); 923let output =run_main ( 924 dir. path (), 925r#" 926import * as path from 'avril:path'; 927import * as win32 from 'avril:path/win32'; 928import { posix } from 'avril:path/posix'; 929const TypeErr = TypeError; 930const before = new URL('file:///a%20b/c'); 931const unc = new URL('file://server/share/a'); 932const poison = () => { throw new Error('Poisoned'); }; 933for (const key of ['slice', 'split', 'charCodeAt', 'indexOf', 'toLowerCase', 934'includes', 'replace', 'repeat', 'lastIndexOf']) { 935String.prototype[key] = poison; 936} 937String.prototype[Symbol.split] = poison; 938String.fromCharCode = poison; 939for (const key of ['join', 'slice', 'push', 'pop']) Array.prototype[key] = poison; 940RegExp.prototype.exec = poison; 941globalThis.encodeURI = poison; 942globalThis.encodeURIComponent = poison; 943globalThis.decodeURIComponent = poison; 944const URLProto = URL.prototype; 945Object.defineProperty(URLProto, 'hostname', { value: 'poisoned' }); 946Object.defineProperty(URLProto, 'pathname', { value: 'poisoned' }); 947globalThis.URL = poison; 948if (path.join('/a', 'b', '../c') !== '/a/c') throw new Error('join'); 949if (path.normalize('a/./b//') !== 'a/b/') throw new Error('normalize'); 950if (path.resolve('/a', 'b') !== '/a/b') throw new Error('absolute resolve'); 951if (path.relative('/a/b', '/a/c') !== '../c') throw new Error('absolute relative'); 952if (path.parse('/a/b.txt').ext !== '.txt') throw new Error('parse'); 953if (path.format({ dir: '/a', base: 'b' }) !== '/a/b') throw new Error('format'); 954if (win32.join('C:\\a', '..\\b') !== 'C:\\b') throw new Error('win32 join'); 955if (win32.relative('C:\\a\\b\\', 'C:\\a\\c') !== '..\\c') throw new Error('win32 relative'); 956if (posix.dirname('/a/b') !== '/a') throw new Error('namespace'); 957if (path.toFileUrl('/a b').href !== 'file:///a%20b') throw new Error('toFileUrl'); 958if (path.fromFileUrl(before.href) !== '/a b/c') throw new Error('fromFileUrl string'); 959if (win32.fromFileUrl(unc) !== '\\\\server\\share\\a') throw new Error('unc instance'); 960try { 961path.fromFileUrl('file:///a%2Fb'); 962throw new Error('lost encoded separator check'); 963} catch (e) { 964if (!(e instanceof TypeErr)) throw e; 965} 966try { 967win32.fromFileUrl({ href: 'file:///C:/a' }); 968throw new Error('lost brand check'); 969} catch (e) { 970if (!(e instanceof TypeErr)) throw e; 971} 972try { 973path.resolve('needs-cwd'); 974throw new Error('lost permission error'); 975} catch (e) { 976if (e.name !== 'PermissionDenied') throw e; 977} 978try { 979path.join(1); 980throw new Error('lost validation'); 981} catch (e) { 982if (!(e instanceof TypeErr)) throw e; 983} 984console.log('hardened'); 985"# , 986& [ "-0" , "main.js" ], 987); 988let output =success ( output); 989assert_eq! ( output, "hardened\n" ); 990} 991 992# [ test ] 993fn workers_import_path_and_its_subpaths () { 994let dir = tempfile:: tempdir (). unwrap (); 995 fs:: write ( 996 dir. path (). join ( "worker.ts" ), 997r#" 998import * as path from 'avril:path'; 999import { fromFileUrl as posixFromFileUrl, join, toFileUrl } from 'avril:path/posix'; 1000import * as win32 from 'avril:path/win32'; 1001import { runtime } from 'avril:std'; 1002const sep = runtime.os === 'windows' ? '\\' : '/'; 1003const result = [ 1004path.sep, join('/w', 'x'), win32.normalize('C:\\a\\.\\b'), toFileUrl('/w x').href, 1005posixFromFileUrl('file:///w%20x'), 1006]; 1007postMessage(result); 1008close(); 1009"# , 1010) 1011. unwrap (); 1012let output =run_main ( 1013 dir. path (), 1014r#" 1015const worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' }); 1016const answer = await new Promise((resolve, reject) => { 1017worker.onmessage = (e) => resolve(e.data); 1018worker.onerror = reject; 1019}); 1020const { runtime } = await import('avril:std'); 1021const sep = runtime.os === 'windows' ? '\\' : '/'; 1022const expected = [sep, '/w/x', 'C:\\a\\b', 'file:///w%20x', '/w x']; 1023if (JSON.stringify(answer) !== JSON.stringify(expected)) { 1024throw new Error(`worker paths: ${JSON.stringify(answer)}`); 1025} 1026console.log('worker paths'); 1027"# , 1028& [ "main.js" ], 1029); 1030let output =success ( output); 1031assert_eq! ( output, "worker paths\n" ); 1032} 1033 1034# [ test ] 1035fn typescript_imports_path_with_types_and_rejects_unknown_exports () { 1036let dir = tempfile:: tempdir (). unwrap (); 1037 fs:: write ( 1038 dir. path (). join ( "main.ts" ), 1039r#" 1040import { join as nativeJoin, parse as nativeParse, posix, resolve as nativeResolve, sep, win32, type ParsedPath } from 'avril:path'; 1041import * as posixns from 'avril:path/posix'; 1042import * as win32ns from 'avril:path/win32'; 1043import { runtime } from 'avril:std'; 1044const parts: ParsedPath = posixns.parse('/a/b.txt'); 1045const combined: string = posixns.join('a', posixns.basename('/x/y')) + posixns.sep + posixns.delimiter; 1046const parent: string = posixns.dirname('/a/b'); 1047const back: string = posixns.fromFileUrl(posixns.toFileUrl('/a b')); 1048const flavored: string = win32ns.normalize('C:\\a') + posix.sep + win32.sep; 1049const native = runtime.os === 'windows' ? win32 : posix; 1050const aliased: boolean = nativeJoin === native.join && nativeResolve === native.resolve 1051&& nativeParse === native.parse && sep === native.sep; 1052console.log([parts.ext, combined, parent, back, flavored, aliased].join('|')); 1053"# , 1054) 1055. unwrap (); 1056let output =run_in ( dir. path (), & [ "-0" , "main.ts" ]); 1057assert! ( 1058 output. status . success (), 1059"{}" , 1060String :: from_utf8_lossy ( & output. stderr ) 1061); 1062assert_eq! ( output. stdout , b".txt|a/y/:|/a|/a b|C:\\a/\\|true\n" ); 1063 1064 fs:: write ( 1065 dir. path (). join ( "main.js" ), 1066"import { missing } from 'avril:path'; console.log(missing);" , 1067) 1068. unwrap (); 1069let output =run_in ( dir. path (), & [ "-0" , "main.js" ]); 1070assert! ( !output. status . success ()); 1071assert! ( 1072String :: from_utf8_lossy ( & output. stderr ) 1073. contains ( "does not provide an export named 'missing'" ) 1074); 1075}