From a7ee0c27e73afceeb3ee201e0ae7051481530edd Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Fri, 3 Jul 2026 12:00:02 -0300 Subject: [PATCH] fix(urlpattern): match capture groups in test()/exec() v8_regex_provider::regex_search had two bugs that made URLPattern matching fail (or crash) for any pattern with capture groups: - The ToLocal(&item) success check was inverted, so the first successfully read match element bailed out as "no match" and test()/exec() always returned false/null. - It included match element 0 (the whole-match string). ada's create_component_match_result pairs exec_result[i] with group_name_list[i] and expects only the capture groups, so the extra element overran group_name_list and crashed once the inverted check was fixed. Iterate from index 1 and bail only when reading an element fails. Add URLPattern regression tests covering test() match/non-match and exec() named capture-group extraction and null-on-no-match. --- NativeScript/runtime/URLPatternImpl.cpp | 14 ++++++++++++-- TestRunner/app/tests/URLPattern.js | 19 +++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/NativeScript/runtime/URLPatternImpl.cpp b/NativeScript/runtime/URLPatternImpl.cpp index 4b4435f5..5ffe5a6a 100644 --- a/NativeScript/runtime/URLPatternImpl.cpp +++ b/NativeScript/runtime/URLPatternImpl.cpp @@ -73,9 +73,19 @@ v8_regex_provider::regex_search( auto array = matches.As(); auto len = array->Length(); ret.reserve(len); - for (int i = 0; i < len; i++) { + // Skip element 0 (the whole-match string). ada's + // url_pattern_component::create_component_match_result pairs exec_result[i] + // with group_name_list[i], i.e. it expects only the capture groups. + // Including element 0 makes exec_result one longer than group_name_list and + // indexes it out of bounds (crash). ada documents this: the provider must + // drop the full match ("start from 1"). + for (int i = 1; i < len; i++) { v8::Local item; - if (array->Get(isolate->GetCurrentContext(), i).ToLocal(&item)) { + // ToLocal returns true on success; bail only when reading the element + // fails. (This condition was inverted, which made every regex match + // report no match, so URLPattern test()/exec() always failed for patterns + // with capture groups.) + if (!array->Get(isolate->GetCurrentContext(), i).ToLocal(&item)) { return std::nullopt; } diff --git a/TestRunner/app/tests/URLPattern.js b/TestRunner/app/tests/URLPattern.js index 0c2d1c1f..889e33cb 100644 --- a/TestRunner/app/tests/URLPattern.js +++ b/TestRunner/app/tests/URLPattern.js @@ -46,4 +46,23 @@ describe("URLPattern", function () { expect(pattern.hostname).toBe("google.com"); }); + it("test() matches a URL against a pattern with a capture group", function () { + const pattern = new URLPattern("https://example.com/books/:id"); + expect(pattern.test("https://example.com/books/123")).toBe(true); + expect(pattern.test("https://example.com/movies/123")).toBe(false); + }); + + it("exec() extracts named capture groups", function () { + const pattern = new URLPattern("https://example.com/books/:id"); + const result = pattern.exec("https://example.com/books/123"); + expect(result).not.toBeNull(); + expect(result.pathname.input).toBe("/books/123"); + expect(result.pathname.groups.id).toBe("123"); + }); + + it("exec() returns null when the URL does not match", function () { + const pattern = new URLPattern("https://example.com/books/:id"); + expect(pattern.exec("https://example.com/movies/123")).toBeNull(); + }); + });