char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somsnapshot: embed shared main and worker startup in the CLI688e96e

main
47.8 KiB1075 linesraw
1mod common;
2
3use std::fs;
4use std::process::{Command, Output};
5
6fn run_in(dir: &std::path::Path, args: &[&str]) -> Output {
7    Command::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();
17    run_in(dir, args)
18}
19
20fn success(output: Output) -> String {
21    assert!(
22        output.status.success(),
23        "{}",
24        String::from_utf8_lossy(&output.stderr)
25    );
26    String::from_utf8(output.stdout).unwrap()
27}
28
29#[test]
30fn posix_lexical_vectors() {
31    let output = common::run(
32        r#"
33        import * as path from 'avril:path/posix';
34        const 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        ];
168        for (let i = 0; i < cases.length; i++) {
169          const [fn, args, expected] = cases[i];
170          const actual = path[fn](...args);
171          if (actual !== expected) {
172            throw new Error(`${fn}(${args.map((a) => JSON.stringify(a)).join(', ')}) = ${JSON.stringify(actual)}, expected ${JSON.stringify(expected)}`);
173          }
174        }
175        if (path.sep !== '/' || path.delimiter !== ':') throw new Error('separators');
176        if (path.toNamespacedPath('/a/b') !== '/a/b' || path.toNamespacedPath('a') !== 'a'
177            || path.toNamespacedPath('') !== '' || path.toNamespacedPath(42) !== 42) {
178          throw new Error('toNamespacedPath passthrough');
179        }
180        const 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        ];
192        for (const [object, expected] of formats) {
193          if (path.format(object) !== expected) throw new Error(`format ${JSON.stringify(object)}`);
194        }
195        const 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        ];
218        for (const [input, [root, dir, base, ext, name]] of parsed) {
219          const actual = path.parse(input);
220          if (actual.root !== root || actual.dir !== dir || actual.base !== base
221              || actual.ext !== ext || actual.name !== name) {
222            throw new Error(`parse(${JSON.stringify(input)}) = ${JSON.stringify(actual)}`);
223          }
224        }
225        console.log('posix vectors');
226    "#,
227        &[],
228    );
229    assert_eq!(output.stdout, b"posix vectors\n");
230}
231
232#[test]
233fn win32_lexical_vectors() {
234    let output = common::run(
235        r#"
236        import * as path from 'avril:path/win32';
237        const 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        ];
398        for (let i = 0; i < cases.length; i++) {
399          const [fn, args, expected] = cases[i];
400          const actual = path[fn](...args);
401          if (actual !== expected) {
402            throw new Error(`${fn}(${args.map((a) => JSON.stringify(a)).join(', ')}) = ${JSON.stringify(actual)}, expected ${JSON.stringify(expected)}`);
403          }
404        }
405        if (path.sep !== '\\' || path.delimiter !== ';') throw new Error('separators');
406        const 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        ];
414        for (const [object, expected] of formats) {
415          if (path.format(object) !== expected) throw new Error(`format ${JSON.stringify(object)}`);
416        }
417        const 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        ];
442        for (const [input, [root, dir, base, ext, name]] of parsed) {
443          const actual = path.parse(input);
444          if (actual.root !== root || actual.dir !== dir || actual.base !== base
445              || actual.ext !== ext || actual.name !== name) {
446            throw new Error(`parse(${JSON.stringify(input)}) = ${JSON.stringify(actual)}`);
447          }
448        }
449        if (path.toNamespacedPath('C:\\a\\b') !== '\\\\?\\C:\\a\\b') throw new Error('device');
450        if (path.toNamespacedPath('\\\\s\\sh\\a') !== '\\\\?\\UNC\\s\\sh\\a') throw new Error('unc');
451        if (path.toNamespacedPath('\\\\?\\C:\\a') !== '\\\\?\\C:\\a') throw new Error('long');
452        if (path.toNamespacedPath('\\a\\b') !== '\\a\\b') throw new Error('rooted');
453        if (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.
456        console.log('win32 vectors');
457    "#,
458        &[],
459    );
460    assert_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.
469    let output = common::run(
470        r#"
471        import * as win32 from 'avril:path/win32';
472        import { process, runtime } from 'avril:std';
473        const cwd = process.cwd();
474        const same = /^[A-Za-z]:[\\/]/.exec(cwd);
475        // A drive the cwd cannot be on: the fallback must be that drive's root.
476        const other = same && same[0][0].toLowerCase() === 'q' ? 'Z:' : 'Q:';
477        if (win32.resolve(`${other}foo`, 'bar') !== `${other}\\foo\\bar`) {
478          throw new Error('drive root fallback');
479        }
480        if (win32.resolve(other, 'b') !== `${other}\\b`) throw new Error('bare fallback');
481        if (same) {
482          const drive = same[0].slice(0, 2);
483          if (win32.resolve(`${drive}foo`) !== win32.join(cwd, 'foo')) {
484            throw 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.
488          if (win32.resolve('C:foo', 'bar') !== 'C:\\foo\\bar') throw new Error('C root');
489          if (win32.resolve('C:', 'b') !== 'C:\\b') throw new Error('C bare root');
490        }
491        console.log('drive relative');
492    "#,
493        &[],
494    );
495    assert_eq!(output.stdout, b"drive relative\n");
496}
497
498#[test]
499fn native_default_aliases_selected_flavor_and_namespaces_stay_frozen() {
500    let output = common::run(
501        r#"
502        import * as path from 'avril:path';
503        import * as explicitPosix from 'avril:path/posix';
504        import * as explicitWin32 from 'avril:path/win32';
505        import { runtime } from 'avril:std';
506        const isWindows = runtime.os === 'windows';
507        const selected = isWindows ? path.win32 : path.posix;
508        if (path.sep !== (isWindows ? '\\' : '/')) throw new Error('native sep');
509        if (path.delimiter !== (isWindows ? ';' : ':')) throw new Error('native delimiter');
510        if (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) {
513          throw new Error('native alias');
514        }
515        if (path.posix.sep !== '/' || path.win32.sep !== '\\') throw new Error('nested sep');
516        if (path.win32.normalize('C:\\a\\.\\b') !== 'C:\\a\\b') throw new Error('nested win32');
517        if (explicitWin32.isAbsolute('C:\\a') !== true) throw new Error('subpath');
518        if (explicitPosix.join('/a', 'b') !== '/a/b') throw new Error('explicit posix');
519        // All entry modules share one frozen namespace object per flavor.
520        if (path.posix !== explicitPosix.posix || path.win32 !== explicitWin32.win32
521            || explicitPosix.win32 !== explicitWin32.win32) {
522          throw new Error('namespace identity');
523        }
524        for (const ns of [path.posix, path.win32]) {
525          if (!Object.isFrozen(ns)) throw new Error('unfrozen namespace');
526        }
527        if (path.posix.posix !== undefined || path.posix.win32 !== undefined
528            || path.win32.posix !== undefined || path.win32.win32 !== undefined) {
529          throw new Error('recursive namespace fields');
530        }
531        const expected = ['basename', 'delimiter', 'dirname', 'extname', 'format', 'fromFileUrl',
532          'isAbsolute', 'join', 'normalize', 'parse', 'posix', 'relative', 'resolve', 'sep',
533          'toFileUrl', 'toNamespacedPath', 'win32'];
534        for (const module of [path, explicitPosix, explicitWin32]) {
535          const keys = Object.keys(module).sort();
536          if (JSON.stringify(keys) !== JSON.stringify(expected)) {
537            throw new Error(`exports: ${JSON.stringify(keys)}`);
538          }
539        }
540        // Detached calls never consult a receiver; expectations use the native separator.
541        const { join, normalize } = path;
542        if (join('a', 'b') !== `a${path.sep}b`) throw new Error('detached join');
543        if (normalize('a/./b') !== `a${path.sep}b`) throw new Error('detached normalize');
544        const { parse, format } = explicitPosix;
545        if (parse('/a/b').base !== 'b' || format({ dir: '/a', base: 'b' }) !== '/a/b') {
546          throw new Error('detached parse/format');
547        }
548        const winJoin = path.win32.join;
549        if (winJoin('C:\\a', 'b') !== 'C:\\a\\b') throw new Error('detached win32');
550        console.log('native default');
551    "#,
552        &[],
553    );
554    assert_eq!(output.stdout, b"native default\n");
555}
556
557#[test]
558fn unicode_strings_survive_lexical_operations_losslessly() {
559    let output = common::run(
560        r#"
561        import * as path from 'avril:path/posix';
562        import * as win32 from 'avril:path/win32';
563        if (path.join('aé', 'b') !== 'aé/b') throw new Error('latin');
564        if (path.normalize('/日本語/./x/') !== '/日本語/x/') throw new Error('cjk');
565        if (path.basename('/😀.txt') !== '😀.txt' || path.extname('/😀.txt') !== '.txt') {
566          throw new Error('emoji');
567        }
568        if (path.parse('/😀.txt').name !== '😀') throw new Error('parse emoji');
569        // Lone surrogates are preserved as-is by purely lexical operations.
570        if (path.join('\ud800', 'x') !== '\ud800/x') throw new Error('join surrogate');
571        if (path.normalize('\ud800') !== '\ud800') throw new Error('normalize surrogate');
572        if (path.basename('\ud800.txt') !== '\ud800.txt') throw new Error('basename surrogate');
573        if (win32.normalize('C:\\\ud800\\x') !== 'C:\\\ud800\\x') throw new Error('win32 surrogate');
574        if (path.extname('\udfff') !== '') throw new Error('trail surrogate');
575        console.log('unicode lossless');
576    "#,
577        &[],
578    );
579    assert_eq!(output.stdout, b"unicode lossless\n");
580}
581
582#[test]
583fn invalid_arguments_throw_type_errors_without_coercion() {
584    let output = common::run(
585        r#"
586        import * as path from 'avril:path';
587        import * as win32 from 'avril:path/win32';
588        const marker = {};
589        const throwing = { toString() { throw marker; } };
590        const 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        ];
603        for (const fn of cases) {
604          try {
605            fn();
606            throw new Error('accepted invalid input');
607          } catch (e) {
608            if (e === marker) throw new Error('coerced throwing input');
609            if (!(e instanceof TypeError)) throw e;
610          }
611        }
612        // Property reads on the format object keep guest exceptions intact.
613        const failing = {};
614        Object.defineProperty(failing, 'dir', { get() { throw marker; } });
615        try {
616          path.format(failing);
617          throw new Error('swallowed getter');
618        } catch (e) {
619          if (e !== marker) throw e;
620        }
621        // Missing arguments fail the same way as explicit undefined.
622        for (const fn of [() => path.normalize(), () => path.parse(), () => path.dirname()]) {
623          try {
624            fn();
625            throw new Error('accepted missing input');
626          } catch (e) {
627            if (!(e instanceof TypeError)) throw e;
628          }
629        }
630        // Non-strings pass straight through toNamespacedPath without validation.
631        const passthrough = {};
632        if (win32.toNamespacedPath(passthrough) !== passthrough) throw new Error('brand');
633        if (path.toNamespacedPath(42) !== 42) throw new Error('number');
634        console.log('invalid args');
635    "#,
636        &[],
637    );
638    assert_eq!(output.stdout, b"invalid args\n");
639}
640
641#[test]
642fn cwd_is_used_only_when_needed_and_rechecked_after_pledge() {
643    let dir = tempfile::tempdir().unwrap();
644    fs::write(
645        dir.path().join("main.js"),
646        r#"
647        import { normalize, relative, resolve, sep, toNamespacedPath } from 'avril:path';
648        import * as win32 from 'avril:path/win32';
649        import { runtime } from 'avril:std';
650        const isWindows = runtime.os === 'windows';
651        const root = isWindows ? 'C:\\abs' : '/abs';
652        const other = isWindows ? 'C:\\y' : '/y';
653        if (normalize('a/./b') !== 'a/b') throw new Error('pure');
654        if (resolve(root, 'x') !== `${root}${sep}x`) throw new Error('absolute resolve');
655        if (relative(isWindows ? 'C:\\x' : '/x', other) !== `..${sep}y`) {
656          throw new Error('absolute relative');
657        }
658        if (relative(root, root) !== '') throw new Error('early return');
659        if (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.
662        if (isWindows) {
663          try {
664            toNamespacedPath('rel');
665            throw new Error('read cwd without a grant');
666          } catch (e) {
667            if (e.name !== 'PermissionDenied') throw e;
668          }
669        } else if (toNamespacedPath('rel') !== 'rel') {
670          throw new Error('passthrough');
671        }
672        for (const fn of [() => resolve('rel'), () => relative('a', other), () => resolve(),
673            () => win32.toNamespacedPath('rel')]) {
674          try {
675            fn();
676            throw new Error('read cwd without a grant');
677          } catch (e) {
678            if (e.name !== 'PermissionDenied') throw e;
679          }
680        }
681        console.log('no cwd without grants');
682    "#,
683    )
684    .unwrap();
685    let output = run_in(dir.path(), &["-0", "main.js"]);
686    assert!(
687        output.status.success(),
688        "{}",
689        String::from_utf8_lossy(&output.stderr)
690    );
691    assert_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"),
696        r#"
697        import { join, relative, resolve, sep } from 'avril:path';
698        import { process } from 'avril:std';
699        const cwd = process.cwd();
700        if (resolve('a', 'b') !== join(cwd, 'a', 'b')) throw new Error('resolve cwd');
701        if (resolve() !== cwd) throw new Error('bare resolve');
702        if (relative('sub1', 'sub2') !== `..${sep}sub2`) throw new Error('relative cwd');
703        console.log('cwd matches');
704    "#,
705    )
706    .unwrap();
707    let output = run_in(dir.path(), &["-0", "--allow-read=.", "main.js"]);
708    assert!(
709        output.status.success(),
710        "{}",
711        String::from_utf8_lossy(&output.stderr)
712    );
713    assert_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#"
717        import { pledge, process, runtime } from 'avril:std';
718        import { join, normalize, resolve } from 'avril:path';
719        import * as win32 from 'avril:path/win32';
720        const cwd = process.cwd();
721        const abs = runtime.os === 'windows' ? 'C:\\still\\absolute' : '/still/absolute';
722        if (resolve('a') !== join(cwd, 'a')) throw new Error('before pledge');
723        pledge({ 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.
726        for (const fn of [() => resolve('b'), () => win32.toNamespacedPath('rel')]) {
727          try {
728            fn();
729            throw new Error('used cached cwd after pledge');
730          } catch (e) {
731            if (e.name !== 'PermissionDenied') throw e;
732          }
733        }
734        if (normalize('x/../y') !== 'y') throw new Error('pure after pledge');
735        if (resolve(abs) !== abs) throw new Error('absolute after pledge');
736        if (win32.toNamespacedPath('C:\\x') !== '\\\\?\\C:\\x') throw new Error('absolute namespaced');
737        console.log('pledge rechecked');
738    "#).unwrap();
739    let output = run_in(dir.path(), &["-0", "--allow-read=.", "main.js"]);
740    assert!(
741        output.status.success(),
742        "{}",
743        String::from_utf8_lossy(&output.stderr)
744    );
745    assert_eq!(output.stdout, b"pledge rechecked\n");
746}
747
748#[test]
749fn file_urls_round_trip_and_reject_without_any_grants() {
750    let dir = tempfile::tempdir().unwrap();
751    let output = run_main(
752        dir.path(),
753        r#"
754        import * as path from 'avril:path';
755        import * as win32 from 'avril:path/win32';
756        const fromCharCode = String.fromCharCode;
757        const roundTrip = (ns, input) => {
758          const back = ns.fromFileUrl(ns.toFileUrl(input));
759          if (back !== input) throw new Error(`round trip ${JSON.stringify(input)} -> ${back}`);
760        };
761        for (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']) {
763          roundTrip(path, input);
764        }
765        // Every C0 control except NUL, plus DEL, survives the round trip encoded.
766        for (let code = 1; code < 32; code++) roundTrip(path, `/a${fromCharCode(code)}b`);
767        roundTrip(path, `/a${fromCharCode(127)}b`);
768        // UTF-8 width boundaries round-trip; lone surrogates never reach the URL.
769        for (const point of [0x7f, 0x80, 0x7ff, 0x800, 0xffff, 0x10000, 0x10ffff]) {
770          roundTrip(path, `/a${String.fromCodePoint(point)}b`);
771        }
772        for (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']) {
774          roundTrip(win32, input);
775        }
776        // Encoding matches Node's pathToFileURL across the ASCII range: only
777        // alphanumerics, sub-delims, `@:`, and `;,=`, `-`, `.`, `_` stay literal.
778        const kept = "!$&'()*+,-./0123456789:;=@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz";
779        for (let code = 1; code < 128; code++) {
780          if (code === 47) continue;
781          const href = path.toFileUrl(`/a${fromCharCode(code)}b`).href;
782          const literal = `file:///a${fromCharCode(code)}b`;
783          if (kept.includes(fromCharCode(code)) !== (href === literal)) {
784            throw new Error(`ascii ${code}: ${href}`);
785          }
786        }
787        if (path.fromFileUrl('file://localhost/a') !== '/a') throw new Error('localhost');
788        if (win32.fromFileUrl('file://localhost/C:/a') !== 'C:\\a') throw new Error('win localhost');
789        if (win32.fromFileUrl('file://server/share/a') !== '\\\\server\\share\\a') {
790          throw new Error('unc host');
791        }
792        if (win32.fromFileUrl('file://server/share/') !== '\\\\server\\share\\') {
793          throw new Error('unc trailing');
794        }
795        if (win32.fromFileUrl('file://[::1]/share') !== '\\\\[::1]\\share') {
796          throw new Error('ipv6');
797        }
798        if (win32.fromFileUrl('file:///c:/a') !== 'c:\\a') throw new Error('lower drive');
799        // Non-ASCII servers normalize through IDNA, deterministically.
800        if (win32.toFileUrl('\\\\münchen\\share').href !== 'file://xn--mnchen-3ya/share') {
801          throw new Error('idna href');
802        }
803        if (win32.fromFileUrl('file://münchen/share') !== '\\\\xn--mnchen-3ya\\share') {
804          throw new Error('idna path');
805        }
806        // DNS names fold ASCII case, so mixed-case servers still round-trip.
807        if (win32.toFileUrl('\\\\SERVER\\share').href !== 'file://server/share') {
808          throw new Error('server case');
809        }
810        if (path.toFileUrl('/a/b/').href !== 'file:///a/b/') throw new Error('trailing slash');
811        if (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.
813        const 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        ];
825        for (const [input, href, back] of clamped) {
826          const url = win32.toFileUrl(input);
827          if (url.href !== href) throw new Error(`href ${JSON.stringify(input)} -> ${url.href}`);
828          if (win32.fromFileUrl(url) !== back) throw new Error(`back ${JSON.stringify(input)}`);
829        }
830        // A URL instance and its href string convert identically.
831        const url = path.toFileUrl('/a b');
832        if (!(url instanceof URL)) throw new Error('returns URL');
833        if (path.fromFileUrl(url) !== path.fromFileUrl(url.href)) throw new Error('instance');
834        const 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        ];
902        for (const fn of rejections) {
903          try {
904            fn();
905            throw new Error('accepted bad file URL input');
906          } catch (e) {
907            if (!(e instanceof TypeError)) throw e;
908          }
909        }
910        if (path.fromFileUrl('file:///a?') !== '/a') throw new Error('empty query');
911        if (path.fromFileUrl('file:///a#') !== '/a') throw new Error('empty fragment');
912        console.log('file urls');
913    "#,
914        &["-0", "main.js"],
915    );
916    let output = success(output);
917    assert_eq!(output, "file urls\n");
918}
919
920#[test]
921fn hardened_calls_survive_primordial_and_url_pollution() {
922    let dir = tempfile::tempdir().unwrap();
923    let output = run_main(
924        dir.path(),
925        r#"
926        import * as path from 'avril:path';
927        import * as win32 from 'avril:path/win32';
928        import { posix } from 'avril:path/posix';
929        const TypeErr = TypeError;
930        const before = new URL('file:///a%20b/c');
931        const unc = new URL('file://server/share/a');
932        const poison = () => { throw new Error('Poisoned'); };
933        for (const key of ['slice', 'split', 'charCodeAt', 'indexOf', 'toLowerCase',
934            'includes', 'replace', 'repeat', 'lastIndexOf']) {
935          String.prototype[key] = poison;
936        }
937        String.prototype[Symbol.split] = poison;
938        String.fromCharCode = poison;
939        for (const key of ['join', 'slice', 'push', 'pop']) Array.prototype[key] = poison;
940        RegExp.prototype.exec = poison;
941        globalThis.encodeURI = poison;
942        globalThis.encodeURIComponent = poison;
943        globalThis.decodeURIComponent = poison;
944        const URLProto = URL.prototype;
945        Object.defineProperty(URLProto, 'hostname', { value: 'poisoned' });
946        Object.defineProperty(URLProto, 'pathname', { value: 'poisoned' });
947        globalThis.URL = poison;
948        if (path.join('/a', 'b', '../c') !== '/a/c') throw new Error('join');
949        if (path.normalize('a/./b//') !== 'a/b/') throw new Error('normalize');
950        if (path.resolve('/a', 'b') !== '/a/b') throw new Error('absolute resolve');
951        if (path.relative('/a/b', '/a/c') !== '../c') throw new Error('absolute relative');
952        if (path.parse('/a/b.txt').ext !== '.txt') throw new Error('parse');
953        if (path.format({ dir: '/a', base: 'b' }) !== '/a/b') throw new Error('format');
954        if (win32.join('C:\\a', '..\\b') !== 'C:\\b') throw new Error('win32 join');
955        if (win32.relative('C:\\a\\b\\', 'C:\\a\\c') !== '..\\c') throw new Error('win32 relative');
956        if (posix.dirname('/a/b') !== '/a') throw new Error('namespace');
957        if (path.toFileUrl('/a b').href !== 'file:///a%20b') throw new Error('toFileUrl');
958        if (path.fromFileUrl(before.href) !== '/a b/c') throw new Error('fromFileUrl string');
959        if (win32.fromFileUrl(unc) !== '\\\\server\\share\\a') throw new Error('unc instance');
960        try {
961          path.fromFileUrl('file:///a%2Fb');
962          throw new Error('lost encoded separator check');
963        } catch (e) {
964          if (!(e instanceof TypeErr)) throw e;
965        }
966        try {
967          win32.fromFileUrl({ href: 'file:///C:/a' });
968          throw new Error('lost brand check');
969        } catch (e) {
970          if (!(e instanceof TypeErr)) throw e;
971        }
972        try {
973          path.resolve('needs-cwd');
974          throw new Error('lost permission error');
975        } catch (e) {
976          if (e.name !== 'PermissionDenied') throw e;
977        }
978        try {
979          path.join(1);
980          throw new Error('lost validation');
981        } catch (e) {
982          if (!(e instanceof TypeErr)) throw e;
983        }
984        console.log('hardened');
985    "#,
986        &["-0", "main.js"],
987    );
988    let output = success(output);
989    assert_eq!(output, "hardened\n");
990}
991
992#[test]
993fn workers_import_path_and_its_subpaths() {
994    let dir = tempfile::tempdir().unwrap();
995    fs::write(
996        dir.path().join("worker.ts"),
997        r#"
998        import * as path from 'avril:path';
999        import { fromFileUrl as posixFromFileUrl, join, toFileUrl } from 'avril:path/posix';
1000        import * as win32 from 'avril:path/win32';
1001        import { runtime } from 'avril:std';
1002        const sep = runtime.os === 'windows' ? '\\' : '/';
1003        const result = [
1004          path.sep, join('/w', 'x'), win32.normalize('C:\\a\\.\\b'), toFileUrl('/w x').href,
1005          posixFromFileUrl('file:///w%20x'),
1006        ];
1007        postMessage(result);
1008        close();
1009    "#,
1010    )
1011    .unwrap();
1012    let output = run_main(
1013        dir.path(),
1014        r#"
1015        const worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' });
1016        const answer = await new Promise((resolve, reject) => {
1017          worker.onmessage = (e) => resolve(e.data);
1018          worker.onerror = reject;
1019        });
1020        const { runtime } = await import('avril:std');
1021        const sep = runtime.os === 'windows' ? '\\' : '/';
1022        const expected = [sep, '/w/x', 'C:\\a\\b', 'file:///w%20x', '/w x'];
1023        if (JSON.stringify(answer) !== JSON.stringify(expected)) {
1024          throw new Error(`worker paths: ${JSON.stringify(answer)}`);
1025        }
1026        console.log('worker paths');
1027    "#,
1028        &["main.js"],
1029    );
1030    let output = success(output);
1031    assert_eq!(output, "worker paths\n");
1032}
1033
1034#[test]
1035fn typescript_imports_path_with_types_and_rejects_unknown_exports() {
1036    let dir = tempfile::tempdir().unwrap();
1037    fs::write(
1038        dir.path().join("main.ts"),
1039        r#"
1040        import { join as nativeJoin, parse as nativeParse, posix, resolve as nativeResolve, sep, win32, type ParsedPath } from 'avril:path';
1041        import * as posixns from 'avril:path/posix';
1042        import * as win32ns from 'avril:path/win32';
1043        import { runtime } from 'avril:std';
1044        const parts: ParsedPath = posixns.parse('/a/b.txt');
1045        const combined: string = posixns.join('a', posixns.basename('/x/y')) + posixns.sep + posixns.delimiter;
1046        const parent: string = posixns.dirname('/a/b');
1047        const back: string = posixns.fromFileUrl(posixns.toFileUrl('/a b'));
1048        const flavored: string = win32ns.normalize('C:\\a') + posix.sep + win32.sep;
1049        const native = runtime.os === 'windows' ? win32 : posix;
1050        const aliased: boolean = nativeJoin === native.join && nativeResolve === native.resolve
1051          && nativeParse === native.parse && sep === native.sep;
1052        console.log([parts.ext, combined, parent, back, flavored, aliased].join('|'));
1053    "#,
1054    )
1055    .unwrap();
1056    let output = run_in(dir.path(), &["-0", "main.ts"]);
1057    assert!(
1058        output.status.success(),
1059        "{}",
1060        String::from_utf8_lossy(&output.stderr)
1061    );
1062    assert_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();
1069    let output = run_in(dir.path(), &["-0", "main.js"]);
1070    assert!(!output.status.success());
1071    assert!(
1072        String::from_utf8_lossy(&output.stderr)
1073            .contains("does not provide an export named 'missing'")
1074    );
1075}