Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 58 additions & 36 deletions src/node_url_pattern.cc
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,11 @@ void URLPattern::New(const FunctionCallbackInfo<Value>& args) {
// - new URLPattern(input, baseURL)
// - new URLPattern(input, options)
// - new URLPattern(input, baseURL, options)
if (args[0]->IsString()) {
// Per WebIDL, null/undefined for a union type including a dictionary
// uses the default value (empty init).
if (args[0]->IsNullOrUndefined()) {
init = ada::url_pattern_init{};
} else if (args[0]->IsString()) {
BufferValue input_buffer(env->isolate(), args[0]);
CHECK_NOT_NULL(*input_buffer);
input = input_buffer.ToString();
Expand All @@ -217,41 +221,56 @@ void URLPattern::New(const FunctionCallbackInfo<Value>& args) {
return;
}

// The next argument can be baseURL or options.
if (args.Length() > 1) {
// Per WebIDL overload resolution:
// With 3+ args, it's always overload 1: (input, baseURL, options)
// With 2 args, if arg1 is string → overload 1 (baseURL),
// else → overload 2 (options)
if (args.Length() >= 3) {
// arg1 is baseURL. Per WebIDL, null/undefined are stringified for
// USVString ("null"/"undefined"), which will be rejected as invalid
// URLs by ada downstream.
if (args[1]->IsString()) {
BufferValue base_url_buffer(env->isolate(), args[1]);
CHECK_NOT_NULL(*base_url_buffer);
base_url = base_url_buffer.ToString();
} else if (args[1]->IsObject()) {
CHECK(!options.has_value());
options = URLPatternOptions::FromJsObject(env, args[1].As<Object>());
if (!options) {
// If options does not have a value, we assume an error was
// thrown and scheduled on the isolate. Return early to
// propagate it.
return;
}
} else if (args[1]->IsNull()) {
base_url = std::string("null");
} else if (args[1]->IsUndefined()) {
base_url = std::string("undefined");
} else {
THROW_ERR_INVALID_ARG_TYPE(env,
"second argument must be a string or object");
"second argument must be a string");
return;
}

// Only remaining argument can be options.
if (args.Length() > 2) {
// arg2 is options. Per WebIDL, null/undefined for a dictionary
// uses the default value (empty dict).
if (!args[2]->IsNullOrUndefined()) {
if (!args[2]->IsObject()) {
THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object");
return;
}
CHECK(!options.has_value());
options = URLPatternOptions::FromJsObject(env, args[2].As<Object>());
if (!options) {
// If options does not have a value, we assume an error was
// thrown and scheduled on the isolate. Return early to
// propagate it.
return;
}
if (!options) return;
}
} else if (args.Length() == 2) {
// Overload resolution: string → overload 1 (baseURL),
// else → overload 2 (options).
if (args[1]->IsString()) {
BufferValue base_url_buffer(env->isolate(), args[1]);
CHECK_NOT_NULL(*base_url_buffer);
base_url = base_url_buffer.ToString();
} else if (args[1]->IsNullOrUndefined()) {
// Overload 2, options uses default → skip
} else if (args[1]->IsObject()) {
CHECK(!options.has_value());
options = URLPatternOptions::FromJsObject(env, args[1].As<Object>());
if (!options) return;
} else {
THROW_ERR_INVALID_ARG_TYPE(env,
"second argument must be a string or object");
return;
}
}

Expand Down Expand Up @@ -493,11 +512,8 @@ URLPattern::URLPatternOptions::FromJsObject(Environment* env,
Local<Value> ignore_case;
if (obj->Get(env->context(), env->ignore_case_string())
.ToLocal(&ignore_case)) {
if (!ignore_case->IsBoolean()) {
THROW_ERR_INVALID_ARG_TYPE(env, "options.ignoreCase must be a boolean");
return std::nullopt;
}
options.ignore_case = ignore_case->IsTrue();
// Per WebIDL, boolean dictionary members are coerced (not type-checked).
options.ignore_case = ignore_case->BooleanValue(env->isolate());
} else {
// If ToLocal returns false, the assumption is that getting the
// ignore_case_string threw an error, let's propagate that now
Expand Down Expand Up @@ -564,7 +580,7 @@ void URLPattern::Exec(const FunctionCallbackInfo<Value>& args) {
ada::url_pattern_input input;
std::optional<std::string> baseURL{};
std::string input_base;
if (args.Length() == 0) {
if (args.Length() == 0 || args[0]->IsNullOrUndefined()) {
input = ada::url_pattern_init{};
} else if (args[0]->IsString()) {
Utf8Value input_value(env->isolate(), args[0].As<String>());
Expand All @@ -580,13 +596,16 @@ void URLPattern::Exec(const FunctionCallbackInfo<Value>& args) {
return;
}

if (args.Length() > 1) {
if (!args[1]->IsString()) {
if (args.Length() > 1 && !args[1]->IsUndefined()) {
if (args[1]->IsNull()) {
baseURL = std::string("null");
} else if (args[1]->IsString()) {
Utf8Value base_url_value(env->isolate(), args[1].As<String>());
baseURL = base_url_value.ToStringView();
} else {
THROW_ERR_INVALID_ARG_TYPE(env, "baseURL must be a string");
return;
}
Utf8Value base_url_value(env->isolate(), args[1].As<String>());
baseURL = base_url_value.ToStringView();
}

Local<Value> result;
Expand All @@ -607,7 +626,7 @@ void URLPattern::Test(const FunctionCallbackInfo<Value>& args) {
ada::url_pattern_input input;
std::optional<std::string> baseURL{};
std::string input_base;
if (args.Length() == 0) {
if (args.Length() == 0 || args[0]->IsNullOrUndefined()) {
input = ada::url_pattern_init{};
} else if (args[0]->IsString()) {
Utf8Value input_value(env->isolate(), args[0].As<String>());
Expand All @@ -623,13 +642,16 @@ void URLPattern::Test(const FunctionCallbackInfo<Value>& args) {
return;
}

if (args.Length() > 1) {
if (!args[1]->IsString()) {
if (args.Length() > 1 && !args[1]->IsUndefined()) {
if (args[1]->IsNull()) {
baseURL = std::string("null");
} else if (args[1]->IsString()) {
Utf8Value base_url_value(env->isolate(), args[1].As<String>());
baseURL = base_url_value.ToStringView();
} else {
THROW_ERR_INVALID_ARG_TYPE(env, "baseURL must be a string");
return;
}
Utf8Value base_url_value(env->isolate(), args[1].As<String>());
baseURL = base_url_value.ToStringView();
}

std::optional<std::string_view> baseURL_opt =
Expand Down
2 changes: 1 addition & 1 deletion test/fixtures/wpt/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Last update:
- resources: https://github.com/web-platform-tests/wpt/tree/6a2f322376/resources
- streams: https://github.com/web-platform-tests/wpt/tree/bc9dcbbf1a/streams
- url: https://github.com/web-platform-tests/wpt/tree/7a3645b79a/url
- urlpattern: https://github.com/web-platform-tests/wpt/tree/a2e15ad405/urlpattern
- urlpattern: https://github.com/web-platform-tests/wpt/tree/f07c03cbed/urlpattern
- user-timing: https://github.com/web-platform-tests/wpt/tree/5ae85bf826/user-timing
- wasm/jsapi: https://github.com/web-platform-tests/wpt/tree/65a2134d50/wasm/jsapi
- wasm/webapi: https://github.com/web-platform-tests/wpt/tree/fd1b23eeaa/wasm/webapi
Expand Down
36 changes: 36 additions & 0 deletions test/fixtures/wpt/urlpattern/resources/urlpatterntestdata.json
Original file line number Diff line number Diff line change
Expand Up @@ -3118,5 +3118,41 @@
"expected_match": {
"hostname": { "input": "localhost", "groups": { "domain" : "localhost"} }
}
},
{
"pattern": ["((?R)):"],
"expected_obj": "error"
},
{
"pattern": ["(\\H):"],
"expected_obj": "error"
},
{
"pattern": [
{"pathname": "/:foo((?<x>a))"}
],
"inputs": [
{"pathname": "/a"}
],
"expected_match": {
"pathname": {
"input": "/a",
"groups": {"foo": "a"}
}
}
},
{
"pattern": [
{"pathname": "/foo/(bar(?<x>baz))"}
],
"inputs": [
{"pathname": "/foo/barbaz"}
],
"expected_match": {
"pathname": {
"input": "/foo/barbaz",
"groups": {"0": "barbaz"}
}
}
}
]
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
<!DOCTYPE html>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script>
// META: global=window,worker
test(() => {
assert_throws_js(TypeError, () => { new URLPattern(new URL('https://example.org/%(')); } );
assert_throws_js(TypeError, () => { new URLPattern(new URL('https://example.org/%((')); } );
Expand All @@ -11,4 +8,3 @@
test(() => {
new URLPattern(undefined, undefined);
}, `Test constructor with undefined`);
</script>
2 changes: 1 addition & 1 deletion test/fixtures/wpt/versions.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
"path": "url"
},
"urlpattern": {
"commit": "a2e15ad40518c30c4e7f649584dbda699a40d531",
"commit": "f07c03cbede41ba677c3d26fd17ff3e02ba26783",
"path": "urlpattern"
},
"user-timing": {
Expand Down
102 changes: 99 additions & 3 deletions test/parallel/test-urlpattern-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,19 @@
code: 'ERR_INVALID_ARG_TYPE',
});

assert.throws(() => new URLPattern({}, { ignoreCase: '' }), {
code: 'ERR_INVALID_ARG_TYPE',
});
// Per WebIDL, ignoreCase is coerced to boolean (not type-checked).
{
const p = new URLPattern({}, { ignoreCase: '' });
assert.strictEqual(p.protocol, '*');
}
{
const p = new URLPattern({}, { ignoreCase: undefined });
assert.strictEqual(p.protocol, '*');
}
{
const p = new URLPattern({}, {});
assert.strictEqual(p.protocol, '*');
}

const pattern = new URLPattern();

Expand All @@ -44,3 +54,89 @@
assert.throws(() => pattern.test('', 1), {
code: 'ERR_INVALID_ARG_TYPE',
});

// Per WebIDL, undefined/null for a URLPatternInput (union including dictionary)
// uses the default value (empty URLPatternInit {}).

// Constructor: undefined input should be treated as empty init.
{
const p = new URLPattern(undefined);
assert.strictEqual(p.protocol, '*');
assert.strictEqual(p.hostname, '*');
}

// Constructor: null input should be treated as empty init (union, dict branch).
{
const p = new URLPattern(null);
assert.strictEqual(p.protocol, '*');
assert.strictEqual(p.hostname, '*');
}

// Constructor: 2-arg with undefined/null uses overload 2 (options defaults).
{
const p1 = new URLPattern(undefined, undefined);
assert.strictEqual(p1.protocol, '*');
const p2 = new URLPattern(null, null);
assert.strictEqual(p2.protocol, '*');
const p3 = new URLPattern({}, null);
assert.strictEqual(p3.protocol, '*');
const p4 = new URLPattern('https://example.com', null);
assert.strictEqual(p4.hostname, 'example.com');
const p5 = new URLPattern('https://example.com', undefined);
assert.strictEqual(p5.hostname, 'example.com');
}

// Constructor: valid input with undefined/null options.
{
const p = new URLPattern({ pathname: '/foo' }, undefined);
assert.strictEqual(p.pathname, '/foo');
}

// Constructor: 3-arg with null/undefined baseURL is stringified per WebIDL,
// rejected as invalid URL by the parser.
assert.throws(() => new URLPattern('https://example.com', null, null));

Check failure on line 97 in test/parallel/test-urlpattern-types.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

`assert.throws()` must be invoked with at least two arguments
assert.throws(() => new URLPattern('https://example.com', undefined, undefined));

Check failure on line 98 in test/parallel/test-urlpattern-types.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

`assert.throws()` must be invoked with at least two arguments

// Constructor: 3-arg with valid baseURL and null options uses defaults.
{
const p = new URLPattern('https://example.com', 'https://example.com', null);
assert.strictEqual(p.hostname, 'example.com');
const p2 = new URLPattern('https://example.com', 'https://example.com', undefined);
assert.strictEqual(p2.hostname, 'example.com');
}

// exec() and test(): undefined input should be treated as empty init.
{
const p = new URLPattern();
assert.strictEqual(p.test(undefined), true);
assert.strictEqual(p.test(undefined, undefined), true);
assert.notStrictEqual(p.exec(undefined), null);
assert.notStrictEqual(p.exec(undefined, undefined), null);
}

// exec() and test(): null input should be treated as empty init.
{
const p = new URLPattern();
assert.strictEqual(p.test(null), true);
assert.notStrictEqual(p.exec(null), null);
}

// exec() and test(): null for baseURL is stringified to "null" per WebIDL.
// With string input, "null" is not a valid base URL so match fails silently.
// With dict input, providing baseURL with a dict throws per spec.
{
const p = new URLPattern();
// String input + null baseURL: no throw, match returns null (false).
assert.strictEqual(p.test('https://example.com', null), false);
assert.strictEqual(p.exec('https://example.com', null), null);
// Dict input + null baseURL: throws (baseURL not allowed with dict input).
assert.throws(() => p.test(null, null));

Check failure on line 133 in test/parallel/test-urlpattern-types.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

`assert.throws()` must be invoked with at least two arguments
assert.throws(() => p.exec(null, null));

Check failure on line 134 in test/parallel/test-urlpattern-types.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

`assert.throws()` must be invoked with at least two arguments
}

// exec() and test(): valid input with undefined baseURL.
{
const p = new URLPattern({ protocol: 'https' });
assert.strictEqual(p.test('https://example.com', undefined), true);
assert.notStrictEqual(p.exec('https://example.com', undefined), null);
}
Loading