diff --git a/patches/chromium/.patches b/patches/chromium/.patches index f647cb9d31ac3..1c23eb660d1b5 100644 --- a/patches/chromium/.patches +++ b/patches/chromium/.patches @@ -104,3 +104,5 @@ don_t_run_pcscan_notifythreadcreated_if_pcscan_is_disabled.patch logging_win32_only_create_a_console_if_logging_to_stderr.patch feat_expose_raw_response_headers_from_urlloader.patch fix_media_key_usage_with_globalshortcuts.patch +cherry-pick-ec42dfd3545f.patch +cherry-pick-39090918efac.patch diff --git a/patches/chromium/cherry-pick-39090918efac.patch b/patches/chromium/cherry-pick-39090918efac.patch new file mode 100644 index 0000000000000..93c258432be49 --- /dev/null +++ b/patches/chromium/cherry-pick-39090918efac.patch @@ -0,0 +1,457 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: cfredric +Date: Mon, 27 Sep 2021 22:14:18 +0000 +Subject: Consider HTTPS and WSS schemes identically for FPS. + +This modifies the FPS implementation to normalize wss:// URLs into +https:// URLs when determining the same-partiness of a request. + +This allows SameParty cookies to be sent on same-party WSS connection +requests. A browsertest is included to verify this. + +Bug: 1251688 +Change-Id: Id277288982805e0d29c6683e0c13d4b7c7cfe359 +Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3182786 +Reviewed-by: Maksim Orlovich +Reviewed-by: Shuran Huang +Commit-Queue: Chris Fredrickson +Cr-Commit-Position: refs/heads/main@{#925457} + +diff --git a/chrome/browser/net/websocket_browsertest.cc b/chrome/browser/net/websocket_browsertest.cc +index 0714f0d0231d677edd0f0cdf82f4129ddc43a5c2..6f2f101743fbd470bafe90d7e5d14351ee0ff708 100644 +--- a/chrome/browser/net/websocket_browsertest.cc ++++ b/chrome/browser/net/websocket_browsertest.cc +@@ -21,6 +21,7 @@ + #include "base/test/bind.h" + #include "build/build_config.h" + #include "chrome/browser/chrome_notification_types.h" ++#include "chrome/browser/profiles/profile.h" + #include "chrome/browser/ui/browser.h" + #include "chrome/browser/ui/login/login_handler.h" + #include "chrome/browser/ui/login/login_handler_test_utils.h" +@@ -45,25 +46,31 @@ + #include "mojo/public/cpp/system/data_pipe.h" + #include "net/base/network_isolation_key.h" + #include "net/cookies/site_for_cookies.h" ++#include "net/dns/mock_host_resolver.h" + #include "net/test/embedded_test_server/embedded_test_server.h" + #include "net/test/spawned_test_server/spawned_test_server.h" + #include "net/test/test_data_directory.h" + #include "net/traffic_annotation/network_traffic_annotation_test_helper.h" ++#include "services/network/public/cpp/network_switches.h" + #include "services/network/public/mojom/network_context.mojom.h" + #include "services/network/public/mojom/websocket.mojom.h" ++#include "testing/gmock/include/gmock/gmock.h" + #include "testing/gtest/include/gtest/gtest.h" + #include "url/gurl.h" + #include "url/origin.h" + + namespace { + ++using SSLOptions = net::SpawnedTestServer::SSLOptions; ++ + class WebSocketBrowserTest : public InProcessBrowserTest { + public: +- WebSocketBrowserTest() ++ explicit WebSocketBrowserTest( ++ SSLOptions::ServerCertificate cert = SSLOptions::CERT_OK) + : ws_server_(net::SpawnedTestServer::TYPE_WS, + net::GetWebSocketTestDataDirectory()), + wss_server_(net::SpawnedTestServer::TYPE_WSS, +- SSLOptions(SSLOptions::CERT_OK), ++ SSLOptions(cert), + net::GetWebSocketTestDataDirectory()) {} + + protected: +@@ -145,7 +152,6 @@ class WebSocketBrowserTest : public InProcessBrowserTest { + net::SpawnedTestServer wss_server_; + + private: +- typedef net::SpawnedTestServer::SSLOptions SSLOptions; + std::unique_ptr watcher_; + + DISALLOW_COPY_AND_ASSIGN(WebSocketBrowserTest); +@@ -162,37 +168,72 @@ class WebSocketBrowserTestWithAllowFileAccessFromFiles + }; + + // Framework for tests using the connect_to.html page served by a separate HTTP +-// server. ++// or HTTPS server. + class WebSocketBrowserConnectToTest : public WebSocketBrowserTest { + protected: +- WebSocketBrowserConnectToTest() { +- http_server_.ServeFilesFromSourceDirectory( +- net::GetWebSocketTestDataDirectory()); +- } ++ explicit WebSocketBrowserConnectToTest( ++ SSLOptions::ServerCertificate cert = SSLOptions::CERT_OK) ++ : WebSocketBrowserTest(cert) {} + + // The title watcher and HTTP server are set up automatically by the test + // framework. Each test case still needs to configure and start the + // WebSocket server(s) it needs. + void SetUpOnMainThread() override { ++ server().ServeFilesFromSourceDirectory( ++ net::GetWebSocketTestDataDirectory()); + WebSocketBrowserTest::SetUpOnMainThread(); +- ASSERT_TRUE(http_server_.Start()); ++ ASSERT_TRUE(server().Start()); + } + +- // Supply a ws: or wss: URL to connect to. +- void ConnectTo(GURL url) { +- ASSERT_TRUE(http_server_.Started()); ++ // Supply a ws: or wss: URL to connect to. Serves connect_to.html from the ++ // server's default host. ++ void ConnectTo(const GURL& url) { ++ ConnectTo(server().base_url().host(), url); ++ } ++ ++ // Supply a ws: or wss: URL to connect to via loading `host`/connect_to.html. ++ void ConnectTo(const std::string& host, const GURL& url) { ++ ASSERT_TRUE(server().Started()); + std::string query("url=" + url.spec()); + GURL::Replacements replacements; + replacements.SetQueryStr(query); +- ui_test_utils::NavigateToURL(browser(), +- http_server_.GetURL("/connect_to.html") +- .ReplaceComponents(replacements)); ++ ASSERT_TRUE(ui_test_utils::NavigateToURL( ++ browser(), server() ++ .GetURL(host, "/connect_to.html") ++ .ReplaceComponents(replacements))); + } + +- private: ++ virtual net::EmbeddedTestServer& server() = 0; ++}; ++ ++// Concrete impl for tests that use connect_to.html over HTTP. ++class WebSocketBrowserHTTPConnectToTest : public WebSocketBrowserConnectToTest { ++ protected: ++ net::EmbeddedTestServer& server() override { return http_server_; } ++ + net::EmbeddedTestServer http_server_; + }; + ++// Concrete impl for tests that use connect_to.html over HTTPS. ++class WebSocketBrowserHTTPSConnectToTest ++ : public WebSocketBrowserConnectToTest { ++ protected: ++ explicit WebSocketBrowserHTTPSConnectToTest( ++ SSLOptions::ServerCertificate cert = SSLOptions::CERT_OK) ++ : WebSocketBrowserConnectToTest(cert), ++ https_server_(net::test_server::EmbeddedTestServer::TYPE_HTTPS) {} ++ ++ void SetUpOnMainThread() override { ++ host_resolver()->AddRule("*", "127.0.0.1"); ++ server().SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES); ++ WebSocketBrowserConnectToTest::SetUpOnMainThread(); ++ } ++ ++ net::EmbeddedTestServer& server() override { return https_server_; } ++ ++ net::EmbeddedTestServer https_server_; ++}; ++ + // Automatically fill in any login prompts that appear with the supplied + // credentials. + class AutoLogin : public content::NotificationObserver { +@@ -352,7 +393,7 @@ IN_PROC_BROWSER_TEST_F(WebSocketBrowserTest, + EXPECT_EQ("PASS", WaitAndGetTitle()); + } + +-IN_PROC_BROWSER_TEST_F(WebSocketBrowserConnectToTest, ++IN_PROC_BROWSER_TEST_F(WebSocketBrowserHTTPConnectToTest, + WebSocketBasicAuthInWSURL) { + // Launch a basic-auth-protected WebSocket server. + ws_server_.set_websocket_basic_auth(true); +@@ -364,7 +405,7 @@ IN_PROC_BROWSER_TEST_F(WebSocketBrowserConnectToTest, + EXPECT_EQ("PASS", WaitAndGetTitle()); + } + +-IN_PROC_BROWSER_TEST_F(WebSocketBrowserConnectToTest, ++IN_PROC_BROWSER_TEST_F(WebSocketBrowserHTTPConnectToTest, + WebSocketBasicAuthInWSURLBadCreds) { + // Launch a basic-auth-protected WebSocket server. + ws_server_.set_websocket_basic_auth(true); +@@ -376,7 +417,7 @@ IN_PROC_BROWSER_TEST_F(WebSocketBrowserConnectToTest, + EXPECT_EQ("FAIL", WaitAndGetTitle()); + } + +-IN_PROC_BROWSER_TEST_F(WebSocketBrowserConnectToTest, ++IN_PROC_BROWSER_TEST_F(WebSocketBrowserHTTPConnectToTest, + WebSocketBasicAuthNoCreds) { + // Launch a basic-auth-protected WebSocket server. + ws_server_.set_websocket_basic_auth(true); +@@ -420,8 +461,7 @@ IN_PROC_BROWSER_TEST_F(WebSocketBrowserTest, MAYBE_WebSocketAppliesHSTS) { + https_server.ServeFilesFromSourceDirectory(GetChromeTestDataDir()); + net::SpawnedTestServer wss_server( + net::SpawnedTestServer::TYPE_WSS, +- net::SpawnedTestServer::SSLOptions( +- net::SpawnedTestServer::SSLOptions::CERT_COMMON_NAME_IS_DOMAIN), ++ SSLOptions(SSLOptions::CERT_COMMON_NAME_IS_DOMAIN), + net::GetWebSocketTestDataDirectory()); + // This test sets HSTS on localhost. To avoid being redirected to https, start + // the http server on 127.0.0.1 instead. +@@ -711,4 +751,43 @@ IN_PROC_BROWSER_TEST_F(WebSocketBrowserTestWithAllowFileAccessFromFiles, + EXPECT_EQ("FILE", WaitAndGetTitle()); + } + ++// A test fixture that enables First-Party Sets. ++class FirstPartySetsWebSocketBrowserTest ++ : public WebSocketBrowserHTTPSConnectToTest { ++ public: ++ FirstPartySetsWebSocketBrowserTest() ++ : WebSocketBrowserHTTPSConnectToTest(SSLOptions::CERT_TEST_NAMES) {} ++ ++ void SetUpCommandLine(base::CommandLine* command_line) override { ++ WebSocketBrowserTest::SetUpCommandLine(command_line); ++ command_line->AppendSwitchASCII( ++ network::switches::kUseFirstPartySet, ++ "https://a.test,https://b.test,https://c.test"); ++ } ++}; ++ ++IN_PROC_BROWSER_TEST_F(FirstPartySetsWebSocketBrowserTest, ++ SendsSamePartyCookies) { ++ ASSERT_TRUE(wss_server_.Start()); ++ ++ ASSERT_TRUE(content::SetCookie(browser()->profile(), ++ server().GetURL("a.test", "/"), ++ "same-party-cookie=1; SameParty; Secure")); ++ ASSERT_TRUE(content::SetCookie(browser()->profile(), ++ server().GetURL("a.test", "/"), ++ "same-site-cookie=1; SameSite=Lax; Secure")); ++ ++ content::DOMMessageQueue message_queue; ++ ConnectTo("b.test", wss_server_.GetURL("a.test", "echo-request-headers")); ++ ++ std::string message; ++ EXPECT_TRUE(message_queue.WaitForMessage(&message)); ++ // Only the SameParty cookie should have been sent, since it was a cross-site ++ // but same-party connection. ++ EXPECT_THAT(message, testing::HasSubstr("same-party-cookie=1")); ++ EXPECT_THAT(message, testing::Not(testing::HasSubstr("same-site-cookie=1"))); ++ ++ EXPECT_EQ("PASS", WaitAndGetTitle()); ++} ++ + } // namespace +diff --git a/net/data/websocket/connect_to.html b/net/data/websocket/connect_to.html +index 05c653fc5d2ab9a333efea5b4c5eee83a03bbe07..8a6d78214fe5974cbb0ec62b61f4d7fdcdf42c3b 100644 +--- a/net/data/websocket/connect_to.html ++++ b/net/data/websocket/connect_to.html +@@ -29,6 +29,17 @@ ws.onclose = function() + document.title = 'FAIL'; + } + ++ws.onmessage = function(evt) ++{ ++ domAutomationController.send(evt.data); ++} ++ ++ws.onerror = function(evt) ++{ ++ console.error(`WebSocket error: '${evt.message}'`); ++} ++ ++ + + + +diff --git a/net/test/spawned_test_server/base_test_server.cc b/net/test/spawned_test_server/base_test_server.cc +index 9caaf0ad501322f480be9867909e2e6cb8c56503..54c84e4d0bbd51640b374532fd92903b2e01de58 100644 +--- a/net/test/spawned_test_server/base_test_server.cc ++++ b/net/test/spawned_test_server/base_test_server.cc +@@ -156,6 +156,8 @@ base::FilePath BaseTestServer::SSLOptions::GetCertificateFile() const { + FILE_PATH_LITERAL("key_usage_rsa_digitalsignature.pem")); + case CERT_AUTO: + return base::FilePath(); ++ case CERT_TEST_NAMES: ++ return base::FilePath(FILE_PATH_LITERAL("test_names.pem")); + default: + NOTREACHED(); + } +@@ -249,6 +251,14 @@ GURL BaseTestServer::GetURL(const std::string& path) const { + return GURL(GetScheme() + "://" + host_port_pair_.ToString() + "/" + path); + } + ++GURL BaseTestServer::GetURL(const std::string& hostname, ++ const std::string& relative_url) const { ++ GURL local_url = GetURL(relative_url); ++ GURL::Replacements replace_host; ++ replace_host.SetHostStr(hostname); ++ return local_url.ReplaceComponents(replace_host); ++} ++ + GURL BaseTestServer::GetURLWithUser(const std::string& path, + const std::string& user) const { + return GURL(GetScheme() + "://" + user + "@" + host_port_pair_.ToString() + +diff --git a/net/test/spawned_test_server/base_test_server.h b/net/test/spawned_test_server/base_test_server.h +index 6c209afcdeeed129ec58f4c55a78501d707fd8f3..848698160b6eba1a02618bfaa968114d10776395 100644 +--- a/net/test/spawned_test_server/base_test_server.h ++++ b/net/test/spawned_test_server/base_test_server.h +@@ -82,6 +82,11 @@ class BaseTestServer { + // A certificate with invalid notBefore and notAfter times. Windows' + // certificate library will not parse this certificate. + CERT_BAD_VALIDITY, ++ ++ // A certificate that covers a number of test names. See [test_names] in ++ // net/data/ssl/scripts/ee.cnf. More may be added by editing this list and ++ // and rerunning net/data/ssl/scripts/generate-test-certs.sh. ++ CERT_TEST_NAMES, + }; + + // Bitmask of key exchange algorithms that the test server supports and that +@@ -277,6 +282,8 @@ class BaseTestServer { + bool GetAddressList(AddressList* address_list) const WARN_UNUSED_RESULT; + + GURL GetURL(const std::string& path) const; ++ GURL GetURL(const std::string& hostname, ++ const std::string& relative_url) const; + + GURL GetURLWithUser(const std::string& path, + const std::string& user) const; +diff --git a/services/network/first_party_sets/first_party_sets.cc b/services/network/first_party_sets/first_party_sets.cc +index 1650c28d8b6c61b30531e1e2ef3e2869d8450360..826b403a2a9702c255ee9b7ea38dc74b9e18791d 100644 +--- a/services/network/first_party_sets/first_party_sets.cc ++++ b/services/network/first_party_sets/first_party_sets.cc +@@ -91,16 +91,17 @@ bool FirstPartySets::IsContextSamePartyWithSite( + const net::SchemefulSite* top_frame_site, + const std::set& party_context, + bool infer_singleton_sets) const { +- const net::SchemefulSite* site_owner = FindOwner(site, infer_singleton_sets); +- if (!site_owner) ++ const absl::optional site_owner = ++ FindOwner(site, infer_singleton_sets); ++ if (!site_owner.has_value()) + return false; + + const auto is_owned_by_site_owner = +- [this, site_owner, ++ [this, &site_owner, + infer_singleton_sets](const net::SchemefulSite& context_site) -> bool { +- const net::SchemefulSite* context_owner = ++ const absl::optional context_owner = + FindOwner(context_site, infer_singleton_sets); +- return context_owner && *context_owner == *site_owner; ++ return context_owner.has_value() && *context_owner == *site_owner; + }; + + if (top_frame_site && !is_owned_by_site_owner(*top_frame_site)) +@@ -131,7 +132,8 @@ net::FirstPartySetsContextType FirstPartySets::ComputeContextType( + const absl::optional& top_frame_site, + const std::set& party_context) const { + constexpr bool infer_singleton_sets = true; +- const net::SchemefulSite* site_owner = FindOwner(site, infer_singleton_sets); ++ const absl::optional site_owner = ++ FindOwner(site, infer_singleton_sets); + // Note: the `party_context` consists of the intermediate frames (for frame + // requests) or intermediate frames and current frame for subresource + // requests. +@@ -152,18 +154,22 @@ net::FirstPartySetsContextType FirstPartySets::ComputeContextType( + : net::FirstPartySetsContextType::kTopResourceMatchMixed; + } + +-const net::SchemefulSite* FirstPartySets::FindOwner( ++const absl::optional FirstPartySets::FindOwner( + const net::SchemefulSite& site, + bool infer_singleton_sets) const { +- const auto it = sets_.find(site); +- if (it == sets_.end()) +- return infer_singleton_sets ? &site : nullptr; +- return &it->second; ++ net::SchemefulSite normalized_site = site; ++ normalized_site.ConvertWebSocketToHttp(); ++ const auto it = sets_.find(normalized_site); ++ if (it != sets_.end()) ++ return it->second; ++ if (infer_singleton_sets) ++ return normalized_site; ++ return absl::nullopt; + } + + bool FirstPartySets::IsInNontrivialFirstPartySet( + const net::SchemefulSite& site) const { +- return base::Contains(sets_, site); ++ return FindOwner(site, /*infer_singleton_sets=*/false).has_value(); + } + + base::flat_map> +@@ -244,7 +250,8 @@ base::flat_set FirstPartySets::ComputeSetsDiff( + for (const auto& old_pair : old_sets) { + const net::SchemefulSite& old_member = old_pair.first; + const net::SchemefulSite& old_owner = old_pair.second; +- const net::SchemefulSite* current_owner = FindOwner(old_member, false); ++ const absl::optional current_owner = ++ FindOwner(old_member, false); + // Look for the removed sites and the ones have owner changed. + if (!current_owner || *current_owner != old_owner) { + result.emplace(old_member); +diff --git a/services/network/first_party_sets/first_party_sets.h b/services/network/first_party_sets/first_party_sets.h +index 8158b555856526170051cba72a08312a5528de51..fc87e5155667befc5a94bbe539ca71dc47d5f7d1 100644 +--- a/services/network/first_party_sets/first_party_sets.h ++++ b/services/network/first_party_sets/first_party_sets.h +@@ -97,11 +97,12 @@ class FirstPartySets { + base::OnceCallback callback); + + private: +- // Returns a pointer to `site`'s owner (optionally inferring a singleton set +- // if necessary), or `nullptr` if `site` has no owner. Must not return +- // `nullptr` if `infer_singleton_sets` is true. +- const net::SchemefulSite* FindOwner(const net::SchemefulSite& site, +- bool infer_singleton_sets) const; ++ // Returns `site`'s owner (optionally inferring a singleton set if necessary), ++ // or `nullopt` if `site` has no owner. Must not return `nullopt` if ++ // `infer_singleton_sets` is true. ++ const absl::optional FindOwner( ++ const net::SchemefulSite& site, ++ bool infer_singleton_sets) const; + + // We must ensure there's no intersection between the manually-specified set + // and the sets that came from Component Updater. (When reconciling the +diff --git a/services/network/first_party_sets/first_party_sets_unittest.cc b/services/network/first_party_sets/first_party_sets_unittest.cc +index 2055619f4c999cbfd5a5ee4780e2eb5c1dad5816..52eb8e8a3d87172353c64bba972311db2889c693 100644 +--- a/services/network/first_party_sets/first_party_sets_unittest.cc ++++ b/services/network/first_party_sets/first_party_sets_unittest.cc +@@ -1167,6 +1167,8 @@ TEST_F(FirstPartySetsTest, ComputeContext) { + net::SchemefulSite nonmember1(GURL("https://nonmember1.test")); + net::SchemefulSite member(GURL("https://member1.test")); + net::SchemefulSite owner(GURL("https://example.test")); ++ net::SchemefulSite wss_member(GURL("wss://member1.test")); ++ net::SchemefulSite wss_nonmember(GURL("wss://nonmember.test")); + + // Works as usual for sites that are in First-Party sets. + EXPECT_THAT(sets().ComputeContext(member, &member, {member}), +@@ -1180,10 +1182,17 @@ TEST_F(FirstPartySetsTest, ComputeContext) { + EXPECT_THAT(sets().ComputeContext(member, &member, {member, owner}), + net::SamePartyContext(SamePartyContextType::kSameParty)); + ++ // Works if the site is provided with WSS scheme instead of HTTPS. ++ EXPECT_THAT(sets().ComputeContext(wss_member, &member, {member, owner}), ++ net::SamePartyContext(SamePartyContextType::kSameParty)); ++ + EXPECT_THAT(sets().ComputeContext(nonmember, &member, {member}), + net::SamePartyContext(SamePartyContextType::kCrossParty)); + EXPECT_THAT(sets().ComputeContext(member, &nonmember, {member}), + net::SamePartyContext(SamePartyContextType::kCrossParty)); ++ EXPECT_THAT( ++ sets().ComputeContext(wss_nonmember, &wss_member, {member, owner}), ++ net::SamePartyContext(SamePartyContextType::kCrossParty)); + + // Top&resource differs from Ancestors. + EXPECT_THAT(sets().ComputeContext(member, &member, {nonmember}), +@@ -1225,6 +1234,12 @@ TEST_F(FirstPartySetsTest, IsInNontrivialFirstPartySet) { + EXPECT_TRUE(sets().IsInNontrivialFirstPartySet( + net::SchemefulSite(GURL("https://member1.test")))); + ++ EXPECT_TRUE(sets().IsInNontrivialFirstPartySet( ++ net::SchemefulSite(GURL("wss://member1.test")))); ++ ++ EXPECT_FALSE(sets().IsInNontrivialFirstPartySet( ++ net::SchemefulSite(GURL("ws://member1.test")))); ++ + EXPECT_FALSE(sets().IsInNontrivialFirstPartySet( + net::SchemefulSite(GURL("https://nonmember.test")))); + } diff --git a/patches/chromium/cherry-pick-ec42dfd3545f.patch b/patches/chromium/cherry-pick-ec42dfd3545f.patch new file mode 100644 index 0000000000000..df1d1c2d4c35e --- /dev/null +++ b/patches/chromium/cherry-pick-ec42dfd3545f.patch @@ -0,0 +1,625 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Shuran Huang +Date: Fri, 24 Sep 2021 00:47:47 +0000 +Subject: Add functions to pass in persisted FPSs and compute diffs. + +Pass the persisted FPSs and a callback that takes a FPSs into Network +Service. The persisted FPSs is parsed and compared to the current FPSs +in the FirstPartySets class, then call the callback with the current +FPSs. The function that passes in the persisted FPSs and the callback +has not been called anywhere yet. + +Bug: 1219656 +Change-Id: I08c531aa08d3aeeb772c1eb9a3a453a07b0349d3 +Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3103693 +Commit-Queue: Shuran Huang +Reviewed-by: Will Harris +Reviewed-by: Matt Menke +Reviewed-by: Chris Fredrickson +Cr-Commit-Position: refs/heads/main@{#924570} + +diff --git a/services/network/first_party_sets/first_party_sets.cc b/services/network/first_party_sets/first_party_sets.cc +index f7e732e88d6e6ebc5daed9169d5eee336a9de8c1..1650c28d8b6c61b30531e1e2ef3e2869d8450360 100644 +--- a/services/network/first_party_sets/first_party_sets.cc ++++ b/services/network/first_party_sets/first_party_sets.cc +@@ -13,6 +13,7 @@ + #include "base/logging.h" + #include "base/ranges/algorithm.h" + #include "base/strings/string_split.h" ++#include "base/task/post_task.h" + #include "net/base/schemeful_site.h" + #include "net/cookies/cookie_constants.h" + #include "net/cookies/same_party_context.h" +@@ -72,12 +73,16 @@ void FirstPartySets::SetManuallySpecifiedSet(const std::string& flag_value) { + flag_value, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY)); + + ApplyManuallySpecifiedSet(); ++ manual_sets_ready_ = true; ++ ClearSiteDataOnChangedSetsIfReady(); + } + + base::flat_map* + FirstPartySets::ParseAndSet(base::StringPiece raw_sets) { + sets_ = FirstPartySetParser::ParseSetsFromComponentUpdater(raw_sets); + ApplyManuallySpecifiedSet(); ++ component_sets_ready_ = true; ++ ClearSiteDataOnChangedSetsIfReady(); + return &sets_; + } + +@@ -218,4 +223,48 @@ void FirstPartySets::ApplyManuallySpecifiedSet() { + sets_.emplace(manual_owner, manual_owner); + } + ++void FirstPartySets::SetPersistedSets(base::StringPiece raw_sets) { ++ raw_persisted_sets_ = std::string(raw_sets); ++ persisted_sets_ready_ = true; ++ ClearSiteDataOnChangedSetsIfReady(); ++} ++ ++void FirstPartySets::SetOnSiteDataCleared( ++ base::OnceCallback callback) { ++ on_site_data_cleared_ = std::move(callback); ++ ClearSiteDataOnChangedSetsIfReady(); ++} ++ ++base::flat_set FirstPartySets::ComputeSetsDiff( ++ const base::flat_map& old_sets) { ++ if (old_sets.empty()) ++ return {}; ++ ++ base::flat_set result; ++ for (const auto& old_pair : old_sets) { ++ const net::SchemefulSite& old_member = old_pair.first; ++ const net::SchemefulSite& old_owner = old_pair.second; ++ const net::SchemefulSite* current_owner = FindOwner(old_member, false); ++ // Look for the removed sites and the ones have owner changed. ++ if (!current_owner || *current_owner != old_owner) { ++ result.emplace(old_member); ++ } ++ } ++ return result; ++} ++ ++void FirstPartySets::ClearSiteDataOnChangedSetsIfReady() { ++ if (!persisted_sets_ready_ || !component_sets_ready_ || !manual_sets_ready_ || ++ on_site_data_cleared_.is_null()) ++ return; ++ ++ base::flat_set diff = ComputeSetsDiff( ++ FirstPartySetParser::DeserializeFirstPartySets(raw_persisted_sets_)); ++ ++ // TODO(shuuran@chromium.org): Implement site state clearing. ++ ++ std::move(on_site_data_cleared_) ++ .Run(FirstPartySetParser::SerializeFirstPartySets(sets_)); ++} ++ + } // namespace network +diff --git a/services/network/first_party_sets/first_party_sets.h b/services/network/first_party_sets/first_party_sets.h +index 81e0e1080d965947a2ebc1635638c25ad75a1bf7..8158b555856526170051cba72a08312a5528de51 100644 +--- a/services/network/first_party_sets/first_party_sets.h ++++ b/services/network/first_party_sets/first_party_sets.h +@@ -9,6 +9,7 @@ + #include + #include + ++#include "base/callback.h" + #include "base/containers/flat_map.h" + #include "base/containers/flat_set.h" + #include "net/base/schemeful_site.h" +@@ -87,6 +88,14 @@ class FirstPartySets { + // the members of the set includes the owner. + base::flat_map> Sets() const; + ++ // Sets the `raw_persisted_sets_`, which is a JSON-encoded ++ // string representation of a map of site -> site. ++ void SetPersistedSets(base::StringPiece persisted_sets); ++ // Sets the `on_site_data_cleared_` callback, which takes input of a ++ // JSON-encoded string representation of a map of site -> site. ++ void SetOnSiteDataCleared( ++ base::OnceCallback callback); ++ + private: + // Returns a pointer to `site`'s owner (optionally inferring a singleton set + // if necessary), or `nullptr` if `site` has no owner. Must not return +@@ -101,6 +110,19 @@ class FirstPartySets { + // `manually_specified_set_`. + void ApplyManuallySpecifiedSet(); + ++ // Compares the map `old_sets` to `sets_` and returns the set of sites that: ++ // 1) were in `old_sets` but are no longer in `sets_`, i.e. leave the FPSs; ++ // or, 2) mapped to a different owner site. ++ base::flat_set ComputeSetsDiff( ++ const base::flat_map& old_sets); ++ ++ // Checks the required inputs have been received, and if so, computes the diff ++ // between the `sets_` and the parsed `raw_persisted_sets_`, and clears the ++ // site data of the set of sites based on the diff. ++ // ++ // TODO(shuuran@chromium.org): Implement the code to clear site state. ++ void ClearSiteDataOnChangedSetsIfReady(); ++ + // Represents the mapping of site -> site, where keys are members of sets, and + // values are owners of the sets. Owners are explicitly represented as members + // of the set. +@@ -108,6 +130,22 @@ class FirstPartySets { + absl::optional< + std::pair>> + manually_specified_set_; ++ ++ std::string raw_persisted_sets_; ++ ++ bool persisted_sets_ready_ = false; ++ bool component_sets_ready_ = false; ++ bool manual_sets_ready_ = false; ++ ++ // The callback runs after the site state clearing is completed. ++ base::OnceCallback on_site_data_cleared_; ++ ++ FRIEND_TEST_ALL_PREFIXES(FirstPartySets, ComputeSetsDiff_SitesJoined); ++ FRIEND_TEST_ALL_PREFIXES(FirstPartySets, ComputeSetsDiff_SitesLeft); ++ FRIEND_TEST_ALL_PREFIXES(FirstPartySets, ComputeSetsDiff_OwnerChanged); ++ FRIEND_TEST_ALL_PREFIXES(FirstPartySets, ComputeSetsDiff_OwnerLeft); ++ FRIEND_TEST_ALL_PREFIXES(FirstPartySets, ComputeSetsDiff_OwnerMemberRotate); ++ FRIEND_TEST_ALL_PREFIXES(FirstPartySets, ComputeSetsDiff_EmptySets); + }; + + } // namespace network +diff --git a/services/network/first_party_sets/first_party_sets_unittest.cc b/services/network/first_party_sets/first_party_sets_unittest.cc +index b929315d9b857e0f86d1d726f7cefefb7ad8e54c..2055619f4c999cbfd5a5ee4780e2eb5c1dad5816 100644 +--- a/services/network/first_party_sets/first_party_sets_unittest.cc ++++ b/services/network/first_party_sets/first_party_sets_unittest.cc +@@ -7,6 +7,7 @@ + #include + + #include "base/json/json_reader.h" ++#include "base/test/bind.h" + #include "net/base/schemeful_site.h" + #include "net/cookies/cookie_constants.h" + #include "net/cookies/same_party_context.h" +@@ -204,6 +205,30 @@ TEST(FirstPartySets, SetsManuallySpecified_Invalid_RegisteredDomain_Member) { + EXPECT_THAT(sets.ParseAndSet("[]"), Pointee(IsEmpty())); + } + ++TEST(FirstPartySets, SetsManuallySpecified_Valid_EmptyValue) { ++ FirstPartySets sets; ++ sets.SetManuallySpecifiedSet(""); ++ ++ // Set non-empty existing sets to distinguish the failure case from the no-op ++ // case when processing the manually-specified sets. ++ const std::string existing_sets = R"( ++ [ ++ { ++ "owner": "https://example.test", ++ "members": ["https://member.test"] ++ } ++ ] ++ )"; ++ ASSERT_TRUE(base::JSONReader::Read(existing_sets)); ++ ++ EXPECT_THAT(sets.ParseAndSet(existing_sets), ++ Pointee(UnorderedElementsAre( ++ Pair(SerializesTo("https://example.test"), ++ SerializesTo("https://example.test")), ++ Pair(SerializesTo("https://member.test"), ++ SerializesTo("https://example.test"))))); ++} ++ + TEST(FirstPartySets, SetsManuallySpecified_Valid_SingleMember) { + FirstPartySets sets; + sets.SetManuallySpecifiedSet("https://example.test,https://member.test"); +@@ -469,6 +494,311 @@ TEST(FirstPartySets, SetsManuallySpecified_PrunesInducedSingletons) { + SerializesTo("https://example.test"))))); + } + ++TEST(FirstPartySets, ComputeSetsDiff_SitesJoined) { ++ auto old_sets = base::flat_map{ ++ {net::SchemefulSite(GURL("https://example.test")), ++ net::SchemefulSite(GURL("https://example.test"))}, ++ {net::SchemefulSite(GURL("https://member1.test")), ++ net::SchemefulSite(GURL("https://example.test"))}, ++ {net::SchemefulSite(GURL("https://member3.test")), ++ net::SchemefulSite(GURL("https://example.test"))}}; ++ ++ // Consistency check the reviewer-friendly JSON format matches the input. ++ ASSERT_THAT(FirstPartySets().ParseAndSet(R"( ++ [ ++ { ++ "owner": "https://example.test", ++ "members": ["https://member1.test", "https://member3.test"] ++ } ++ ] ++ )"), ++ Pointee(old_sets)); ++ ++ FirstPartySets sets; ++ sets.ParseAndSet(R"( ++ [ ++ { ++ "owner": "https://example.test", ++ "members": ["https://member1.test", "https://member3.test"] ++ }, ++ { ++ "owner": "https://foo.test", ++ "members": ["https://member2.test"] ++ } ++ ] ++ )"); ++ // "https://foo.test" and "https://member2.test" joined FPSs. We don't clear ++ // site data upon joining, so the computed diff should be empty set. ++ EXPECT_THAT(sets.ComputeSetsDiff(old_sets), IsEmpty()); ++} ++ ++TEST(FirstPartySets, ComputeSetsDiff_SitesLeft) { ++ auto old_sets = base::flat_map{ ++ {net::SchemefulSite(GURL("https://example.test")), ++ net::SchemefulSite(GURL("https://example.test"))}, ++ {net::SchemefulSite(GURL("https://member1.test")), ++ net::SchemefulSite(GURL("https://example.test"))}, ++ {net::SchemefulSite(GURL("https://member3.test")), ++ net::SchemefulSite(GURL("https://example.test"))}, ++ {net::SchemefulSite(GURL("https://foo.test")), ++ net::SchemefulSite(GURL("https://foo.test"))}, ++ {net::SchemefulSite(GURL("https://member2.test")), ++ net::SchemefulSite(GURL("https://foo.test"))}}; ++ ++ // Consistency check the reviewer-friendly JSON format matches the input. ++ ASSERT_THAT(FirstPartySets().ParseAndSet(R"( ++ [ ++ { ++ "owner": "https://example.test", ++ "members": ["https://member1.test", "https://member3.test"] ++ }, ++ { ++ "owner": "https://foo.test", ++ "members": ["https://member2.test"] ++ }, ++ ] ++ )"), ++ Pointee(old_sets)); ++ ++ FirstPartySets sets; ++ sets.ParseAndSet(R"( ++ [ ++ { ++ "owner": "https://example.test", ++ "members": ["https://member1.test"] ++ }, ++ ] ++ )"); ++ // Expected diff: "https://foo.test", "https://member2.test" and ++ // "https://member3.test" left FPSs. ++ EXPECT_THAT(sets.ComputeSetsDiff(old_sets), ++ UnorderedElementsAre(SerializesTo("https://foo.test"), ++ SerializesTo("https://member2.test"), ++ SerializesTo("https://member3.test"))); ++} ++ ++TEST(FirstPartySets, ComputeSetsDiff_OwnerChanged) { ++ auto old_sets = base::flat_map{ ++ {net::SchemefulSite(GURL("https://example.test")), ++ net::SchemefulSite(GURL("https://example.test"))}, ++ {net::SchemefulSite(GURL("https://member1.test")), ++ net::SchemefulSite(GURL("https://example.test"))}, ++ {net::SchemefulSite(GURL("https://foo.test")), ++ net::SchemefulSite(GURL("https://foo.test"))}, ++ {net::SchemefulSite(GURL("https://member2.test")), ++ net::SchemefulSite(GURL("https://foo.test"))}, ++ {net::SchemefulSite(GURL("https://member3.test")), ++ net::SchemefulSite(GURL("https://foo.test"))}}; ++ ++ // Consistency check the reviewer-friendly JSON format matches the input. ++ ASSERT_THAT(FirstPartySets().ParseAndSet(R"( ++ [ ++ { ++ "owner": "https://example.test", ++ "members": ["https://member1.test"] ++ }, ++ { ++ "owner": "https://foo.test", ++ "members": ["https://member2.test", "https://member3.test"] ++ }, ++ ] ++ )"), ++ Pointee(old_sets)); ++ ++ FirstPartySets sets; ++ sets.ParseAndSet(R"( ++ [ ++ { ++ "owner": "https://example.test", ++ "members": ["https://member1.test", "https://member3.test"] ++ }, ++ { ++ "owner": "https://foo.test", ++ "members": ["https://member2.test"] ++ } ++ ] ++ )"); ++ // Expected diff: "https://member3.test" changed owner. ++ EXPECT_THAT(sets.ComputeSetsDiff(old_sets), ++ UnorderedElementsAre(SerializesTo("https://member3.test"))); ++} ++ ++TEST(FirstPartySets, ComputeSetsDiff_OwnerLeft) { ++ auto old_sets = base::flat_map{ ++ {net::SchemefulSite(GURL("https://example.test")), ++ net::SchemefulSite(GURL("https://example.test"))}, ++ {net::SchemefulSite(GURL("https://foo.test")), ++ net::SchemefulSite(GURL("https://example.test"))}, ++ {net::SchemefulSite(GURL("https://bar.test")), ++ net::SchemefulSite(GURL("https://example.test"))}}; ++ ++ // Consistency check the reviewer-friendly JSON format matches the input. ++ ASSERT_THAT(FirstPartySets().ParseAndSet(R"( ++ [ ++ { ++ "owner": "https://example.test", ++ "members": ["https://foo.test", "https://bar.test"] ++ } ++ ] ++ )"), ++ Pointee(old_sets)); ++ ++ FirstPartySets sets; ++ sets.ParseAndSet(R"( ++ [ ++ { ++ "owner": "https://foo.test", ++ "members": ["https://bar.test"] ++ } ++ ] ++ )"); ++ // Expected diff: "https://example.test" left FPSs, "https://foo.test" and ++ // "https://bar.test" changed owner. ++ // It would be valid to only have example.test in the diff, but our logic ++ // isn't sophisticated enough yet to know that foo.test and bar.test don't ++ // need to be included in the result. ++ EXPECT_THAT(sets.ComputeSetsDiff(old_sets), ++ UnorderedElementsAre(SerializesTo("https://example.test"), ++ SerializesTo("https://foo.test"), ++ SerializesTo("https://bar.test"))); ++} ++ ++TEST(FirstPartySets, ComputeSetsDiff_OwnerMemberRotate) { ++ auto old_sets = base::flat_map{ ++ {net::SchemefulSite(GURL("https://example.test")), ++ net::SchemefulSite(GURL("https://example.test"))}, ++ {net::SchemefulSite(GURL("https://foo.test")), ++ net::SchemefulSite(GURL("https://example.test"))}}; ++ ++ // Consistency check the reviewer-friendly JSON format matches the input. ++ ASSERT_THAT(FirstPartySets().ParseAndSet(R"( ++ [ ++ { ++ "owner": "https://example.test", ++ "members": ["https://foo.test"] ++ } ++ ] ++ )"), ++ Pointee(old_sets)); ++ ++ FirstPartySets sets; ++ sets.ParseAndSet(R"( ++ [ ++ { ++ "owner": "https://foo.test", ++ "members": ["https://example.test"] ++ } ++ ] ++ )"); ++ // Expected diff: "https://example.test" and "https://foo.test" changed owner. ++ // It would be valid to not include example.test and foo.test in the result, ++ // but our logic isn't sophisticated enough yet to know that.ß ++ EXPECT_THAT(sets.ComputeSetsDiff(old_sets), ++ UnorderedElementsAre(SerializesTo("https://example.test"), ++ SerializesTo("https://foo.test"))); ++} ++ ++TEST(FirstPartySets, ComputeSetsDiff_EmptySets) { ++ // Empty old_sets. ++ FirstPartySets sets; ++ sets.ParseAndSet(R"( ++ [ ++ { ++ "owner": "https://example.test", ++ "members": ["https://member1.test"] ++ }, ++ ] ++ )"); ++ EXPECT_THAT(sets.ComputeSetsDiff({}), IsEmpty()); ++ ++ // Empty current sets. ++ auto old_sets = base::flat_map{ ++ {net::SchemefulSite(GURL("https://example.test")), ++ net::SchemefulSite(GURL("https://example.test"))}, ++ {net::SchemefulSite(GURL("https://member1.test")), ++ net::SchemefulSite(GURL("https://example.test"))}}; ++ // Consistency check the reviewer-friendly JSON format matches the input. ++ ASSERT_THAT(FirstPartySets().ParseAndSet(R"( ++ [ ++ { ++ "owner": "https://example.test", ++ "members": ["https://member1.test"] ++ } ++ ] ++ )"), ++ Pointee(old_sets)); ++ EXPECT_THAT(FirstPartySets().ComputeSetsDiff(old_sets), ++ UnorderedElementsAre(SerializesTo("https://example.test"), ++ SerializesTo("https://member1.test"))); ++} ++ ++TEST(FirstPartySets, ClearSiteDataOnChangedSetsIfReady_NotReady) { ++ int callback_calls = 0; ++ auto callback = base::BindLambdaForTesting( ++ [&](const std::string& got) { callback_calls++; }); ++ // component sets not ready. ++ { ++ FirstPartySets sets; ++ callback_calls = 0; ++ sets.SetPersistedSets("{}"); ++ sets.SetManuallySpecifiedSet(""); ++ sets.SetOnSiteDataCleared(callback); ++ EXPECT_EQ(callback_calls, 0); ++ } ++ // manual sets not ready. ++ { ++ FirstPartySets sets; ++ callback_calls = 0; ++ sets.ParseAndSet("[]"); ++ sets.SetPersistedSets("{}"); ++ sets.SetOnSiteDataCleared(callback); ++ EXPECT_EQ(callback_calls, 0); ++ } ++ // persisted sets not ready. ++ { ++ FirstPartySets sets; ++ callback_calls = 0; ++ sets.ParseAndSet("[]"); ++ sets.SetManuallySpecifiedSet(""); ++ sets.SetOnSiteDataCleared(callback); ++ EXPECT_EQ(callback_calls, 0); ++ } ++ // callback not set. ++ { ++ FirstPartySets sets; ++ callback_calls = 0; ++ sets.ParseAndSet("[]"); ++ sets.SetManuallySpecifiedSet(""); ++ sets.SetPersistedSets("{}"); ++ EXPECT_EQ(callback_calls, 0); ++ } ++} ++ ++// The callback only runs when `old_sets` is generated and `sets` has merged the ++// inputs from Component Updater and command line flag. ++TEST(FirstPartySets, ClearSiteDataOnChangedSetsIfReady_Ready) { ++ FirstPartySets sets; ++ int callback_calls = 0; ++ sets.ParseAndSet(R"([ ++ { ++ "owner": "https://example.test", ++ "members": ["https://member1.test"] ++ } ++ ])"); ++ sets.SetManuallySpecifiedSet("https://example2.test,https://member2.test"); ++ sets.SetPersistedSets( ++ R"({"https://example.test":"https://example.test", ++ "https://member1.test":"https://example.test"})"); ++ sets.SetOnSiteDataCleared(base::BindLambdaForTesting([&](const std::string& ++ got) { ++ EXPECT_EQ( ++ got, ++ R"({"https://member1.test":"https://example.test","https://member2.test":"https://example2.test"})"); ++ callback_calls++; ++ })); ++ EXPECT_EQ(callback_calls, 1); ++} ++ + class FirstPartySetsTest : public ::testing::Test { + public: + FirstPartySetsTest() { +diff --git a/services/network/network_service.cc b/services/network/network_service.cc +index 3f34171edf0c96df3b08de77e4e2be5b26eeca67..975eedaff9c38d0ee20d0fe617cc6391ea083d70 100644 +--- a/services/network/network_service.cc ++++ b/services/network/network_service.cc +@@ -343,8 +343,7 @@ void NetworkService::Initialize(mojom::NetworkServiceParamsPtr params, + } + + first_party_sets_ = std::make_unique(); +- if (net::cookie_util::IsFirstPartySetsEnabled() && +- command_line->HasSwitch(switches::kUseFirstPartySet)) { ++ if (net::cookie_util::IsFirstPartySetsEnabled()) { + first_party_sets_->SetManuallySpecifiedSet( + command_line->GetSwitchValueASCII(switches::kUseFirstPartySet)); + } +@@ -785,6 +784,14 @@ void NetworkService::SetFirstPartySets(const std::string& raw_sets) { + first_party_sets_->ParseAndSet(raw_sets); + } + ++void NetworkService::SetPersistedFirstPartySetsAndGetCurrentSets( ++ const std::string& persisted_sets, ++ mojom::NetworkService::SetPersistedFirstPartySetsAndGetCurrentSetsCallback ++ callback) { ++ first_party_sets_->SetPersistedSets(persisted_sets); ++ first_party_sets_->SetOnSiteDataCleared(std::move(callback)); ++} ++ + void NetworkService::SetExplicitlyAllowedPorts( + const std::vector& ports) { + net::SetExplicitlyAllowedPorts(ports); +diff --git a/services/network/network_service.h b/services/network/network_service.h +index 1da4505fc9fe478e00353cd55e615878ea875aa0..963e22f6d5e957684dc56dd6e3ae31fa430a355e 100644 +--- a/services/network/network_service.h ++++ b/services/network/network_service.h +@@ -204,6 +204,10 @@ class COMPONENT_EXPORT(NETWORK_SERVICE) NetworkService + void BindTestInterface( + mojo::PendingReceiver receiver) override; + void SetFirstPartySets(const std::string& raw_sets) override; ++ void SetPersistedFirstPartySetsAndGetCurrentSets( ++ const std::string& persisted_sets, ++ mojom::NetworkService::SetPersistedFirstPartySetsAndGetCurrentSetsCallback ++ callback) override; + void SetExplicitlyAllowedPorts(const std::vector& ports) override; + + // Returns an HttpAuthHandlerFactory for the given NetworkContext. +diff --git a/services/network/network_service_unittest.cc b/services/network/network_service_unittest.cc +index 7c8cbbbba3dd2084095d91b9195d69c335809263..6d1fdfd9b236c7129548fe626143636cfdb56bd5 100644 +--- a/services/network/network_service_unittest.cc ++++ b/services/network/network_service_unittest.cc +@@ -927,6 +927,7 @@ class NetworkServiceTestWithService : public testing::Test { + void SetUp() override { + test_server_.AddDefaultHandlers(base::FilePath(kServicesTestData)); + ASSERT_TRUE(test_server_.Start()); ++ scoped_features_.InitAndEnableFeature(net::features::kFirstPartySets); + service_ = NetworkService::CreateForTesting(); + service_->Bind(network_service_.BindNewPipeAndPassReceiver()); + } +@@ -992,6 +993,7 @@ class NetworkServiceTestWithService : public testing::Test { + mojo::Remote loader_; + + DISALLOW_COPY_AND_ASSIGN(NetworkServiceTestWithService); ++ base::test::ScopedFeatureList scoped_features_; + }; + + // Verifies that loading a URL through the network service's mojo interface +@@ -1171,6 +1173,18 @@ TEST_F(NetworkServiceTestWithService, GetNetworkList) { + run_loop.Run(); + } + ++TEST_F(NetworkServiceTestWithService, ++ SetPersistedFirstPartySetsAndGetCurrentSets) { ++ base::RunLoop run_loop; ++ network_service_->SetPersistedFirstPartySetsAndGetCurrentSets( ++ "", base::BindLambdaForTesting([&](const std::string& got) { ++ EXPECT_EQ(got, "{}"); ++ run_loop.Quit(); ++ })); ++ network_service_->SetFirstPartySets(""); ++ run_loop.Run(); ++} ++ + class TestNetworkChangeManagerClient + : public mojom::NetworkChangeManagerClient { + public: +diff --git a/services/network/public/mojom/network_service.mojom b/services/network/public/mojom/network_service.mojom +index fe5450b20b3c4a8490e853dd236bf6baefa90b81..59fbbde6ffc30d51304a72f402eee7c664ea111b 100644 +--- a/services/network/public/mojom/network_service.mojom ++++ b/services/network/public/mojom/network_service.mojom +@@ -373,6 +373,14 @@ interface NetworkService { + // cleared (except for the manually-specified set, if one exists). + SetFirstPartySets(string raw_sets); + ++ // Sets the First-Party Sets data that was persisted to compare it with the ++ // current First-Party Sets data set by `SetFirstPartySets()`, which is ++ // considered more up-to-date, and returns a serialized version of the current ++ // one. Both input and output are in format of the JSON-encoded string ++ // representation of a map of site -> site. ++ SetPersistedFirstPartySetsAndGetCurrentSets(string persisted_sets) ++ => (string up_to_date_sets); ++ + // Sets the list of ports which will be permitted even if they normally would + // be restricted. + SetExplicitlyAllowedPorts(array ports);