diff --git a/blink/public/mojom/web_feature/web_feature.mojom b/blink/public/mojom/web_feature/web_feature.mojom index d00cb41c5b19..90e7d02e98da 100644 --- a/blink/public/mojom/web_feature/web_feature.mojom +++ b/blink/public/mojom/web_feature/web_feature.mojom @@ -3278,6 +3278,7 @@ enum WebFeature { kBlobStoreAccessAcrossAgentClustersInResolveAsURLLoaderFactory = 3963, kBlobStoreAccessAcrossAgentClustersInResolveForNavigation = 3964, kTapDelayEnabled = 3965, + kV8URLPattern_CompareComponent_Method = 3966, // Add new features immediately above this line. Don't change assigned // numbers of any item, and don't reuse removed slots. diff --git a/blink/renderer/bindings/generated_in_modules.gni b/blink/renderer/bindings/generated_in_modules.gni index be695e387ee0..998b6e7a0012 100644 --- a/blink/renderer/bindings/generated_in_modules.gni +++ b/blink/renderer/bindings/generated_in_modules.gni @@ -873,6 +873,8 @@ generated_dictionary_sources_in_modules = [ "$root_gen_dir/third_party/blink/renderer/bindings/modules/v8/v8_text_decoder_options.h", "$root_gen_dir/third_party/blink/renderer/bindings/modules/v8/v8_text_encoder_encode_into_result.cc", "$root_gen_dir/third_party/blink/renderer/bindings/modules/v8/v8_text_encoder_encode_into_result.h", + "$root_gen_dir/third_party/blink/renderer/bindings/modules/v8/v8_url_pattern_component.cc", + "$root_gen_dir/third_party/blink/renderer/bindings/modules/v8/v8_url_pattern_component.h", "$root_gen_dir/third_party/blink/renderer/bindings/modules/v8/v8_url_pattern_component_result.cc", "$root_gen_dir/third_party/blink/renderer/bindings/modules/v8/v8_url_pattern_component_result.h", "$root_gen_dir/third_party/blink/renderer/bindings/modules/v8/v8_url_pattern_init.cc", diff --git a/blink/renderer/modules/url_pattern/url_pattern.cc b/blink/renderer/modules/url_pattern/url_pattern.cc index 60e69a511858..3abf9254028f 100644 --- a/blink/renderer/modules/url_pattern/url_pattern.cc +++ b/blink/renderer/modules/url_pattern/url_pattern.cc @@ -390,6 +390,36 @@ String URLPattern::hash() const { return hash_->GeneratePatternString(); } +// static +int URLPattern::compareComponent(const V8URLPatternComponent& component, + const URLPattern* left, + const URLPattern* right) { + switch (component.AsEnum()) { + case V8URLPatternComponent::Enum::kProtocol: + return url_pattern::Component::Compare(*left->protocol_, + *right->protocol_); + case V8URLPatternComponent::Enum::kUsername: + return url_pattern::Component::Compare(*left->username_, + *right->username_); + case V8URLPatternComponent::Enum::kPassword: + return url_pattern::Component::Compare(*left->password_, + *right->password_); + case V8URLPatternComponent::Enum::kHostname: + return url_pattern::Component::Compare(*left->hostname_, + *right->hostname_); + case V8URLPatternComponent::Enum::kPort: + return url_pattern::Component::Compare(*left->port_, *right->port_); + case V8URLPatternComponent::Enum::kPathname: + return url_pattern::Component::Compare(*left->pathname_, + *right->pathname_); + case V8URLPatternComponent::Enum::kSearch: + return url_pattern::Component::Compare(*left->search_, *right->search_); + case V8URLPatternComponent::Enum::kHash: + return url_pattern::Component::Compare(*left->hash_, *right->hash_); + } + NOTREACHED(); +} + void URLPattern::Trace(Visitor* visitor) const { visitor->Trace(protocol_); visitor->Trace(username_); diff --git a/blink/renderer/modules/url_pattern/url_pattern.h b/blink/renderer/modules/url_pattern/url_pattern.h index 387235391c3b..4dd1affda57a 100644 --- a/blink/renderer/modules/url_pattern/url_pattern.h +++ b/blink/renderer/modules/url_pattern/url_pattern.h @@ -1,4 +1,3 @@ -// Copyright 2020 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -7,6 +6,7 @@ #include "base/types/pass_key.h" #include "third_party/blink/renderer/bindings/modules/v8/v8_typedefs.h" +#include "third_party/blink/renderer/bindings/modules/v8/v8_url_pattern_component.h" #include "third_party/blink/renderer/modules/modules_export.h" #include "third_party/blink/renderer/platform/bindings/script_wrappable.h" #include "third_party/liburlpattern/parse.h" @@ -69,6 +69,10 @@ class MODULES_EXPORT URLPattern : public ScriptWrappable { String search() const; String hash() const; + static int compareComponent(const V8URLPatternComponent& component, + const URLPattern* left, + const URLPattern* right); + void Trace(Visitor* visitor) const override; private: diff --git a/blink/renderer/modules/url_pattern/url_pattern.idl b/blink/renderer/modules/url_pattern/url_pattern.idl index bc677cd67119..4140668a70e1 100644 --- a/blink/renderer/modules/url_pattern/url_pattern.idl +++ b/blink/renderer/modules/url_pattern/url_pattern.idl @@ -4,6 +4,9 @@ typedef (USVString or URLPatternInit) URLPatternInput; +enum URLPatternComponent { "protocol", "username", "password", "hostname", + "port", "pathname", "search", "hash" }; + // https://wicg.github.io/urlpattern/ [ Exposed=(Window,Worker), @@ -26,4 +29,8 @@ typedef (USVString or URLPatternInit) URLPatternInput; readonly attribute USVString pathname; readonly attribute USVString search; readonly attribute USVString hash; + + [Measure] + static short compareComponent(URLPatternComponent component, + URLPattern left, URLPattern right); }; diff --git a/blink/renderer/modules/url_pattern/url_pattern_component.cc b/blink/renderer/modules/url_pattern/url_pattern_component.cc index 2f7516961d5a..8e029755c12b 100644 --- a/blink/renderer/modules/url_pattern/url_pattern_component.cc +++ b/blink/renderer/modules/url_pattern/url_pattern_component.cc @@ -92,10 +92,12 @@ liburlpattern::EncodeCallback GetEncodeCallback(Component::Type type, // Utility method to get the correct liburlpattern parse options for a given // type. const liburlpattern::Options& GetOptions(Component::Type type) { + using liburlpattern::Options; + // The liburlpattern::Options to use for most component patterns. We // default to strict mode and case sensitivity. In addition, most // components have no concept of a delimiter or prefix character. - DEFINE_THREAD_SAFE_STATIC_LOCAL(liburlpattern::Options, default_options, + DEFINE_THREAD_SAFE_STATIC_LOCAL(Options, default_options, ({.delimiter_list = "", .prefix_list = "", .sensitive = true, @@ -106,7 +108,7 @@ const liburlpattern::Options& GetOptions(Component::Type type) { // by default. Note, hostnames are case insensitive but we require case // sensitivity here. This assumes that the hostname values have already // been normalized to lower case as in URL(). - DEFINE_THREAD_SAFE_STATIC_LOCAL(liburlpattern::Options, hostname_options, + DEFINE_THREAD_SAFE_STATIC_LOCAL(Options, hostname_options, ({.delimiter_list = ".", .prefix_list = "", .sensitive = true, @@ -116,7 +118,7 @@ const liburlpattern::Options& GetOptions(Component::Type type) { // "/" delimiter controlling how far a named group like ":bar" will match // by default. It also configures "/" to be treated as an automatic // prefix before groups. - DEFINE_THREAD_SAFE_STATIC_LOCAL(liburlpattern::Options, pathname_options, + DEFINE_THREAD_SAFE_STATIC_LOCAL(Options, pathname_options, ({.delimiter_list = "/", .prefix_list = "/", .sensitive = true, @@ -138,6 +140,88 @@ const liburlpattern::Options& GetOptions(Component::Type type) { NOTREACHED(); } +// Utility function to return a statically allocated Part list. +const std::vector& GetWildcardOnlyPartList() { + using liburlpattern::Modifier; + using liburlpattern::Part; + using liburlpattern::PartType; + DEFINE_THREAD_SAFE_STATIC_LOCAL( + std::vector, instance, + ({Part(PartType::kFullWildcard, + /*name=*/"", + /*prefix=*/"", /*value=*/"", /*suffix=*/"", Modifier::kNone)})); + return instance; +} + +int ComparePart(const liburlpattern::Part& lh, const liburlpattern::Part& rh) { + // We prioritize PartType in the ordering so we can favor fixed text. The + // type ordering is: + // + // kFixed > kRegex > kSegmentWildcard > kFullWildcard. + // + // We considered kRegex greater than the wildcards because it is likely to be + // used for imposing some constraint and not just duplicating wildcard + // behavior. + // + // This comparison depends on the PartType enum in liburlpattern having the + // correct corresponding numeric values. + // + // Next the Modifier is considered: + // + // kNone > kOneOrMore > kOptional > kZeroOrMore. + // + // The rationale here is that requring the match group to exist is more + // restrictive then making it optional and requiring an exact count is more + // restrictive than repeating. + // + // This comparison depends on the Modifier enum in liburlpattern having the + // correct corresponding numeric values. + // + // Finally we lexicographically compare the text components from left to + // right; `prefix`, `value`, and `suffix`. Its ok to depend on simple + // byte-wise string comparison here because the values have all been URL + // encoded. This guarantees the strings contain only ASCII. + auto left = std::tie(lh.type, lh.modifier, lh.prefix, lh.value, lh.suffix); + auto right = std::tie(rh.type, rh.modifier, rh.prefix, rh.value, rh.suffix); + if (left < right) + return -1; + else if (left == right) + return 0; + else + return 1; +} + +// Utility method to compare two part lists. +int ComparePartList(const std::vector& lh, + const std::vector& rh) { + using liburlpattern::Modifier; + using liburlpattern::Part; + using liburlpattern::PartType; + + // Begin by comparing each Part in the lists with each other. If any + // are not equal, then we are done. + size_t i = 0; + for (; i < lh.size() && i < rh.size(); ++i) { + int r = ComparePart(lh[i], rh[i]); + if (r) + return r; + } + + // We reached the end of at least one of the lists without finding a + // difference. However, we must handle the case where one list is longer + // than the other. In this case we compare the next Part from the + // longer list to a synthetically created empty kFixed Part. This is + // necessary in order for "/foo/" to be considered more restrictive, and + // therefore greater, than "/foo/*". + if (i == lh.size() && i != rh.size()) + return ComparePart(Part(PartType::kFixed, "", Modifier::kNone), rh[i]); + else if (i != lh.size() && i == rh.size()) + return ComparePart(lh[i], Part(PartType::kFixed, "", Modifier::kNone)); + + // No differences were found, so declare them equal. + return 0; +} + } // anonymous namespace // static @@ -218,6 +302,31 @@ Component* Component::Compile(const String& pattern, std::move(wtf_name_list), base::PassKey()); } +// static +int Component::Compare(const Component& lh, const Component& rh) { + using liburlpattern::Modifier; + using liburlpattern::Part; + using liburlpattern::PartType; + + // If both the left and right components are empty wildcards, then they are + // effectively equal. + if (!lh.pattern_.has_value() && !rh.pattern_.has_value()) + return 0; + + // If one side has a real pattern and the other side is an empty component, + // then we have to compare to a part list with a single full wildcard. + if (lh.pattern_.has_value() && !rh.pattern_.has_value()) { + return ComparePartList(lh.pattern_->PartList(), GetWildcardOnlyPartList()); + } + + if (!lh.pattern_.has_value() && rh.pattern_.has_value()) { + return ComparePartList(GetWildcardOnlyPartList(), rh.pattern_->PartList()); + } + + // Otherwise compare the part lists of the patterns on each side. + return ComparePartList(lh.pattern_->PartList(), rh.pattern_->PartList()); +} + Component::Component(Type type, liburlpattern::Pattern pattern, ScriptRegexp* regexp, diff --git a/blink/renderer/modules/url_pattern/url_pattern_component.h b/blink/renderer/modules/url_pattern/url_pattern_component.h index f5793e04c2d7..31ca11e1f621 100644 --- a/blink/renderer/modules/url_pattern/url_pattern_component.h +++ b/blink/renderer/modules/url_pattern/url_pattern_component.h @@ -52,6 +52,13 @@ class Component final : public GarbageCollected { Component* protocol_component, ExceptionState& exception_state); + // Compare the pattern strings in the two given components. This provides a + // mostly lexicographical ordering based on fixed text in the patterns. + // Matching groups and modifiers are treated such that more restrictive + // patterns are greater in value. Group names are not considered in the + // comparison. + static int Compare(const Component& lh, const Component& rh); + // Constructs a Component with a real `pattern` that compiled to the given // `regexp`. Component(Type type, diff --git a/blink/web_tests/external/wpt/urlpattern/resources/urlpattern-compare-test-data.json b/blink/web_tests/external/wpt/urlpattern/resources/urlpattern-compare-test-data.json new file mode 100644 index 000000000000..54dd80a91a23 --- /dev/null +++ b/blink/web_tests/external/wpt/urlpattern/resources/urlpattern-compare-test-data.json @@ -0,0 +1,116 @@ +[ + { + "component": "pathname", + "left": { "pathname": "/foo/a" }, + "right": { "pathname": "/foo/b" }, + "expected": -1 + }, + { + "component": "pathname", + "left": { "pathname": "/foo/b" }, + "right": { "pathname": "/foo/bar" }, + "expected": -1 + }, + { + "component": "pathname", + "left": { "pathname": "/foo/bar" }, + "right": { "pathname": "/foo/:bar" }, + "expected": 1 + }, + { + "component": "pathname", + "left": { "pathname": "/foo/" }, + "right": { "pathname": "/foo/:bar" }, + "expected": 1 + }, + { + "component": "pathname", + "left": { "pathname": "/foo/:bar" }, + "right": { "pathname": "/foo/*" }, + "expected": 1 + }, + { + "component": "pathname", + "left": { "pathname": "/foo/{bar}" }, + "right": { "pathname": "/foo/(bar)" }, + "expected": 1 + }, + { + "component": "pathname", + "left": { "pathname": "/foo/{bar}" }, + "right": { "pathname": "/foo/{bar}+" }, + "expected": 1 + }, + { + "component": "pathname", + "left": { "pathname": "/foo/{bar}+" }, + "right": { "pathname": "/foo/{bar}?" }, + "expected": 1 + }, + { + "component": "pathname", + "left": { "pathname": "/foo/{bar}?" }, + "right": { "pathname": "/foo/{bar}*" }, + "expected": 1 + }, + { + "component": "pathname", + "left": { "pathname": "/foo/(123)" }, + "right": { "pathname": "/foo/(12)" }, + "expected": 1 + }, + { + "component": "pathname", + "left": { "pathname": "/foo/:b" }, + "right": { "pathname": "/foo/:a" }, + "expected": 0 + }, + { + "component": "pathname", + "left": { "pathname": "*/foo" }, + "right": { "pathname": "*" }, + "expected": 1 + }, + { + "component": "port", + "left": { "port": "9" }, + "right": { "port": "100" }, + "expected": 1 + }, + { + "component": "pathname", + "left": { "pathname": "foo/:bar?/baz" }, + "right": { "pathname": "foo/{:bar}?/baz" }, + "expected": -1 + }, + { + "component": "pathname", + "left": { "pathname": "foo/:bar?/baz" }, + "right": { "pathname": "foo{/:bar}?/baz" }, + "expected": 0 + }, + { + "component": "pathname", + "left": { "pathname": "foo/:bar?/baz" }, + "right": { "pathname": "fo{o/:bar}?/baz" }, + "expected": 1 + }, + { + "component": "pathname", + "left": { "pathname": "foo/:bar?/baz" }, + "right": { "pathname": "foo{/:bar/}?baz" }, + "expected": -1 + }, + { + "component": "pathname", + "left": "https://a.example.com/b?a", + "right": "https://b.example.com/a?b", + "expected": 1 + }, + { + "component": "pathname", + "left": { "pathname": "/foo/{bar}/baz" }, + "right": { "pathname": "/foo/bar/baz" }, + "expected": 0 + } +] diff --git a/blink/web_tests/external/wpt/urlpattern/resources/urlpattern-compare-tests.js b/blink/web_tests/external/wpt/urlpattern/resources/urlpattern-compare-tests.js new file mode 100644 index 000000000000..b92b53ee45cd --- /dev/null +++ b/blink/web_tests/external/wpt/urlpattern/resources/urlpattern-compare-tests.js @@ -0,0 +1,26 @@ +function runTests(data) { + for (let entry of data) { + test(function() { + const left = new URLPattern(entry.left); + const right = new URLPattern(entry.right); + + assert_equals(URLPattern.compareComponent(entry.component, left, right), entry.expected); + + // We have to coerce to an integer here in order to avoid asserting + // that `+0` is `-0`. + const reverse_expected = ~~(entry.expected * -1); + assert_equals(URLPattern.compareComponent(entry.component, right, left), reverse_expected, "reverse order"); + + assert_equals(URLPattern.compareComponent(entry.component, left, left), 0, "left equality"); + assert_equals(URLPattern.compareComponent(entry.component, right, right), 0, "right equality"); + }, `Component: ${entry.component} ` + + `Left: ${JSON.stringify(entry.left)} ` + + `Right: ${JSON.stringify(entry.right)}`); + } +} + +promise_test(async function() { + const response = await fetch('resources/urlpattern-compare-test-data.json'); + const data = await response.json(); + runTests(data); +}, 'Loading data...'); diff --git a/blink/web_tests/external/wpt/urlpattern/urlpattern-compare.any.js b/blink/web_tests/external/wpt/urlpattern/urlpattern-compare.any.js new file mode 100644 index 000000000000..8db38adfd41d --- /dev/null +++ b/blink/web_tests/external/wpt/urlpattern/urlpattern-compare.any.js @@ -0,0 +1,2 @@ +// META: global=window,worker +// META: script=resources/urlpattern-compare-tests.js diff --git a/blink/web_tests/external/wpt/urlpattern/urlpattern-compare.https.any.js b/blink/web_tests/external/wpt/urlpattern/urlpattern-compare.https.any.js new file mode 100644 index 000000000000..8db38adfd41d --- /dev/null +++ b/blink/web_tests/external/wpt/urlpattern/urlpattern-compare.https.any.js @@ -0,0 +1,2 @@ +// META: global=window,worker +// META: script=resources/urlpattern-compare-tests.js diff --git a/blink/web_tests/http/tests/serviceworker/webexposed/global-interface-listing-service-worker-expected.txt b/blink/web_tests/http/tests/serviceworker/webexposed/global-interface-listing-service-worker-expected.txt index 9b56893def95..b5a4908bcf22 100644 --- a/blink/web_tests/http/tests/serviceworker/webexposed/global-interface-listing-service-worker-expected.txt +++ b/blink/web_tests/http/tests/serviceworker/webexposed/global-interface-listing-service-worker-expected.txt @@ -1611,6 +1611,7 @@ interface URL setter search setter username interface URLPattern + static method compareComponent attribute @@toStringTag getter hash getter hostname diff --git a/blink/web_tests/webexposed/global-interface-listing-dedicated-worker-expected.txt b/blink/web_tests/webexposed/global-interface-listing-dedicated-worker-expected.txt index 2a79ce4d0213..4012bf329884 100644 --- a/blink/web_tests/webexposed/global-interface-listing-dedicated-worker-expected.txt +++ b/blink/web_tests/webexposed/global-interface-listing-dedicated-worker-expected.txt @@ -1673,6 +1673,7 @@ Starting worker: resources/global-interface-listing-worker.js [Worker] setter search [Worker] setter username [Worker] interface URLPattern +[Worker] static method compareComponent [Worker] attribute @@toStringTag [Worker] getter hash [Worker] getter hostname diff --git a/blink/web_tests/webexposed/global-interface-listing-expected.txt b/blink/web_tests/webexposed/global-interface-listing-expected.txt index c83c9e3d1eb0..b19ef5cfb653 100644 --- a/blink/web_tests/webexposed/global-interface-listing-expected.txt +++ b/blink/web_tests/webexposed/global-interface-listing-expected.txt @@ -8873,6 +8873,7 @@ interface URL setter search setter username interface URLPattern + static method compareComponent attribute @@toStringTag getter hash getter hostname diff --git a/blink/web_tests/webexposed/global-interface-listing-shared-worker-expected.txt b/blink/web_tests/webexposed/global-interface-listing-shared-worker-expected.txt index cf4e0f86d638..09829a21f854 100644 --- a/blink/web_tests/webexposed/global-interface-listing-shared-worker-expected.txt +++ b/blink/web_tests/webexposed/global-interface-listing-shared-worker-expected.txt @@ -1488,6 +1488,7 @@ Starting worker: resources/global-interface-listing-worker.js [Worker] setter search [Worker] setter username [Worker] interface URLPattern +[Worker] static method compareComponent [Worker] attribute @@toStringTag [Worker] getter hash [Worker] getter hostname diff --git a/liburlpattern/pattern.cc b/liburlpattern/pattern.cc index 52d78bc53835..6b996f2072f7 100644 --- a/liburlpattern/pattern.cc +++ b/liburlpattern/pattern.cc @@ -15,28 +15,28 @@ namespace { void AppendModifier(Modifier modifier, std::string& append_target) { switch (modifier) { - case Modifier::kNone: + case Modifier::kZeroOrMore: + append_target += '*'; break; case Modifier::kOptional: append_target += '?'; break; - case Modifier::kZeroOrMore: - append_target += '*'; - break; case Modifier::kOneOrMore: append_target += '+'; break; + case Modifier::kNone: + break; } } size_t ModifierLength(Modifier modifier) { switch (modifier) { - case Modifier::kNone: - return 0; - case Modifier::kOptional: case Modifier::kZeroOrMore: + case Modifier::kOptional: case Modifier::kOneOrMore: return 1; + case Modifier::kNone: + return 0; } } diff --git a/liburlpattern/pattern.h b/liburlpattern/pattern.h index 523bb8f12e75..b5ec74e091c8 100644 --- a/liburlpattern/pattern.h +++ b/liburlpattern/pattern.h @@ -13,33 +13,37 @@ namespace liburlpattern { +// Numeric values are set such that more restrictive values come last. This +// is important for comparison routines in calling code, like URLPattern. enum class PartType { - // A fixed, non-variable part of the pattern. Consists of kChar and - // kEscapedChar Tokens. - kFixed, - - // A part with a custom regular expression. - kRegex, + // A part that matches any character to the end of the input string. + kFullWildcard = 0, // A part that matches any character to the next segment separator. - kSegmentWildcard, + kSegmentWildcard = 1, - // A part that matches any character to the end of the input string. - kFullWildcard, + // A part with a custom regular expression. + kRegex = 2, + + // A fixed, non-variable part of the pattern. Consists of kChar and + // kEscapedChar Tokens. + kFixed = 3, }; +// Numeric values are set such that more restrictive values come last. This +// is important for comparison routines in calling code, like URLPattern. enum class Modifier { - // No modifier. - kNone, + // The `*` modifier. + kZeroOrMore = 0, // The `?` modifier. - kOptional, - - // The `*` modifier. - kZeroOrMore, + kOptional = 1, // The `+` modifier. - kOneOrMore, + kOneOrMore = 2, + + // No modifier. + kNone = 3, }; // A structure representing one part of a parsed Pattern. A full Pattern