mod common; use common::run; #[test] fn matches_components_and_returns_named_optional_and_numbered_groups() { let output = run( r#" const p = new URLPattern({ protocol: 'https', hostname: ':sub.example.com', pathname: '/books/:id(\\d+){/:slug}?', search: 'q=:term', hash: ':section' }); const url = 'https://api.example.com/books/42?q=rust#intro'; const result = p.exec(url); console.log(p.protocol, p.username, p.password, p.hostname, p.port, p.pathname, p.search, p.hash, p.hasRegExpGroups); console.log(p.test(url), p.test('https://api.example.com/books/no?q=rust#intro')); console.log(JSON.stringify(result.inputs), result.hostname.groups.sub, result.pathname.groups.id, Object.hasOwn(result.pathname.groups, 'slug'), result.pathname.groups.slug, result.search.groups.term, result.hash.groups.section, result.port.groups[0]); const second = p.exec('https://www.example.com/books/7/title?q=js#end'); console.log(second.pathname.groups.slug, result.pathname.groups.id, second !== result, second.pathname !== result.pathname, second.pathname.groups !== result.pathname.groups); const numbered = new URLPattern({pathname: '/(foo|bar)/*'}).exec({pathname:'/foo/rest/of/path'}); console.log(JSON.stringify(numbered.pathname)); console.log(Object.keys(result).join(','), Object.keys(result.pathname).join(',')); console.log(p.exec('https://api.example.com/nope')); console.log(Object.prototype.toString.call(p), Object.getPrototypeOf(result) === Object.prototype, Object.getPrototypeOf(result.pathname.groups) === Object.prototype); "#, &[], ); assert_eq!( String::from_utf8(output.stdout).unwrap(), concat!( "https * * :sub.example.com * /books/:id(\\d+)/:slug? q=:term :section true\n", "true false\n", "[\"https://api.example.com/books/42?q=rust#intro\"] api 42 true undefined rust intro \n", "title 42 true true true\n", "{\"groups\":{\"0\":\"foo\",\"1\":\"rest/of/path\"},\"input\":\"/foo/rest/of/path\"}\n", "hash,hostname,inputs,password,pathname,port,protocol,search,username groups,input\n", "null\n", "[object URLPattern] true true\n", ) ); } #[test] fn accepts_constructor_overloads_relative_inputs_and_normalized_urls() { let output = run( r#" const p = new URLPattern('./books/:id', 'https://example.com/store/index'); console.log(p.hostname, p.pathname, p.test('./books/42', 'https://example.com/store/index')); console.log(JSON.stringify(p.exec('./books/42', 'https://example.com/store/index').inputs)); const init = { pathname: './books/:id', baseURL: 'https://example.com/store/index' }; console.log(new URLPattern(init).test('https://example.com/store/books/42')); const international = new URLPattern('https://BÜCHER.example:443/café/:id'); console.log(international.hostname, international.port, international.pathname, international.exec('https://xn--bcher-kva.example/caf%C3%A9/42').pathname.groups.id); const ipv6 = new URLPattern({ protocol: 'https', hostname: '[\\:\\:1]', port: '443' }); console.log(ipv6.test('https://[::1]/'), ipv6.port); console.log(new URLPattern(new URL('https://example.com/path')).test('https://example.com/path')); console.log(new URLPattern().test(), new URLPattern(null).test(null), new URLPattern(undefined, undefined).test(undefined)); console.log(JSON.stringify(new URLPattern().exec().inputs)); console.log(new URLPattern({pathname:'/foo'}).test('/foo'), new URLPattern().test('http://['), new URLPattern().exec({baseURL:'invalid'})); const dictionary = { pathname: 42, extra: 'ignored' }; const r = new URLPattern({pathname:':id'}).exec(dictionary); console.log(JSON.stringify(r.inputs), r.pathname.groups.id, r.inputs[0] !== dictionary); console.log(new URLPattern(42, 'https://example.com/').pathname); "#, &[], ); assert_eq!( String::from_utf8(output.stdout).unwrap(), concat!( "example.com /store/books/:id true\n", "[\"./books/42\",\"https://example.com/store/index\"]\n", "true\n", "xn--bcher-kva.example /caf%C3%A9/:id 42\n", "true \n", "true\ntrue true true\n[{}]\n", "false false null\n", "[{\"pathname\":\"42\"}] 42 true\n", "/42\n", ) ); } #[test] fn uses_ecmascript_regexps_unicode_sets_and_component_specific_case_rules() { let output = run( r#" const p = new URLPattern({protocol:'((?=https)https)', pathname:'/((?!admin)[a-z]+)'}); console.log(p.test('https://example.com/public'), p.test('https://example.com/admin'), p.hasRegExpGroups); const sets = new URLPattern({pathname:'/([[a-z]--a])'}); console.log(sets.test({pathname:'/a'}), sets.exec({pathname:'/z'}).pathname.groups[0]); const intersection = new URLPattern({pathname:'/([\\d&&[0-1]])'}); console.log(intersection.test({pathname:'/0'}), intersection.test({pathname:'/3'})); const unicode = new URLPattern({hash:':letter(\\p{Letter}+)' }); console.log(unicode.exec({hash:'abc'}).hash.groups.letter); const opts = {ignoreCase: 'truthy'}; for (const component of ['protocol','username','password','hostname','pathname','search','hash']) { const value = component === 'protocol' ? '(HTTPS)' : 'FOO'; const input = component === 'protocol' ? 'https' : 'foo'; console.log(component, new URLPattern({[component]:value}, opts).test({[component]:input})); } console.log(new URLPattern({pathname:'/x/:id?'}).hasRegExpGroups, new URLPattern({pathname:'/x/:id(.*)'}).hasRegExpGroups, new URLPattern({pathname:'/x/:id([a-z]+)'}).hasRegExpGroups); "#, &[], ); assert_eq!(output.stdout, b"true false true\nfalse z\ntrue false\nabc\nprotocol false\nusername false\npassword false\nhostname true\npathname true\nsearch true\nhash true\nfalse false true\n"); } #[test] fn validates_patterns_receivers_and_webidl_conversion_without_poisoning_later_calls() { let output = run( r#" const typeError = fn => { try { fn(); } catch (e) { if (e instanceof TypeError) return; throw e; } throw new Error('Expected TypeError'); }; const p = new URLPattern(); for (const fn of [() => URLPattern(), () => new URLPattern('/relative'), () => new URLPattern({pathname:'()'}), () => new URLPattern({pathname:'/(a(b))'}), () => new URLPattern({pathname:'/(a{1,0})'}), () => new URLPattern({pathname:'/:x/:x'}), () => new URLPattern({pathname:'\\'}), () => new URLPattern(Symbol()), () => new URLPattern({}, 'https://example.com'), () => new URLPattern({}, null, {}), () => new URLPattern({}, 1), () => new URLPattern({}, {}, {}), () => p.test({}, 'https://example.com'), () => p.exec({}, ''), () => p.test(Symbol()), () => URLPattern.prototype.test.call({}), () => URLPattern.prototype.exec.call({}), () => Object.getOwnPropertyDescriptor(URLPattern.prototype,'pathname').get.call({}), () => Object.getOwnPropertyDescriptor(URLPattern.prototype,'hasRegExpGroups').get.call({}), ]) typeError(fn); console.log(new URLPattern({pathname:'/x'}).test('https://example.com/x')); const order = []; const input = new Proxy({}, {get(_, key) {order.push(key); return undefined;}}); new URLPattern(input, {get ignoreCase() {order.push('ignoreCase'); return false;}}); console.log(order.join(',')); console.log(new URLPattern({pathname:'/\uD800'}).pathname); const r = p.exec({pathname:'\uD800'}); console.log(r.inputs[0].pathname === '\uFFFD'); class Child extends URLPattern {} console.log(new Child({pathname:'/x'}) instanceof Child, URLPattern.length, p.test.length, p.exec.length); const descriptor = Object.getOwnPropertyDescriptor(URLPattern.prototype,'pathname'); console.log(descriptor.enumerable, descriptor.configurable, descriptor.set, descriptor.get.name); "#, &[], ); assert_eq!(output.stdout, b"true\nbaseURL,hash,hostname,password,pathname,port,protocol,search,username,ignoreCase\n/%EF%BF%BD\ntrue\ntrue 0 0 0\ntrue true undefined get pathname\n"); } #[test] fn captures_primordials_and_materializes_groups_without_prototype_setters() { let output = run( r#" const p = new URLPattern({pathname:'/:__proto__/:constructor/:id'}); const re = RegExp.prototype, strings = String.prototype, weak = WeakMap.prototype; const poison = () => {throw new Error('Poisoned intrinsic');}; globalThis.RegExp = poison; globalThis.URL = poison; globalThis.String = poison; globalThis.WeakMap = poison; re.exec = poison; strings.toWellFormed = poison; weak.get = poison; weak.set = poison; Object.defineProperty(Object.prototype,'id',{set:poison,configurable:true}); Object.defineProperty(Array.prototype,'1',{set:poison,configurable:true}); Object.defineProperty(p,'pathname',{value:'overridden'}); const r = p.exec('https://example.com/first/second/third'); const fresh = new URLPattern({protocol:'((?=https)https)', pathname:'/x/:id'}); const matched = fresh.exec('https://example.com/x/42'); delete Array.prototype[1]; delete Object.prototype.id; console.log(r.pathname.groups.__proto__, r.pathname.groups.constructor, r.pathname.groups.id, Object.getPrototypeOf(r.pathname.groups) === Object.prototype, Object.hasOwn(r.pathname.groups,'__proto__')); console.log(fresh.test('https://example.com/x/7'), matched.pathname.groups.id, new URLPattern({pathname:'/x'}).test('https://example.com/x')); "#, &[], ); assert_eq!( output.stdout, b"first second third true true\ntrue 42 true\n" ); } #[test] fn native_protocol_checks_never_call_user_regexp_exec() { let output = run( r#" const original = Object.getOwnPropertyDescriptor(RegExp.prototype, 'exec'); let calls = 0; const build = () => [ new URLPattern({protocol:'(https)', pathname:'x y'}).pathname, new URLPattern({protocol:'(foo)', pathname:'x y'}).pathname, ].join('|'); const baseline = build(); for (const replacement of [() => {calls++; return {};}, () => {calls++; throw new Error('Poisoned exec');}, () => {calls++; new URLPattern({protocol:'(x)'}); return null;}]) { Object.defineProperty(RegExp.prototype, 'exec', {value:replacement, writable:true, configurable:true}); if (build() !== baseline) throw new Error('User exec changed parsing'); } Object.defineProperty(RegExp.prototype, 'exec', {get() {calls++; throw new Error('Poisoned exec getter');}, configurable:true}); if (build() !== baseline) throw new Error('User getter changed parsing'); Object.defineProperty(RegExp.prototype, 'exec', original); console.log(baseline, calls); "#, &[], ); assert_eq!(output.stdout, b"x%20y|x y 0\n"); }