Commit b6712a6e authored by Charlie Harrison's avatar Charlie Harrison Committed by Commit Bot

[subresource_filter] Introduce the rule_parser

Bug: 833419
Change-Id: I9b1d6f3b7b5c9229443b71f41d422d78be40ba8d
Reviewed-on: https://chromium-review.googlesource.com/1014026Reviewed-by: default avatarJosh Karlin <jkarlin@chromium.org>
Commit-Queue: Charlie Harrison <csharrison@chromium.org>
Cr-Commit-Position: refs/heads/master@{#550990}
parent a1df5f31
......@@ -28,6 +28,7 @@ source_set("unit_tests") {
":tools_lib",
"../core/common",
"../core/common:test_support",
"rule_parser:unit_tests",
"//base",
"//base/test:test_support",
"//components/url_pattern_index:test_support",
......
# Copyright 2018 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.
source_set("rule_parser") {
sources = [
"rule.cc",
"rule.h",
"rule_parser.cc",
"rule_parser.h",
]
deps = [
"//base",
"//components/url_pattern_index/proto:url_pattern_index",
"//third_party/protobuf:protobuf_lite",
]
}
source_set("unit_tests") {
testonly = true
sources = [
"rule_parser_unittest.cc",
"rule_unittest.cc",
]
deps = [
":rule_parser",
"//base",
"//components/url_pattern_index/proto:url_pattern_index",
"//testing/gtest",
"//third_party/protobuf:protobuf_lite",
]
}
This diff is collapsed.
// Copyright 2018 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.
//
// This file contains definitions of data structures needed for representing
// filtering/hiding rules as parsed from EasyList. See the following links for
// detailed explanation on available rule types and their syntax:
// https://adblockplus.org/en/filters
// https://adblockplus.org/en/filter-cheatsheet
// For out-of-documentation options see the following:
// https://adblockplus.org/forum/viewtopic.php?t=9353
// https://adblockplus.org/development-builds/experimental-pop-up-blocking-support
// https://adblockplus.org/development-builds/new-filter-options-generichide-and-genericblock
//
// TODO(pkalinnikov): Consider removing these classes, leaving only the
// corresponding protobuf structures.
#ifndef COMPONENTS_SUBRESOURCE_FILTER_TOOLS_RULE_PARSER_RULE_H_
#define COMPONENTS_SUBRESOURCE_FILTER_TOOLS_RULE_PARSER_RULE_H_
#include <ostream>
#include <string>
#include <vector>
#include "components/subresource_filter/tools/rule_parser/rule_options.h"
#include "components/url_pattern_index/proto/rules.pb.h"
namespace subresource_filter {
using UrlPatternType = url_pattern_index::proto::UrlPatternType;
using RuleType = url_pattern_index::proto::RuleType;
using AnchorType = url_pattern_index::proto::AnchorType;
// Represents the three values in a kind of tri-state logic.
enum class TriState {
DONT_CARE = 0,
YES = 1,
NO = 2,
};
// A single URL filtering rule as parsed from EasyList.
// TODO(pkalinnikov): Add 'sitekey', 'collapse', and 'donottrack' options.
struct UrlRule {
// Constructs a default empty blacklist rule. That means URL pattern is empty,
// not case sensitive and not anchored, domain list is empty, and the rule is
// associated with all ElementType's (except POPUP), and none of the
// ActivationType's.
UrlRule();
UrlRule(const UrlRule&);
~UrlRule();
UrlRule& operator=(const UrlRule&);
bool operator==(const UrlRule& other) const;
bool operator!=(const UrlRule& other) const { return !operator==(other); }
// Returns a protobuf representation of the rule.
url_pattern_index::proto::UrlRule ToProtobuf() const;
// Canonicalizes the rule, i.e. orders the |domains| list properly, determines
// |url_pattern_type| based on |url_pattern| together with |anchor_*|'s and
// modifies the |url_pattern| accordingly to reduce the amount of redundancy
// in the pattern.
void Canonicalize();
// Canonicalizes the rule's |url_pattern|.
void CanonicalizeUrlPattern();
// This is a whitelist rule (aka rule-level exception).
bool is_whitelist = false;
// An anchor used at the beginning of the URL pattern.
AnchorType anchor_left = url_pattern_index::proto::ANCHOR_TYPE_NONE;
// The same for the end of the pattern. Never equals to ANCHOR_TYPE_SUBDOMAIN.
AnchorType anchor_right = url_pattern_index::proto::ANCHOR_TYPE_NONE;
// Restriction to first-party/third-party requests.
TriState is_third_party = TriState::DONT_CARE;
// Apply the filter only to addresses with matching case.
// TODO(pkalinnikov): Implement case insensitivity in matcher.
bool match_case = false;
// A bitmask that reflects what ElementType's to block/whitelist and what
// kinds of ActivationType's to associate this rule with.
TypeMask type_mask = kDefaultElementTypes;
// The list of domains to be included/excluded from the filter's affected set.
// If a particular string in the list starts with '~' then the respective
// domain is excluded, otherwise included. The list should be ordered by
// |CanonicalizeDomainList|.
std::vector<std::string> domains;
// The type of the URL pattern's format.
UrlPatternType url_pattern_type =
url_pattern_index::proto::URL_PATTERN_TYPE_SUBSTRING;
// A URL pattern in either of UrlPatternType's formats, corresponding to
// |url_pattern_type|.
std::string url_pattern;
};
// A single CSS element hiding rule as parsed from EasyList.
struct CssRule {
// Constructs a default empty blacklist rule (no domains, empty selector).
CssRule();
CssRule(const CssRule&);
~CssRule();
CssRule& operator=(const CssRule&);
bool operator==(const CssRule& other) const;
bool operator!=(const CssRule& other) const { return !operator==(other); }
// Returns a protobuf representation of the rule.
url_pattern_index::proto::CssRule ToProtobuf() const;
// Canonicalizes the rule, i.e. orders the |domains| list properly.
void Canonicalize();
// This is a whitelist rule (aka rule-level exception).
bool is_whitelist = false;
// The list of domains, same as UrlRule::domains.
std::vector<std::string> domains;
// A CSS selector as specified in http://www.w3.org/TR/css3-selectors.
std::string css_selector;
};
// Sorts domain patterns in decreasing order of length (and alphabetically
// within same-length groups).
void CanonicalizeDomainList(std::vector<std::string>* domains);
// Converts protobuf |rule| into its canonical EasyList string representation.
std::string ToString(const url_pattern_index::proto::UrlRule& rule);
std::string ToString(const url_pattern_index::proto::CssRule& rule);
// For testing.
std::ostream& operator<<(std::ostream& os, const UrlRule& rule);
std::ostream& operator<<(std::ostream& os, const CssRule& rule);
} // namespace subresource_filter
#endif // COMPONENTS_SUBRESOURCE_FILTER_TOOLS_RULE_PARSER_RULE_H_
// Copyright 2018 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.
#ifndef COMPONENTS_SUBRESOURCE_FILTER_TOOLS_RULE_PARSER_RULE_OPTIONS_H_
#define COMPONENTS_SUBRESOURCE_FILTER_TOOLS_RULE_PARSER_RULE_OPTIONS_H_
#include <limits>
#include "components/url_pattern_index/proto/rules.pb.h"
namespace subresource_filter {
// A type used for representing bitmask with ElementType's and ActivationType's.
using TypeMask = uint32_t;
static constexpr uint32_t kActivationTypesShift = 24;
static constexpr uint32_t kActivationTypesBits =
std::numeric_limits<TypeMask>::digits - kActivationTypesShift;
static_assert(kActivationTypesShift <= std::numeric_limits<TypeMask>::digits,
"TypeMask layout is broken");
static_assert(url_pattern_index::proto::ElementType_MAX <
(1 << kActivationTypesShift),
"TypeMask layout is broken");
static_assert(url_pattern_index::proto::ActivationType_MAX <
(1 << kActivationTypesBits),
"TypeMask layout is broken");
// The functions used to calculate masks for individual types.
inline constexpr TypeMask type_mask_for(
url_pattern_index::proto::ElementType type) {
return type;
}
inline constexpr TypeMask type_mask_for(
url_pattern_index::proto::ActivationType type) {
return type << kActivationTypesShift;
}
static constexpr TypeMask kAllElementTypes =
type_mask_for(url_pattern_index::proto::ELEMENT_TYPE_ALL);
static constexpr TypeMask kAllActivationTypes =
type_mask_for(url_pattern_index::proto::ACTIVATION_TYPE_ALL);
static constexpr TypeMask kDefaultElementTypes =
kAllElementTypes &
~type_mask_for(url_pattern_index::proto::ELEMENT_TYPE_POPUP);
// A list of items mapping element type options to their names.
const struct {
url_pattern_index::proto::ElementType type;
const char* name;
} kElementTypes[] = {
{url_pattern_index::proto::ELEMENT_TYPE_OTHER, "other"},
{url_pattern_index::proto::ELEMENT_TYPE_SCRIPT, "script"},
{url_pattern_index::proto::ELEMENT_TYPE_IMAGE, "image"},
{url_pattern_index::proto::ELEMENT_TYPE_STYLESHEET, "stylesheet"},
{url_pattern_index::proto::ELEMENT_TYPE_OBJECT, "object"},
{url_pattern_index::proto::ELEMENT_TYPE_XMLHTTPREQUEST, "xmlhttprequest"},
{url_pattern_index::proto::ELEMENT_TYPE_OBJECT_SUBREQUEST,
"object-subrequest"},
{url_pattern_index::proto::ELEMENT_TYPE_SUBDOCUMENT, "subdocument"},
{url_pattern_index::proto::ELEMENT_TYPE_PING, "ping"},
{url_pattern_index::proto::ELEMENT_TYPE_MEDIA, "media"},
{url_pattern_index::proto::ELEMENT_TYPE_FONT, "font"},
{url_pattern_index::proto::ELEMENT_TYPE_POPUP, "popup"},
{url_pattern_index::proto::ELEMENT_TYPE_WEBSOCKET, "websocket"},
};
// A mapping from deprecated element type names to active element types.
const struct {
const char* name;
url_pattern_index::proto::ElementType maps_to_type;
} kDeprecatedElementTypes[] = {
{"background", url_pattern_index::proto::ELEMENT_TYPE_IMAGE},
{"xbl", url_pattern_index::proto::ELEMENT_TYPE_OTHER},
{"dtd", url_pattern_index::proto::ELEMENT_TYPE_OTHER},
};
// A list of items mapping activation type options to their names.
const struct {
url_pattern_index::proto::ActivationType type;
const char* name;
} kActivationTypes[] = {
{url_pattern_index::proto::ACTIVATION_TYPE_DOCUMENT, "document"},
{url_pattern_index::proto::ACTIVATION_TYPE_ELEMHIDE, "elemhide"},
{url_pattern_index::proto::ACTIVATION_TYPE_GENERICHIDE, "generichide"},
{url_pattern_index::proto::ACTIVATION_TYPE_GENERICBLOCK, "genericblock"},
};
} // namespace subresource_filter
#endif // COMPONENTS_SUBRESOURCE_FILTER_TOOLS_RULE_PARSER_RULE_OPTIONS_H_
This diff is collapsed.
// Copyright 2018 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.
#ifndef COMPONENTS_SUBRESOURCE_FILTER_TOOLS_RULE_PARSER_RULE_PARSER_H_
#define COMPONENTS_SUBRESOURCE_FILTER_TOOLS_RULE_PARSER_RULE_PARSER_H_
#include <stddef.h>
#include <ostream>
#include <string>
#include "base/macros.h"
#include "base/strings/string_piece.h"
#include "components/subresource_filter/tools/rule_parser/rule.h"
namespace subresource_filter {
// A parser of EasyList rules. It is intended to be (re-)used for parsing
// multiple rules.
// TODO(pkalinnikov): Support 'sitekey', 'collapse', and 'donottrack' options.
class RuleParser {
public:
// Detailed information about a parse error (if any).
struct ParseError {
// Indicates the type of an error occured during a Parse(...) call.
enum ErrorCode {
NONE, // Parsing was successful.
EMPTY_RULE, // The parsed line does not contain any rule.
BAD_WHITELIST_SYNTAX, // Used wrong sytnax for a whitelist rule.
UNKNOWN_OPTION, // Using of unknown option in a URL rule.
NOT_A_TRISTATE_OPTION, // Used negation for a non-tristate option.
DEPRECATED_OPTION, // Used a deprecated option.
WHITELIST_ONLY_OPTION, // The option applies to whitelist rules only.
NO_VALUE_PROVIDED, // A valued option is used without a value.
WRONG_CSS_RULE_DELIM, // Using of a wrong delimiter in a CSS rule.
EMPTY_CSS_SELECTOR, // No CSS selector specified in a CSS rule.
UNSUPPORTED_FEATURE, // Using not currently supported EasyList feature.
};
// TODO(pkalinnikov): Introduce warnings for, e.g., using an inverted
// "document" activation type, using unsupported option, etc. This would let
// a client have a best-effort version of the rule. Leave it up to clients
// to decide what warnings/errors are critical for them.
// Constructs a ParseError in a default (no error) state.
ParseError();
~ParseError();
ErrorCode error_code = NONE;
// A copy of the parsed line. If no error occurred, it is empty.
std::string line;
// Position of the character in the |line| that introduced the error. If
// |error_code| != NONE, then 0 <= |error_index| <= line.size(), otherwise
// |error_index| == std::string::npos.
size_t error_index = std::string::npos;
};
RuleParser();
~RuleParser();
// Returns a human-readable detailed explanation of a parsing error.
static const char* GetParseErrorCodeDescription(ParseError::ErrorCode code);
// Parses a rule from the |line|. Returns the type of the rule parsed, or
// RULE_TYPE_UNSPECIFIED on error. Notes:
// - When parsing a URL rule, URL syntax is not verified.
// - When parsing a CSS rule, the CSS selector syntax is not verified.
RuleType Parse(base::StringPiece line);
// Returns error diagnostics on the latest parsed line.
const ParseError& parse_error() const { return parse_error_; }
// Gets the last parsed rule type. It is guaranteed to return the same value
// as the last Parse(...) invocation, or RULE_TYPE_UNSPECIFIED if no calls
// were done.
RuleType rule_type() const { return rule_type_; }
// Gets the last parsed URL filtering rule. The result is undefined if
// rule_type() != RULE_TYPE_URL,
const UrlRule& url_rule() const { return url_rule_; }
// Gets the last parsed CSS element hiding rule. The result is undefined if
// rule_type() != RULE_TYPE_CSS.
const CssRule& css_rule() const { return css_rule_; }
private:
// Parses the |part| and saves parsed URL filtering rule to the |url_rule_|
// member. |origin| is used for a proper error reporting. Returns
// RULE_TYPE_URL ff the |part| is a well-formed URL rule. Otherwise returns
// RULE_TYPE_UNSPECIFIED and sets |parse_error_|.
RuleType ParseUrlRule(base::StringPiece origin, base::StringPiece part);
// Parses the |options| segment of a URL filtering rule and saves the parsed
// options to the |url_rule_| member. Returns true if the options were parsed
// correctly. Otherwise sets an error in |parse_error_| and returns false.
bool ParseUrlRuleOptions(base::StringPiece origin, base::StringPiece options);
// Parses the |part| and saves parsed CSS rule to the |css_rule_| member.
// |css_section_start| denotes a position of '#' in the |part|, used to
// separate a CSS selector. Returns true iff the line is a well-formed CSS
// rule. Sets |parse_error_| on error.
RuleType ParseCssRule(base::StringPiece origin,
base::StringPiece part,
size_t css_section_start);
// Sets |parse_error_| to contain specific error, starting at |error_begin|.
void SetParseError(ParseError::ErrorCode code,
base::StringPiece origin,
const char* error_begin);
ParseError parse_error_;
RuleType rule_type_;
UrlRule url_rule_;
CssRule css_rule_;
DISALLOW_COPY_AND_ASSIGN(RuleParser);
};
// Pretty-prints the parsing |error| to |out|, e.g. like this:
// (error:22) Unknown URL rule option:
// @@example.org$script,unknown_option
// ^
std::ostream& operator<<(std::ostream& out,
const RuleParser::ParseError& error);
} // namespace subresource_filter
#endif // COMPONENTS_SUBRESOURCE_FILTER_TOOLS_RULE_PARSER_RULE_PARSER_H_
// Copyright 2018 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.
#include "components/subresource_filter/tools/rule_parser/rule.h"
#include <stddef.h>
#include <memory>
#include "components/subresource_filter/tools/rule_parser/rule_options.h"
#include "components/subresource_filter/tools/rule_parser/rule_parser.h"
#include "components/url_pattern_index/proto/rules.pb.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace subresource_filter {
namespace {
constexpr auto kScript = url_pattern_index::proto::ELEMENT_TYPE_SCRIPT;
constexpr auto kImage = url_pattern_index::proto::ELEMENT_TYPE_IMAGE;
constexpr auto kPopup = url_pattern_index::proto::ELEMENT_TYPE_POPUP;
constexpr auto kWebsocket = url_pattern_index::proto::ELEMENT_TYPE_WEBSOCKET;
constexpr auto kAnchorNone = url_pattern_index::proto::ANCHOR_TYPE_NONE;
} // namespace
TEST(RuleTest, DefaultUrlRule) {
UrlRule rule;
EXPECT_TRUE(rule.url_pattern.empty());
EXPECT_EQ(url_pattern_index::proto::URL_PATTERN_TYPE_SUBSTRING,
rule.url_pattern_type);
EXPECT_FALSE(rule.match_case);
EXPECT_EQ(kAnchorNone, rule.anchor_left);
EXPECT_EQ(kAnchorNone, rule.anchor_right);
EXPECT_TRUE(rule.domains.empty());
EXPECT_TRUE(rule.type_mask &
type_mask_for(url_pattern_index::proto::ELEMENT_TYPE_OTHER));
EXPECT_TRUE(rule.type_mask & type_mask_for(kScript));
EXPECT_TRUE(rule.type_mask & type_mask_for(kImage));
EXPECT_FALSE(rule.type_mask & type_mask_for(kPopup));
EXPECT_TRUE(rule.type_mask & type_mask_for(kWebsocket));
EXPECT_FALSE(
rule.type_mask &
type_mask_for(url_pattern_index::proto::ACTIVATION_TYPE_DOCUMENT));
EXPECT_FALSE(
rule.type_mask &
type_mask_for(url_pattern_index::proto::ACTIVATION_TYPE_ELEMHIDE));
EXPECT_FALSE(
rule.type_mask &
type_mask_for(url_pattern_index::proto::ACTIVATION_TYPE_GENERICBLOCK));
EXPECT_FALSE(
rule.type_mask &
type_mask_for(url_pattern_index::proto::ACTIVATION_TYPE_GENERICHIDE));
}
TEST(RuleTest, CanonicalizeUrlPattern) {
const struct {
const char* rule;
const char* expected_url_pattern;
AnchorType expected_anchor_left;
AnchorType expected_anchor_right;
} kTestCases[] = {
{"*/text/*", "/text/", kAnchorNone, kAnchorNone},
{"*/text", "/text", kAnchorNone, kAnchorNone},
{"text/*", "text/", kAnchorNone, kAnchorNone},
{"*te*xt*", "te*xt", kAnchorNone, kAnchorNone},
{"|*te*xt*", "te*xt", kAnchorNone,
url_pattern_index::proto::ANCHOR_TYPE_NONE},
{"*te*xt*|", "te*xt", kAnchorNone, kAnchorNone},
{"|*te*xt*|", "te*xt", kAnchorNone, kAnchorNone},
{"||*te*xt*", "*te*xt", url_pattern_index::proto::ANCHOR_TYPE_SUBDOMAIN,
kAnchorNone},
};
for (const auto& test_case : kTestCases) {
SCOPED_TRACE(testing::Message() << "Rule: " << test_case.rule);
RuleParser parser;
ASSERT_EQ(url_pattern_index::proto::RULE_TYPE_URL,
parser.Parse(test_case.rule));
const UrlRule& rule = parser.url_rule();
EXPECT_EQ(test_case.expected_url_pattern, rule.url_pattern);
EXPECT_EQ(test_case.expected_anchor_left, rule.anchor_left);
EXPECT_EQ(test_case.expected_anchor_right, rule.anchor_right);
}
}
TEST(RuleTest, CanonicalizeDomainList) {
static const size_t kMaxDomainsCount = 3;
static const char* const kTestCases[][kMaxDomainsCount] = {
{"a.com", "c.com", "b.com"},
{"a.com", "aa.aa.com", "long-example.com"},
{"~sub.ex1.com", "ex2.com", "ex1.com"},
{"example.com", "b.exmpl.com", "~a.b.example.com"},
{"~example.com", "b.example.com", "~a.b.example.com"},
};
for (const auto& test_case : kTestCases) {
std::vector<std::string> domains;
size_t count = 0;
for (; count < kMaxDomainsCount && test_case[count]; ++count)
domains.push_back(test_case[count]);
CanonicalizeDomainList(&domains);
EXPECT_EQ(count, domains.size());
for (size_t i = 1; i < domains.size(); ++i) {
EXPECT_GE(domains[i - 1].size(), domains[i].size());
if (domains[i - 1].size() == domains[i].size())
EXPECT_LE(domains[i - 1], domains[i]);
}
}
}
TEST(RuleTest, UrlRuleToString) {
UrlRule rule;
rule.url_pattern = "domain*.example.com^";
rule.anchor_left = url_pattern_index::proto::ANCHOR_TYPE_SUBDOMAIN;
rule.url_pattern_type = url_pattern_index::proto::URL_PATTERN_TYPE_WILDCARDED;
rule.is_whitelist = true;
rule.is_third_party = TriState::NO;
rule.type_mask = kScript | kImage;
rule.domains = {"example.com", "~exception.example.com"};
EXPECT_EQ(
"@@||domain*.example.com^$script,image,~third-party,"
"domain=example.com|~exception.example.com",
ToString(rule.ToProtobuf()));
rule = UrlRule();
rule.url_pattern = "example.com";
rule.type_mask =
url_pattern_index::proto::ELEMENT_TYPE_ALL & ~kImage & ~kPopup;
EXPECT_EQ("example.com$~image", ToString(rule.ToProtobuf()));
rule.type_mask = url_pattern_index::proto::ELEMENT_TYPE_ALL & ~kPopup;
EXPECT_EQ("example.com", ToString(rule.ToProtobuf()));
rule.type_mask = url_pattern_index::proto::ELEMENT_TYPE_ALL & ~kImage;
EXPECT_EQ("example.com$~image,popup", ToString(rule.ToProtobuf()));
rule.type_mask = url_pattern_index::proto::ELEMENT_TYPE_ALL & ~kWebsocket;
EXPECT_EQ("example.com$~websocket,popup", ToString(rule.ToProtobuf()));
rule.type_mask = kPopup;
EXPECT_EQ("example.com$popup", ToString(rule.ToProtobuf()));
rule.type_mask = kPopup | kImage;
EXPECT_EQ("example.com$image,popup", ToString(rule.ToProtobuf()));
rule.type_mask = kPopup | kWebsocket;
EXPECT_EQ("example.com$websocket,popup", ToString(rule.ToProtobuf()));
rule.type_mask =
url_pattern_index::proto::ELEMENT_TYPE_SUBDOCUMENT |
type_mask_for(url_pattern_index::proto::ACTIVATION_TYPE_DOCUMENT);
EXPECT_EQ("example.com$subdocument,document", ToString(rule.ToProtobuf()));
rule.type_mask =
type_mask_for(url_pattern_index::proto::ACTIVATION_TYPE_DOCUMENT);
EXPECT_EQ("example.com$document", ToString(rule.ToProtobuf()));
rule.type_mask =
(url_pattern_index::proto::ELEMENT_TYPE_ALL & ~kImage & ~kPopup) |
type_mask_for(url_pattern_index::proto::ACTIVATION_TYPE_DOCUMENT);
EXPECT_EQ("example.com$~image,document", ToString(rule.ToProtobuf()));
// A workaround for when no type is specified. This is to avoid ambiguity with
// the pure "example.com" rule which targets a bunch of default element types.
rule.type_mask = 0;
EXPECT_EQ("example.com$image,~image", ToString(rule.ToProtobuf()));
}
TEST(RuleTest, CssRuleToString) {
CssRule rule;
rule.is_whitelist = true;
rule.domains = {"example.com", "~exception.example.com"};
rule.css_selector = "#example-id";
EXPECT_EQ("example.com,~exception.example.com#@##example-id",
ToString(rule.ToProtobuf()));
}
} // namespace subresource_filter
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment