blob: 1b1d4a9510e71bdb435b35a88e5782a61ca0663b [file] [log] [blame]
// Copyright 2023 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/sync_preferences/preferences_merge_helper.h"
#include "base/check_is_test.h"
#include "base/containers/contains.h"
#include "base/notreached.h"
#include "components/sync_preferences/pref_model_associator_client.h"
#include "components/sync_preferences/syncable_prefs_database.h"
namespace sync_preferences::helper {
namespace {
MergeBehavior GetMergeBehavior(const PrefModelAssociatorClient& client,
std::string_view pref_name) {
std::optional<SyncablePrefMetadata> metadata =
client.GetSyncablePrefsDatabase().GetSyncablePrefMetadata(pref_name);
CHECK(metadata.has_value());
return metadata->merge_behavior();
}
} // namespace
base::Value::List MergeListValues(const base::Value::List& local_value,
const base::Value::List& server_value) {
base::Value::List result = server_value.Clone();
for (const auto& value : local_value) {
if (!base::Contains(result, value)) {
result.Append(value.Clone());
}
}
return result;
}
base::Value::Dict MergeDictionaryValues(const base::Value::Dict& local_value,
const base::Value::Dict& server_value) {
base::Value::Dict result = server_value.Clone();
for (auto it : local_value) {
// It's not clear whether using a C++17 structured binding here would cause
// a copy of the value or not, so in doubt unpack the old way.
const base::Value* local_key_value = &it.second;
base::Value* server_key_value = result.Find(it.first);
if (server_key_value) {
if (local_key_value->is_dict() && server_key_value->is_dict()) {
*server_key_value = base::Value(MergeDictionaryValues(
local_key_value->GetDict(), server_key_value->GetDict()));
}
// Note that for all other types we want to preserve the "server"
// values so we do nothing here.
} else {
result.Set(it.first, local_key_value->Clone());
}
}
return result;
}
base::Value MergePreference(const PrefModelAssociatorClient* client,
std::string_view pref_name,
const base::Value& local_value,
const base::Value& server_value) {
if (!client) {
CHECK_IS_TEST();
// No client was registered. Directly let server value win.
return server_value.Clone();
}
switch (GetMergeBehavior(*client, pref_name)) {
case MergeBehavior::kMergeableDict:
if (!server_value.is_dict()) {
// Server value is corrupt or missing, keep pref value unchanged.
// TODO(crbug.com/40901973): Investigate in which scenarios can the
// value be corrupt.
return local_value.Clone();
}
// TODO(crbug.com/40933499): Investigate if this is valid or if this
// should be a CHECK instead.
if (!local_value.is_dict()) {
return server_value.Clone();
}
return base::Value(
MergeDictionaryValues(local_value.GetDict(), server_value.GetDict()));
case MergeBehavior::kMergeableListWithRewriteOnUpdate:
if (!server_value.is_list()) {
// Server value is corrupt or missing, keep pref value unchanged.
// TODO(crbug.com/40901973): Investigate in which scenarios can the
// value be corrupt.
return local_value.Clone();
}
// TODO(crbug.com/40933499): Investigate if this is valid or if this
// should be a CHECK instead.
if (!local_value.is_list()) {
return server_value.Clone();
}
return base::Value(
MergeListValues(local_value.GetList(), server_value.GetList()));
case MergeBehavior::kCustom:
if (base::Value merged_value = client->MaybeMergePreferenceValues(
pref_name, local_value, server_value);
!merged_value.is_none()) {
return merged_value;
}
[[fallthrough]];
case MergeBehavior::kNone:
// If this is not a specially handled preference, server wins.
return server_value.Clone();
}
NOTREACHED();
}
std::pair<base::Value::Dict, base::Value::Dict> UnmergeDictionaryValues(
base::Value::Dict new_dict,
const base::Value::Dict& original_local_dict,
const base::Value::Dict& original_account_dict) {
base::Value::Dict new_local_dict;
base::Value::Dict new_account_dict;
// Keep only keys that exist in the `new_dict`.
for (auto [k, v] : original_local_dict) {
if (new_dict.contains(k)) {
new_local_dict.Set(k, v.Clone());
}
}
for (auto [k, v] : original_account_dict) {
if (new_dict.contains(k)) {
new_account_dict.Set(k, v.Clone());
}
}
// Add or update individual keys.
for (auto [k, new_dict_value] : new_dict) {
// If contained value is again a dict, recursively un-merge.
if (new_dict_value.is_dict()) {
base::Value local_dict_value(base::Value::Type::DICT);
if (base::Value::Dict* local_dict_value_dict =
new_local_dict.FindDict(k)) {
local_dict_value = base::Value(std::move(*local_dict_value_dict));
}
base::Value account_dict_value(base::Value::Type::DICT);
if (base::Value::Dict* account_dict_value_dict =
new_account_dict.FindDict(k)) {
account_dict_value = base::Value(std::move(*account_dict_value_dict));
}
// Note: Passing empty dict values in case the key does not exist or has a
// different type.
auto [local_v, account_v] = UnmergeDictionaryValues(
std::move(new_dict_value).TakeDict(), local_dict_value.GetDict(),
account_dict_value.GetDict());
// If the new unmerged dict value is empty, remove the key.
if (!local_v.empty()) {
new_local_dict.Set(k, std::move(local_v));
} else {
new_local_dict.Remove(k);
}
// If the new unmerged dict value is empty, remove the key.
if (!account_v.empty()) {
new_account_dict.Set(k, std::move(account_v));
} else {
new_account_dict.Remove(k);
}
} else if (base::Value* account_dict_value = new_account_dict.Find(k)) {
// The key already existed in the account store. Copy to the local store
// if its value changed.
if (*account_dict_value != new_dict_value) {
new_local_dict.Set(k, new_dict_value.Clone());
*account_dict_value = std::move(new_dict_value);
}
} else if (base::Value* local_dict_value = new_local_dict.Find(k)) {
// The key already existed in the local store. Copy to the account store
// if its value changed.
if (*local_dict_value != new_dict_value) {
new_account_dict.Set(k, new_dict_value.Clone());
*local_dict_value = std::move(new_dict_value);
}
} else {
// New dict entry - the key didn't exist before. Add it to both the
// stores.
new_account_dict.Set(k, new_dict_value.Clone());
new_local_dict.Set(k, std::move(new_dict_value));
}
}
return {std::move(new_local_dict), std::move(new_account_dict)};
}
} // namespace sync_preferences::helper