blob: f64c8751fe5e6b78900119879563af6760ba7598 [file]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/subresource_filter/core/common/style_rule_matcher.h"
#include <memory>
#include <string_view>
#include <vector>
#include "components/subresource_filter/core/common/indexed_ruleset.h"
#include "components/subresource_filter/core/common/style_rule_indexer.h"
#include "components/subresource_filter/core/common/test_ruleset_utils.h"
#include "components/url_pattern_index/proto/rules.pb.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/flatbuffers/src/include/flatbuffers/flatbuffers.h"
#include "url/gurl.h"
#include "url/origin.h"
namespace subresource_filter {
using testing::StyleRuleParams;
class StyleRuleMatcherTest : public ::testing::Test {
protected:
StyleRuleMatcherTest() : indexer_(&builder_) {}
bool AddStyleRule(const testing::StyleRuleParams& params) {
return indexer_.AddStyleRuleFromProto(testing::CreateStyleRule(params));
}
void Finish() {
auto offset = indexer_.Finish();
builder_.Finish(offset);
const flat::StyleRuleIndex* index =
flatbuffers::GetRoot<flat::StyleRuleIndex>(builder_.GetBufferPointer());
matcher_ = std::make_unique<StyleRuleMatcher>(index);
}
flatbuffers::FlatBufferBuilder builder_;
StyleRuleIndexer indexer_;
std::unique_ptr<StyleRuleMatcher> matcher_;
};
TEST_F(StyleRuleMatcherTest, StyleRules) {
AddStyleRule(StyleRuleParams().SetSelector("#ad-1").SetIds({"ad-1"}));
AddStyleRule(StyleRuleParams()
.SetSelector("#ad-2")
.SetDomains({"example.com"})
.SetIds({"ad-2"}));
AddStyleRule(StyleRuleParams()
.SetSelector("#ad-4")
.SetDomains({"sub.example.com", "other.com"})
.SetIds({"ad-4"}));
Finish();
std::vector<std::string_view> rules;
matcher_->GetDomainSelectors(
url::Origin::Create(GURL("http://non-example.com")), rules);
EXPECT_TRUE(rules.empty());
rules.clear();
matcher_->GetDomainSelectors(url::Origin::Create(GURL("http://example.com")),
rules);
EXPECT_EQ(1u, rules.size());
EXPECT_EQ("#ad-2", rules[0]);
rules.clear();
matcher_->GetDomainSelectors(
url::Origin::Create(GURL("http://sub.example.com")), rules);
EXPECT_EQ(2u, rules.size());
// Order depends on domain map sorting.
EXPECT_EQ("#ad-4", rules[0]);
EXPECT_EQ("#ad-2", rules[1]);
rules.clear();
matcher_->GetDomainSelectors(url::Origin::Create(GURL("http://other.com")),
rules);
EXPECT_EQ(1u, rules.size());
EXPECT_EQ("#ad-4", rules[0]);
}
TEST_F(StyleRuleMatcherTest, ClassMatching) {
AddStyleRule(
StyleRuleParams().SetSelector(".class-1").SetClasses({"class-1"}));
AddStyleRule(
StyleRuleParams().SetSelector(".class-2").SetClasses({"class-2"}));
Finish();
std::vector<std::string_view> rules;
// Lookup by class "class-1"
matcher_->GetSelectorsByClass(url::Origin(), "class-1",
GetStyleRuleHash("class-1"), rules);
ASSERT_EQ(1u, rules.size());
EXPECT_EQ(".class-1", rules[0]);
// Lookup by class "class-3" (non-existent)
rules.clear();
matcher_->GetSelectorsByClass(url::Origin(), "class-3",
GetStyleRuleHash("class-3"), rules);
EXPECT_TRUE(rules.empty());
}
TEST_F(StyleRuleMatcherTest, IdMatching) {
AddStyleRule(StyleRuleParams().SetSelector("#id-1").SetIds({"id-1"}));
AddStyleRule(StyleRuleParams().SetSelector("#id-2").SetIds({"id-2"}));
Finish();
std::vector<std::string_view> rules;
// Lookup by ID "id-1"
matcher_->GetSelectorsById(url::Origin(), "id-1", GetStyleRuleHash("id-1"),
rules);
ASSERT_EQ(1u, rules.size());
EXPECT_EQ("#id-1", rules[0]);
// Lookup by ID "id-3" (non-existent)
rules.clear();
matcher_->GetSelectorsById(url::Origin(), "id-3", GetStyleRuleHash("id-3"),
rules);
EXPECT_TRUE(rules.empty());
}
TEST_F(StyleRuleMatcherTest, Exclusions) {
AddStyleRule(StyleRuleParams()
.SetSelector("#ad-1")
.SetDomains({"example.com"})
.SetExclusion(true)
.SetIds({"ad-1"}));
AddStyleRule(StyleRuleParams().SetSelector("#ad-1").SetIds({"ad-1"}));
AddStyleRule(StyleRuleParams()
.SetSelector(".class-1")
.SetDomains({"example.com"})
.SetExclusion(true)
.SetClasses({"class-1"}));
AddStyleRule(
StyleRuleParams().SetSelector(".class-1").SetClasses({"class-1"}));
Finish();
std::vector<std::string_view> rules;
url::Origin example_origin = url::Origin::Create(GURL("http://example.com"));
url::Origin other_origin =
url::Origin::Create(GURL("http://other-domain.com"));
// 1. ID Lookups
// On other-domain.com: rule is NOT excluded.
matcher_->GetSelectorsById(other_origin, "ad-1", GetStyleRuleHash("ad-1"),
rules);
ASSERT_EQ(1u, rules.size());
EXPECT_EQ("#ad-1", rules[0]);
// On example.com: rule is EXCLUDED.
rules.clear();
matcher_->GetSelectorsById(example_origin, "ad-1", GetStyleRuleHash("ad-1"),
rules);
EXPECT_TRUE(rules.empty());
// Repeat lookup on example.com (should hit cache).
rules.clear();
matcher_->GetSelectorsById(example_origin, "ad-1", GetStyleRuleHash("ad-1"),
rules);
EXPECT_TRUE(rules.empty());
// Repeat lookup on other-domain.com (should update cache).
rules.clear();
matcher_->GetSelectorsById(other_origin, "ad-1", GetStyleRuleHash("ad-1"),
rules);
ASSERT_EQ(1u, rules.size());
EXPECT_EQ("#ad-1", rules[0]);
// 2. Class Lookups
// On other-domain.com: rule is NOT excluded.
rules.clear();
matcher_->GetSelectorsByClass(other_origin, "class-1",
GetStyleRuleHash("class-1"), rules);
ASSERT_EQ(1u, rules.size());
EXPECT_EQ(".class-1", rules[0]);
// On example.com: rule is EXCLUDED.
rules.clear();
matcher_->GetSelectorsByClass(example_origin, "class-1",
GetStyleRuleHash("class-1"), rules);
EXPECT_TRUE(rules.empty());
// Repeat lookup on example.com (should hit cache).
rules.clear();
matcher_->GetSelectorsByClass(example_origin, "class-1",
GetStyleRuleHash("class-1"), rules);
EXPECT_TRUE(rules.empty());
// Repeat lookup on other-domain.com (should update cache).
rules.clear();
matcher_->GetSelectorsByClass(other_origin, "class-1",
GetStyleRuleHash("class-1"), rules);
ASSERT_EQ(1u, rules.size());
EXPECT_EQ(".class-1", rules[0]);
}
TEST_F(StyleRuleMatcherTest, CombinedClassAndId) {
AddStyleRule(StyleRuleParams()
.SetSelector(".class-1#id-1")
.SetClasses({"class-1"})
.SetIds({"id-1"}));
Finish();
std::vector<std::string_view> rules;
// Lookup by class
matcher_->GetSelectorsByClass(url::Origin(), "class-1",
GetStyleRuleHash("class-1"), rules);
ASSERT_EQ(1u, rules.size());
EXPECT_EQ(".class-1#id-1", rules[0]);
// Lookup by ID
rules.clear();
matcher_->GetSelectorsById(url::Origin(), "id-1", GetStyleRuleHash("id-1"),
rules);
ASSERT_EQ(1u, rules.size());
EXPECT_EQ(".class-1#id-1", rules[0]);
}
TEST_F(StyleRuleMatcherTest, OpaqueOriginWithoutPrecursor) {
AddStyleRule(StyleRuleParams()
.SetSelector("#ad-1")
.SetDomains({"example.com"})
.SetIds({"ad-1"}));
Finish();
std::vector<std::string_view> rules;
// Opaque origin without precursor should not match domain-specific selectors.
url::Origin opaque_origin;
EXPECT_TRUE(opaque_origin.opaque());
EXPECT_TRUE(opaque_origin.GetTupleOrPrecursorTupleIfOpaque().host().empty());
matcher_->GetDomainSelectors(opaque_origin, rules);
EXPECT_TRUE(rules.empty());
}
TEST_F(StyleRuleMatcherTest, OpaqueOriginWithPrecursor) {
AddStyleRule(StyleRuleParams()
.SetSelector("#ad-1")
.SetDomains({"example.com"})
.SetIds({"ad-1"}));
AddStyleRule(StyleRuleParams()
.SetSelector("#ad-2")
.SetDomains({"example.com"})
.SetExclusion(true)
.SetIds({"ad-2"}));
AddStyleRule(StyleRuleParams().SetSelector("#ad-2").SetIds({"ad-2"}));
Finish();
std::vector<std::string_view> rules;
// Opaque origin with precursor should match domain-specific selectors of
// precursor.
url::Origin base_origin = url::Origin::Create(GURL("http://example.com"));
url::Origin opaque_origin =
url::Origin::Resolve(GURL("data:text/html,foo"), base_origin);
EXPECT_TRUE(opaque_origin.opaque());
EXPECT_EQ("example.com",
opaque_origin.GetTupleOrPrecursorTupleIfOpaque().host());
matcher_->GetDomainSelectors(opaque_origin, rules);
ASSERT_EQ(1u, rules.size());
EXPECT_EQ("#ad-1", rules[0]);
// Test exclusions with opaque origin.
rules.clear();
matcher_->GetSelectorsById(opaque_origin, "ad-2", GetStyleRuleHash("ad-2"),
rules);
EXPECT_TRUE(rules.empty()); // Should be excluded on example.com (precursor)
// Test on another origin (opaque but different precursor).
url::Origin other_base = url::Origin::Create(GURL("http://other.com"));
url::Origin other_opaque =
url::Origin::Resolve(GURL("data:text/html,foo"), other_base);
rules.clear();
matcher_->GetSelectorsById(other_opaque, "ad-2", GetStyleRuleHash("ad-2"),
rules);
ASSERT_EQ(1u, rules.size());
EXPECT_EQ("#ad-2", rules[0]); // Should NOT be excluded on other.com
}
TEST_F(StyleRuleMatcherTest, BloomFilterShortCircuit) {
AddStyleRule(
StyleRuleParams().SetSelector(".class-1").SetClasses({"class-1"}));
Finish();
std::vector<std::string_view> rules;
// Lookup by class "class-1" with CORRECT hash should find it.
matcher_->GetSelectorsByClass(url::Origin(), "class-1",
GetStyleRuleHash("class-1"), rules);
ASSERT_EQ(1u, rules.size());
EXPECT_EQ(".class-1", rules[0]);
// Lookup by class "class-1" with INCORRECT hash should NOT find it
// (short-circuit). We use the hash of "non-existent" which is highly unlikely
// to be in the filter.
rules.clear();
matcher_->GetSelectorsByClass(url::Origin(), "class-1",
GetStyleRuleHash("non-existent"), rules);
EXPECT_TRUE(rules.empty());
}
TEST_F(StyleRuleMatcherTest, PreParsedAnchorIndexing) {
// Test that the indexer trusts the anchors provided in the proto.
AddStyleRule(StyleRuleParams().SetSelector("#fast-id").SetIds({"fast-id"}));
AddStyleRule(
StyleRuleParams().SetSelector(".fast-class").SetClasses({"fast-class"}));
AddStyleRule(StyleRuleParams()
.SetSelector(".parent > .child")
.SetClasses({"parent", "child"}));
AddStyleRule(StyleRuleParams()
.SetSelector(".fast-class-with-attr[attr=val]")
.SetClasses({"fast-class-with-attr"}));
AddStyleRule(StyleRuleParams()
.SetSelector("div[attr=site-specific]")
.SetDomains({"site.com"}));
Finish();
std::vector<std::string_view> rules;
matcher_->GetDomainSelectors(url::Origin::Create(GURL("http://example.com")),
rules);
// Global selectors are not in the domain index.
EXPECT_EQ(0u, rules.size());
// Verified lookup by ID.
rules.clear();
matcher_->GetSelectorsById(url::Origin(), "fast-id",
GetStyleRuleHash("fast-id"), rules);
ASSERT_EQ(1u, rules.size());
EXPECT_EQ("#fast-id", rules[0]);
// Verified lookup by Class.
rules.clear();
matcher_->GetSelectorsByClass(url::Origin(), "fast-class",
GetStyleRuleHash("fast-class"), rules);
ASSERT_EQ(1u, rules.size());
EXPECT_EQ(".fast-class", rules[0]);
rules.clear();
matcher_->GetSelectorsByClass(url::Origin(), "child",
GetStyleRuleHash("child"), rules);
ASSERT_EQ(1u, rules.size());
EXPECT_EQ(".parent > .child", rules[0]);
rules.clear();
matcher_->GetSelectorsByClass(url::Origin(), "parent",
GetStyleRuleHash("parent"), rules);
ASSERT_EQ(1u, rules.size());
EXPECT_EQ(".parent > .child", rules[0]);
// Verified site-specific selector is in the domain index.
rules.clear();
matcher_->GetDomainSelectors(url::Origin::Create(GURL("http://site.com")),
rules);
ASSERT_EQ(1u, rules.size());
EXPECT_EQ("div[attr=site-specific]", rules[0]);
}
TEST_F(StyleRuleMatcherTest, MultipleRulesAndKeys) {
AddStyleRule(StyleRuleParams().SetSelector("#id1").SetIds({"id1"}));
AddStyleRule(StyleRuleParams().SetSelector(".class1").SetClasses({"class1"}));
AddStyleRule(StyleRuleParams()
.SetSelector(".class1.class2")
.SetClasses({"class1", "class2"}));
Finish();
std::vector<std::string_view> rules;
// Lookup by class "class1"
rules.clear();
matcher_->GetSelectorsByClass(url::Origin(), "class1",
GetStyleRuleHash("class1"), rules);
ASSERT_EQ(2u, rules.size());
EXPECT_EQ(".class1", rules[0]);
EXPECT_EQ(".class1.class2", rules[1]);
// Lookup by class "class2"
rules.clear();
matcher_->GetSelectorsByClass(url::Origin(), "class2",
GetStyleRuleHash("class2"), rules);
ASSERT_EQ(1u, rules.size());
EXPECT_EQ(".class1.class2", rules[0]);
// Lookup by ID "id1"
rules.clear();
matcher_->GetSelectorsById(url::Origin(), "id1", GetStyleRuleHash("id1"),
rules);
ASSERT_EQ(1u, rules.size());
EXPECT_EQ("#id1", rules[0]);
}
TEST_F(StyleRuleMatcherTest, MaybeHasStyleRule) {
AddStyleRule(StyleRuleParams().SetSelector("#ad-1").SetIds({"ad-1"}));
AddStyleRule(
StyleRuleParams().SetSelector(".class-1").SetClasses({"class-1"}));
Finish();
// Hashes that exist should return true.
EXPECT_TRUE(matcher_->MaybeHasStyleRule(GetStyleRuleHash("ad-1")));
EXPECT_TRUE(matcher_->MaybeHasStyleRule(GetStyleRuleHash("class-1")));
// Hash that does not exist should return false.
EXPECT_FALSE(matcher_->MaybeHasStyleRule(GetStyleRuleHash("non-existent")));
}
// This test documents the existing behavior that suffix stripping applies to IP
// addresses as well as domains. It is not particularly important that it
// behaves this way, but this test ensures we are aware of it.
TEST_F(StyleRuleMatcherTest, IPAddressDomain) {
AddStyleRule(StyleRuleParams()
.SetSelector("#ad-1")
.SetDomains({"127.0.0.1"})
.SetIds({"ad-1"}));
AddStyleRule(
StyleRuleParams().SetSelector("#ad-2").SetDomains({"0.0.1"}).SetIds(
{"ad-2"}));
Finish();
std::vector<std::string_view> rules;
url::Origin ip_origin = url::Origin::Create(GURL("http://127.0.0.1"));
matcher_->GetDomainSelectors(ip_origin, rules);
// Suffix stripping applies to IP addresses too.
ASSERT_EQ(2u, rules.size());
EXPECT_EQ("#ad-1", rules[0]);
EXPECT_EQ("#ad-2", rules[1]);
}
} // namespace subresource_filter