1 // Copyright 2022 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "net/first_party_sets/global_first_party_sets.h"
6
7 #include <iterator>
8 #include <map>
9 #include <optional>
10 #include <set>
11 #include <tuple>
12 #include <utility>
13
14 #include "base/containers/contains.h"
15 #include "base/containers/flat_map.h"
16 #include "base/containers/flat_set.h"
17 #include "base/functional/function_ref.h"
18 #include "base/not_fatal_until.h"
19 #include "base/ranges/algorithm.h"
20 #include "base/types/optional_ref.h"
21 #include "net/base/schemeful_site.h"
22 #include "net/first_party_sets/addition_overlaps_union_find.h"
23 #include "net/first_party_sets/first_party_set_entry.h"
24 #include "net/first_party_sets/first_party_set_entry_override.h"
25 #include "net/first_party_sets/first_party_set_metadata.h"
26 #include "net/first_party_sets/first_party_sets_context_config.h"
27 #include "net/first_party_sets/first_party_sets_validator.h"
28 #include "net/first_party_sets/local_set_declaration.h"
29
30 namespace net {
31
32 namespace {
33
34 using FlattenedSets = base::flat_map<SchemefulSite, FirstPartySetEntry>;
35 using SingleSet = base::flat_map<SchemefulSite, FirstPartySetEntry>;
36
37 // Converts a list of First-Party Sets from a SingleSet to a FlattenedSet
38 // representation.
Flatten(const std::vector<SingleSet> & set_list)39 FlattenedSets Flatten(const std::vector<SingleSet>& set_list) {
40 FlattenedSets sets;
41 for (const auto& set : set_list) {
42 for (const auto& site_and_entry : set) {
43 bool inserted = sets.emplace(site_and_entry).second;
44 CHECK(inserted);
45 }
46 }
47 return sets;
48 }
49
50 std::pair<SchemefulSite, FirstPartySetEntryOverride>
SiteAndEntryToSiteAndOverride(const std::pair<SchemefulSite,FirstPartySetEntry> & pair)51 SiteAndEntryToSiteAndOverride(
52 const std::pair<SchemefulSite, FirstPartySetEntry>& pair) {
53 return std::make_pair(pair.first, FirstPartySetEntryOverride(pair.second));
54 }
55
56 } // namespace
57
58 GlobalFirstPartySets::GlobalFirstPartySets() = default;
59
GlobalFirstPartySets(base::Version public_sets_version,base::flat_map<SchemefulSite,FirstPartySetEntry> entries,base::flat_map<SchemefulSite,SchemefulSite> aliases)60 GlobalFirstPartySets::GlobalFirstPartySets(
61 base::Version public_sets_version,
62 base::flat_map<SchemefulSite, FirstPartySetEntry> entries,
63 base::flat_map<SchemefulSite, SchemefulSite> aliases)
64 : GlobalFirstPartySets(
65 public_sets_version,
66 public_sets_version.IsValid()
67 ? std::move(entries)
68 : base::flat_map<SchemefulSite, FirstPartySetEntry>(),
69 public_sets_version.IsValid()
70 ? std::move(aliases)
71 : base::flat_map<SchemefulSite, SchemefulSite>(),
72 FirstPartySetsContextConfig(),
73 base::flat_map<SchemefulSite, SchemefulSite>()) {}
74
GlobalFirstPartySets(base::Version public_sets_version,base::flat_map<SchemefulSite,FirstPartySetEntry> entries,base::flat_map<SchemefulSite,SchemefulSite> aliases,FirstPartySetsContextConfig manual_config,base::flat_map<SchemefulSite,SchemefulSite> manual_aliases)75 GlobalFirstPartySets::GlobalFirstPartySets(
76 base::Version public_sets_version,
77 base::flat_map<SchemefulSite, FirstPartySetEntry> entries,
78 base::flat_map<SchemefulSite, SchemefulSite> aliases,
79 FirstPartySetsContextConfig manual_config,
80 base::flat_map<SchemefulSite, SchemefulSite> manual_aliases)
81 : public_sets_version_(std::move(public_sets_version)),
82 entries_(std::move(entries)),
83 aliases_(std::move(aliases)),
84 manual_config_(std::move(manual_config)),
85 manual_aliases_(std::move(manual_aliases)) {
86 if (!public_sets_version_.IsValid()) {
87 CHECK(entries_.empty());
88 CHECK(aliases_.empty());
89 }
90
91 CHECK(base::ranges::all_of(aliases_, [&](const auto& pair) {
92 return entries_.contains(pair.second);
93 }));
94 CHECK(IsValid(), base::NotFatalUntil::M130) << "Sets must be valid";
95 }
96
97 GlobalFirstPartySets::GlobalFirstPartySets(GlobalFirstPartySets&&) = default;
98 GlobalFirstPartySets& GlobalFirstPartySets::operator=(GlobalFirstPartySets&&) =
99 default;
100
101 GlobalFirstPartySets::~GlobalFirstPartySets() = default;
102
103 bool GlobalFirstPartySets::operator==(const GlobalFirstPartySets& other) const =
104 default;
105
106 bool GlobalFirstPartySets::operator!=(const GlobalFirstPartySets& other) const =
107 default;
108
Clone() const109 GlobalFirstPartySets GlobalFirstPartySets::Clone() const {
110 return GlobalFirstPartySets(public_sets_version_, entries_, aliases_,
111 manual_config_.Clone(), manual_aliases_);
112 }
113
FindEntry(const SchemefulSite & site,const FirstPartySetsContextConfig & config) const114 std::optional<FirstPartySetEntry> GlobalFirstPartySets::FindEntry(
115 const SchemefulSite& site,
116 const FirstPartySetsContextConfig& config) const {
117 return FindEntry(site, &config);
118 }
119
FindEntry(const SchemefulSite & site,const FirstPartySetsContextConfig * config) const120 std::optional<FirstPartySetEntry> GlobalFirstPartySets::FindEntry(
121 const SchemefulSite& site,
122 const FirstPartySetsContextConfig* config) const {
123 // Check if `site` can be found in the customizations first.
124 if (config) {
125 if (const auto override = config->FindOverride(site);
126 override.has_value()) {
127 return override->IsDeletion() ? std::nullopt
128 : std::make_optional(override->GetEntry());
129 }
130 }
131
132 // Now see if it's in the manual config (with or without a manual alias).
133 if (const auto manual_override = manual_config_.FindOverride(site);
134 manual_override.has_value()) {
135 return manual_override->IsDeletion()
136 ? std::nullopt
137 : std::make_optional(manual_override->GetEntry());
138 }
139
140 // Finally, look up in `entries_`, applying an alias if applicable.
141 const auto canonical_it = aliases_.find(site);
142 const SchemefulSite& canonical_site =
143 canonical_it == aliases_.end() ? site : canonical_it->second;
144 if (const auto entry_it = entries_.find(canonical_site);
145 entry_it != entries_.end()) {
146 return entry_it->second;
147 }
148
149 return std::nullopt;
150 }
151
152 base::flat_map<SchemefulSite, FirstPartySetEntry>
FindEntries(const base::flat_set<SchemefulSite> & sites,const FirstPartySetsContextConfig & config) const153 GlobalFirstPartySets::FindEntries(
154 const base::flat_set<SchemefulSite>& sites,
155 const FirstPartySetsContextConfig& config) const {
156 std::vector<std::pair<SchemefulSite, FirstPartySetEntry>> sites_to_entries;
157 for (const SchemefulSite& site : sites) {
158 const std::optional<FirstPartySetEntry> entry = FindEntry(site, config);
159 if (entry.has_value()) {
160 sites_to_entries.emplace_back(site, entry.value());
161 }
162 }
163 return sites_to_entries;
164 }
165
ComputeMetadata(const SchemefulSite & site,base::optional_ref<const SchemefulSite> top_frame_site,const FirstPartySetsContextConfig & fps_context_config) const166 FirstPartySetMetadata GlobalFirstPartySets::ComputeMetadata(
167 const SchemefulSite& site,
168 base::optional_ref<const SchemefulSite> top_frame_site,
169 const FirstPartySetsContextConfig& fps_context_config) const {
170 return FirstPartySetMetadata(
171 FindEntry(site, fps_context_config),
172 top_frame_site ? FindEntry(*top_frame_site, fps_context_config)
173 : std::nullopt);
174 }
175
ApplyManuallySpecifiedSet(const LocalSetDeclaration & local_set_declaration)176 void GlobalFirstPartySets::ApplyManuallySpecifiedSet(
177 const LocalSetDeclaration& local_set_declaration) {
178 CHECK(manual_config_.empty());
179 CHECK(manual_aliases_.empty());
180 if (local_set_declaration.empty()) {
181 // Nothing to do.
182 return;
183 }
184
185 base::flat_map<SchemefulSite, SchemefulSite> manual_aliases =
186 local_set_declaration.aliases();
187
188 base::flat_map<SchemefulSite, FirstPartySetEntry> manual_entries =
189 local_set_declaration.entries();
190 for (const auto& [alias, canonical] : manual_aliases) {
191 manual_entries.emplace(alias, manual_entries.find(canonical)->second);
192 }
193
194 // We handle the manually-specified set the same way as we handle
195 // replacement enterprise policy sets.
196 manual_config_ = ComputeConfig(SetsMutation(
197 /*replacement_sets=*/{manual_entries},
198 /*addition_sets=*/{}));
199 manual_aliases_ = std::move(manual_aliases);
200
201 CHECK(IsValid(), base::NotFatalUntil::M130) << "Sets must be valid";
202 }
203
UnsafeSetManualConfig(FirstPartySetsContextConfig manual_config)204 void GlobalFirstPartySets::UnsafeSetManualConfig(
205 FirstPartySetsContextConfig manual_config) {
206 CHECK(manual_config_.empty());
207 manual_config_ = std::move(manual_config);
208 }
209
210 base::flat_map<SchemefulSite, FirstPartySetEntry>
FindPrimariesAffectedByAdditions(const FlattenedSets & additions) const211 GlobalFirstPartySets::FindPrimariesAffectedByAdditions(
212 const FlattenedSets& additions) const {
213 std::vector<std::pair<SchemefulSite, FirstPartySetEntry>>
214 addition_intersected_primaries;
215 for (const auto& [new_member, new_entry] : additions) {
216 if (const auto entry = FindEntry(new_member, /*config=*/nullptr);
217 entry.has_value()) {
218 // Found an overlap with the existing list of sets.
219 addition_intersected_primaries.emplace_back(entry->primary(), new_entry);
220 }
221 }
222 return addition_intersected_primaries;
223 }
224
225 std::pair<base::flat_map<SchemefulSite, base::flat_set<SchemefulSite>>,
226 base::flat_set<SchemefulSite>>
FindPrimariesAffectedByReplacements(const FlattenedSets & replacements,const FlattenedSets & additions,const base::flat_map<SchemefulSite,FirstPartySetEntry> & addition_intersected_primaries) const227 GlobalFirstPartySets::FindPrimariesAffectedByReplacements(
228 const FlattenedSets& replacements,
229 const FlattenedSets& additions,
230 const base::flat_map<SchemefulSite, FirstPartySetEntry>&
231 addition_intersected_primaries) const {
232 if (replacements.empty()) {
233 return {{}, {}};
234 }
235
236 const auto canonicalize = [&](const SchemefulSite& site) {
237 const auto it = aliases_.find(site);
238 return it != aliases_.end() ? it->second : site;
239 };
240 std::map<SchemefulSite, std::set<SchemefulSite>> canonical_to_aliases;
241 ForEachAlias([&](const SchemefulSite& alias, const SchemefulSite& canonical) {
242 canonical_to_aliases[canonical].insert(alias);
243 });
244 // Runs the given FunctionRef for all (existing) variants of the given site,
245 // i.e. all the aliases and the "canonical" variant.
246 const auto for_all_variants =
247 [canonical_to_aliases = std::move(canonical_to_aliases),
248 canonicalize = std::move(canonicalize)](
249 const SchemefulSite& site,
250 const base::FunctionRef<void(const SchemefulSite&)> f) {
251 const SchemefulSite canonical = canonicalize(site);
252 f(canonical);
253 if (const auto it = canonical_to_aliases.find(canonical);
254 it != canonical_to_aliases.end()) {
255 for (const auto& alias : it->second) {
256 f(alias);
257 }
258 }
259 };
260
261 // Maps an existing primary site to the members it lost due to replacement.
262 base::flat_map<SchemefulSite, base::flat_set<SchemefulSite>>
263 potential_singletons;
264 // Stores existing primary sites which have left their sets (via
265 // replacement), and whose existing members should be removed from the set
266 // (excluding any custom sets that those members are involved in).
267 base::flat_set<SchemefulSite> replaced_existing_primaries;
268 for (const auto& [new_site, unused_entry] : replacements) {
269 const auto existing_entry = FindEntry(new_site, /*config=*/nullptr);
270 if (!existing_entry.has_value()) {
271 continue;
272 }
273 if (!addition_intersected_primaries.contains(existing_entry->primary()) &&
274 !additions.contains(existing_entry->primary()) &&
275 !replacements.contains(existing_entry->primary())) {
276 // The existing site's primary isn't involved in any of the customized
277 // sets, so it might become a singleton (if all of its variants and
278 // non-primaries [and their variants] are replaced by the
279 // customizations).
280 for_all_variants(new_site, [&](const SchemefulSite& variant) {
281 if (existing_entry->primary() != variant) {
282 potential_singletons[existing_entry->primary()].insert(variant);
283 }
284 });
285 }
286
287 if (existing_entry->primary() == new_site) {
288 // `new_site` was a primary in the existing sets, but is in the
289 // replacement sets, so its non-primaries (and aliases) might need to be
290 // deleted/hidden.
291 bool inserted =
292 replaced_existing_primaries.emplace(existing_entry->primary()).second;
293 CHECK(inserted);
294 }
295 }
296
297 return std::make_pair(potential_singletons, replaced_existing_primaries);
298 }
299
ComputeConfig(const SetsMutation & mutation) const300 FirstPartySetsContextConfig GlobalFirstPartySets::ComputeConfig(
301 const SetsMutation& mutation) const {
302 if (base::ranges::all_of(mutation.replacements(), &SingleSet::empty) &&
303 base::ranges::all_of(mutation.additions(), &SingleSet::empty)) {
304 // Nothing to do.
305 return FirstPartySetsContextConfig();
306 }
307
308 const FlattenedSets replacements = Flatten(mutation.replacements());
309 const FlattenedSets additions =
310 Flatten(NormalizeAdditionSets(mutation.additions()));
311
312 // Maps a site to its override.
313 std::vector<std::pair<SchemefulSite, FirstPartySetEntryOverride>>
314 site_to_override;
315 base::ranges::transform(replacements, std::back_inserter(site_to_override),
316 SiteAndEntryToSiteAndOverride);
317 base::ranges::transform(additions, std::back_inserter(site_to_override),
318 SiteAndEntryToSiteAndOverride);
319
320 // Maps old primary site to new entry.
321 const base::flat_map<SchemefulSite, FirstPartySetEntry>
322 addition_intersected_primaries =
323 FindPrimariesAffectedByAdditions(additions);
324
325 auto [potential_singletons, replaced_existing_primaries] =
326 FindPrimariesAffectedByReplacements(replacements, additions,
327 addition_intersected_primaries);
328
329 if (!addition_intersected_primaries.empty() ||
330 !potential_singletons.empty() || !replaced_existing_primaries.empty()) {
331 // Find out which potential singletons are actually singletons; delete
332 // members whose primaries left; and reparent the sets that intersected with
333 // an addition set.
334 // Note: use a null config here, to avoid taking unrelated policy sets into
335 // account.
336 ForEachEffectiveSetEntry(
337 /*config=*/nullptr,
338 [&](const SchemefulSite& member, const FirstPartySetEntry& set_entry) {
339 // Reparent all sites in any intersecting addition sets.
340 if (const auto entry =
341 addition_intersected_primaries.find(set_entry.primary());
342 entry != addition_intersected_primaries.end() &&
343 !replacements.contains(member)) {
344 site_to_override.emplace_back(
345 member, FirstPartySetEntry(entry->second.primary(),
346 member == entry->second.primary()
347 ? SiteType::kPrimary
348 : SiteType::kAssociated,
349 std::nullopt));
350 }
351 if (member == set_entry.primary())
352 return true;
353 // Remove non-singletons from the potential list.
354 if (const auto entry = potential_singletons.find(set_entry.primary());
355 entry != potential_singletons.end() &&
356 !entry->second.contains(member)) {
357 // This primary lost members, but it still has at least one
358 // (`member`), so it's not a singleton.
359 potential_singletons.erase(entry);
360 }
361 // Remove members from sets whose primary left.
362 if (replaced_existing_primaries.contains(set_entry.primary()) &&
363 !replacements.contains(member) &&
364 !addition_intersected_primaries.contains(set_entry.primary())) {
365 site_to_override.emplace_back(member, FirstPartySetEntryOverride());
366 }
367
368 return true;
369 });
370
371 // Any primary remaining in `potential_singleton` is a real singleton, so
372 // delete it:
373 for (const auto& [primary, members] : potential_singletons) {
374 site_to_override.emplace_back(primary, FirstPartySetEntryOverride());
375 }
376 }
377
378 // For every pre-existing alias that would now refer to a site in the overlay,
379 // which is not already contained in the overlay, we explicitly ignore that
380 // alias.
381 ForEachAlias([&](const SchemefulSite& alias, const SchemefulSite& canonical) {
382 if (base::Contains(
383 site_to_override, canonical,
384 &std::pair<SchemefulSite, FirstPartySetEntryOverride>::first) &&
385 !base::Contains(
386 site_to_override, alias,
387 &std::pair<SchemefulSite, FirstPartySetEntryOverride>::first)) {
388 site_to_override.emplace_back(alias, FirstPartySetEntryOverride());
389 }
390 });
391
392 FirstPartySetsContextConfig config(std::move(site_to_override));
393 CHECK(IsValid(&config), base::NotFatalUntil::M130)
394 << "Sets must not contain singleton or orphan";
395 return config;
396 }
397
398 std::vector<base::flat_map<SchemefulSite, FirstPartySetEntry>>
NormalizeAdditionSets(const std::vector<base::flat_map<SchemefulSite,FirstPartySetEntry>> & addition_sets) const399 GlobalFirstPartySets::NormalizeAdditionSets(
400 const std::vector<base::flat_map<SchemefulSite, FirstPartySetEntry>>&
401 addition_sets) const {
402 if (base::ranges::all_of(addition_sets, &SingleSet::empty)) {
403 // Nothing to do.
404 return {};
405 }
406
407 // Find all the addition sets that intersect with any given public set.
408 base::flat_map<SchemefulSite, base::flat_set<size_t>> addition_set_overlaps;
409 for (size_t set_idx = 0; set_idx < addition_sets.size(); set_idx++) {
410 for (const auto& site_and_entry : addition_sets[set_idx]) {
411 if (const auto entry =
412 FindEntry(site_and_entry.first, /*config=*/nullptr);
413 entry.has_value()) {
414 addition_set_overlaps[entry->primary()].insert(set_idx);
415 }
416 }
417 }
418
419 // Union together all transitively-overlapping addition sets.
420 AdditionOverlapsUnionFind union_finder(addition_sets.size());
421 for (const auto& [public_site, addition_set_indices] :
422 addition_set_overlaps) {
423 for (size_t representative : addition_set_indices) {
424 union_finder.Union(*addition_set_indices.begin(), representative);
425 }
426 }
427
428 // Now build the new addition sets, with all transitive overlaps eliminated.
429 std::vector<SingleSet> normalized_additions;
430 for (const auto& [rep, children] : union_finder.SetsMapping()) {
431 SingleSet normalized = addition_sets[rep];
432 const SchemefulSite& rep_primary =
433 addition_sets[rep].begin()->second.primary();
434 for (size_t child_set_idx : children) {
435 for (const auto& child_site_and_entry : addition_sets[child_set_idx]) {
436 bool inserted =
437 normalized
438 .emplace(child_site_and_entry.first,
439 FirstPartySetEntry(rep_primary, SiteType::kAssociated,
440 std::nullopt))
441 .second;
442 CHECK(inserted);
443 }
444 }
445 normalized_additions.push_back(normalized);
446 }
447 return normalized_additions;
448 }
449
ForEachPublicSetEntry(base::FunctionRef<bool (const SchemefulSite &,const FirstPartySetEntry &)> f) const450 bool GlobalFirstPartySets::ForEachPublicSetEntry(
451 base::FunctionRef<bool(const SchemefulSite&, const FirstPartySetEntry&)> f)
452 const {
453 for (const auto& [site, entry] : entries_) {
454 if (!f(site, entry))
455 return false;
456 }
457 for (const auto& [alias, canonical] : aliases_) {
458 auto it = entries_.find(canonical);
459 CHECK(it != entries_.end());
460 if (!f(alias, it->second))
461 return false;
462 }
463 return true;
464 }
465
ForEachManualConfigEntry(base::FunctionRef<bool (const SchemefulSite &,const FirstPartySetEntryOverride &)> f) const466 bool GlobalFirstPartySets::ForEachManualConfigEntry(
467 base::FunctionRef<bool(const SchemefulSite&,
468 const FirstPartySetEntryOverride&)> f) const {
469 return manual_config_.ForEachCustomizationEntry(f);
470 }
471
ForEachEffectiveSetEntry(const FirstPartySetsContextConfig & config,base::FunctionRef<bool (const SchemefulSite &,const FirstPartySetEntry &)> f) const472 bool GlobalFirstPartySets::ForEachEffectiveSetEntry(
473 const FirstPartySetsContextConfig& config,
474 base::FunctionRef<bool(const SchemefulSite&, const FirstPartySetEntry&)> f)
475 const {
476 return ForEachEffectiveSetEntry(&config, f);
477 }
478
ForEachEffectiveSetEntry(const FirstPartySetsContextConfig * config,base::FunctionRef<bool (const SchemefulSite &,const FirstPartySetEntry &)> f) const479 bool GlobalFirstPartySets::ForEachEffectiveSetEntry(
480 const FirstPartySetsContextConfig* config,
481 base::FunctionRef<bool(const SchemefulSite&, const FirstPartySetEntry&)> f)
482 const {
483 // Policy sets have highest precedence:
484 if (config != nullptr) {
485 if (!config->ForEachCustomizationEntry(
486 [&](const SchemefulSite& site,
487 const FirstPartySetEntryOverride& override) {
488 if (!override.IsDeletion())
489 return f(site, override.GetEntry());
490 return true;
491 })) {
492 return false;
493 }
494 }
495
496 // Then the manual set:
497 if (!manual_config_.ForEachCustomizationEntry(
498 [&](const SchemefulSite& site,
499 const FirstPartySetEntryOverride& override) {
500 if (!override.IsDeletion() && (!config || !config->Contains(site)))
501 return f(site, override.GetEntry());
502 return true;
503 })) {
504 return false;
505 }
506
507 // Finally, the public sets.
508 return ForEachPublicSetEntry([&](const SchemefulSite& site,
509 const FirstPartySetEntry& entry) {
510 if ((!config || !config->Contains(site)) && !manual_config_.Contains(site))
511 return f(site, entry);
512 return true;
513 });
514 }
515
ForEachAlias(base::FunctionRef<void (const SchemefulSite &,const SchemefulSite &)> f) const516 void GlobalFirstPartySets::ForEachAlias(
517 base::FunctionRef<void(const SchemefulSite&, const SchemefulSite&)> f)
518 const {
519 for (const auto& [alias, site] : manual_aliases_) {
520 f(alias, site);
521 }
522 for (const auto& [alias, site] : aliases_) {
523 if (manual_config_.Contains(alias)) {
524 continue;
525 }
526 f(alias, site);
527 }
528 }
529
IsValid(const FirstPartySetsContextConfig * config) const530 bool GlobalFirstPartySets::IsValid(
531 const FirstPartySetsContextConfig* config) const {
532 FirstPartySetsValidator validator;
533 ForEachEffectiveSetEntry(
534 config,
535 [&](const SchemefulSite& site, const FirstPartySetEntry& entry) -> bool {
536 validator.Update(site, entry.primary());
537 return true;
538 });
539
540 return validator.IsValid();
541 }
542
operator <<(std::ostream & os,const GlobalFirstPartySets & sets)543 std::ostream& operator<<(std::ostream& os, const GlobalFirstPartySets& sets) {
544 os << "{entries = {";
545 for (const auto& [site, entry] : sets.entries_) {
546 os << "{" << site.Serialize() << ": " << entry << "}, ";
547 }
548 os << "}, aliases = {";
549 for (const auto& [alias, canonical] : sets.aliases_) {
550 os << "{" << alias.Serialize() << ": " << canonical.Serialize() << "}, ";
551 }
552 os << "}, manual_config = {";
553 sets.ForEachManualConfigEntry(
554 [&](const net::SchemefulSite& site,
555 const FirstPartySetEntryOverride& override) {
556 os << "{" << site.Serialize() << ": " << override << "},";
557 return true;
558 });
559 os << "}, manual_aliases = {";
560 for (const auto& [alias, canonical] : sets.manual_aliases_) {
561 os << "{" << alias.Serialize() << ": " << canonical.Serialize() << "}, ";
562 }
563 os << "}}";
564 return os;
565 }
566
567 } // namespace net
568