blob: d976ee21ef78303efab41270cc182d9c3f00487c [file]
// Copyright 2026 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/model/processor_entity_metadata.h"
#include <utility>
#include "base/base64.h"
#include "base/check.h"
#include "base/files/file_path.h"
#include "base/hash/hash.h"
#include "base/hash/sha1.h"
#include "base/memory/ptr_util.h"
#include "base/trace_event/memory_usage_estimator.h"
#include "components/sync/base/deletion_origin.h"
#include "components/sync/base/time.h"
#include "components/sync/engine/commit_and_get_updates_types.h"
#include "components/sync/protocol/collaboration_metadata.h"
#include "components/sync/protocol/entity_data.h"
#include "components/sync/protocol/entity_metadata.pb.h"
#include "components/sync/protocol/entity_specifics.pb.h"
#include "components/sync/protocol/proto_memory_estimations.h"
#include "components/sync/protocol/unique_position.pb.h"
#include "components/version_info/version_info.h"
#include "url/gurl.h"
namespace syncer {
namespace {
std::string HashSpecifics(const sync_pb::EntitySpecifics& specifics) {
DCHECK_GT(specifics.ByteSizeLong(), 0u);
return base::Base64Encode(
base::SHA1HashString(specifics.SerializeAsString()));
}
bool IsValidPendingUpload(
const sync_pb::EntityMetadata::AttachmentMetadata::PendingUpload&
pending_upload) {
return !pending_upload.file_path().empty();
}
} // namespace
// static
bool ProcessorEntityMetadata::IsValid(const sync_pb::EntityMetadata& metadata) {
// Reject corrupted metadata where `pending_upload` is present without a local
// staging `file_path`, so `ProcessorEntityMetadata` can guarantee a non-empty
// path whenever `HasPendingAttachmentUpload()` is true.
if (metadata.attachment_metadata().has_pending_upload() &&
!IsValidPendingUpload(metadata.attachment_metadata().pending_upload())) {
return false;
}
return !metadata.client_tag_hash().empty() && metadata.has_creation_time() &&
metadata.sequence_number() >= metadata.acked_sequence_number();
}
// static
ProcessorEntityMetadata ProcessorEntityMetadata::CreateNew(
const ClientTagHash& client_tag_hash,
std::string server_id,
base::Time creation_time) {
sync_pb::EntityMetadata metadata;
metadata.set_client_tag_hash(client_tag_hash.value());
if (!server_id.empty()) {
metadata.set_server_id(std::move(server_id));
}
metadata.set_sequence_number(0);
metadata.set_acked_sequence_number(0);
metadata.set_server_version(kUncommittedVersion);
metadata.set_creation_time(TimeToProtoTime(creation_time));
return ProcessorEntityMetadata(std::move(metadata));
}
// static
std::unique_ptr<ProcessorEntityMetadata> ProcessorEntityMetadata::FromProto(
sync_pb::EntityMetadata metadata) {
if (!IsValid(metadata)) {
return nullptr;
}
return base::WrapUnique(new ProcessorEntityMetadata(std::move(metadata)));
}
ProcessorEntityMetadata::ProcessorEntityMetadata(
sync_pb::EntityMetadata metadata)
: metadata_(std::move(metadata)) {
CHECK(IsValid(metadata_));
}
ProcessorEntityMetadata::~ProcessorEntityMetadata() = default;
bool ProcessorEntityMetadata::IsDeleted() const {
return metadata_.is_deleted();
}
bool ProcessorEntityMetadata::IsUnsynced() const {
return metadata_.sequence_number() > metadata_.acked_sequence_number();
}
bool ProcessorEntityMetadata::IsUnsyncedLocalCreation() const {
return metadata_.server_version() == kUncommittedVersion && IsUnsynced();
}
bool ProcessorEntityMetadata::IsVersionAlreadyKnown(
int64_t update_version) const {
return metadata_.server_version() >= update_version;
}
void ProcessorEntityMetadata::OverrideServerMetadata(
const std::string& server_id,
int64_t server_version) {
metadata_.set_server_id(server_id);
metadata_.set_server_version(server_version);
}
size_t ProcessorEntityMetadata::EstimateMemoryUsage() const {
using base::trace_event::EstimateMemoryUsage;
return EstimateMemoryUsage(metadata_);
}
ClientTagHash ProcessorEntityMetadata::GetClientTagHash() const {
return ClientTagHash::FromHashed(metadata_.client_tag_hash());
}
void ProcessorEntityMetadata::IncrementSequenceNumber() {
CHECK(metadata_.has_sequence_number());
if (!IsUnsynced()) {
// Update the base specifics hash if this entity wasn't already out of sync.
metadata_.set_base_specifics_hash(metadata_.specifics_hash());
}
metadata_.set_sequence_number(metadata_.sequence_number() + 1);
CHECK(IsUnsynced());
}
bool ProcessorEntityMetadata::MatchesData(const EntityData& data) const {
if (metadata_.is_deleted() || data.is_deleted()) {
// If either of them is deleted, the data matches only if both are deleted.
return metadata_.is_deleted() == data.is_deleted();
}
// Do not check for unique position changes explicitly because they are
// supposed to be in specifics.
return MatchesSpecificsHash(data.specifics);
}
bool ProcessorEntityMetadata::MatchesBaseData(const EntityData& data) const {
DCHECK(IsUnsynced());
if (data.is_deleted() || metadata_.base_specifics_hash().empty()) {
return false;
}
return HashSpecifics(data.specifics) == metadata_.base_specifics_hash();
}
bool ProcessorEntityMetadata::MatchesOwnBaseData() const {
DCHECK(IsUnsynced());
if (metadata_.is_deleted()) {
return false;
}
DCHECK(!metadata_.specifics_hash().empty());
return metadata_.specifics_hash() == metadata_.base_specifics_hash();
}
bool ProcessorEntityMetadata::MatchesSpecificsHash(
const sync_pb::EntitySpecifics& specifics) const {
DCHECK(!metadata_.is_deleted());
DCHECK_GT(specifics.ByteSizeLong(), 0u);
return HashSpecifics(specifics) == metadata_.specifics_hash();
}
bool ProcessorEntityMetadata::MatchesFaviconHash(
const std::string& favicon_png_bytes) const {
DCHECK(!metadata_.is_deleted());
return metadata_.bookmark_favicon_hash() ==
base::PersistentHash(favicon_png_bytes);
}
void ProcessorEntityMetadata::UpdateSpecificsHash(
const sync_pb::EntitySpecifics& specifics) {
if (specifics.ByteSizeLong() > 0) {
metadata_.set_specifics_hash(HashSpecifics(specifics));
if (specifics.has_bookmark()) {
metadata_.set_bookmark_favicon_hash(
base::PersistentHash(specifics.bookmark().favicon()));
}
} else {
metadata_.clear_specifics_hash();
metadata_.clear_bookmark_favicon_hash();
}
}
void ProcessorEntityMetadata::RecordIgnoredRemoteUpdate(
const UpdateResponseData& update) {
metadata_.set_server_id(update.entity.id);
metadata_.set_server_version(update.response_version);
}
void ProcessorEntityMetadata::RecordAcceptedRemoteUpdate(
const UpdateResponseData& update,
sync_pb::EntitySpecifics trimmed_specifics,
std::optional<sync_pb::UniquePosition> unique_position) {
RecordIgnoredRemoteUpdate(update);
metadata_.set_is_deleted(update.entity.is_deleted());
metadata_.set_modification_time(
TimeToProtoTime(update.entity.modification_time));
if (update.entity.collaboration_metadata.has_value()) {
metadata_.mutable_collaboration()->set_collaboration_id(
update.entity.collaboration_metadata->collaboration_id().value());
if (!update.entity.collaboration_metadata->created_by().empty()) {
metadata_.mutable_collaboration()
->mutable_creation_attribution()
->set_obfuscated_gaia_id(
update.entity.collaboration_metadata->created_by().ToString());
}
if (!update.entity.collaboration_metadata->last_updated_by().empty()) {
metadata_.mutable_collaboration()
->mutable_last_update_attribution()
->set_obfuscated_gaia_id(
update.entity.collaboration_metadata->last_updated_by()
.ToString());
}
}
UpdateSpecificsHash(update.entity.specifics);
if (trimmed_specifics.ByteSizeLong() > 0) {
*metadata_.mutable_possibly_trimmed_base_specifics() =
std::move(trimmed_specifics);
} else {
metadata_.clear_possibly_trimmed_base_specifics();
}
if (unique_position) {
*metadata_.mutable_unique_position() = std::move(unique_position.value());
} else {
metadata_.clear_unique_position();
}
}
void ProcessorEntityMetadata::RecordForcedRemoteUpdate(
const UpdateResponseData& update,
sync_pb::EntitySpecifics trimmed_specifics,
std::optional<sync_pb::UniquePosition> unique_position) {
CHECK(!HasPendingAttachmentUpload());
metadata_.set_acked_sequence_number(metadata_.sequence_number());
metadata_.clear_attachment_metadata();
RecordAcceptedRemoteUpdate(update, std::move(trimmed_specifics),
std::move(unique_position));
}
void ProcessorEntityMetadata::RecordCommitResponse(
const CommitResponseData& data) {
metadata_.set_server_id(data.id);
metadata_.set_acked_sequence_number(data.sequence_number);
metadata_.set_server_version(data.response_version);
if (metadata_.has_attachment_metadata() &&
data.attachment_modification_time.has_value()) {
metadata_.mutable_attachment_metadata()
->mutable_attachment()
->set_modification_time_unix_epoch_millis(
TimeToProtoTime(*data.attachment_modification_time));
}
if (!IsUnsynced()) {
metadata_.clear_base_specifics_hash();
if (metadata_.has_attachment_metadata()) {
metadata_.mutable_attachment_metadata()
->mutable_attachment()
->clear_temporary_blob_id();
}
} else {
metadata_.set_base_specifics_hash(data.specifics_hash);
}
}
void ProcessorEntityMetadata::RecordLocalUpdate(
const EntityData& data,
sync_pb::EntitySpecifics trimmed_specifics,
std::optional<sync_pb::UniquePosition> unique_position) {
const base::Time modification_time = !data.modification_time.is_null()
? data.modification_time
: base::Time::Now();
IncrementSequenceNumber();
UpdateSpecificsHash(data.specifics);
metadata_.set_modification_time(TimeToProtoTime(modification_time));
metadata_.set_is_deleted(false);
if (unique_position.has_value()) {
*metadata_.mutable_unique_position() = std::move(unique_position).value();
} else {
metadata_.clear_unique_position();
}
if (trimmed_specifics.ByteSizeLong() > 0) {
*metadata_.mutable_possibly_trimmed_base_specifics() =
std::move(trimmed_specifics);
} else {
metadata_.clear_possibly_trimmed_base_specifics();
}
if (!data.creation_time.is_null()) {
metadata_.set_creation_time(TimeToProtoTime(data.creation_time));
}
// Collaboration metadata is updated only on creation (i.e. for the first
// time). Only `last_updated` field can be changed on local updates.
if (data.collaboration_metadata.has_value()) {
if (!metadata_.has_collaboration()) {
metadata_.mutable_collaboration()->set_collaboration_id(
data.collaboration_metadata->collaboration_id().value());
metadata_.mutable_collaboration()
->mutable_creation_attribution()
->set_obfuscated_gaia_id(
data.collaboration_metadata->created_by().ToString());
}
metadata_.mutable_collaboration()
->mutable_last_update_attribution()
->set_obfuscated_gaia_id(
data.collaboration_metadata->last_updated_by().ToString());
// Collaboration ID must never change.
CHECK_EQ(metadata_.collaboration().collaboration_id(),
data.collaboration_metadata->collaboration_id().value());
}
}
void ProcessorEntityMetadata::RecordLocalDeletion(
const DeletionOrigin& origin) {
IncrementSequenceNumber();
metadata_.set_modification_time(TimeToProtoTime(base::Time::Now()));
metadata_.set_is_deleted(true);
metadata_.clear_specifics_hash();
metadata_.clear_bookmark_favicon_hash();
metadata_.clear_possibly_trimmed_base_specifics();
metadata_.clear_unique_position();
CHECK(!HasPendingAttachmentUpload());
metadata_.clear_attachment_metadata();
if (origin.is_specified()) {
*metadata_.mutable_deletion_origin() =
origin.ToProto(version_info::GetVersionNumber());
}
metadata_.set_deleted_by_version(
std::string(version_info::GetVersionNumber()));
}
bool ProcessorEntityMetadata::HasPendingAttachmentUpload() const {
return metadata_.attachment_metadata().has_pending_upload();
}
void ProcessorEntityMetadata::RecordAttachmentUploadQueued(
const base::FilePath& file_path) {
CHECK(!IsDeleted());
CHECK(IsUnsynced());
CHECK(!file_path.empty());
sync_pb::EntityMetadata::AttachmentMetadata* attachment_metadata =
metadata_.mutable_attachment_metadata();
attachment_metadata->clear_attachment();
sync_pb::EntityMetadata::AttachmentMetadata::PendingUpload* pending_upload =
attachment_metadata->mutable_pending_upload();
pending_upload->clear_upload_url();
pending_upload->set_file_path(file_path.AsUTF8Unsafe());
}
void ProcessorEntityMetadata::RecordAttachmentUploadResumeUrlReceived(
const GURL& upload_url) {
CHECK(!IsDeleted());
CHECK(HasPendingAttachmentUpload());
CHECK(upload_url.is_valid());
metadata_.mutable_attachment_metadata()
->mutable_pending_upload()
->set_upload_url(upload_url.spec());
}
void ProcessorEntityMetadata::RecordAttachmentUploadCompleted(
const std::string& temporary_blob_id) {
CHECK(!IsDeleted());
CHECK(HasPendingAttachmentUpload());
CHECK(!temporary_blob_id.empty());
sync_pb::EntityMetadata::AttachmentMetadata* attachment_metadata =
metadata_.mutable_attachment_metadata();
attachment_metadata->clear_pending_upload();
attachment_metadata->mutable_attachment()->set_temporary_blob_id(
temporary_blob_id);
}
void ProcessorEntityMetadata::RecordPendingAttachmentUploadCleared() {
if (!HasPendingAttachmentUpload()) {
return;
}
metadata_.mutable_attachment_metadata()->clear_pending_upload();
if (!metadata_.attachment_metadata().has_attachment()) {
metadata_.clear_attachment_metadata();
}
}
} // namespace syncer