char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
bf382b3
main
1mod common; 2 3use common:: run; 4 5# [ test ] 6fn parses_resolves_and_updates_url_components () { 7let output =run ( 8r#" 9const u = new URL('../child?x=a b#f', 'https://u:p@BÜCHER.example:443/dir/file'); 10console.log(JSON.stringify([u.href, u.origin, u.protocol, u.username, u.password, 11u.host, u.hostname, u.port, u.pathname, u.search, u.hash])); 12u.protocol = 'http:'; 13u.host = '[2001:db8::1]:8080'; 14u.port = '443junk'; 15u.username = 'a b'; u.password = '#p'; 16u.pathname = '/old/../a b'; u.hash = 'two words'; 17console.log(u.href, u.origin); 18const before = u.href; 19u.hostname = 'invalid host'; u.port = '70000'; u.protocol = '1bad:'; 20if (u.href !== before) throw new Error('Invalid setter changed the URL'); 21try { u.href = '/relative'; throw new Error('Invalid href accepted'); } 22catch (e) { if (!(e instanceof TypeError)) throw e; } 23if (u.href !== before) throw new Error('Failed href assignment changed the URL'); 24console.log(URL.canParse('../ok', u), URL.canParse('/no-base'), URL.parse('/no-base')); 25console.log(URL.parse('../ok', u).href, JSON.stringify(u)); 26console.log(new URL('http://0x7f.1/').hostname, new URL('data:text/plain,hello').origin, 27new URL('blob:https://example.com/id').origin, new URL('blob:ftp://host/id').origin, 28new URL('blob:blob:https://example.com/id').origin); 29console.log(Object.prototype.toString.call(u), Object.prototype.toString.call(u.searchParams)); 30"# , 31& [], 32); 33assert_eq! ( 34String :: from_utf8 ( output. stdout ). unwrap (), 35concat! ( 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 () { 48let output =run ( 49r#" 50const u = new URL('https://example.com/?a=b%20~&a=second'); 51const p = u.searchParams; 52console.log(p.size, p.get('a'), JSON.stringify(p.getAll('a'))); 53p.sort(); console.log(u.href); 54u.search = '?z=1&z=2'; 55if (u.searchParams !== p) throw new Error('Replaced searchParams'); 56console.log(p.toString(), p.has('z', '2'), p.has('z', '3')); 57p.delete('z', '1'); p.append('space', 'a b'); console.log(u.href); 58u.href = 'https://other.example/?new=value#hash'; 59console.log(p.toString()); 60p.set('new', 'updated'); p.append('new', 'duplicate'); p.set('new', 'one'); 61console.log(p.size, u.href); 62p.delete('new', undefined); console.log(u.href); 63const empty = new URL('https://example.com/?#'); 64empty.searchParams.delete('absent'); console.log(empty.href); 65const names = new URLSearchParams([['\uE000', 'bmp'], ['👽', 'astral'], ['a', 'first'], ['a', 'second']]); 66names.sort(); console.log(names.toString()); 67"# , 68& [], 69); 70assert_eq! ( 71String :: from_utf8 ( output. stdout ). unwrap (), 72concat! ( 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 () { 88let output =run ( 89r#" 90console.log(new URLSearchParams('?a=1&&a=2&x=%FF+%2B&empty').toString()); 91console.log(new URLSearchParams({ number: 42, empty: null, missing: undefined }).toString()); 92console.log(new URLSearchParams(null).toString(), new URLSearchParams().toString()); 93const collapsed = { ['\uD800']: 'first', ['\uDC00']: 'second' }; 94console.log(new URLSearchParams(collapsed).toString()); 95let outer = 0, inner = 0; 96const pair = { get [Symbol.iterator]() { inner++; return function*() { yield 'a'; yield 'b'; }; } }; 97const input = { get [Symbol.iterator]() { outer++; return function*() { yield pair; }; } }; 98console.log(new URLSearchParams(input).toString(), outer, inner); 99const p = new URLSearchParams('a=1&b=2'); 100const iterator = p.entries(); 101console.log(Object.prototype.toString.call(iterator), iterator[Symbol.iterator]() === iterator); 102console.log(JSON.stringify(iterator.next())); 103p.delete('a'); p.append('c', '3'); 104console.log(JSON.stringify(iterator.next()), JSON.stringify(iterator.next())); 105p.append('d', '4'); console.log(JSON.stringify(iterator.next())); 106const owner = new URL('https://example.com/?one=1&two=2'); 107const keys = owner.searchParams.keys(); 108console.log(keys.next().value); owner.search = '?three=3&four=4'; console.log(keys.next().value); 109const each = new URLSearchParams('x=1'); const context = {}; 110const seen = []; 111each.forEach(function(value, name, params) { 112if (this !== context || params !== each) throw new Error('Incorrect forEach arguments'); 113seen.push([name, value]); if (name === 'x') each.append('y', '2'); 114}, context); 115console.log(JSON.stringify(seen), JSON.stringify([...each.values()])); 116let closed = false; 117const bad = { *[Symbol.iterator]() { try { yield ['one']; } finally { closed = true; } } }; 118try { new URLSearchParams(bad); } catch (error) { if (!(error instanceof TypeError)) throw error; } 119console.log(closed); 120"# , 121& [], 122); 123assert_eq! ( 124String :: from_utf8 ( output. stdout ). unwrap (), 125concat! ( 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 () { 144let output =run ( 145r#" 146const typeError = fn => { try { fn(); } catch (e) { if (e instanceof TypeError) return; throw e; } throw new Error('Expected TypeError'); }; 147for (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); 161console.log(new URL('/\uD800', 'https://example.com').href); 162console.log(new URLSearchParams([['\uD800', '\uDC00']]).toString()); 163class Child extends URL {} 164const child = new Child('https://example.com'); 165console.log(child.href, child instanceof Child, URL.parse('https://example.com') instanceof URL); 166console.log(Object.getOwnPropertyDescriptor(URL.prototype, 'href').get.name, 167Object.getOwnPropertyDescriptor(URL.prototype, 'href').set.name); 168const u = new URL('https://example.com/?a=1'); 169u.searchParams.append({ toString() { u.search = '?b=2'; return 'c'; } }, '3'); 170console.log(u.href); 171console.log('ok'); 172"# , 173& [], 174); 175assert_eq! ( 176String :: from_utf8 ( output. stdout ). unwrap (), 177concat! ( 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 () { 190let output =run ( 191r#" 192const u = new URL('https://example.com/?b=2&a=1'); 193const p = u.searchParams; 194const weak = WeakMap.prototype, map = Map.prototype, strings = String.prototype; 195const poison = () => { throw new Error('Poisoned intrinsic'); }; 196globalThis.String = poison; globalThis.WeakMap = poison; globalThis.Map = poison; 197Array.prototype.push = poison; Array.prototype.splice = poison; Array.prototype.sort = poison; 198strings.toWellFormed = poison; 199weak.get = poison; weak.set = poison; 200map.get = poison; map.set = poison; map.has = poison; 201Array.prototype[Symbol.iterator] = poison; 202Object.defineProperty(u, 'href', { value: 'overridden' }); 203p.delete('b'); p.append('x', 'a b'); p.sort(); 204console.log(u.toString(), u.toJSON(), p.toString(), p.get('x')); 205console.log(new URL('/next', u).href, new URLSearchParams('x=a+b').toString()); 206console.log(new URLSearchParams({ a: 'b' }).toString()); 207"# , 208& [], 209); 210assert_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 () { 215let output =run ( 216r#" 217const failure = Error('item conversion'), log = []; 218const pair = { 219*[Symbol.iterator]() { 220try { 221yield 'key'; 222yield {toString() {throw failure;}}; 223} finally { log.push('inner'); } 224}, 225}; 226const input = { 227*[Symbol.iterator]() { 228try { 229yield ['short']; 230yield pair; 231} finally { log.push('outer'); } 232}, 233}; 234try { new URLSearchParams(input); throw Error('Accepted invalid input'); } 235catch (error) { if (error !== failure) throw error; } 236if (log.join() !== 'inner,outer') throw Error('IteratorClose order'); 237let closed = false; 238const invalid = { 239*[Symbol.iterator]() { 240try { yield {[Symbol.iterator]() {return 1;}}; } 241finally { closed = true; } 242}, 243}; 244try { new URLSearchParams(invalid); throw Error('Accepted primitive iterator'); } 245catch (error) { if (!(error instanceof TypeError)) throw error; } 246if (!closed) throw Error('Outer iterator was not closed'); 247console.log('query conversion'); 248"# , 249& [], 250); 251assert_eq! ( output. stdout , b"query conversion\n" ); 252} 253 254# [ test ] 255fn url_coercions_reenter_before_native_updates_and_preserve_callable_shape () { 256let output =run ( 257r#" 258for (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]) { 263const log = []; 264parse( 265{toString() {log.push('input'); return './child';}}, 266{toString() {log.push('base'); return 'https://example.com/';}}, 267); 268if (log.join() !== 'input,base') throw Error('Argument conversion order'); 269const failure = Error('base conversion'); 270try { 271parse('http://[', {toString() {throw failure;}}); 272throw Error('Skipped base conversion'); 273} catch (error) { if (error !== failure) throw error; } 274} 275const url = new URL('https://first.example/?a=1'), live = url.searchParams; 276url.search = {toString() { 277url.href = 'https://second.example/path?old=1#fragment'; 278return '?new=2'; 279}}; 280if (url.href !== 'https://second.example/path?new=2#fragment' || 281url.searchParams !== live || live.toString() !== 'new=2') throw Error('Reentrant setter'); 282if (URL.length !== 1 || URL.parse.length !== 1 || URL.canParse.length !== 1 || 283URLSearchParams.length !== 0) throw Error('Constructor/static arity'); 284for (const name of ['href', 'origin', 'protocol', 'username', 'password', 'host', 285'hostname', 'port', 'pathname', 'search', 'hash']) { 286const descriptor = Object.getOwnPropertyDescriptor(URL.prototype, name); 287if (descriptor.get.length !== 0 || Object.hasOwn(url, name)) throw Error('Component getter'); 288if (name === 'origin') { 289if (descriptor.set !== undefined) throw Error('Writable origin'); 290} else if (descriptor.set.length !== 1) throw Error('Component setter arity'); 291} 292for (const name of ['append', 'set']) { 293if (URLSearchParams.prototype[name].length !== 2) throw Error('Mutation arity'); 294} 295for (const name of ['delete', 'get', 'getAll', 'has', 'forEach']) { 296if (URLSearchParams.prototype[name].length !== 1) throw Error('Query arity'); 297} 298console.log('URL coercion'); 299"# , 300& [], 301); 302assert_eq! ( output. stdout , b"URL coercion\n" ); 303} 304 305# [ test ] 306fn url_objects_work_as_module_specifiers_with_fetch_available () { 307let dir = tempfile:: tempdir (). unwrap (); 308 std:: fs:: write ( 309 dir. path (). join ( "main.js" ), 310r#" 311const target = new URL('./dep%20space%23%F0%9F%98%80.ts', import.meta.url); 312const { answer } = await import(target); 313console.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 (); 322let 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 (); 328assert! ( 329 output. status . success (), 330"{}" , 331String :: from_utf8_lossy ( & output. stderr ) 332); 333assert_eq! ( output. stdout , b"true true 42 function\n" ); 334} 335 336# [ test ] 337fn preserves_literal_query_question_marks_and_hostless_path_serialization () { 338let output =run ( 339r#" 340const u = new URL('https://example.com/??a=1'); 341const parsed = URL.parse(u.href); 342console.log(u.searchParams.toString(), parsed.searchParams.toString()); 343u.searchParams.sort(); console.log(u.href); 344u.href = 'https://example.com/??b=2'; console.log(u.searchParams.toString()); 345u.search = '??c=3'; console.log(u.searchParams.toString()); 346const hostless = new URL('web+demo:/x'); 347hostless.pathname = '//p'; 348console.log(hostless.href, hostless.host, hostless.pathname); 349hostless.hash = 'h'; 350console.log(hostless.href, hostless.host, hostless.pathname); 351hostless.hostname = 'example.com'; console.log(hostless.href, hostless.pathname); 352hostless.hostname = ''; console.log(hostless.href, hostless.pathname); 353let converted = false; 354try { new URLSearchParams([['bad'], { *[Symbol.iterator]() { converted = true; yield 'k'; yield 'v'; } }]); } 355catch (e) { if (!(e instanceof TypeError)) throw e; } 356console.log(converted); 357const setter = Object.getOwnPropertyDescriptor(URL.prototype, 'href').set; 358console.log(Object.hasOwn(setter, 'prototype')); 359"# , 360& [], 361); 362assert_eq! ( 363String :: from_utf8 ( output. stdout ). unwrap (), 364concat! ( 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 () { 379let output =run ( 380r#" 381const u = new URL('https://example.com/?a=1'); 382const p = u.searchParams; 383let intercepted; 384Object.defineProperty(Array, Symbol.species, { get() { throw new Error('Species consulted'); }, configurable: true }); 385Object.defineProperty(Array.prototype, '1', { set() { intercepted = this; }, configurable: true }); 386p.append('a', '2'); p.set('a', '3'); p.append('b', '4'); p.delete('b'); 387const all = p.getAll('a'); 388u.search = '?x=1&x=2'; p.set('x', '5'); p.append('x', '6'); 389const fresh = new URLSearchParams([['x', 'y']]); fresh.append('z', 'v'); 390delete Array.prototype[1]; 391console.log(intercepted === undefined, all instanceof Array, JSON.stringify(all), p.toString(), u.href, fresh.toString()); 392"# , 393& [], 394); 395assert_eq! ( 396 output. stdout , 397b"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 () { 403let output =run ( 404r#" 405const url = URL.parse('https://example.com/?a=1&b=2'); 406const live = url.searchParams; 407const copy = new URLSearchParams(live); 408const iterator = live.entries(); 409if (JSON.stringify(iterator.next().value) !== '["a","1"]') throw Error('Initial iterator'); 410url.href = 'https://other.example/?x=3&y=4#fragment'; 411if (url.searchParams !== live) throw Error('Lost live identity'); 412if (JSON.stringify(iterator.next().value) !== '["y","4"]') throw Error('Iterator retained stale query'); 413live.set('x', 'five six'); 414if (url.href !== 'https://other.example/?x=five+six&y=4#fragment') throw Error('Lost URL link'); 415url.search = '?z=7'; 416live.append('z', '8'); 417if (url.search !== '?z=7&z=8') throw Error('Lost replacement link'); 418copy.append('independent', 'yes'); 419if (copy.toString() !== 'a=1&b=2&independent=yes' || live.has('independent')) throw Error('Copy linked to URL'); 420if (!iterator.next().done) throw Error('Iterator did not finish on current query'); 421console.log('live'); 422"# , 423& [], 424); 425assert_eq! ( output. stdout , b"live\n" ); 426} 427 428# [ test ] 429fn url_interface_descriptors_and_iterator_alias_are_preserved () { 430let output =run ( 431r#" 432const iterator = Object.getOwnPropertyDescriptor(URLSearchParams.prototype, Symbol.iterator); 433if (iterator.value !== URLSearchParams.prototype.entries || !iterator.writable || iterator.enumerable || !iterator.configurable) throw Error('Invalid iterator descriptor'); 434for (const name of ['parse', 'canParse']) { 435const descriptor = Object.getOwnPropertyDescriptor(URL, name); 436if (!descriptor.enumerable || !descriptor.writable || !descriptor.configurable) throw Error('Invalid static descriptor'); 437} 438for (const ctor of [URL, URLSearchParams]) { 439for (const name of Object.getOwnPropertyNames(ctor.prototype)) { 440if (name === 'constructor') continue; 441const descriptor = Object.getOwnPropertyDescriptor(ctor.prototype, name); 442if (!descriptor.enumerable || !descriptor.configurable) throw Error('Invalid prototype descriptor'); 443} 444const tag = Object.getOwnPropertyDescriptor(ctor.prototype, Symbol.toStringTag); 445if (tag.value !== ctor.name || tag.enumerable || tag.writable || !tag.configurable) throw Error('Invalid interface tag'); 446} 447console.log('interfaces'); 448"# , 449& [], 450); 451assert_eq! ( output. stdout , b"interfaces\n" ); 452}