char/avril

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

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

Charlotte Somport URL extension to typescriptbf382b3

main
20.4 KiB452 linesraw
1mod common;
2
3use common::run;
4
5#[test]
6fn parses_resolves_and_updates_url_components() {
7    let output = run(
8        r#"
9        const u = new URL('../child?x=a b#f', 'https://u:p@BÜCHER.example:443/dir/file');
10        console.log(JSON.stringify([u.href, u.origin, u.protocol, u.username, u.password,
11            u.host, u.hostname, u.port, u.pathname, u.search, u.hash]));
12        u.protocol = 'http:';
13        u.host = '[2001:db8::1]:8080';
14        u.port = '443junk';
15        u.username = 'a b'; u.password = '#p';
16        u.pathname = '/old/../a b'; u.hash = 'two words';
17        console.log(u.href, u.origin);
18        const before = u.href;
19        u.hostname = 'invalid host'; u.port = '70000'; u.protocol = '1bad:';
20        if (u.href !== before) throw new Error('Invalid setter changed the URL');
21        try { u.href = '/relative'; throw new Error('Invalid href accepted'); }
22        catch (e) { if (!(e instanceof TypeError)) throw e; }
23        if (u.href !== before) throw new Error('Failed href assignment changed the URL');
24        console.log(URL.canParse('../ok', u), URL.canParse('/no-base'), URL.parse('/no-base'));
25        console.log(URL.parse('../ok', u).href, JSON.stringify(u));
26        console.log(new URL('http://0x7f.1/').hostname, new URL('data:text/plain,hello').origin,
27            new URL('blob:https://example.com/id').origin, new URL('blob:ftp://host/id').origin,
28            new URL('blob:blob:https://example.com/id').origin);
29        console.log(Object.prototype.toString.call(u), Object.prototype.toString.call(u.searchParams));
30    "#,
31        &[],
32    );
33    assert_eq!(
34        String::from_utf8(output.stdout).unwrap(),
35        concat!(
36            "[\"https://u:p@xn--bcher-kva.example/child?x=a%20b#f\",\"https://xn--bcher-kva.example\",\"https:\",\"u\",\"p\",\"xn--bcher-kva.example\",\"xn--bcher-kva.example\",\"\",\"/child\",\"?x=a%20b\",\"#f\"]\n",
37            "http://a%20b:%23p@[2001:db8::1]:443/a%20b?x=a%20b#two%20words http://[2001:db8::1]:443\n",
38            "true false null\n",
39            "http://a%20b:%23p@[2001:db8::1]:443/ok \"http://a%20b:%23p@[2001:db8::1]:443/a%20b?x=a%20b#two%20words\"\n",
40            "127.0.0.1 null https://example.com null null\n",
41            "[object URL] [object URLSearchParams]\n",
42        )
43    );
44}
45
46#[test]
47fn search_params_stay_live_in_both_directions() {
48    let output = run(
49        r#"
50        const u = new URL('https://example.com/?a=b%20~&a=second');
51        const p = u.searchParams;
52        console.log(p.size, p.get('a'), JSON.stringify(p.getAll('a')));
53        p.sort(); console.log(u.href);
54        u.search = '?z=1&z=2';
55        if (u.searchParams !== p) throw new Error('Replaced searchParams');
56        console.log(p.toString(), p.has('z', '2'), p.has('z', '3'));
57        p.delete('z', '1'); p.append('space', 'a b'); console.log(u.href);
58        u.href = 'https://other.example/?new=value#hash';
59        console.log(p.toString());
60        p.set('new', 'updated'); p.append('new', 'duplicate'); p.set('new', 'one');
61        console.log(p.size, u.href);
62        p.delete('new', undefined); console.log(u.href);
63        const empty = new URL('https://example.com/?#');
64        empty.searchParams.delete('absent'); console.log(empty.href);
65        const names = new URLSearchParams([['\uE000', 'bmp'], ['👽', 'astral'], ['a', 'first'], ['a', 'second']]);
66        names.sort(); console.log(names.toString());
67    "#,
68        &[],
69    );
70    assert_eq!(
71        String::from_utf8(output.stdout).unwrap(),
72        concat!(
73            "2 b ~ [\"b ~\",\"second\"]\n",
74            "https://example.com/?a=b+%7E&a=second\n",
75            "z=1&z=2 true false\n",
76            "https://example.com/?z=2&space=a+b\n",
77            "new=value\n",
78            "1 https://other.example/?new=one#hash\n",
79            "https://other.example/#hash\n",
80            "https://example.com/#\n",
81            "a=first&a=second&%F0%9F%91%BD=astral&%EE%80%80=bmp\n",
82        )
83    );
84}
85
86#[test]
87fn search_params_support_webidl_inputs_and_live_iteration() {
88    let output = run(
89        r#"
90        console.log(new URLSearchParams('?a=1&&a=2&x=%FF+%2B&empty').toString());
91        console.log(new URLSearchParams({ number: 42, empty: null, missing: undefined }).toString());
92        console.log(new URLSearchParams(null).toString(), new URLSearchParams().toString());
93        const collapsed = { ['\uD800']: 'first', ['\uDC00']: 'second' };
94        console.log(new URLSearchParams(collapsed).toString());
95        let outer = 0, inner = 0;
96        const pair = { get [Symbol.iterator]() { inner++; return function*() { yield 'a'; yield 'b'; }; } };
97        const input = { get [Symbol.iterator]() { outer++; return function*() { yield pair; }; } };
98        console.log(new URLSearchParams(input).toString(), outer, inner);
99        const p = new URLSearchParams('a=1&b=2');
100        const iterator = p.entries();
101        console.log(Object.prototype.toString.call(iterator), iterator[Symbol.iterator]() === iterator);
102        console.log(JSON.stringify(iterator.next()));
103        p.delete('a'); p.append('c', '3');
104        console.log(JSON.stringify(iterator.next()), JSON.stringify(iterator.next()));
105        p.append('d', '4'); console.log(JSON.stringify(iterator.next()));
106        const owner = new URL('https://example.com/?one=1&two=2');
107        const keys = owner.searchParams.keys();
108        console.log(keys.next().value); owner.search = '?three=3&four=4'; console.log(keys.next().value);
109        const each = new URLSearchParams('x=1'); const context = {};
110        const seen = [];
111        each.forEach(function(value, name, params) {
112            if (this !== context || params !== each) throw new Error('Incorrect forEach arguments');
113            seen.push([name, value]); if (name === 'x') each.append('y', '2');
114        }, context);
115        console.log(JSON.stringify(seen), JSON.stringify([...each.values()]));
116        let closed = false;
117        const bad = { *[Symbol.iterator]() { try { yield ['one']; } finally { closed = true; } } };
118        try { new URLSearchParams(bad); } catch (error) { if (!(error instanceof TypeError)) throw error; }
119        console.log(closed);
120    "#,
121        &[],
122    );
123    assert_eq!(
124        String::from_utf8(output.stdout).unwrap(),
125        concat!(
126            "a=1&a=2&x=%EF%BF%BD+%2B&empty=\n",
127            "number=42&empty=null&missing=undefined\n",
128            "null= \n",
129            "%EF%BF%BD=second\n",
130            "a=b 1 1\n",
131            "[object URLSearchParams Iterator] true\n",
132            "{\"value\":[\"a\",\"1\"],\"done\":false}\n",
133            "{\"value\":[\"c\",\"3\"],\"done\":false} {\"done\":true}\n",
134            "{\"value\":[\"d\",\"4\"],\"done\":false}\n",
135            "one\nfour\n",
136            "[[\"x\",\"1\"],[\"y\",\"2\"]] [\"1\",\"2\"]\n",
137            "true\n",
138        )
139    );
140}
141
142#[test]
143fn url_interfaces_validate_receivers_arguments_and_usv_strings() {
144    let output = run(
145        r#"
146        const typeError = fn => { try { fn(); } catch (e) { if (e instanceof TypeError) return; throw e; } throw new Error('Expected TypeError'); };
147        for (const fn of [() => URL('https://example.com'), () => URLSearchParams(), () => new URL(),
148            () => URL.canParse(), () => URL.parse(), () => new URL(Symbol()),
149            () => new URL('https://example.com', 'invalid'), () => new URLSearchParams(Symbol()),
150            () => new URLSearchParams([['a', 'b', 'c']]), () => new URLSearchParams(['ab']),
151            () => new URLSearchParams({ [Symbol()]: 'value' }),
152            () => URL.prototype.toString.call({}), () => URL.prototype.toJSON.call({}),
153            () => Object.getOwnPropertyDescriptor(URL.prototype, 'href').get.call({}),
154            () => URLSearchParams.prototype.get.call({}),
155            () => new URLSearchParams().append('a'), () => new URLSearchParams().get(),
156            () => new URLSearchParams().set('a'), () => new URLSearchParams().has(),
157            () => new URLSearchParams().delete(), () => new URLSearchParams().getAll(),
158            () => new URLSearchParams().forEach(null),
159            () => new URLSearchParams().append('a', Symbol()),
160        ]) typeError(fn);
161        console.log(new URL('/\uD800', 'https://example.com').href);
162        console.log(new URLSearchParams([['\uD800', '\uDC00']]).toString());
163        class Child extends URL {}
164        const child = new Child('https://example.com');
165        console.log(child.href, child instanceof Child, URL.parse('https://example.com') instanceof URL);
166        console.log(Object.getOwnPropertyDescriptor(URL.prototype, 'href').get.name,
167            Object.getOwnPropertyDescriptor(URL.prototype, 'href').set.name);
168        const u = new URL('https://example.com/?a=1');
169        u.searchParams.append({ toString() { u.search = '?b=2'; return 'c'; } }, '3');
170        console.log(u.href);
171        console.log('ok');
172    "#,
173        &[],
174    );
175    assert_eq!(
176        String::from_utf8(output.stdout).unwrap(),
177        concat!(
178            "https://example.com/%EF%BF%BD\n",
179            "%EF%BF%BD=%EF%BF%BD\n",
180            "https://example.com/ true true\n",
181            "get href set href\n",
182            "https://example.com/?b=2&c=3\n",
183            "ok\n",
184        )
185    );
186}
187
188#[test]
189fn url_operations_capture_intrinsics_and_do_not_depend_on_public_overrides() {
190    let output = run(
191        r#"
192        const u = new URL('https://example.com/?b=2&a=1');
193        const p = u.searchParams;
194        const weak = WeakMap.prototype, map = Map.prototype, strings = String.prototype;
195        const poison = () => { throw new Error('Poisoned intrinsic'); };
196        globalThis.String = poison; globalThis.WeakMap = poison; globalThis.Map = poison;
197        Array.prototype.push = poison; Array.prototype.splice = poison; Array.prototype.sort = poison;
198        strings.toWellFormed = poison;
199        weak.get = poison; weak.set = poison;
200        map.get = poison; map.set = poison; map.has = poison;
201        Array.prototype[Symbol.iterator] = poison;
202        Object.defineProperty(u, 'href', { value: 'overridden' });
203        p.delete('b'); p.append('x', 'a b'); p.sort();
204        console.log(u.toString(), u.toJSON(), p.toString(), p.get('x'));
205        console.log(new URL('/next', u).href, new URLSearchParams('x=a+b').toString());
206        console.log(new URLSearchParams({ a: 'b' }).toString());
207    "#,
208        &[],
209    );
210    assert_eq!(output.stdout, b"https://example.com/?a=1&x=a+b https://example.com/?a=1&x=a+b a=1&x=a+b a b\nhttps://example.com/next x=a+b\na=b\n");
211}
212
213#[test]
214fn query_sequence_conversion_keeps_iterator_closing_and_error_precedence() {
215    let output = run(
216        r#"
217        const failure = Error('item conversion'), log = [];
218        const pair = {
219            *[Symbol.iterator]() {
220                try {
221                    yield 'key';
222                    yield {toString() {throw failure;}};
223                } finally { log.push('inner'); }
224            },
225        };
226        const input = {
227            *[Symbol.iterator]() {
228                try {
229                    yield ['short'];
230                    yield pair;
231                } finally { log.push('outer'); }
232            },
233        };
234        try { new URLSearchParams(input); throw Error('Accepted invalid input'); }
235        catch (error) { if (error !== failure) throw error; }
236        if (log.join() !== 'inner,outer') throw Error('IteratorClose order');
237        let closed = false;
238        const invalid = {
239            *[Symbol.iterator]() {
240                try { yield {[Symbol.iterator]() {return 1;}}; }
241                finally { closed = true; }
242            },
243        };
244        try { new URLSearchParams(invalid); throw Error('Accepted primitive iterator'); }
245        catch (error) { if (!(error instanceof TypeError)) throw error; }
246        if (!closed) throw Error('Outer iterator was not closed');
247        console.log('query conversion');
248    "#,
249        &[],
250    );
251    assert_eq!(output.stdout, b"query conversion\n");
252}
253
254#[test]
255fn url_coercions_reenter_before_native_updates_and_preserve_callable_shape() {
256    let output = run(
257        r#"
258        for (const parse of [
259            (input, base) => new URL(input, base),
260            (input, base) => URL.parse(input, base),
261            (input, base) => URL.canParse(input, base),
262        ]) {
263            const log = [];
264            parse(
265                {toString() {log.push('input'); return './child';}},
266                {toString() {log.push('base'); return 'https://example.com/';}},
267            );
268            if (log.join() !== 'input,base') throw Error('Argument conversion order');
269            const failure = Error('base conversion');
270            try {
271                parse('http://[', {toString() {throw failure;}});
272                throw Error('Skipped base conversion');
273            } catch (error) { if (error !== failure) throw error; }
274        }
275        const url = new URL('https://first.example/?a=1'), live = url.searchParams;
276        url.search = {toString() {
277            url.href = 'https://second.example/path?old=1#fragment';
278            return '?new=2';
279        }};
280        if (url.href !== 'https://second.example/path?new=2#fragment' ||
281            url.searchParams !== live || live.toString() !== 'new=2') throw Error('Reentrant setter');
282        if (URL.length !== 1 || URL.parse.length !== 1 || URL.canParse.length !== 1 ||
283            URLSearchParams.length !== 0) throw Error('Constructor/static arity');
284        for (const name of ['href', 'origin', 'protocol', 'username', 'password', 'host',
285            'hostname', 'port', 'pathname', 'search', 'hash']) {
286            const descriptor = Object.getOwnPropertyDescriptor(URL.prototype, name);
287            if (descriptor.get.length !== 0 || Object.hasOwn(url, name)) throw Error('Component getter');
288            if (name === 'origin') {
289                if (descriptor.set !== undefined) throw Error('Writable origin');
290            } else if (descriptor.set.length !== 1) throw Error('Component setter arity');
291        }
292        for (const name of ['append', 'set']) {
293            if (URLSearchParams.prototype[name].length !== 2) throw Error('Mutation arity');
294        }
295        for (const name of ['delete', 'get', 'getAll', 'has', 'forEach']) {
296            if (URLSearchParams.prototype[name].length !== 1) throw Error('Query arity');
297        }
298        console.log('URL coercion');
299    "#,
300        &[],
301    );
302    assert_eq!(output.stdout, b"URL coercion\n");
303}
304
305#[test]
306fn url_objects_work_as_module_specifiers_with_fetch_available() {
307    let dir = tempfile::tempdir().unwrap();
308    std::fs::write(
309        dir.path().join("main.js"),
310        r#"
311        const target = new URL('./dep%20space%23%F0%9F%98%80.ts', import.meta.url);
312        const { answer } = await import(target);
313        console.log(import.meta.main, URL.canParse(import.meta.url), answer, typeof fetch);
314    "#,
315    )
316    .unwrap();
317    std::fs::write(
318        dir.path().join("dep space#😀.ts"),
319        "export const answer: number = 42;",
320    )
321    .unwrap();
322    let output = std::process::Command::new(env!("CARGO_BIN_EXE_avril"))
323        .arg("run")
324        .current_dir(dir.path())
325        .arg("main.js")
326        .output()
327        .unwrap();
328    assert!(
329        output.status.success(),
330        "{}",
331        String::from_utf8_lossy(&output.stderr)
332    );
333    assert_eq!(output.stdout, b"true true 42 function\n");
334}
335
336#[test]
337fn preserves_literal_query_question_marks_and_hostless_path_serialization() {
338    let output = run(
339        r#"
340        const u = new URL('https://example.com/??a=1');
341        const parsed = URL.parse(u.href);
342        console.log(u.searchParams.toString(), parsed.searchParams.toString());
343        u.searchParams.sort(); console.log(u.href);
344        u.href = 'https://example.com/??b=2'; console.log(u.searchParams.toString());
345        u.search = '??c=3'; console.log(u.searchParams.toString());
346        const hostless = new URL('web+demo:/x');
347        hostless.pathname = '//p';
348        console.log(hostless.href, hostless.host, hostless.pathname);
349        hostless.hash = 'h';
350        console.log(hostless.href, hostless.host, hostless.pathname);
351        hostless.hostname = 'example.com'; console.log(hostless.href, hostless.pathname);
352        hostless.hostname = ''; console.log(hostless.href, hostless.pathname);
353        let converted = false;
354        try { new URLSearchParams([['bad'], { *[Symbol.iterator]() { converted = true; yield 'k'; yield 'v'; } }]); }
355        catch (e) { if (!(e instanceof TypeError)) throw e; }
356        console.log(converted);
357        const setter = Object.getOwnPropertyDescriptor(URL.prototype, 'href').set;
358        console.log(Object.hasOwn(setter, 'prototype'));
359    "#,
360        &[],
361    );
362    assert_eq!(
363        String::from_utf8(output.stdout).unwrap(),
364        concat!(
365            "%3Fa=1 %3Fa=1\n",
366            "https://example.com/?%3Fa=1\n",
367            "%3Fb=2\n%3Fc=3\n",
368            "web+demo:/.//p  //p\n",
369            "web+demo:/.//p#h  //p\n",
370            "web+demo://example.com//p#h //p\n",
371            "web+demo:/.//p#h //p\n",
372            "true\nfalse\n",
373        )
374    );
375}
376
377#[test]
378fn private_query_storage_cannot_be_intercepted_by_array_prototype_hooks() {
379    let output = run(
380        r#"
381        const u = new URL('https://example.com/?a=1');
382        const p = u.searchParams;
383        let intercepted;
384        Object.defineProperty(Array, Symbol.species, { get() { throw new Error('Species consulted'); }, configurable: true });
385        Object.defineProperty(Array.prototype, '1', { set() { intercepted = this; }, configurable: true });
386        p.append('a', '2'); p.set('a', '3'); p.append('b', '4'); p.delete('b');
387        const all = p.getAll('a');
388        u.search = '?x=1&x=2'; p.set('x', '5'); p.append('x', '6');
389        const fresh = new URLSearchParams([['x', 'y']]); fresh.append('z', 'v');
390        delete Array.prototype[1];
391        console.log(intercepted === undefined, all instanceof Array, JSON.stringify(all), p.toString(), u.href, fresh.toString());
392    "#,
393        &[],
394    );
395    assert_eq!(
396        output.stdout,
397        b"true true [\"3\"] x=5&x=6 https://example.com/?x=5&x=6 x=y&z=v\n"
398    );
399}
400
401#[test]
402fn retained_query_iterators_follow_url_replacements_without_linking_copies() {
403    let output = run(
404        r#"
405        const url = URL.parse('https://example.com/?a=1&b=2');
406        const live = url.searchParams;
407        const copy = new URLSearchParams(live);
408        const iterator = live.entries();
409        if (JSON.stringify(iterator.next().value) !== '["a","1"]') throw Error('Initial iterator');
410        url.href = 'https://other.example/?x=3&y=4#fragment';
411        if (url.searchParams !== live) throw Error('Lost live identity');
412        if (JSON.stringify(iterator.next().value) !== '["y","4"]') throw Error('Iterator retained stale query');
413        live.set('x', 'five six');
414        if (url.href !== 'https://other.example/?x=five+six&y=4#fragment') throw Error('Lost URL link');
415        url.search = '?z=7';
416        live.append('z', '8');
417        if (url.search !== '?z=7&z=8') throw Error('Lost replacement link');
418        copy.append('independent', 'yes');
419        if (copy.toString() !== 'a=1&b=2&independent=yes' || live.has('independent')) throw Error('Copy linked to URL');
420        if (!iterator.next().done) throw Error('Iterator did not finish on current query');
421        console.log('live');
422    "#,
423        &[],
424    );
425    assert_eq!(output.stdout, b"live\n");
426}
427
428#[test]
429fn url_interface_descriptors_and_iterator_alias_are_preserved() {
430    let output = run(
431        r#"
432        const iterator = Object.getOwnPropertyDescriptor(URLSearchParams.prototype, Symbol.iterator);
433        if (iterator.value !== URLSearchParams.prototype.entries || !iterator.writable || iterator.enumerable || !iterator.configurable) throw Error('Invalid iterator descriptor');
434        for (const name of ['parse', 'canParse']) {
435            const descriptor = Object.getOwnPropertyDescriptor(URL, name);
436            if (!descriptor.enumerable || !descriptor.writable || !descriptor.configurable) throw Error('Invalid static descriptor');
437        }
438        for (const ctor of [URL, URLSearchParams]) {
439            for (const name of Object.getOwnPropertyNames(ctor.prototype)) {
440                if (name === 'constructor') continue;
441                const descriptor = Object.getOwnPropertyDescriptor(ctor.prototype, name);
442                if (!descriptor.enumerable || !descriptor.configurable) throw Error('Invalid prototype descriptor');
443            }
444            const tag = Object.getOwnPropertyDescriptor(ctor.prototype, Symbol.toStringTag);
445            if (tag.value !== ctor.name || tag.enumerable || tag.writable || !tag.configurable) throw Error('Invalid interface tag');
446        }
447        console.log('interfaces');
448    "#,
449        &[],
450    );
451    assert_eq!(output.stdout, b"interfaces\n");
452}