| // Copyright 2022 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 "content/test/attribution_simulator_input_parser.h" |
| |
| #include <stddef.h> |
| #include <stdint.h> |
| |
| #include <ostream> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/bind.h" |
| #include "base/callback.h" |
| #include "base/strings/abseil_string_number_conversions.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_piece.h" |
| #include "base/strings/string_util.h" |
| #include "base/test/bind.h" |
| #include "base/time/time.h" |
| #include "base/values.h" |
| #include "content/browser/attribution_reporting/attribution_aggregatable_trigger_data.h" |
| #include "content/browser/attribution_reporting/attribution_aggregatable_values.h" |
| #include "content/browser/attribution_reporting/attribution_aggregation_keys.h" |
| #include "content/browser/attribution_reporting/attribution_filter_data.h" |
| #include "content/browser/attribution_reporting/attribution_parser_test_utils.h" |
| #include "content/browser/attribution_reporting/attribution_source_type.h" |
| #include "content/browser/attribution_reporting/attribution_trigger.h" |
| #include "content/browser/attribution_reporting/common_source_info.h" |
| #include "content/browser/attribution_reporting/storable_source.h" |
| #include "net/cookies/canonical_cookie.h" |
| #include "services/network/public/cpp/is_potentially_trustworthy.h" |
| #include "third_party/abseil-cpp/absl/numeric/int128.h" |
| #include "third_party/abseil-cpp/absl/types/optional.h" |
| #include "third_party/abseil-cpp/absl/types/variant.h" |
| #include "third_party/blink/public/common/attribution_reporting/constants.h" |
| #include "url/gurl.h" |
| #include "url/origin.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| constexpr char kTimestampKey[] = "timestamp"; |
| |
| class AttributionSimulatorInputParser { |
| public: |
| AttributionSimulatorInputParser(base::Time offset_time, |
| std::ostream& error_stream) |
| : offset_time_(offset_time), error_manager_(error_stream) {} |
| |
| ~AttributionSimulatorInputParser() = default; |
| |
| AttributionSimulatorInputParser(const AttributionSimulatorInputParser&) = |
| delete; |
| AttributionSimulatorInputParser(AttributionSimulatorInputParser&&) = delete; |
| |
| AttributionSimulatorInputParser& operator=( |
| const AttributionSimulatorInputParser&) = delete; |
| AttributionSimulatorInputParser& operator=( |
| AttributionSimulatorInputParser&&) = delete; |
| |
| absl::optional<AttributionSimulationEventAndValues> Parse( |
| base::Value input) && { |
| if (!EnsureDictionary(input)) |
| return absl::nullopt; |
| |
| static constexpr char kKeyCookies[] = "cookies"; |
| if (base::Value* cookies = input.FindKey(kKeyCookies)) { |
| auto context = PushContext(kKeyCookies); |
| ParseList( |
| std::move(*cookies), |
| base::BindRepeating(&AttributionSimulatorInputParser::ParseCookie, |
| base::Unretained(this))); |
| } |
| |
| static constexpr char kKeyDataClears[] = "data_clears"; |
| if (base::Value* data_clears = input.FindKey(kKeyDataClears)) { |
| auto context = PushContext(kKeyDataClears); |
| ParseList( |
| std::move(*data_clears), |
| base::BindRepeating(&AttributionSimulatorInputParser::ParseDataClear, |
| base::Unretained(this))); |
| } |
| |
| static constexpr char kKeySources[] = "sources"; |
| if (base::Value* sources = input.FindKey(kKeySources)) { |
| auto context = PushContext(kKeySources); |
| ParseList( |
| std::move(*sources), |
| base::BindRepeating(&AttributionSimulatorInputParser::ParseSource, |
| base::Unretained(this))); |
| } |
| |
| static constexpr char kKeyTriggers[] = "triggers"; |
| if (base::Value* triggers = input.FindKey(kKeyTriggers)) { |
| auto context = PushContext(kKeyTriggers); |
| ParseList( |
| std::move(*triggers), |
| base::BindRepeating(&AttributionSimulatorInputParser::ParseTrigger, |
| base::Unretained(this))); |
| } |
| |
| if (has_error()) |
| return absl::nullopt; |
| |
| return std::move(events_); |
| } |
| |
| private: |
| const base::Time offset_time_; |
| AttributionParserErrorManager error_manager_; |
| |
| std::vector<AttributionSimulationEventAndValue> events_; |
| |
| [[nodiscard]] std::unique_ptr<AttributionParserErrorManager::ScopedContext> |
| PushContext(AttributionParserErrorManager::Context context) { |
| return error_manager_.PushContext(context); |
| } |
| |
| AttributionParserErrorManager::ErrorWriter Error() { |
| return error_manager_.Error(); |
| } |
| |
| bool has_error() const { return error_manager_.has_error(); } |
| |
| template <typename T> |
| void ParseList(T&& values, |
| base::RepeatingCallback<void(decltype(values))> callback, |
| size_t max_size = 0) { |
| if (!values.is_list()) { |
| *Error() << "must be a list"; |
| return; |
| } |
| |
| if (max_size > 0 && values.GetList().size() > max_size) { |
| *Error() << "too many elements"; |
| return; |
| } |
| |
| size_t index = 0; |
| for (auto&& value : values.GetList()) { |
| auto index_context = PushContext(index); |
| callback.Run(std::forward<T>(value)); |
| index++; |
| } |
| } |
| |
| void ParseCookie(base::Value&& cookie) { |
| if (!EnsureDictionary(cookie)) |
| return; |
| |
| const base::Value::Dict& dict = cookie.GetDict(); |
| |
| base::Time time = ParseTime(dict, kTimestampKey); |
| |
| static constexpr char kKeyUrl[] = "url"; |
| GURL url = ParseURL(dict, kKeyUrl); |
| if (!url.is_valid()) { |
| auto context = PushContext(kKeyUrl); |
| *Error() << "must be a valid URL"; |
| } |
| |
| static constexpr char kKeySetCookie[] = "Set-Cookie"; |
| const std::string* line = dict.FindString(kKeySetCookie); |
| if (!line) { |
| auto context = PushContext(kKeySetCookie); |
| *Error() << "must be present"; |
| return; |
| } |
| |
| // `CanonicalCookie::Create()` will DCHECK. |
| if (time.is_null()) |
| return; |
| |
| std::unique_ptr<net::CanonicalCookie> canonical_cookie = |
| net::CanonicalCookie::Create(url, *line, time, |
| /*server_time=*/absl::nullopt, |
| /*cookie_partition_key=*/absl::nullopt); |
| if (!canonical_cookie) |
| *Error() << "invalid cookie"; |
| |
| if (has_error()) |
| return; |
| |
| events_.emplace_back( |
| AttributionSimulatorCookie{ |
| .cookie = std::move(*canonical_cookie), |
| .source_url = std::move(url), |
| }, |
| std::move(cookie)); |
| } |
| |
| void ParseDataClear(base::Value&& data_clear) { |
| if (!EnsureDictionary(data_clear)) |
| return; |
| |
| const base::Value::Dict& dict = data_clear.GetDict(); |
| |
| base::Time time = ParseTime(dict, kTimestampKey); |
| |
| static constexpr char kKeyDeleteBegin[] = "delete_begin"; |
| base::Time delete_begin = base::Time::Min(); |
| if (dict.contains(kKeyDeleteBegin)) |
| delete_begin = ParseTime(dict, kKeyDeleteBegin); |
| |
| static constexpr char kKeyDeleteEnd[] = "delete_end"; |
| base::Time delete_end = base::Time::Max(); |
| if (dict.contains(kKeyDeleteEnd)) |
| delete_end = ParseTime(dict, kKeyDeleteEnd); |
| |
| absl::optional<base::flat_set<url::Origin>> origin_set; |
| |
| static constexpr char kKeyOrigins[] = "origins"; |
| if (const base::Value* origins = dict.Find(kKeyOrigins)) { |
| auto context = PushContext(kKeyOrigins); |
| origin_set.emplace(); |
| |
| ParseList( |
| *origins, base::BindLambdaForTesting([&](const base::Value& value) { |
| if (!value.is_string()) { |
| *Error() << "must be a string"; |
| } else { |
| origin_set->emplace(url::Origin::Create(GURL(value.GetString()))); |
| } |
| })); |
| } |
| |
| if (has_error()) |
| return; |
| |
| events_.emplace_back(AttributionDataClear(time, delete_begin, delete_end, |
| std::move(origin_set)), |
| std::move(data_clear)); |
| } |
| |
| void ParseSource(base::Value&& source) { |
| if (!EnsureDictionary(source)) |
| return; |
| |
| const base::Value::Dict& source_dict = source.GetDict(); |
| |
| base::Time source_time = ParseTime(source_dict, kTimestampKey); |
| url::Origin source_origin = ParseOrigin(source_dict, "source_origin"); |
| url::Origin reporting_origin = ParseOrigin(source_dict, "reporting_origin"); |
| absl::optional<AttributionSourceType> source_type = |
| ParseSourceType(source_dict); |
| |
| uint64_t source_event_id = 0; |
| url::Origin destination_origin; |
| absl::optional<uint64_t> debug_key; |
| int64_t priority = 0; |
| base::TimeDelta expiry; |
| AttributionFilterData filter_data; |
| AttributionAggregationKeys aggregation_keys; |
| |
| if (!ParseAttributionEvent( |
| source_dict, "Attribution-Reporting-Register-Source", |
| base::BindLambdaForTesting([&](const base::Value::Dict& dict) { |
| source_event_id = ParseRequiredUint64(dict, "source_event_id"); |
| destination_origin = ParseOrigin(dict, "destination"); |
| debug_key = ParseOptionalUint64(dict, "debug_key"); |
| priority = ParseOptionalInt64(dict, "priority").value_or(0); |
| expiry = ParseSourceExpiry(dict).value_or(base::Days(30)); |
| filter_data = ParseFilterData( |
| dict, "filter_data", |
| &AttributionFilterData::FromSourceFilterValues); |
| aggregation_keys = ParseAggregationKeys(dict); |
| }))) { |
| return; |
| } |
| |
| if (has_error()) |
| return; |
| |
| events_.emplace_back( |
| StorableSource(CommonSourceInfo( |
| source_event_id, std::move(source_origin), |
| std::move(destination_origin), std::move(reporting_origin), |
| source_time, |
| CommonSourceInfo::GetExpiryTime(expiry, source_time, *source_type), |
| *source_type, priority, std::move(filter_data), debug_key, |
| std::move(aggregation_keys))), |
| std::move(source)); |
| } |
| |
| void ParseTrigger(base::Value&& trigger) { |
| if (!EnsureDictionary(trigger)) |
| return; |
| |
| const base::Value::Dict& trigger_dict = trigger.GetDict(); |
| |
| base::Time trigger_time = ParseTime(trigger_dict, kTimestampKey); |
| url::Origin reporting_origin = |
| ParseOrigin(trigger_dict, "reporting_origin"); |
| url::Origin destination_origin = |
| ParseOrigin(trigger_dict, "destination_origin"); |
| |
| absl::optional<uint64_t> debug_key; |
| AttributionFilterData filters; |
| AttributionFilterData not_filters; |
| std::vector<AttributionTrigger::EventTriggerData> event_triggers; |
| std::vector<AttributionAggregatableTriggerData> aggregatable_trigger_data; |
| AttributionAggregatableValues aggregatable_values; |
| |
| if (!ParseAttributionEvent( |
| trigger_dict, |
| "Attribution-Reporting-Register-Trigger", |
| base::BindLambdaForTesting( |
| [&](const base::Value::Dict& dict) { |
| debug_key = ParseOptionalUint64(dict, "debug_key"); |
| filters = ParseFilterData( |
| dict, "filters", |
| &AttributionFilterData::FromTriggerFilterValues); |
| not_filters = ParseFilterData( |
| dict, "not_filters", |
| &AttributionFilterData::FromTriggerFilterValues); |
| event_triggers = ParseEventTriggers(dict); |
| |
| aggregatable_trigger_data = |
| ParseAggregatableTriggerData(dict); |
| |
| aggregatable_values = ParseAggregatableValues(dict); |
| }))) { |
| return; |
| } |
| |
| if (has_error()) |
| return; |
| |
| events_.emplace_back( |
| AttributionTriggerAndTime{ |
| .trigger = AttributionTrigger( |
| std::move(destination_origin), std::move(reporting_origin), |
| std::move(filters), std::move(not_filters), debug_key, |
| std::move(event_triggers), std::move(aggregatable_trigger_data), |
| std::move(aggregatable_values)), |
| .time = trigger_time, |
| }, |
| std::move(trigger)); |
| } |
| |
| std::vector<AttributionTrigger::EventTriggerData> ParseEventTriggers( |
| const base::Value::Dict& cfg) { |
| std::vector<AttributionTrigger::EventTriggerData> event_triggers; |
| |
| static constexpr char kKey[] = "event_trigger_data"; |
| |
| const base::Value* values = cfg.Find(kKey); |
| if (!values) |
| return event_triggers; |
| |
| auto context = PushContext(kKey); |
| ParseList( |
| *values, |
| base::BindLambdaForTesting([&](const base::Value& event_trigger) { |
| if (!EnsureDictionary(event_trigger)) |
| return; |
| |
| const base::Value::Dict& dict = event_trigger.GetDict(); |
| |
| uint64_t trigger_data = |
| ParseOptionalUint64(dict, "trigger_data").value_or(0); |
| |
| int64_t priority = ParseOptionalInt64(dict, "priority").value_or(0); |
| |
| absl::optional<uint64_t> dedup_key = |
| ParseOptionalUint64(dict, "deduplication_key"); |
| |
| AttributionFilterData filters = ParseFilterData( |
| dict, "filters", &AttributionFilterData::FromTriggerFilterValues); |
| |
| AttributionFilterData not_filters = |
| ParseFilterData(dict, "not_filters", |
| &AttributionFilterData::FromTriggerFilterValues); |
| |
| if (has_error()) |
| return; |
| |
| event_triggers.emplace_back(trigger_data, priority, dedup_key, |
| std::move(filters), |
| std::move(not_filters)); |
| }), |
| /*max_size=*/blink::kMaxAttributionEventTriggerData); |
| |
| return event_triggers; |
| } |
| |
| GURL ParseURL(const base::Value::Dict& dict, base::StringPiece key) const { |
| if (const std::string* v = dict.FindString(key)) |
| return GURL(*v); |
| |
| return GURL(); |
| } |
| |
| url::Origin ParseOrigin(const base::Value::Dict& dict, |
| base::StringPiece key) { |
| auto context = PushContext(key); |
| |
| auto origin = url::Origin::Create(ParseURL(dict, key)); |
| |
| if (!network::IsOriginPotentiallyTrustworthy(origin)) |
| *Error() << "must be a valid, secure origin"; |
| |
| return origin; |
| } |
| |
| base::Time ParseTime(const base::Value::Dict& dict, base::StringPiece key) { |
| auto context = PushContext(key); |
| |
| const std::string* v = dict.FindString(key); |
| int64_t milliseconds; |
| |
| if (v && base::StringToInt64(*v, &milliseconds)) { |
| base::Time time = offset_time_ + base::Milliseconds(milliseconds); |
| if (!time.is_null() && !time.is_inf()) |
| return time; |
| } |
| |
| *Error() << "must be an integer number of milliseconds since the Unix " |
| "epoch formatted as a base-10 string"; |
| return base::Time(); |
| } |
| |
| uint64_t ParseUint64(const std::string* s, base::StringPiece key) { |
| auto context = PushContext(key); |
| |
| uint64_t value = 0; |
| |
| if (!s || !base::StringToUint64(*s, &value)) |
| *Error() << "must be a uint64 formatted as a base-10 string"; |
| |
| return value; |
| } |
| |
| int64_t ParseInt64(const std::string* s, base::StringPiece key) { |
| auto context = PushContext(key); |
| |
| int64_t value = 0; |
| |
| if (!s || !base::StringToInt64(*s, &value)) |
| *Error() << "must be an int64 formatted as a base-10 string"; |
| |
| return value; |
| } |
| |
| uint64_t ParseRequiredUint64(const base::Value::Dict& dict, |
| base::StringPiece key) { |
| return ParseUint64(dict.FindString(key), key); |
| } |
| |
| absl::optional<uint64_t> ParseOptionalUint64(const base::Value::Dict& dict, |
| base::StringPiece key) { |
| const base::Value* value = dict.Find(key); |
| if (!value) |
| return absl::nullopt; |
| |
| return ParseUint64(value->GetIfString(), key); |
| } |
| |
| absl::optional<int64_t> ParseOptionalInt64(const base::Value::Dict& dict, |
| base::StringPiece key) { |
| const base::Value* value = dict.Find(key); |
| if (!value) |
| return absl::nullopt; |
| |
| return ParseInt64(value->GetIfString(), key); |
| } |
| |
| absl::optional<AttributionSourceType> ParseSourceType( |
| const base::Value::Dict& dict) { |
| static constexpr char kKey[] = "source_type"; |
| static constexpr char kNavigation[] = "navigation"; |
| static constexpr char kEvent[] = "event"; |
| |
| auto context = PushContext(kKey); |
| |
| absl::optional<AttributionSourceType> source_type; |
| |
| if (const std::string* v = dict.FindString(kKey)) { |
| if (*v == kNavigation) { |
| source_type = AttributionSourceType::kNavigation; |
| } else if (*v == kEvent) { |
| source_type = AttributionSourceType::kEvent; |
| } |
| } |
| |
| if (!source_type) { |
| *Error() << "must be either \"" << kNavigation << "\" or \"" << kEvent |
| << "\""; |
| } |
| |
| return source_type; |
| } |
| |
| bool ParseAttributionEvent( |
| const base::Value::Dict& value, |
| base::StringPiece key, |
| base::OnceCallback<void(const base::Value::Dict&)> callback) { |
| auto context = PushContext(key); |
| |
| const base::Value* dict = value.Find(key); |
| if (!dict) { |
| *Error() << "must be present"; |
| return false; |
| } |
| |
| if (!EnsureDictionary(*dict)) |
| return false; |
| |
| std::move(callback).Run(dict->GetDict()); |
| return true; |
| } |
| |
| using FromFilterValuesFunc = absl::optional<AttributionFilterData>( |
| AttributionFilterData::FilterValues&&); |
| |
| AttributionFilterData ParseFilterData( |
| const base::Value::Dict& dict, |
| base::StringPiece key, |
| FromFilterValuesFunc from_filter_values) { |
| auto context = PushContext(key); |
| |
| const base::Value* value = dict.Find(key); |
| if (!value) |
| return AttributionFilterData(); |
| |
| if (!EnsureDictionary(*value)) |
| return AttributionFilterData(); |
| |
| AttributionFilterData::FilterValues::container_type container; |
| for (auto [filter, values_list] : value->GetDict()) { |
| auto filter_context = PushContext(filter); |
| std::vector<std::string> values; |
| |
| ParseList(values_list, |
| base::BindLambdaForTesting([&](const base::Value& value) { |
| if (!value.is_string()) { |
| *Error() << "must be a string"; |
| } else { |
| values.emplace_back(value.GetString()); |
| } |
| })); |
| |
| container.emplace_back(filter, std::move(values)); |
| } |
| |
| absl::optional<AttributionFilterData> filter_data = |
| from_filter_values(std::move(container)); |
| // TODO(apaseltiner): Provide more detailed information. |
| if (!filter_data) |
| *Error() << "invalid"; |
| |
| return std::move(filter_data).value_or(AttributionFilterData()); |
| } |
| |
| absl::optional<base::TimeDelta> ParseSourceExpiry( |
| const base::Value::Dict& dict) { |
| static constexpr char kKey[] = "expiry"; |
| |
| auto context = PushContext(kKey); |
| |
| const base::Value* value = dict.Find(kKey); |
| if (!value) |
| return absl::nullopt; |
| |
| absl::optional<base::TimeDelta> expiry; |
| |
| if (const std::string* s = value->GetIfString()) { |
| int64_t seconds = 0; |
| if (base::StringToInt64(*s, &seconds)) |
| expiry = base::Seconds(seconds); |
| } |
| |
| if (!expiry || *expiry < base::TimeDelta()) { |
| *Error() << "must be a positive number of seconds formatted as a " |
| "base-10 string"; |
| } |
| |
| return expiry; |
| } |
| |
| absl::uint128 ParseAggregationKey(const base::Value& key_value) { |
| const std::string* s = key_value.GetIfString(); |
| |
| absl::uint128 value = 0; |
| if (!s || |
| !base::StartsWith(*s, "0x", base::CompareCase::INSENSITIVE_ASCII) || |
| !base::HexStringToUInt128(*s, &value)) { |
| *Error() << "must be a uint128 formatted as a base-16 string"; |
| } |
| |
| return value; |
| } |
| |
| AttributionAggregationKeys ParseAggregationKeys( |
| const base::Value::Dict& cfg) { |
| static constexpr char kKey[] = "aggregation_keys"; |
| |
| const base::Value* value = cfg.Find(kKey); |
| if (!value) |
| return AttributionAggregationKeys(); |
| |
| auto context = PushContext(kKey); |
| |
| if (!EnsureDictionary(*value)) |
| return AttributionAggregationKeys(); |
| |
| AttributionAggregationKeys::Keys::container_type keys; |
| |
| for (auto [id, key_value] : value->GetDict()) { |
| auto key_context = PushContext(id); |
| absl::uint128 key = ParseAggregationKey(key_value); |
| if (!has_error()) |
| keys.emplace_back(std::move(id), key); |
| } |
| |
| absl::optional<AttributionAggregationKeys> aggregation_keys = |
| AttributionAggregationKeys::FromKeys(std::move(keys)); |
| if (!aggregation_keys) |
| *Error() << "invalid"; |
| |
| return std::move(aggregation_keys).value_or(AttributionAggregationKeys()); |
| } |
| |
| base::flat_set<std::string> ParseAggregatableTriggerDataSourceKeys( |
| const base::Value::Dict& dict) { |
| static constexpr char kKey[] = "source_keys"; |
| |
| base::flat_set<std::string> source_keys; |
| |
| auto context = PushContext(kKey); |
| |
| const base::Value* values = dict.Find(kKey); |
| if (!values) { |
| *Error() << "must be present"; |
| return source_keys; |
| } |
| |
| ParseList(*values, |
| base::BindLambdaForTesting([&](const base::Value& value) { |
| if (!value.is_string()) { |
| *Error() << "must be a string"; |
| } else { |
| source_keys.emplace(value.GetString()); |
| } |
| })); |
| |
| return source_keys; |
| } |
| |
| std::vector<AttributionAggregatableTriggerData> ParseAggregatableTriggerData( |
| const base::Value::Dict& dict) { |
| static constexpr char kKey[] = "aggregatable_trigger_data"; |
| |
| std::vector<AttributionAggregatableTriggerData> aggregatable_triggers; |
| |
| const base::Value* values = dict.Find(kKey); |
| if (!values) |
| return aggregatable_triggers; |
| |
| auto context = PushContext(kKey); |
| ParseList( |
| *values, |
| base::BindLambdaForTesting( |
| [&](const base::Value& aggregatable_trigger) { |
| if (!EnsureDictionary(aggregatable_trigger)) |
| return; |
| |
| const base::Value::Dict& trigger_dict = |
| aggregatable_trigger.GetDict(); |
| |
| base::flat_set<std::string> source_keys = |
| ParseAggregatableTriggerDataSourceKeys(trigger_dict); |
| |
| absl::uint128 key; |
| { |
| static constexpr char kKeyKeyPiece[] = "key_piece"; |
| auto key_context = PushContext(kKeyKeyPiece); |
| const base::Value* key_piece = trigger_dict.Find(kKeyKeyPiece); |
| if (key_piece) { |
| key = ParseAggregationKey(*key_piece); |
| } else { |
| *Error() << "must be present"; |
| } |
| } |
| |
| AttributionFilterData filters = ParseFilterData( |
| trigger_dict, "filters", |
| &AttributionFilterData::FromTriggerFilterValues); |
| |
| AttributionFilterData not_filters = ParseFilterData( |
| trigger_dict, "not_filters", |
| &AttributionFilterData::FromTriggerFilterValues); |
| |
| auto trigger_data = AttributionAggregatableTriggerData::Create( |
| key, std::move(source_keys), std::move(filters), |
| std::move(not_filters)); |
| if (!trigger_data) |
| *Error() << "invalid"; |
| |
| if (has_error()) |
| return; |
| |
| aggregatable_triggers.push_back(std::move(*trigger_data)); |
| }), |
| blink::kMaxAttributionAggregatableTriggerDataPerTrigger); |
| |
| return aggregatable_triggers; |
| } |
| |
| AttributionAggregatableValues ParseAggregatableValues( |
| const base::Value::Dict& dict) { |
| static constexpr char kKey[] = "aggregatable_values"; |
| |
| const base::Value* value = dict.Find(kKey); |
| if (!value) |
| return AttributionAggregatableValues(); |
| |
| auto context = PushContext(kKey); |
| |
| if (!EnsureDictionary(*value)) |
| return AttributionAggregatableValues(); |
| |
| AttributionAggregatableValues::Values::container_type container; |
| |
| for (auto [id, key_value] : value->GetDict()) { |
| auto key_context = PushContext(id); |
| if (!key_value.is_int() || key_value.GetInt() <= 0) { |
| *Error() << "must be a positive integer"; |
| } else { |
| container.emplace_back(id, key_value.GetInt()); |
| } |
| } |
| |
| absl::optional<AttributionAggregatableValues> aggregatable_values = |
| AttributionAggregatableValues::FromValues(std::move(container)); |
| if (!aggregatable_values.has_value()) |
| *Error() << "invalid"; |
| |
| return aggregatable_values.value_or(AttributionAggregatableValues()); |
| } |
| |
| bool EnsureDictionary(const base::Value& value) { |
| if (!value.is_dict()) { |
| *Error() << "must be a dictionary"; |
| return false; |
| } |
| return true; |
| } |
| }; |
| |
| } // namespace |
| |
| AttributionDataClear::AttributionDataClear( |
| base::Time time, |
| base::Time delete_begin, |
| base::Time delete_end, |
| absl::optional<base::flat_set<url::Origin>> origins) |
| : time(time), |
| delete_begin(delete_begin), |
| delete_end(delete_end), |
| origins(std::move(origins)) {} |
| |
| AttributionDataClear::~AttributionDataClear() = default; |
| |
| AttributionDataClear::AttributionDataClear(const AttributionDataClear&) = |
| default; |
| |
| AttributionDataClear::AttributionDataClear(AttributionDataClear&&) = default; |
| |
| AttributionDataClear& AttributionDataClear::operator=( |
| const AttributionDataClear&) = default; |
| |
| AttributionDataClear& AttributionDataClear::operator=(AttributionDataClear&&) = |
| default; |
| |
| absl::optional<AttributionSimulationEventAndValues> |
| ParseAttributionSimulationInput(base::Value input, |
| const base::Time offset_time, |
| std::ostream& error_stream) { |
| return AttributionSimulatorInputParser(offset_time, error_stream) |
| .Parse(std::move(input)); |
| } |
| |
| } // namespace content |