blob: 1c54ac4dba26a6752440fa77ed204ee778b66f63 [file]
// Copyright 2022 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/attribution_reporting/filters.h"
#include <stddef.h>
#include <algorithm>
#include <limits>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include "base/check.h"
#include "base/containers/flat_set.h"
#include "base/notreached.h"
#include "base/strings/string_util.h"
#include "base/time/time.h"
#include "base/types/expected.h"
#include "base/types/expected_macros.h"
#include "base/values.h"
#include "components/attribution_reporting/constants.h"
#include "components/attribution_reporting/parsing_utils.h"
#include "components/attribution_reporting/source_registration_error.mojom.h"
#include "components/attribution_reporting/source_type.h"
#include "components/attribution_reporting/source_type.mojom-forward.h"
#include "components/attribution_reporting/trigger_registration_error.mojom.h"
namespace attribution_reporting {
namespace {
using ::attribution_reporting::mojom::SourceRegistrationError;
using ::attribution_reporting::mojom::TriggerRegistrationError;
constexpr char kNotFilters[] = "not_filters";
base::expected<void, FilterValuesError> ValidateForSource(
const FilterValues& filter_values) {
if (filter_values.contains(FilterData::kSourceTypeFilterKey)) {
return base::unexpected(FilterValuesError::kKeyReserved);
}
if (filter_values.size() > kMaxFiltersPerSource) {
return base::unexpected(FilterValuesError::kTooManyKeys);
}
for (const auto& [filter, values] : filter_values) {
if (filter.size() > kMaxBytesPerFilterString) {
return base::unexpected(FilterValuesError::kKeyTooLong);
}
if (values.size() > kMaxValuesPerFilter) {
return base::unexpected(FilterValuesError::kListTooLong);
}
for (const auto& value : values) {
if (value.size() > kMaxBytesPerFilterString) {
return base::unexpected(FilterValuesError::kValueTooLong);
}
}
}
return base::ok();
}
base::expected<FilterValues, FilterValuesError> ParseFilterValuesFromJSON(
base::DictValue dict,
const size_t max_filters,
const size_t max_string_size,
const size_t max_set_size) {
const size_t num_filters = dict.size();
if (num_filters > max_filters) {
return base::unexpected(FilterValuesError::kTooManyKeys);
}
FilterValues::container_type filter_values;
filter_values.reserve(dict.size());
for (auto [filter, value] : dict) {
if (base::StartsWith(filter, FilterConfig::kReservedKeyPrefix)) {
return base::unexpected(FilterValuesError::kKeyReserved);
}
if (filter.size() > max_string_size) {
return base::unexpected(FilterValuesError::kKeyTooLong);
}
base::ListValue* list = value.GetIfList();
if (!list) {
return base::unexpected(FilterValuesError::kListWrongType);
}
ASSIGN_OR_RETURN(
base::flat_set<std::string> values,
ExtractStringSet(std::move(*list), max_string_size, max_set_size),
[](StringSetError error) {
switch (error) {
case StringSetError::kWrongType:
return FilterValuesError::kValueWrongType;
case StringSetError::kStringTooLong:
return FilterValuesError::kValueTooLong;
case StringSetError::kSetTooLong:
return FilterValuesError::kListTooLong;
}
NOTREACHED();
});
filter_values.emplace_back(filter, std::move(values).extract());
}
return FilterValues(base::sorted_unique, std::move(filter_values));
}
} // namespace
base::DictValue FilterValuesToJson(const FilterValues& filter_values) {
base::DictValue dict;
for (const auto& [key, values] : filter_values) {
auto list = base::ListValue::with_capacity(values.size());
for (const auto& value : values) {
list.Append(value);
}
dict.Set(key, std::move(list));
}
return dict;
}
// static
std::optional<FilterData> FilterData::Create(FilterValues filter_values) {
if (!ValidateForSource(filter_values).has_value()) {
return std::nullopt;
}
return FilterData(std::move(filter_values));
}
// static
base::expected<FilterData, FilterValuesError> FilterData::CreateForTesting(
FilterValues filter_values) {
RETURN_IF_ERROR(ValidateForSource(filter_values));
return FilterData(std::move(filter_values));
}
// static
base::expected<FilterData, SourceRegistrationError> FilterData::FromJSON(
base::Value* input_value) {
if (!input_value) {
return FilterData();
}
base::DictValue* dict = input_value->GetIfDict();
if (!dict) {
return base::unexpected(SourceRegistrationError::kFilterDataDictInvalid);
}
if (dict->contains(kSourceTypeFilterKey)) {
return base::unexpected(SourceRegistrationError::kFilterDataKeyReserved);
}
const auto map_errors = [](FilterValuesError error) {
switch (error) {
case FilterValuesError::kKeyReserved:
return SourceRegistrationError::kFilterDataKeyReserved;
case FilterValuesError::kTooManyKeys:
return SourceRegistrationError::kFilterDataDictInvalid;
case FilterValuesError::kKeyTooLong:
return SourceRegistrationError::kFilterDataKeyTooLong;
case FilterValuesError::kListWrongType:
case FilterValuesError::kListTooLong:
return SourceRegistrationError::kFilterDataListInvalid;
case FilterValuesError::kValueWrongType:
case FilterValuesError::kValueTooLong:
return SourceRegistrationError::kFilterDataListValueInvalid;
}
};
ASSIGN_OR_RETURN(
auto filter_values,
ParseFilterValuesFromJSON(std::move(*dict),
/*max_filters=*/kMaxFiltersPerSource,
/*max_string_size=*/kMaxBytesPerFilterString,
/*max_set_size=*/kMaxValuesPerFilter)
.transform_error(map_errors));
return FilterData(std::move(filter_values));
}
FilterData::FilterData() = default;
FilterData::FilterData(FilterValues filter_values)
: filter_values_(std::move(filter_values)) {
CHECK(ValidateForSource(filter_values_).has_value());
}
FilterData::~FilterData() = default;
FilterData::FilterData(const FilterData&) = default;
FilterData::FilterData(FilterData&&) = default;
FilterData& FilterData::operator=(const FilterData&) = default;
FilterData& FilterData::operator=(FilterData&&) = default;
base::DictValue FilterData::ToJson() const {
return FilterValuesToJson(filter_values_);
}
bool FilterData::Matches(mojom::SourceType source_type,
base::Time source_time,
base::Time trigger_time,
const FiltersDisjunction& filters,
bool negated) const {
if (filters.empty()) {
return true;
}
// While contradictory, it is possible for a source to have an assigned time
// of T and a trigger that is attributed to it to have a time of T-X e.g. due
// to user-initiated clock changes. see: https://crbug.com/1486489
//
// TODO(crbug.com/40282914): Assume `source_time` is smaller than
// `trigger_time` once attribution time resolution is implemented in storage.
const base::TimeDelta duration_since_source_registration =
(source_time < trigger_time) ? trigger_time - source_time
: base::Microseconds(0);
// A filter_value is considered matched if the filter key is only present
// either on the source or trigger, or the intersection of the filter values
// is non-empty. Returns true if all the filters matched.
//
// If the filters are negated, the behavior should be that every single filter
// key does not match between the two (negating the function result is not
// sufficient by the API definition).
return std::ranges::any_of(filters, [&](const FilterConfig& config) {
if (config.lookback_window()) {
if (duration_since_source_registration >
config.lookback_window().value()) {
if (!negated) {
return false;
}
} else if (negated) {
return false;
}
}
return std::ranges::all_of(
config.filter_values(), [&](const auto& trigger_filter) {
if (trigger_filter.first == kSourceTypeFilterKey) {
bool has_intersection = std::ranges::any_of(
trigger_filter.second, [&](const std::string& value) {
return value == SourceTypeName(source_type);
});
return negated != has_intersection;
}
auto source_filter = filter_values_.find(trigger_filter.first);
if (source_filter == filter_values_.end()) {
return true;
}
// Desired behavior is to treat any empty set of values as a
// single unique value itself. This means:
// - x:[] match x:[] is false when negated, and true otherwise.
// - x:[1,2,3] match x:[] is true when negated, and false
// otherwise.
if (trigger_filter.second.empty()) {
return negated != source_filter->second.empty();
}
bool has_intersection = std::ranges::any_of(
trigger_filter.second, [&](const std::string& value) {
return std::ranges::contains(source_filter->second, value);
});
// Negating filters are considered matched if the intersection of
// the filter values is empty.
return negated != has_intersection;
});
});
}
bool FilterData::MatchesForTesting(mojom::SourceType source_type,
base::Time source_time,
base::Time trigger_time,
const FiltersDisjunction& filters,
bool negated) const {
return Matches(source_type, source_time, trigger_time, filters, negated);
}
bool FilterData::Matches(mojom::SourceType source_type,
base::Time source_time,
base::Time trigger_time,
const FilterPair& filters) const {
return Matches(source_type, source_time, trigger_time, filters.positive,
/*negated=*/false) &&
Matches(source_type, source_time, trigger_time, filters.negative,
/*negated=*/true);
}
FilterConfig::FilterConfig() = default;
std::optional<FilterConfig> FilterConfig::Create(
FilterValues filter_values,
std::optional<base::TimeDelta> lookback_window) {
if (lookback_window && !lookback_window->is_positive()) {
return std::nullopt;
}
return FilterConfig(std::move(filter_values), lookback_window);
}
FilterConfig::FilterConfig(FilterValues filter_values,
std::optional<base::TimeDelta> lookback_window)
: lookback_window_(lookback_window),
filter_values_(std::move(filter_values)) {
CHECK(!lookback_window_.has_value() || lookback_window_->is_positive());
}
FilterConfig::~FilterConfig() = default;
FilterConfig::FilterConfig(const FilterConfig&) = default;
FilterConfig::FilterConfig(FilterConfig&&) = default;
FilterConfig& FilterConfig::operator=(const FilterConfig&) = default;
FilterConfig& FilterConfig::operator=(FilterConfig&&) = default;
namespace {
base::expected<FiltersDisjunction, TriggerRegistrationError> FiltersFromJSON(
base::Value* input_value) {
if (!input_value) {
return FiltersDisjunction();
}
const auto map_errors = [](FilterValuesError error,
TriggerRegistrationError value_error,
TriggerRegistrationError reserved_key_error) {
switch (error) {
case FilterValuesError::kValueWrongType:
case FilterValuesError::kListWrongType:
return value_error;
case FilterValuesError::kKeyReserved:
return reserved_key_error;
case FilterValuesError::kTooManyKeys:
case FilterValuesError::kKeyTooLong:
case FilterValuesError::kListTooLong:
case FilterValuesError::kValueTooLong:
NOTREACHED();
}
};
FiltersDisjunction disjunction;
using AppendIfValidResult = base::expected<void, TriggerRegistrationError>;
const auto append_if_valid =
[&](base::Value& value, TriggerRegistrationError value_error,
TriggerRegistrationError lookback_window_error,
TriggerRegistrationError reserved_key_error) -> AppendIfValidResult {
base::DictValue* dict = value.GetIfDict();
if (!dict) {
return base::unexpected(TriggerRegistrationError::kFiltersWrongType);
}
std::optional<base::TimeDelta> lookback_window;
if (std::optional<base::Value> lookback_window_value =
dict->Extract(FilterConfig::kLookbackWindowKey)) {
ASSIGN_OR_RETURN(lookback_window, ParseDuration(*lookback_window_value),
[lookback_window_error](ParseError) {
return lookback_window_error;
});
if (!lookback_window->is_positive()) {
return base::unexpected(lookback_window_error);
}
}
ASSIGN_OR_RETURN(auto filter_values,
ParseFilterValuesFromJSON(
std::move(*dict),
/*max_filters=*/std::numeric_limits<size_t>::max(),
/*max_string_size=*/std::numeric_limits<size_t>::max(),
/*max_set_size=*/std::numeric_limits<size_t>::max())
.transform_error([&](FilterValuesError error) {
return map_errors(error, value_error,
reserved_key_error);
}));
if (!filter_values.empty() || lookback_window.has_value()) {
auto config =
FilterConfig::Create(std::move(filter_values), lookback_window);
CHECK(config.has_value());
disjunction.emplace_back(*std::move(config));
}
return base::ok();
};
if (base::ListValue* list = input_value->GetIfList()) {
disjunction.reserve(list->size());
for (base::Value& item : *list) {
RETURN_IF_ERROR(append_if_valid(
item, TriggerRegistrationError::kFiltersListValueInvalid,
TriggerRegistrationError::kFiltersListLookbackWindowValueInvalid,
TriggerRegistrationError::kFiltersListUsingReservedKey));
}
} else {
RETURN_IF_ERROR(append_if_valid(
*input_value, TriggerRegistrationError::kFiltersValueInvalid,
TriggerRegistrationError::kFiltersLookbackWindowValueInvalid,
TriggerRegistrationError::kFiltersUsingReservedKey));
}
return disjunction;
}
base::ListValue ToJson(const FiltersDisjunction& filters) {
auto list = base::ListValue::with_capacity(filters.size());
for (const auto& filter_config : filters) {
base::DictValue dict = FilterValuesToJson(filter_config.filter_values());
if (filter_config.lookback_window().has_value()) {
dict.Set(FilterConfig::kLookbackWindowKey,
static_cast<int>(
filter_config.lookback_window().value().InSeconds()));
}
list.Append(std::move(dict));
}
return list;
}
} // namespace
// static
base::expected<FilterPair, TriggerRegistrationError> FilterPair::FromJSON(
base::DictValue& dict) {
ASSIGN_OR_RETURN(auto positive, FiltersFromJSON(dict.Find(kFilters)));
ASSIGN_OR_RETURN(auto negative, FiltersFromJSON(dict.Find(kNotFilters)));
return FilterPair(std::move(positive), std::move(negative));
}
FilterPair::FilterPair() = default;
FilterPair::FilterPair(FiltersDisjunction positive, FiltersDisjunction negative)
: positive(std::move(positive)), negative(std::move(negative)) {}
FilterPair::~FilterPair() = default;
FilterPair::FilterPair(const FilterPair&) = default;
FilterPair::FilterPair(FilterPair&&) = default;
FilterPair& FilterPair::operator=(const FilterPair&) = default;
FilterPair& FilterPair::operator=(FilterPair&&) = default;
void FilterPair::SerializeIfNotEmpty(base::DictValue& dict) const {
if (!positive.empty()) {
dict.Set(kFilters, ToJson(positive));
}
if (!negative.empty()) {
dict.Set(kNotFilters, ToJson(negative));
}
}
base::expected<FiltersDisjunction, TriggerRegistrationError>
FiltersFromJSONForTesting(base::Value* input_value) {
return FiltersFromJSON(input_value);
}
base::ListValue ToJsonForTesting(const FiltersDisjunction& filters) {
return ToJson(filters);
}
} // namespace attribution_reporting