blob: 7efab45f30f5f310b9085996fa123dc4290e7b23 [file] [log] [blame]
// Copyright 2014 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/bookmarks/browser/bookmark_codec.h"
#include <stddef.h>
#include <algorithm>
#include <memory>
#include <string_view>
#include <utility>
#include "base/base64.h"
#include "base/containers/contains.h"
#include "base/containers/span.h"
#include "base/json/json_string_value_serializer.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "base/uuid.h"
#include "base/values.h"
#include "components/bookmarks/browser/bookmark_uuids.h"
#include "components/bookmarks/common/bookmark_features.h"
#include "components/strings/grit/components_strings.h"
#include "crypto/hash.h"
#include "ui/base/l10n/l10n_util.h"
#include "url/gurl.h"
using base::Time;
namespace bookmarks {
const char BookmarkCodec::kRootsKey[] = "roots";
const char BookmarkCodec::kBookmarkBarFolderNameKey[] = "bookmark_bar";
const char BookmarkCodec::kOtherBookmarkFolderNameKey[] = "other";
// The value is left as 'synced' for historical reasons.
const char BookmarkCodec::kMobileBookmarkFolderNameKey[] = "synced";
const char BookmarkCodec::kVersionKey[] = "version";
const char BookmarkCodec::kChecksumKey[] = "checksum";
const char BookmarkCodec::kChecksumSHA256Key[] = "checksum_sha256";
const char BookmarkCodec::kIdKey[] = "id";
const char BookmarkCodec::kTypeKey[] = "type";
const char BookmarkCodec::kNameKey[] = "name";
const char BookmarkCodec::kGuidKey[] = "guid";
const char BookmarkCodec::kDateAddedKey[] = "date_added";
const char BookmarkCodec::kURLKey[] = "url";
const char BookmarkCodec::kDateModifiedKey[] = "date_modified";
const char BookmarkCodec::kChildrenKey[] = "children";
const char BookmarkCodec::kMetaInfo[] = "meta_info";
const char BookmarkCodec::kTypeURL[] = "url";
const char BookmarkCodec::kTypeFolder[] = "folder";
const char BookmarkCodec::kSyncMetadata[] = "sync_metadata";
const char BookmarkCodec::kDateLastUsed[] = "date_last_used";
// Current version of the file.
static const int kCurrentVersion = 1;
namespace {
// Encodes Sync metadata and cleans up the input string to decrease peak memory
// usage during encoding.
base::Value EncodeSyncMetadata(std::string sync_metadata_str) {
return base::Value(base::Base64Encode(sync_metadata_str));
}
// Helper function to convert Time to microseconds since Windows epoch.
int64_t ToMicrosecondsSinceWindowsEpoch(Time time) {
return time.ToDeltaSinceWindowsEpoch().InMicroseconds();
}
// Helper function to parse date from dictionary, returns nullopt if not found.
std::optional<Time> FindMicrosecondsSinceWindowsEpoch(
const base::Value::Dict& dict,
std::string_view key) {
const std::string* string_value = dict.FindString(key);
if (!string_value) {
return std::nullopt;
}
int64_t microseconds = 0;
if (!base::StringToInt64(*string_value, &microseconds)) {
return std::nullopt;
}
return Time::FromDeltaSinceWindowsEpoch(base::Microseconds(microseconds));
}
} // namespace
BookmarkCodec::BookmarkCodec() = default;
BookmarkCodec::~BookmarkCodec() = default;
base::Value::Dict BookmarkCodec::Encode(
const BookmarkNode* bookmark_bar_node,
const BookmarkNode* other_folder_node,
const BookmarkNode* mobile_folder_node,
std::string sync_metadata_str) {
ids_reassigned_ = false;
uuids_reassigned_ = false;
base::Value::Dict main;
main.Set(kVersionKey, kCurrentVersion);
// Encode Sync metadata before encoding other fields to reduce peak memory
// usage.
if (!sync_metadata_str.empty()) {
main.Set(kSyncMetadata, EncodeSyncMetadata(std::move(sync_metadata_str)));
sync_metadata_str.clear();
}
InitializeChecksum();
base::Value::Dict roots;
if (bookmark_bar_node) {
// If one permanent node is provided, all permanent nodes should have been
// provided.
CHECK(other_folder_node);
CHECK(mobile_folder_node);
roots.Set(kBookmarkBarFolderNameKey, EncodeNode(bookmark_bar_node));
roots.Set(kOtherBookmarkFolderNameKey, EncodeNode(other_folder_node));
roots.Set(kMobileBookmarkFolderNameKey, EncodeNode(mobile_folder_node));
} else {
// No permanent node should have been provided.
CHECK(!other_folder_node);
CHECK(!mobile_folder_node);
}
FinalizeChecksum();
// We are going to store the computed checksum. So set stored checksum to be
// the same as computed checksum.
stored_checksum_ = computed_checksum_;
main.Set(kChecksumKey, computed_checksum_);
if (base::FeatureList::IsEnabled(kEnableBookmarkCodecSHA256)) {
stored_sha256_checksum_ = computed_sha256_checksum_;
main.Set(kChecksumSHA256Key, computed_sha256_checksum_);
}
main.Set(kRootsKey, std::move(roots));
return main;
}
bool BookmarkCodec::Decode(const base::Value::Dict& value,
std::set<int64_t> already_assigned_ids,
BookmarkNode* bb_node,
BookmarkNode* other_folder_node,
BookmarkNode* mobile_folder_node,
int64_t* max_id,
std::string* sync_metadata_str) {
const int64_t max_already_assigned_id =
already_assigned_ids.empty() ? 0 : *already_assigned_ids.rbegin();
if (sync_metadata_str) {
sync_metadata_str->clear();
}
ids_ = std::move(already_assigned_ids);
uuids_ = {base::Uuid::ParseLowercase(kRootNodeUuid),
base::Uuid::ParseLowercase(kBookmarkBarNodeUuid),
base::Uuid::ParseLowercase(kOtherBookmarksNodeUuid),
base::Uuid::ParseLowercase(kMobileBookmarksNodeUuid),
base::Uuid::ParseLowercase(kManagedNodeUuid)};
ids_reassigned_ = false;
uuids_reassigned_ = false;
ids_valid_ = true;
maximum_id_ = 0;
stored_checksum_.clear();
stored_sha256_checksum_.clear();
InitializeChecksum();
bool success = DecodeHelper(bb_node, other_folder_node, mobile_folder_node,
value, sync_metadata_str);
FinalizeChecksum();
// If either the checksums differ or some IDs were missing/not unique,
// reassign IDs.
bool use_sha256 = base::FeatureList::IsEnabled(kEnableBookmarkCodecSHA256);
if (!ids_valid_ || (computed_checksum_ != stored_checksum_) ||
(use_sha256 && computed_sha256_checksum_ != stored_sha256_checksum_)) {
maximum_id_ = max_already_assigned_id;
ReassignIDs(bb_node, other_folder_node, mobile_folder_node);
}
*max_id = maximum_id_ + 1;
return success;
}
bool BookmarkCodec::required_recovery() const {
bool use_sha256 = base::FeatureList::IsEnabled(kEnableBookmarkCodecSHA256);
return ids_reassigned_ || uuids_reassigned_ ||
(computed_checksum_ != stored_checksum_) ||
(use_sha256 && computed_sha256_checksum_ != stored_sha256_checksum_);
}
base::Value::Dict BookmarkCodec::EncodeNode(const BookmarkNode* node) {
base::Value::Dict value;
std::string id = base::NumberToString(node->id());
value.Set(kIdKey, id);
const std::u16string& title = node->GetTitle();
value.Set(kNameKey, title);
const std::string& uuid = node->uuid().AsLowercaseString();
value.Set(kGuidKey, uuid);
value.Set(kDateAddedKey, base::NumberToString(ToMicrosecondsSinceWindowsEpoch(
node->date_added())));
value.Set(kDateLastUsed, base::NumberToString(ToMicrosecondsSinceWindowsEpoch(
node->date_last_used())));
if (node->is_url()) {
value.Set(kTypeKey, kTypeURL);
std::string url = node->url().possibly_invalid_spec();
value.Set(kURLKey, url);
UpdateChecksumWithUrlNode(id, title, url);
} else {
value.Set(kTypeKey, kTypeFolder);
value.Set(kDateModifiedKey,
base::NumberToString(ToMicrosecondsSinceWindowsEpoch(
node->date_folder_modified())));
UpdateChecksumWithFolderNode(id, title);
base::Value::List child_values;
for (const auto& child : node->children())
child_values.Append(EncodeNode(child.get()));
value.Set(kChildrenKey, base::Value(std::move(child_values)));
}
const BookmarkNode::MetaInfoMap* meta_info_map = node->GetMetaInfoMap();
if (meta_info_map)
value.Set(kMetaInfo, EncodeMetaInfo(*meta_info_map));
return value;
}
base::Value::Dict BookmarkCodec::EncodeMetaInfo(
const BookmarkNode::MetaInfoMap& meta_info_map) {
base::Value::Dict meta_info;
for (const auto& item : meta_info_map)
meta_info.Set(item.first, base::Value(item.second));
return meta_info;
}
bool BookmarkCodec::DecodeHelper(BookmarkNode* bb_node,
BookmarkNode* other_folder_node,
BookmarkNode* mobile_folder_node,
const base::Value::Dict& value,
std::string* sync_metadata_str) {
std::optional<int> version = value.FindInt(kVersionKey);
if (!version || *version != kCurrentVersion)
return false; // Unknown version.
const base::Value* checksum_value = value.Find(kChecksumKey);
if (checksum_value) {
const std::string* checksum = checksum_value->GetIfString();
if (checksum)
stored_checksum_ = *checksum;
else
return false;
}
if (base::FeatureList::IsEnabled(kEnableBookmarkCodecSHA256)) {
const std::string* checksum_sha256 = value.FindString(kChecksumSHA256Key);
if (checksum_sha256) {
stored_sha256_checksum_ = *checksum_sha256;
}
// If checksum is missing, stored data must predate md5->sha256 migration.
// Expect a md5 checksum to have been set by the previous block.
else if (!checksum_value->GetIfString()) {
// If no checksum was set, then the decode should fail.
return false;
}
}
if (sync_metadata_str) {
const std::string* sync_metadata_str_base64 =
value.FindString(kSyncMetadata);
if (sync_metadata_str_base64) {
base::Base64Decode(*sync_metadata_str_base64, sync_metadata_str);
}
}
const base::Value::Dict* roots = value.FindDict(kRootsKey);
if (!roots)
return false; // No roots, or invalid type for roots.
const base::Value::Dict* bb_value =
roots->FindDict(kBookmarkBarFolderNameKey);
const base::Value::Dict* other_folder_value =
roots->FindDict(kOtherBookmarkFolderNameKey);
const base::Value::Dict* mobile_folder_value =
roots->FindDict(kMobileBookmarkFolderNameKey);
if (!bb_value || !other_folder_value || !mobile_folder_value)
return false;
DecodeNode(*bb_value, nullptr, bb_node);
DecodeNode(*other_folder_value, nullptr, other_folder_node);
DecodeNode(*mobile_folder_value, nullptr, mobile_folder_node);
// Need to reset the title as the title is persisted and restored from
// the file.
bb_node->SetTitle(l10n_util::GetStringUTF16(IDS_BOOKMARK_BAR_FOLDER_NAME));
other_folder_node->SetTitle(
l10n_util::GetStringUTF16(IDS_BOOKMARK_BAR_OTHER_FOLDER_NAME));
mobile_folder_node->SetTitle(
l10n_util::GetStringUTF16(IDS_BOOKMARK_BAR_MOBILE_FOLDER_NAME));
return true;
}
bool BookmarkCodec::DecodeChildren(const base::Value::List& child_value_list,
BookmarkNode* parent) {
for (const base::Value& child_value : child_value_list) {
if (!child_value.is_dict())
return false;
DecodeNode(child_value.GetDict(), parent, nullptr);
}
return true;
}
bool BookmarkCodec::DecodeNode(const base::Value::Dict& value,
BookmarkNode* parent,
BookmarkNode* node) {
// If no `node` is specified, we'll create one and add it to the `parent`.
// Therefore, in that case, `parent` must be non-NULL.
if (!node && !parent) {
NOTREACHED();
}
// It's not valid to have both a node and a specified parent.
if (node && parent) {
NOTREACHED();
}
std::string id_string;
int64_t id = 0;
{
const std::string* string = value.FindString(kIdKey);
if (!string || !base::StringToInt64(*string, &id) || id <= 0 ||
ids_.count(id) != 0) {
ids_valid_ = false;
} else {
ids_.insert(id);
id_string = *string;
}
}
maximum_id_ = std::max(maximum_id_, id);
std::u16string title;
const std::string* string_value = value.FindString(kNameKey);
if (string_value)
title = base::UTF8ToUTF16(*string_value);
base::Uuid uuid;
// `node` is only passed in for bookmarks of type BookmarkPermanentNode, in
// which case we do not need to check for UUID validity as their UUIDs are
// hard-coded and not read from the persisted file.
if (!node) {
// UUIDs can be empty for bookmarks that were created before UUIDs were
// required. When encountering one such bookmark we thus assign to it a new
// UUID. The same applies if the stored UUID is invalid or a duplicate.
const std::string* uuid_str = value.FindString(kGuidKey);
if (uuid_str && !uuid_str->empty()) {
uuid = base::Uuid::ParseCaseInsensitive(*uuid_str);
}
if (!uuid.is_valid()) {
uuid = base::Uuid::GenerateRandomV4();
uuids_reassigned_ = true;
}
if (uuid.AsLowercaseString() == kBannedUuidDueToPastSyncBug) {
uuid = base::Uuid::GenerateRandomV4();
uuids_reassigned_ = true;
}
// Guard against UUID collisions, which would violate BookmarkModel's
// invariant that each UUID is unique.
if (base::Contains(uuids_, uuid)) {
uuid = base::Uuid::GenerateRandomV4();
uuids_reassigned_ = true;
}
uuids_.insert(uuid);
}
const std::string* type_string = value.FindString(kTypeKey);
if (!type_string)
return false;
if (*type_string != kTypeURL && *type_string != kTypeFolder)
return false; // Unknown type.
if (*type_string == kTypeURL) {
const std::string* url_string = value.FindString(kURLKey);
if (!url_string)
return false;
GURL url = GURL(*url_string);
if (!node && url.is_valid()) {
DCHECK(uuid.is_valid());
node = new BookmarkNode(id, uuid, url);
} else {
return false; // Node invalid.
}
if (parent)
parent->Add(base::WrapUnique(node));
UpdateChecksumWithUrlNode(id_string, title, *url_string);
} else {
const base::Value::List* child_values = value.FindList(kChildrenKey);
if (!child_values)
return false;
if (!node) {
DCHECK(uuid.is_valid());
node = new BookmarkNode(id, uuid, GURL());
} else {
// If a new node is not created, explicitly assign ID to the existing one.
node->set_id(id);
}
node->set_date_folder_modified(
FindMicrosecondsSinceWindowsEpoch(value, kDateModifiedKey)
.value_or(Time::Now()));
if (parent)
parent->Add(base::WrapUnique(node));
UpdateChecksumWithFolderNode(id_string, title);
if (!DecodeChildren(*child_values, node))
return false;
}
node->SetTitle(title);
node->set_date_added(FindMicrosecondsSinceWindowsEpoch(value, kDateAddedKey)
.value_or(Time::Now()));
node->set_date_last_used(
FindMicrosecondsSinceWindowsEpoch(value, kDateLastUsed).value_or(Time()));
BookmarkNode::MetaInfoMap meta_info_map;
if (!DecodeMetaInfo(value, &meta_info_map))
return false;
node->SetMetaInfoMap(meta_info_map);
return true;
}
bool BookmarkCodec::DecodeMetaInfo(const base::Value::Dict& value,
BookmarkNode::MetaInfoMap* meta_info_map) {
DCHECK(meta_info_map);
meta_info_map->clear();
const base::Value* meta_info = value.Find(kMetaInfo);
if (!meta_info)
return true;
std::unique_ptr<base::Value> deserialized_holder;
// Meta info used to be stored as a serialized dictionary, so attempt to
// parse the value as one.
const std::string* meta_info_str = meta_info->GetIfString();
if (meta_info_str) {
JSONStringValueDeserializer deserializer(*meta_info_str);
deserialized_holder = deserializer.Deserialize(nullptr, nullptr);
if (!deserialized_holder)
return false;
meta_info = deserialized_holder.get();
}
// meta_info is now either the kMetaInfo node, or the deserialized node if it
// was stored as a string. Either way it should now be a (possibly nested)
// dictionary of meta info values.
if (!meta_info->is_dict())
return false;
DecodeMetaInfoHelper(meta_info->GetDict(), std::string(), meta_info_map);
return true;
}
void BookmarkCodec::DecodeMetaInfoHelper(
const base::Value::Dict& dict,
const std::string& prefix,
BookmarkNode::MetaInfoMap* meta_info_map) {
for (const auto it : dict) {
// Deprecated keys should be excluded after removing enhanced bookmarks
// feature crrev.com/1638413003.
if (base::StartsWith(it.first, "stars.", base::CompareCase::SENSITIVE)) {
continue;
}
if (it.second.is_dict()) {
DecodeMetaInfoHelper(it.second.GetDict(), prefix + it.first + ".",
meta_info_map);
} else {
const std::string* str = it.second.GetIfString();
if (str)
(*meta_info_map)[prefix + it.first] = *str;
}
}
}
void BookmarkCodec::ReassignIDs(BookmarkNode* bb_node,
BookmarkNode* other_node,
BookmarkNode* mobile_node) {
ids_.clear();
reassigned_ids_per_old_id_.clear();
ReassignIDsHelper(bb_node);
ReassignIDsHelper(other_node);
ReassignIDsHelper(mobile_node);
ids_reassigned_ = true;
}
void BookmarkCodec::ReassignIDsHelper(BookmarkNode* node) {
DCHECK(node);
const int64_t old_id = node->id();
node->set_id(++maximum_id_);
reassigned_ids_per_old_id_.emplace(old_id, node->id());
ids_.insert(node->id());
for (const auto& child : node->children())
ReassignIDsHelper(child.get());
}
void BookmarkCodec::UpdateChecksum(const std::string& str) {
md5_hasher_.Update(str);
sha256_hasher_.Update(str);
}
void BookmarkCodec::UpdateChecksum(const std::u16string& str) {
auto bytes = base::as_byte_span(str);
md5_hasher_.Update(bytes);
sha256_hasher_.Update(bytes);
}
void BookmarkCodec::UpdateChecksumWithUrlNode(const std::string& id,
const std::u16string& title,
const std::string& url) {
DCHECK(base::IsStringUTF8(url));
UpdateChecksum(id);
UpdateChecksum(title);
UpdateChecksum(kTypeURL);
UpdateChecksum(url);
}
void BookmarkCodec::UpdateChecksumWithFolderNode(const std::string& id,
const std::u16string& title) {
UpdateChecksum(id);
UpdateChecksum(title);
UpdateChecksum(kTypeFolder);
}
void BookmarkCodec::InitializeChecksum() {
md5_hasher_ = crypto::obsolete::Md5();
sha256_hasher_ = crypto::hash::Hasher(crypto::hash::kSha256);
}
void BookmarkCodec::FinalizeChecksum() {
computed_checksum_ =
base::ToLowerASCII(base::HexEncode(md5_hasher_.Finish()));
std::string result(crypto::hash::kSha256Size, 0);
sha256_hasher_.Finish(base::as_writable_byte_span(result));
computed_sha256_checksum_ = base::ToLowerASCII(base::HexEncode(result));
}
} // namespace bookmarks