LibURL: Use libpsl for public suffix matching
Replace the generated public suffix table and custom matcher with a direct LibURL PublicSuffixData implementation backed by libpsl. This drops our PSL download/generator path and uses the same library already used by libcurl. Performance is comparable before and after, while LibURL binary size is smaller.
This commit is contained in:
parent
ec3ea7ed25
commit
5ef34ecb7a
13 changed files with 250 additions and 390 deletions
|
|
@ -162,7 +162,7 @@ bool cookie_matches_url(Cookie const& cookie, URL::URL const& url, String const&
|
|||
// the cookie's domain.
|
||||
// - The cookie's domain is not a public suffix, for user agents configured to reject "public suffixes".
|
||||
bool is_not_host_only_and_domain_matches = (!cookie.host_only && domain_matches(retrieval_host_canonical, cookie.domain))
|
||||
&& !URL::PublicSuffixData::the()->is_matching_public_suffix(cookie.domain);
|
||||
&& !URL::PublicSuffixData::is_matching_public_suffix(cookie.domain);
|
||||
|
||||
if (!is_host_only_and_has_identical_domain && !is_not_host_only_and_domain_matches)
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -1,17 +1,15 @@
|
|||
include(public_suffix)
|
||||
|
||||
set(SOURCES
|
||||
Host.cpp
|
||||
Origin.cpp
|
||||
Parser.cpp
|
||||
PublicSuffixData.cpp
|
||||
RustIntegration.cpp
|
||||
Site.cpp
|
||||
URL.cpp
|
||||
${PUBLIC_SUFFIX_SOURCES}
|
||||
)
|
||||
|
||||
ladybird_lib(LibURL url)
|
||||
target_link_libraries(LibURL PRIVATE liburl_rust LibUnicode LibTextCodec LibRegex)
|
||||
target_link_libraries(LibURL PRIVATE liburl_rust LibUnicode LibTextCodec LibRegex PkgConfig::LIBPSL)
|
||||
|
||||
import_rust_crate(MANIFEST_PATH Rust/Cargo.toml CRATE_NAME liburl_rust FEATURES allocator FFI_HEADER RustFFI.h)
|
||||
target_link_libraries(liburl_rust INTERFACE LibUnicode LibTextCodec LibRegex)
|
||||
|
|
|
|||
|
|
@ -199,7 +199,7 @@ Optional<String> Host::public_suffix() const
|
|||
host_without_trailing_dot = host_without_trailing_dot.substring_view(0, host_without_trailing_dot.length() - 1);
|
||||
|
||||
// FIXME: Unify this logic with registrable domain.
|
||||
auto public_suffix = PublicSuffixData::the()->find_matching_public_suffix(host_without_trailing_dot);
|
||||
auto public_suffix = PublicSuffixData::find_matching_public_suffix(host_without_trailing_dot);
|
||||
if (!public_suffix.has_value()) {
|
||||
auto last_dot = host_without_trailing_dot.find_last('.');
|
||||
if (last_dot.has_value())
|
||||
|
|
|
|||
165
Libraries/LibURL/PublicSuffixData.cpp
Normal file
165
Libraries/LibURL/PublicSuffixData.cpp
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
/*
|
||||
* Copyright (c) 2026-present, the Ladybird developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/ByteString.h>
|
||||
#include <AK/String.h>
|
||||
#include <LibURL/Host.h>
|
||||
#include <LibURL/Parser.h>
|
||||
#include <LibURL/PublicSuffixData.h>
|
||||
|
||||
#include <libpsl.h>
|
||||
|
||||
namespace URL {
|
||||
|
||||
static psl_ctx_t const* public_suffix_context()
|
||||
{
|
||||
static auto const* context = psl_builtin();
|
||||
VERIFY(context);
|
||||
return context;
|
||||
}
|
||||
|
||||
static constexpr auto public_suffix_match_types = PSL_TYPE_ANY | PSL_TYPE_NO_STAR_RULE;
|
||||
|
||||
struct NormalizedDomain {
|
||||
StringView host;
|
||||
StringView trailing_dot;
|
||||
};
|
||||
|
||||
static Optional<NormalizedDomain> normalized_domain_for_host(Host const& host)
|
||||
{
|
||||
if (!host.is_domain())
|
||||
return OptionalNone {};
|
||||
|
||||
auto domain = host.get<String>().bytes_as_string_view().trim("."sv, TrimMode::Left);
|
||||
if (domain.is_empty())
|
||||
return OptionalNone {};
|
||||
|
||||
auto trailing_dot = ""sv;
|
||||
if (domain.ends_with('.')) {
|
||||
trailing_dot = "."sv;
|
||||
domain = domain.substring_view(0, domain.length() - 1);
|
||||
if (domain.is_empty())
|
||||
return OptionalNone {};
|
||||
}
|
||||
|
||||
return NormalizedDomain { domain, trailing_dot };
|
||||
}
|
||||
|
||||
static bool is_matching_public_suffix_impl(StringView host)
|
||||
{
|
||||
ByteString lookup_host { host };
|
||||
return psl_is_public_suffix2(public_suffix_context(), lookup_host.characters(), public_suffix_match_types);
|
||||
}
|
||||
|
||||
bool PublicSuffixData::is_matching_public_suffix(StringView host)
|
||||
{
|
||||
if (host.is_empty())
|
||||
return false;
|
||||
|
||||
auto parsed_host = Parser::parse_host(host);
|
||||
if (!parsed_host.has_value())
|
||||
return false;
|
||||
|
||||
return is_matching_public_suffix(*parsed_host);
|
||||
}
|
||||
|
||||
bool PublicSuffixData::is_matching_public_suffix(Host const& host)
|
||||
{
|
||||
auto normalized_domain = normalized_domain_for_host(host);
|
||||
if (!normalized_domain.has_value())
|
||||
return false;
|
||||
|
||||
return is_matching_public_suffix_impl(normalized_domain->host);
|
||||
}
|
||||
|
||||
static Optional<String> find_matching_public_suffix_impl(StringView host)
|
||||
{
|
||||
auto remaining_host = host;
|
||||
while (!remaining_host.is_empty()) {
|
||||
if (is_matching_public_suffix_impl(remaining_host))
|
||||
return MUST(String::from_utf8(remaining_host));
|
||||
|
||||
auto next_label_separator = remaining_host.find('.');
|
||||
if (!next_label_separator.has_value())
|
||||
return OptionalNone {};
|
||||
|
||||
remaining_host = remaining_host.substring_view(*next_label_separator + 1);
|
||||
}
|
||||
|
||||
return OptionalNone {};
|
||||
}
|
||||
|
||||
Optional<String> PublicSuffixData::find_matching_public_suffix(StringView string)
|
||||
{
|
||||
if (string.is_empty())
|
||||
return {};
|
||||
|
||||
auto parsed_host = Parser::parse_host(string);
|
||||
if (!parsed_host.has_value())
|
||||
return {};
|
||||
|
||||
return find_matching_public_suffix(*parsed_host);
|
||||
}
|
||||
|
||||
Optional<String> PublicSuffixData::find_matching_public_suffix(Host const& host)
|
||||
{
|
||||
auto normalized_domain = normalized_domain_for_host(host);
|
||||
if (!normalized_domain.has_value())
|
||||
return {};
|
||||
|
||||
auto public_suffix = find_matching_public_suffix_impl(normalized_domain->host);
|
||||
if (!public_suffix.has_value())
|
||||
return {};
|
||||
|
||||
return MUST(String::formatted("{}{}", public_suffix.value(), normalized_domain->trailing_dot));
|
||||
}
|
||||
|
||||
static Optional<String> find_matching_registrable_domain_impl(StringView host)
|
||||
{
|
||||
// find_matching_public_suffix_impl() always returns a tail of host, so it is by construction a suffix of it.
|
||||
auto public_suffix = find_matching_public_suffix_impl(host);
|
||||
if (!public_suffix.has_value() || host == *public_suffix)
|
||||
return {};
|
||||
|
||||
auto subhost = host.substring_view(0, host.length() - public_suffix->bytes_as_string_view().length());
|
||||
subhost = subhost.trim("."sv, TrimMode::Right);
|
||||
|
||||
if (subhost.is_empty())
|
||||
return {};
|
||||
|
||||
size_t start_index = 0;
|
||||
if (auto index = subhost.find_last('.'); index.has_value())
|
||||
start_index = *index + 1;
|
||||
|
||||
return MUST(String::from_utf8(host.substring_view(start_index)));
|
||||
}
|
||||
|
||||
Optional<String> PublicSuffixData::find_matching_registrable_domain(StringView string)
|
||||
{
|
||||
if (string.is_empty())
|
||||
return {};
|
||||
|
||||
auto parsed_host = Parser::parse_host(string);
|
||||
if (!parsed_host.has_value())
|
||||
return {};
|
||||
|
||||
return find_matching_registrable_domain(*parsed_host);
|
||||
}
|
||||
|
||||
Optional<String> PublicSuffixData::find_matching_registrable_domain(Host const& host)
|
||||
{
|
||||
auto normalized_domain = normalized_domain_for_host(host);
|
||||
if (!normalized_domain.has_value())
|
||||
return {};
|
||||
|
||||
auto registrable_domain = find_matching_registrable_domain_impl(normalized_domain->host);
|
||||
if (!registrable_domain.has_value())
|
||||
return {};
|
||||
|
||||
return MUST(String::formatted("{}{}", registrable_domain.value(), normalized_domain->trailing_dot));
|
||||
}
|
||||
|
||||
}
|
||||
25
Libraries/LibURL/PublicSuffixData.h
Normal file
25
Libraries/LibURL/PublicSuffixData.h
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright (c) 2026-present, the Ladybird developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Forward.h>
|
||||
#include <AK/Optional.h>
|
||||
#include <LibURL/Forward.h>
|
||||
|
||||
namespace URL {
|
||||
|
||||
class PublicSuffixData {
|
||||
public:
|
||||
static bool is_matching_public_suffix(StringView host);
|
||||
static bool is_matching_public_suffix(Host const& host);
|
||||
static Optional<String> find_matching_public_suffix(StringView string);
|
||||
static Optional<String> find_matching_public_suffix(Host const& host);
|
||||
static Optional<String> find_matching_registrable_domain(StringView string);
|
||||
static Optional<String> find_matching_registrable_domain(Host const& host);
|
||||
};
|
||||
|
||||
}
|
||||
|
|
@ -199,7 +199,7 @@ void CookieJar::set_cookie(URL::URL const& url, HTTP::Cookie::ParsedCookie const
|
|||
return;
|
||||
|
||||
// 9. If the user agent is configured to reject "public suffixes" and the domain-attribute is a public suffix:
|
||||
if (URL::PublicSuffixData::the()->is_matching_public_suffix(domain_attribute)) {
|
||||
if (URL::PublicSuffixData::is_matching_public_suffix(domain_attribute)) {
|
||||
// 1. Let request-host-canonical be the canonicalized request-host.
|
||||
// 2. If request-host fails to be canonicalized then abort this algorithm and ignore the cookie entirely.
|
||||
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ Optional<URL::URL> sanitize_url(StringView location, Optional<SearchEngine> cons
|
|||
if (any_of(RESERVED_TLDS, [&](StringView const& tld) { return domain.byte_count() > tld.length() && domain.ends_with_bytes(tld); }))
|
||||
return url;
|
||||
|
||||
auto public_suffix = URL::PublicSuffixData::the()->find_matching_public_suffix(domain);
|
||||
auto public_suffix = URL::PublicSuffixData::find_matching_public_suffix(domain);
|
||||
if (!public_suffix.has_value() || *public_suffix == domain) {
|
||||
if (append_tld == AppendTLD::Yes)
|
||||
url->set_host(MUST(String::formatted("{}.com", domain)));
|
||||
|
|
@ -248,7 +248,7 @@ static URLParts break_web_url_into_parts(URL::URL const& url, StringView url_str
|
|||
domain = url_without_scheme;
|
||||
}
|
||||
|
||||
auto public_suffix = URL::PublicSuffixData::the()->find_matching_public_suffix(domain);
|
||||
auto public_suffix = URL::PublicSuffixData::find_matching_public_suffix(domain);
|
||||
if (!public_suffix.has_value() || !domain.ends_with(*public_suffix))
|
||||
return { scheme, domain, remainder };
|
||||
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ find_package(SQLite3 REQUIRED)
|
|||
find_package(Threads REQUIRED)
|
||||
find_package(ZLIB REQUIRED)
|
||||
|
||||
pkg_check_modules(LIBPSL REQUIRED IMPORTED_TARGET libpsl)
|
||||
pkg_check_modules(libtommath REQUIRED IMPORTED_TARGET libtommath)
|
||||
|
||||
find_package(unofficial-angle CONFIG)
|
||||
|
|
|
|||
|
|
@ -237,6 +237,32 @@
|
|||
"/share/icu"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "libpsl",
|
||||
"buildsystem": "meson",
|
||||
"sources": [
|
||||
{
|
||||
"type": "git",
|
||||
"url": "https://github.com/rockdaboot/libpsl.git",
|
||||
"tag": "0.21.5"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"url": "https://raw.githubusercontent.com/publicsuffix/list/303b81a2140a3192d336f22af389a28daf6efab6/public_suffix_list.dat",
|
||||
"sha256": "82d4d57e397177be3a627df27f88dc0ab04f54d081bc94c68f82c3e100855e0b",
|
||||
"dest": "list/"
|
||||
}
|
||||
],
|
||||
"config-opts": [
|
||||
"-Ddocs=false",
|
||||
"-Druntime=libicu",
|
||||
"-Dtests=false",
|
||||
"--libdir=/app/lib"
|
||||
],
|
||||
"cleanup": [
|
||||
"/bin"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "libxml2",
|
||||
"buildsystem": "cmake-ninja",
|
||||
|
|
@ -727,12 +753,6 @@
|
|||
"name": "Ladybird",
|
||||
"buildsystem": "cmake-ninja",
|
||||
"sources": [
|
||||
{
|
||||
"type": "file",
|
||||
"url": "https://raw.githubusercontent.com/publicsuffix/list/32c68ce9d52e12df5b914b2b248cb893147301f7/public_suffix_list.dat",
|
||||
"dest": "Caches/PublicSuffix/",
|
||||
"sha256": "e79e372bcc6fcdb51f7a31e3c0c504530838432669af2ac544d2491de0a86030"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"url": "https://raw.githubusercontent.com/chromium/chromium/aa04f175415addb04bb78936b0d8b973fbd8ea61/net/http/transport_security_state_static.json",
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
include(${CMAKE_CURRENT_LIST_DIR}/utils.cmake)
|
||||
|
||||
set(PUBLIC_SUFFIX_PATH "${LADYBIRD_CACHE_DIR}/PublicSuffix" CACHE PATH "Download location for PublicSuffix files")
|
||||
set(PUBLIC_SUFFIX_DATA_URL "https://raw.githubusercontent.com/publicsuffix/list/master/public_suffix_list.dat")
|
||||
set(PUBLIC_SUFFIX_DATA_PATH "${PUBLIC_SUFFIX_PATH}/public_suffix_list.dat")
|
||||
set(PUBLIC_SUFFIX_DATA_HEADER PublicSuffixData.h)
|
||||
set(PUBLIC_SUFFIX_DATA_IMPLEMENTATION PublicSuffixData.cpp)
|
||||
if (ENABLE_NETWORK_DOWNLOADS)
|
||||
download_file("${PUBLIC_SUFFIX_DATA_URL}" "${PUBLIC_SUFFIX_DATA_PATH}")
|
||||
else()
|
||||
message(STATUS "Skipping download of ${PUBLIC_SUFFIX_DATA_URL}, expecting it to be in ${PUBLIC_SUFFIX_DATA_PATH}")
|
||||
endif()
|
||||
invoke_py_generator(
|
||||
"PublicSuffixData"
|
||||
"generate_public_suffix_data.py"
|
||||
"${PUBLIC_SUFFIX_PATH}/"
|
||||
"${PUBLIC_SUFFIX_DATA_HEADER}"
|
||||
"${PUBLIC_SUFFIX_DATA_IMPLEMENTATION}"
|
||||
arguments -p "${PUBLIC_SUFFIX_DATA_PATH}"
|
||||
)
|
||||
set(PUBLIC_SUFFIX_SOURCES
|
||||
${PUBLIC_SUFFIX_DATA_HEADER}
|
||||
${PUBLIC_SUFFIX_DATA_IMPLEMENTATION}
|
||||
)
|
||||
|
|
@ -1,322 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright (c) 2023, Cameron Youell <cameronyouell@gmail.com>
|
||||
# Copyright (c) 2025, ayeteadoe <ayeteadoe@gmail.com>
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
import argparse
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def generate_header_file(output_path: Path) -> None:
|
||||
content = """#pragma once
|
||||
|
||||
#include <AK/Forward.h>
|
||||
#include <AK/Trie.h>
|
||||
#include <AK/Variant.h>
|
||||
#include <LibURL/Forward.h>
|
||||
|
||||
namespace URL {
|
||||
|
||||
class PublicSuffixData {
|
||||
protected:
|
||||
PublicSuffixData();
|
||||
|
||||
public:
|
||||
PublicSuffixData(PublicSuffixData const&) = delete;
|
||||
PublicSuffixData& operator=(PublicSuffixData const&) = delete;
|
||||
|
||||
static PublicSuffixData* the()
|
||||
{
|
||||
static PublicSuffixData* s_the;
|
||||
if (!s_the)
|
||||
s_the = new PublicSuffixData;
|
||||
return s_the;
|
||||
}
|
||||
|
||||
bool is_matching_public_suffix(StringView host);
|
||||
bool is_matching_public_suffix(Host const& host);
|
||||
Optional<String> find_matching_public_suffix(StringView string);
|
||||
Optional<String> find_matching_public_suffix(Host const& host);
|
||||
Optional<String> find_matching_registrable_domain(StringView string);
|
||||
Optional<String> find_matching_registrable_domain(Host const& host);
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
with open(output_path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
|
||||
def canonicalize_public_suffix_rule(rule: str) -> str:
|
||||
def canonicalize_label(label: str) -> str:
|
||||
if label == "*":
|
||||
return label
|
||||
|
||||
prefix = ""
|
||||
if label.startswith("!"):
|
||||
prefix = "!"
|
||||
label = label[1:]
|
||||
|
||||
return prefix + label.encode("idna").decode("ascii").lower()
|
||||
|
||||
return ".".join(canonicalize_label(label) for label in rule.split("."))
|
||||
|
||||
|
||||
def generate_implementation_file(input_path: Path, output_path: Path) -> None:
|
||||
content = """#include <AK/String.h>
|
||||
#include <AK/BinarySearch.h>
|
||||
#include <AK/Vector.h>
|
||||
#include <LibURL/Host.h>
|
||||
#include <LibURL/Parser.h>
|
||||
#include <LibURL/PublicSuffixData.h>
|
||||
|
||||
namespace URL {
|
||||
|
||||
static constexpr auto s_public_suffixes = Array {"""
|
||||
|
||||
reversed_lines = []
|
||||
with open(input_path, "r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
|
||||
if line.startswith("//") or not line:
|
||||
continue
|
||||
|
||||
line = canonicalize_public_suffix_rule(line)
|
||||
reversed_line = ".".join(line.split(".")[::-1])
|
||||
reversed_lines.append(reversed_line)
|
||||
|
||||
reversed_lines.sort()
|
||||
|
||||
for item in reversed_lines:
|
||||
content += f'\n "{item}"sv,'
|
||||
|
||||
content += """
|
||||
};
|
||||
|
||||
PublicSuffixData::PublicSuffixData()
|
||||
{
|
||||
}
|
||||
|
||||
static bool is_reversed_public_suffix(StringView reversed_host)
|
||||
{
|
||||
return binary_search(s_public_suffixes, reversed_host);
|
||||
}
|
||||
|
||||
static Optional<StringView> serialized_domain(Host const& host)
|
||||
{
|
||||
if (!host.is_domain())
|
||||
return OptionalNone {};
|
||||
|
||||
return host.get<String>().bytes_as_string_view();
|
||||
}
|
||||
|
||||
struct NormalizedDomain {
|
||||
StringView host;
|
||||
StringView trailing_dot;
|
||||
};
|
||||
|
||||
static Optional<NormalizedDomain> normalize_domain_for_matching(StringView host)
|
||||
{
|
||||
host = host.trim("."sv, TrimMode::Left);
|
||||
if (host.is_empty())
|
||||
return OptionalNone {};
|
||||
|
||||
auto trailing_dot = ""sv;
|
||||
if (host.ends_with('.')) {
|
||||
trailing_dot = "."sv;
|
||||
host = host.substring_view(0, host.length() - 1);
|
||||
if (host.is_empty())
|
||||
return OptionalNone {};
|
||||
}
|
||||
|
||||
return NormalizedDomain { host, trailing_dot };
|
||||
}
|
||||
|
||||
static Optional<NormalizedDomain> normalized_domain_for_host(Host const& host)
|
||||
{
|
||||
auto domain = serialized_domain(host);
|
||||
if (!domain.has_value())
|
||||
return OptionalNone {};
|
||||
|
||||
return normalize_domain_for_matching(*domain);
|
||||
}
|
||||
|
||||
static bool is_matching_public_suffix_impl(StringView host)
|
||||
{
|
||||
// Empty labels are kept so that inputs such as "com." do not match the bare "com" entry.
|
||||
auto labels = host.split_view('.', SplitBehavior::KeepEmpty);
|
||||
labels.reverse();
|
||||
|
||||
StringBuilder reversed_host;
|
||||
reversed_host.join('.', labels);
|
||||
|
||||
return is_reversed_public_suffix(reversed_host.string_view());
|
||||
}
|
||||
|
||||
bool PublicSuffixData::is_matching_public_suffix(StringView host)
|
||||
{
|
||||
if (host.is_empty())
|
||||
return false;
|
||||
|
||||
auto parsed_host = Parser::parse_host(host);
|
||||
if (!parsed_host.has_value())
|
||||
return false;
|
||||
|
||||
return is_matching_public_suffix(*parsed_host);
|
||||
}
|
||||
|
||||
bool PublicSuffixData::is_matching_public_suffix(Host const& host)
|
||||
{
|
||||
auto normalized_domain = normalized_domain_for_host(host);
|
||||
if (!normalized_domain.has_value())
|
||||
return false;
|
||||
|
||||
return is_matching_public_suffix_impl(normalized_domain->host);
|
||||
}
|
||||
|
||||
static Optional<String> find_matching_public_suffix_impl(StringView host)
|
||||
{
|
||||
auto input = host.split_view('.');
|
||||
input.reverse();
|
||||
|
||||
StringBuilder overall_search_string;
|
||||
StringBuilder search_string;
|
||||
for (auto part : input) {
|
||||
search_string.clear();
|
||||
search_string.append(overall_search_string.string_view());
|
||||
search_string.append(part);
|
||||
|
||||
if (is_reversed_public_suffix(search_string.string_view())) {
|
||||
overall_search_string.append(part);
|
||||
overall_search_string.append('.');
|
||||
continue;
|
||||
}
|
||||
|
||||
search_string.clear();
|
||||
search_string.append(overall_search_string.string_view());
|
||||
search_string.append('.');
|
||||
|
||||
if (is_reversed_public_suffix(search_string.string_view())) {
|
||||
overall_search_string.append(part);
|
||||
overall_search_string.append('.');
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
auto view = overall_search_string.string_view().split_view('.');
|
||||
view.reverse();
|
||||
|
||||
StringBuilder return_string_builder;
|
||||
return_string_builder.join('.', view);
|
||||
if (return_string_builder.is_empty())
|
||||
return Optional<String> {};
|
||||
return MUST(return_string_builder.to_string());
|
||||
}
|
||||
|
||||
Optional<String> PublicSuffixData::find_matching_public_suffix(StringView string)
|
||||
{
|
||||
if (string.is_empty())
|
||||
return {};
|
||||
|
||||
auto parsed_host = Parser::parse_host(string);
|
||||
if (!parsed_host.has_value())
|
||||
return {};
|
||||
|
||||
return find_matching_public_suffix(*parsed_host);
|
||||
}
|
||||
|
||||
Optional<String> PublicSuffixData::find_matching_public_suffix(Host const& host)
|
||||
{
|
||||
auto normalized_domain = normalized_domain_for_host(host);
|
||||
if (!normalized_domain.has_value())
|
||||
return {};
|
||||
|
||||
auto public_suffix = find_matching_public_suffix_impl(normalized_domain->host);
|
||||
if (!public_suffix.has_value())
|
||||
return {};
|
||||
|
||||
return MUST(String::formatted("{}{}", public_suffix.value(), normalized_domain->trailing_dot));
|
||||
}
|
||||
|
||||
// https://github.com/publicsuffix/list/wiki/Format#algorithm
|
||||
static Optional<String> find_matching_registrable_domain_impl(StringView host)
|
||||
{
|
||||
// The registered or registrable domain is the public suffix plus one additional label.
|
||||
auto public_suffix = find_matching_public_suffix_impl(host);
|
||||
if (!public_suffix.has_value() || !host.ends_with(*public_suffix))
|
||||
return {};
|
||||
|
||||
if (host == *public_suffix)
|
||||
return {};
|
||||
|
||||
auto subhost = host.substring_view(0, host.length() - public_suffix->bytes_as_string_view().length());
|
||||
subhost = subhost.trim("."sv, TrimMode::Right);
|
||||
|
||||
if (subhost.is_empty())
|
||||
return {};
|
||||
|
||||
size_t start_index = 0;
|
||||
if (auto index = subhost.find_last('.'); index.has_value())
|
||||
start_index = *index + 1;
|
||||
|
||||
return MUST(String::from_utf8(host.substring_view(start_index)));
|
||||
}
|
||||
|
||||
Optional<String> PublicSuffixData::find_matching_registrable_domain(StringView string)
|
||||
{
|
||||
if (string.is_empty())
|
||||
return {};
|
||||
|
||||
auto parsed_host = Parser::parse_host(string);
|
||||
if (!parsed_host.has_value())
|
||||
return {};
|
||||
|
||||
return find_matching_registrable_domain(*parsed_host);
|
||||
}
|
||||
|
||||
Optional<String> PublicSuffixData::find_matching_registrable_domain(Host const& host)
|
||||
{
|
||||
auto normalized_domain = normalized_domain_for_host(host);
|
||||
if (!normalized_domain.has_value())
|
||||
return {};
|
||||
|
||||
auto registrable_domain = find_matching_registrable_domain_impl(normalized_domain->host);
|
||||
if (!registrable_domain.has_value())
|
||||
return {};
|
||||
|
||||
return MUST(String::formatted("{}{}", registrable_domain.value(), normalized_domain->trailing_dot));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
with open(output_path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Generate public suffix data files", add_help=False)
|
||||
parser.add_argument("--help", action="help", help="Show this help message and exit")
|
||||
parser.add_argument("-h", "--generated-header-path", required=True, help="Path to the header file to generate")
|
||||
parser.add_argument(
|
||||
"-c", "--generated-implementation-path", required=True, help="Path to the implementation file to generate"
|
||||
)
|
||||
parser.add_argument("-p", "--public-suffix-list-path", required=True, help="Path to the public suffix list")
|
||||
args = parser.parse_args()
|
||||
|
||||
generate_header_file(Path(args.generated_header_path))
|
||||
generate_implementation_file(Path(args.public_suffix_list_path), Path(args.generated_implementation_path))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -49,18 +49,16 @@ TEST_CASE(public_suffix_matching_for_psl_rules)
|
|||
{ "www.xn--55qx5d.cn"sv, false, "xn--55qx5d.cn"sv, "www.xn--55qx5d.cn"sv },
|
||||
};
|
||||
|
||||
auto* public_suffix_data = URL::PublicSuffixData::the();
|
||||
|
||||
for (auto const& test_case : test_cases) {
|
||||
EXPECT_EQ(public_suffix_data->is_matching_public_suffix(test_case.input), test_case.is_public_suffix);
|
||||
EXPECT_EQ(public_suffix_data->find_matching_public_suffix(test_case.input), test_case.public_suffix);
|
||||
EXPECT_EQ(public_suffix_data->find_matching_registrable_domain(test_case.input), test_case.registrable_domain);
|
||||
EXPECT_EQ(URL::PublicSuffixData::is_matching_public_suffix(test_case.input), test_case.is_public_suffix);
|
||||
EXPECT_EQ(URL::PublicSuffixData::find_matching_public_suffix(test_case.input), test_case.public_suffix);
|
||||
EXPECT_EQ(URL::PublicSuffixData::find_matching_registrable_domain(test_case.input), test_case.registrable_domain);
|
||||
|
||||
auto host = URL::Parser::parse_host(test_case.input);
|
||||
VERIFY(host.has_value());
|
||||
EXPECT_EQ(public_suffix_data->is_matching_public_suffix(*host), test_case.is_public_suffix);
|
||||
EXPECT_EQ(public_suffix_data->find_matching_public_suffix(*host), test_case.public_suffix);
|
||||
EXPECT_EQ(public_suffix_data->find_matching_registrable_domain(*host), test_case.registrable_domain);
|
||||
EXPECT_EQ(URL::PublicSuffixData::is_matching_public_suffix(*host), test_case.is_public_suffix);
|
||||
EXPECT_EQ(URL::PublicSuffixData::find_matching_public_suffix(*host), test_case.public_suffix);
|
||||
EXPECT_EQ(URL::PublicSuffixData::find_matching_registrable_domain(*host), test_case.registrable_domain);
|
||||
EXPECT_EQ(host->public_suffix(), test_case.public_suffix);
|
||||
EXPECT_EQ(host->registrable_domain(), test_case.registrable_domain);
|
||||
}
|
||||
|
|
@ -90,18 +88,16 @@ TEST_CASE(public_suffix_matching_without_psl_rule)
|
|||
{ "sub.example.إختبار"sv, "xn--kgbechtv"sv, "example.xn--kgbechtv"sv },
|
||||
};
|
||||
|
||||
auto* public_suffix_data = URL::PublicSuffixData::the();
|
||||
|
||||
for (auto const& test_case : test_cases) {
|
||||
EXPECT(!public_suffix_data->is_matching_public_suffix(test_case.input));
|
||||
EXPECT_EQ(public_suffix_data->find_matching_public_suffix(test_case.input), OptionalNone {});
|
||||
EXPECT_EQ(public_suffix_data->find_matching_registrable_domain(test_case.input), OptionalNone {});
|
||||
EXPECT(!URL::PublicSuffixData::is_matching_public_suffix(test_case.input));
|
||||
EXPECT_EQ(URL::PublicSuffixData::find_matching_public_suffix(test_case.input), OptionalNone {});
|
||||
EXPECT_EQ(URL::PublicSuffixData::find_matching_registrable_domain(test_case.input), OptionalNone {});
|
||||
|
||||
auto host = URL::Parser::parse_host(test_case.input);
|
||||
VERIFY(host.has_value());
|
||||
EXPECT(!public_suffix_data->is_matching_public_suffix(*host));
|
||||
EXPECT_EQ(public_suffix_data->find_matching_public_suffix(*host), OptionalNone {});
|
||||
EXPECT_EQ(public_suffix_data->find_matching_registrable_domain(*host), OptionalNone {});
|
||||
EXPECT(!URL::PublicSuffixData::is_matching_public_suffix(*host));
|
||||
EXPECT_EQ(URL::PublicSuffixData::find_matching_public_suffix(*host), OptionalNone {});
|
||||
EXPECT_EQ(URL::PublicSuffixData::find_matching_registrable_domain(*host), OptionalNone {});
|
||||
EXPECT_EQ(host->public_suffix(), test_case.host_public_suffix);
|
||||
EXPECT_EQ(host->registrable_domain(), test_case.host_registrable_domain);
|
||||
}
|
||||
|
|
@ -119,14 +115,12 @@ TEST_CASE(invalid_hosts)
|
|||
};
|
||||
|
||||
// Above inputs are not valid hosts, so should not be able to be parsed or matched in the PSL.
|
||||
auto* public_suffix_data = URL::PublicSuffixData::the();
|
||||
|
||||
for (auto const& input : raw_invalid_inputs) {
|
||||
auto host = URL::Parser::parse_host(input);
|
||||
EXPECT(!host.has_value());
|
||||
EXPECT(!public_suffix_data->is_matching_public_suffix(input));
|
||||
EXPECT_EQ(public_suffix_data->find_matching_public_suffix(input), OptionalNone {});
|
||||
EXPECT_EQ(public_suffix_data->find_matching_registrable_domain(input), OptionalNone {});
|
||||
EXPECT(!URL::PublicSuffixData::is_matching_public_suffix(input));
|
||||
EXPECT_EQ(URL::PublicSuffixData::find_matching_public_suffix(input), OptionalNone {});
|
||||
EXPECT_EQ(URL::PublicSuffixData::find_matching_registrable_domain(input), OptionalNone {});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -137,18 +131,16 @@ TEST_CASE(public_suffix_matching_for_ip_addresses)
|
|||
"[2001:0db8:85a3:0000:0000:8a2e:0370:7334]"sv,
|
||||
};
|
||||
|
||||
auto* public_suffix_data = URL::PublicSuffixData::the();
|
||||
|
||||
for (auto const& input : test_cases) {
|
||||
EXPECT(!public_suffix_data->is_matching_public_suffix(input));
|
||||
EXPECT_EQ(public_suffix_data->find_matching_public_suffix(input), OptionalNone {});
|
||||
EXPECT_EQ(public_suffix_data->find_matching_registrable_domain(input), OptionalNone {});
|
||||
EXPECT(!URL::PublicSuffixData::is_matching_public_suffix(input));
|
||||
EXPECT_EQ(URL::PublicSuffixData::find_matching_public_suffix(input), OptionalNone {});
|
||||
EXPECT_EQ(URL::PublicSuffixData::find_matching_registrable_domain(input), OptionalNone {});
|
||||
|
||||
auto host = URL::Parser::parse_host(input);
|
||||
VERIFY(host.has_value());
|
||||
EXPECT(!public_suffix_data->is_matching_public_suffix(*host));
|
||||
EXPECT_EQ(public_suffix_data->find_matching_public_suffix(*host), OptionalNone {});
|
||||
EXPECT_EQ(public_suffix_data->find_matching_registrable_domain(*host), OptionalNone {});
|
||||
EXPECT(!URL::PublicSuffixData::is_matching_public_suffix(*host));
|
||||
EXPECT_EQ(URL::PublicSuffixData::find_matching_public_suffix(*host), OptionalNone {});
|
||||
EXPECT_EQ(URL::PublicSuffixData::find_matching_registrable_domain(*host), OptionalNone {});
|
||||
EXPECT_EQ(host->public_suffix(), OptionalNone {});
|
||||
EXPECT_EQ(host->registrable_domain(), OptionalNone {});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@
|
|||
"name": "libproxy",
|
||||
"platform": "!(android | bsd)"
|
||||
},
|
||||
"libpsl",
|
||||
{
|
||||
"name": "libedit",
|
||||
"platform": "!windows & !android"
|
||||
|
|
@ -349,6 +350,10 @@
|
|||
"name": "libproxy",
|
||||
"version": "0.4.18#3"
|
||||
},
|
||||
{
|
||||
"name": "libpsl",
|
||||
"version": "0.21.5#1"
|
||||
},
|
||||
{
|
||||
"name": "libtommath",
|
||||
"version": "1.3.0#2"
|
||||
|
|
|
|||
Loading…
Reference in a new issue