| // 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/hash/hash.h" |
| #include "base/hash/sha1.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/not_fatal_until.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" |
| |
| namespace syncer { |
| |
| namespace { |
| |
| std::string HashSpecifics(const sync_pb::EntitySpecifics& specifics) { |
| DCHECK_GT(specifics.ByteSizeLong(), 0u); |
| return base::Base64Encode( |
| base::SHA1HashString(specifics.SerializeAsString())); |
| } |
| |
| } // namespace |
| |
| // static |
| bool ProcessorEntityMetadata::IsValid(const sync_pb::EntityMetadata& metadata) { |
| 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(), base::NotFatalUntil::M141); |
| 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(), base::NotFatalUntil::M141); |
| } |
| |
| 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) { |
| metadata_.set_acked_sequence_number(metadata_.sequence_number()); |
| 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 (!IsUnsynced()) { |
| metadata_.clear_base_specifics_hash(); |
| } 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(); |
| |
| if (origin.is_specified()) { |
| *metadata_.mutable_deletion_origin() = |
| origin.ToProto(version_info::GetVersionNumber()); |
| } |
| |
| metadata_.set_deleted_by_version( |
| std::string(version_info::GetVersionNumber())); |
| } |
| |
| } // namespace syncer |