blob: aabe85be3aad2a036f1da8f210e3f9e1c898dc39 [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/sync/engine_impl/bookmark_update_preprocessing.h"
#include <array>
#include "base/containers/span.h"
#include "base/guid.h"
#include "base/hash/sha1.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "components/sync/base/hash_util.h"
#include "components/sync/base/unique_position.h"
#include "components/sync/model/entity_data.h"
#include "components/sync/protocol/sync.pb.h"
namespace syncer {
namespace {
// Enumeration of possible values for the positioning schemes used in Sync
// entities. Used in UMA metrics. Do not re-order or delete these entries; they
// are used in a UMA histogram. Please edit SyncPositioningScheme in enums.xml
// if a value is added.
enum class SyncPositioningScheme {
kUniquePosition = 0,
kPositionInParent = 1,
kInsertAfterItemId = 2,
kMissing = 3,
kMaxValue = kMissing
};
// Used in metric "Sync.BookmarkGUIDSource2". These values are persisted to
// logs. Entries should not be renumbered and numeric values should never be
// reused.
enum class BookmarkGuidSource {
// GUID came from specifics.
kSpecifics = 0,
// GUID came from originator_client_item_id and is valid.
kValidOCII = 1,
// GUID not found in the specifics and originator_client_item_id is invalid,
// so field left empty (currently unused).
kDeprecatedLeftEmpty = 2,
// GUID not found in the specifics and originator_client_item_id is invalid,
// so the GUID is inferred from combining originator_client_item_id and
// originator_cache_guid.
kInferred = 3,
kMaxValue = kInferred,
};
inline void LogGuidSource(BookmarkGuidSource source) {
base::UmaHistogramEnumeration("Sync.BookmarkGUIDSource2", source);
}
std::string ComputeGuidFromBytes(base::span<const uint8_t> bytes) {
DCHECK_GE(bytes.size(), 16U);
// This implementation is based on the equivalent logic in base/guid.cc.
// Set the GUID to version 4 as described in RFC 4122, section 4.4.
// The format of GUID version 4 must be xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx,
// where y is one of [8, 9, A, B].
// Clear the version bits and set the version to 4:
const uint8_t byte6 = (bytes[6] & 0x0fU) | 0xf0U;
// Set the two most significant bits (bits 6 and 7) of the
// clock_seq_hi_and_reserved to zero and one, respectively:
const uint8_t byte8 = (bytes[8] & 0x3fU) | 0x80U;
return base::StringPrintf(
"%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], byte6,
bytes[7], byte8, bytes[9], bytes[10], bytes[11], bytes[12], bytes[13],
bytes[14], bytes[15]);
}
// Bookmarks created before 2015 (https://codereview.chromium.org/1136953013)
// have an originator client item ID that is NOT a GUID. Hence, an alternative
// method must be used to infer a GUID deterministically from a combination of
// sync fields that is known to be a) immutable and b) unique per synced
// bookmark.
std::string InferGuidForLegacyBookmark(
const std::string& originator_cache_guid,
const std::string& originator_client_item_id) {
DCHECK(!base::IsValidGUID(originator_client_item_id));
const std::string unique_tag =
base::StrCat({originator_cache_guid, originator_client_item_id});
const std::array<uint8_t, base::kSHA1Length> hash =
base::SHA1HashSpan(base::as_bytes(base::make_span(unique_tag)));
static_assert(base::kSHA1Length >= 16, "16 bytes needed to infer GUID");
const std::string guid = ComputeGuidFromBytes(base::make_span(hash));
DCHECK(base::IsValidGUIDOutputString(guid));
return guid;
}
} // namespace
void AdaptUniquePositionForBookmark(const sync_pb::SyncEntity& update_entity,
EntityData* data) {
DCHECK(data);
// Tombstones don't need positioning information.
if (update_entity.deleted()) {
return;
}
// Permanent folders don't need positioning information.
if (update_entity.folder() &&
!update_entity.server_defined_unique_tag().empty()) {
return;
}
bool has_position_scheme = false;
SyncPositioningScheme sync_positioning_scheme;
if (update_entity.has_unique_position()) {
data->unique_position = update_entity.unique_position();
has_position_scheme = true;
sync_positioning_scheme = SyncPositioningScheme::kUniquePosition;
} else if (update_entity.has_position_in_parent() ||
update_entity.has_insert_after_item_id()) {
bool missing_originator_fields = false;
if (!update_entity.has_originator_cache_guid() ||
!update_entity.has_originator_client_item_id()) {
DLOG(ERROR) << "Update is missing requirements for bookmark position.";
missing_originator_fields = true;
}
std::string suffix = missing_originator_fields
? UniquePosition::RandomSuffix()
: GenerateSyncableBookmarkHash(
update_entity.originator_cache_guid(),
update_entity.originator_client_item_id());
if (update_entity.has_position_in_parent()) {
data->unique_position =
UniquePosition::FromInt64(update_entity.position_in_parent(), suffix)
.ToProto();
has_position_scheme = true;
sync_positioning_scheme = SyncPositioningScheme::kPositionInParent;
} else {
// If update_entity has insert_after_item_id, use 0 index.
DCHECK(update_entity.has_insert_after_item_id());
data->unique_position = UniquePosition::FromInt64(0, suffix).ToProto();
has_position_scheme = true;
sync_positioning_scheme = SyncPositioningScheme::kInsertAfterItemId;
}
} else {
DLOG(ERROR) << "Missing required position information in update: "
<< update_entity.id_string();
has_position_scheme = true;
sync_positioning_scheme = SyncPositioningScheme::kMissing;
}
if (has_position_scheme) {
UMA_HISTOGRAM_ENUMERATION("Sync.Entities.PositioningScheme",
sync_positioning_scheme);
}
}
void AdaptTitleForBookmark(const sync_pb::SyncEntity& update_entity,
sync_pb::EntitySpecifics* specifics,
bool specifics_were_encrypted) {
DCHECK(specifics);
if (specifics_were_encrypted || update_entity.deleted()) {
// If encrypted, the name field is never populated (unencrypted) for privacy
// reasons. Encryption was also introduced after moving the name out of
// SyncEntity so this hack is not needed at all.
return;
}
// Legacy clients populate the name field in the SyncEntity instead of the
// title field in the BookmarkSpecifics.
if (!specifics->bookmark().has_title() && !update_entity.name().empty()) {
specifics->mutable_bookmark()->set_title(update_entity.name());
}
}
void AdaptGuidForBookmark(const sync_pb::SyncEntity& update_entity,
sync_pb::EntitySpecifics* specifics) {
DCHECK(specifics);
// Tombstones and permanent entities don't have a GUID.
if (update_entity.deleted() ||
!update_entity.server_defined_unique_tag().empty()) {
return;
}
// Legacy clients don't populate the guid field in the BookmarkSpecifics, so
// we use the originator_client_item_id instead, if it is a valid GUID.
// Otherwise, we leave the field empty.
if (specifics->bookmark().has_guid()) {
LogGuidSource(BookmarkGuidSource::kSpecifics);
} else if (base::IsValidGUID(update_entity.originator_client_item_id())) {
// Bookmarks created around 2016, between [M44..M52) use an uppercase GUID
// as originator client item ID, so it needs to be lowercased to adhere to
// the invariant that GUIDs in specifics are canonicalized.
specifics->mutable_bookmark()->set_guid(
base::ToLowerASCII(update_entity.originator_client_item_id()));
DCHECK(base::IsValidGUIDOutputString(specifics->bookmark().guid()));
LogGuidSource(BookmarkGuidSource::kValidOCII);
} else {
specifics->mutable_bookmark()->set_guid(
InferGuidForLegacyBookmark(update_entity.originator_cache_guid(),
update_entity.originator_client_item_id()));
DCHECK(base::IsValidGUIDOutputString(specifics->bookmark().guid()));
LogGuidSource(BookmarkGuidSource::kInferred);
}
}
std::string InferGuidForLegacyBookmarkForTesting(
const std::string& originator_cache_guid,
const std::string& originator_client_item_id) {
return InferGuidForLegacyBookmark(originator_cache_guid,
originator_client_item_id);
}
} // namespace syncer