ladybird/Libraries/LibURL/PublicSuffixData.cpp
Shannon Booth 3f7b31fc78 LibURL: Let Host use PublicSuffixData star rule matching
Ever since PublicSuffixData was created, it was using "no star rule"
matching, which is what is needed for the address bar to distinguish
between a domain and a search. URL::Host on the other hand requires
the fallback star rule. Which rule is needed depends on the use case
of the PSL. Support both use cases by a flag in PublicSuffixData.
2026-06-16 06:14:07 +02:00

170 lines
5.3 KiB
C++

/*
* 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(PublicSuffixData::IncludeStarRule include_star_rule)
{
if (include_star_rule == PublicSuffixData::IncludeStarRule::Yes)
return PSL_TYPE_ANY;
return 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, PublicSuffixData::IncludeStarRule include_star_rule)
{
ByteString lookup_host { host };
return psl_is_public_suffix2(public_suffix_context(), lookup_host.characters(), public_suffix_match_types(include_star_rule));
}
bool PublicSuffixData::is_matching_public_suffix(StringView host, IncludeStarRule include_star_rule)
{
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, include_star_rule);
}
bool PublicSuffixData::is_matching_public_suffix(Host const& host, IncludeStarRule include_star_rule)
{
auto normalized_domain = normalized_domain_for_host(host);
if (!normalized_domain.has_value())
return false;
return is_matching_public_suffix_impl(normalized_domain->host, include_star_rule);
}
static Optional<String> find_matching_public_suffix_impl(StringView host, PublicSuffixData::IncludeStarRule include_star_rule)
{
auto remaining_host = host;
while (!remaining_host.is_empty()) {
if (is_matching_public_suffix_impl(remaining_host, include_star_rule))
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, IncludeStarRule include_star_rule)
{
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, include_star_rule);
}
Optional<String> PublicSuffixData::find_matching_public_suffix(Host const& host, IncludeStarRule include_star_rule)
{
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, include_star_rule);
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, PublicSuffixData::IncludeStarRule include_star_rule)
{
// 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, include_star_rule);
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, IncludeStarRule include_star_rule)
{
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, include_star_rule);
}
Optional<String> PublicSuffixData::find_matching_registrable_domain(Host const& host, IncludeStarRule include_star_rule)
{
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, include_star_rule);
if (!registrable_domain.has_value())
return {};
return MUST(String::formatted("{}{}", registrable_domain.value(), normalized_domain->trailing_dot));
}
}