blob: 8bc1c8e0b9cc19ee8d3a61c4a781c3876613ed8f [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/indexed_db/instance/backing_store_util.h"
#include "base/containers/to_vector.h"
#include "base/strings/string_number_conversions.h"
#include "base/values.h"
#include "content/browser/indexed_db/indexed_db_value.h"
#include "content/browser/indexed_db/instance/backing_store.h"
#include "crypto/hash.h"
#include "third_party/blink/public/common/indexeddb/indexeddb_key_path.h"
#include "third_party/blink/public/common/indexeddb/indexeddb_metadata.h"
#include "third_party/blink/public/mojom/indexeddb/indexeddb.mojom.h"
namespace content::indexed_db {
namespace {
// Converts an integer that may have different length or signedness to a
// base::Value, since base::Value only natively supports int. If it doesn't fit
// in an int, the output will be a string.
template <typename T>
base::Value ToIntValue(T value) {
T small_value = static_cast<int>(value);
if (value == small_value) {
return base::Value(static_cast<int>(small_value));
}
return base::Value(base::NumberToString(value));
}
// When keys or values are longer than this number, or an object store has more
// records than this number, the contents will be hashed instead of stored
// verbatim. This limits total memory usage.
static const int kHashingThreshold = 256;
base::DictValue DatabaseMetadataToDictValue(const BackingStore::Database& db) {
base::DictValue result;
const blink::IndexedDBDatabaseMetadata& metadata = db.GetMetadata();
result.Set("name", metadata.name);
result.Set("version", ToIntValue(metadata.version));
base::ListValue object_stores;
for (const auto& [_, object_store] : metadata.object_stores) {
base::DictValue object_store_dict;
object_store_dict.Set("name", object_store.name);
object_store_dict.Set("id", ToIntValue(object_store.id));
object_store_dict.Set("auto_increment", object_store.auto_increment);
auto key_path_to_dict = [](const blink::IndexedDBKeyPath& key_path) {
base::DictValue key_path_dict;
key_path_dict.Set("type", static_cast<int>(key_path.type()));
if (key_path.type() == blink::mojom::IDBKeyPathType::String) {
key_path_dict.Set("string", key_path.string());
} else if (key_path.type() == blink::mojom::IDBKeyPathType::Array) {
base::ListValue key_path_list =
base::ListValue::with_capacity(key_path.array().size());
for (const std::u16string& component : key_path.array()) {
key_path_list.Append(component);
}
key_path_dict.Set("path", std::move(key_path_list));
} else {
CHECK_EQ(key_path.type(), blink::mojom::IDBKeyPathType::Null);
}
return key_path_dict;
};
object_store_dict.Set("key_path", key_path_to_dict(object_store.key_path));
base::ListValue indexes;
for (const auto& [_, index] : object_store.indexes) {
base::DictValue index_dict;
index_dict.Set("name", index.name);
index_dict.Set("id", ToIntValue(index.id));
index_dict.Set("key_path", key_path_to_dict(index.key_path));
index_dict.Set("unique", index.unique);
index_dict.Set("multi_entry", index.multi_entry);
indexes.Append(std::move(index_dict));
}
object_store_dict.Set("indexes", std::move(indexes));
object_stores.Append(std::move(object_store_dict));
}
result.Set("object_stores", std::move(object_stores));
return result;
}
// Fully hashes keys and values from the cursor. For use when there are a lot of
// rows (high `key_count`).
StatusOr<base::BlobStorage> CursorToSummaryValue(
std::unique_ptr<BackingStore::Cursor>& cursor,
bool include_primary_key,
size_t key_count) {
crypto::hash::Hasher streaming_hasher(crypto::hash::HashKind::kSha256);
while (cursor) {
streaming_hasher.Update(cursor->GetKey().DebugString());
if (include_primary_key) {
streaming_hasher.Update(cursor->GetPrimaryKey().DebugString());
}
streaming_hasher.Update(cursor->GetValue().bits);
std::vector<int64_t> object_data;
object_data.reserve(2 * cursor->GetValue().external_objects.size());
for (const IndexedDBExternalObject& object :
cursor->GetValue().external_objects) {
object_data.push_back(static_cast<int64_t>(object.object_type()));
object_data.push_back(object.size());
}
streaming_hasher.Update(base::as_byte_span(object_data));
StatusOr<bool> continue_result = cursor->Continue();
if (!continue_result.has_value()) {
return base::unexpected(continue_result.error());
}
if (!*continue_result) {
break;
}
}
base::BlobStorage digest(crypto::hash::kSha256Size);
streaming_hasher.Finish(digest);
return digest;
}
// Turns the record pointed to by `Cursor` into a dictionary, and applies some
// "light" hashing to keys and values.
base::DictValue RecordToDictValue(BackingStore::Cursor& cursor,
bool include_primary_key) {
base::DictValue record;
std::string key_string = cursor.GetKey().DebugString();
if (key_string.size() > kHashingThreshold) {
record.Set(
"key_digest",
base::ToVector(crypto::hash::Sha256(base::as_byte_span(key_string))));
} else {
record.Set("key", std::move(key_string));
}
if (include_primary_key) {
key_string = cursor.GetPrimaryKey().DebugString();
if (key_string.size() > kHashingThreshold) {
record.Set(
"primary_key_digest",
base::ToVector(crypto::hash::Sha256(base::as_byte_span(key_string))));
} else {
record.Set("primary_key", std::move(key_string));
}
}
IndexedDBValue value = std::move(cursor.GetValue());
if (value.bits.size() > kHashingThreshold) {
record.Set("value_digest",
base::ToVector(crypto::hash::Sha256(value.bits)));
} else {
record.Set("value", std::move(value.bits));
}
// Include some limited metadata for blobs (and other external objects).
base::ListValue external_objects;
for (const IndexedDBExternalObject& object : value.external_objects) {
base::DictValue object_dict;
object_dict.Set("type", static_cast<int>(object.object_type()));
object_dict.Set("size", ToIntValue(object.size()));
external_objects.Append(std::move(object_dict));
}
record.Set("external_objects", std::move(external_objects));
return record;
}
StatusOr<base::ListValue> CursorToListValue(
std::unique_ptr<BackingStore::Cursor>& cursor,
bool include_primary_key,
size_t key_count) {
base::ListValue records = base::ListValue::with_capacity(key_count);
while (cursor) {
records.Append(RecordToDictValue(*cursor, include_primary_key));
StatusOr<bool> continue_result = cursor->Continue();
if (!continue_result.has_value()) {
return base::unexpected(continue_result.error());
}
if (!*continue_result) {
break;
}
}
return records;
}
StatusOr<base::DictValue> IndexToDictValue(
const blink::IndexedDBObjectStoreMetadata& object_store,
const blink::IndexedDBIndexMetadata& index,
BackingStore::Transaction& txn) {
base::DictValue contents;
// This is technically unnecessary.
StatusOr<uint32_t> key_count =
txn.GetIndexKeyCount(object_store.id, index.id, /*key_range=*/{});
if (!key_count.has_value()) {
return base::unexpected(key_count.error());
}
contents.Set("record_count", ToIntValue(*key_count));
// Print all records via Cursor.
StatusOr<std::unique_ptr<BackingStore::Cursor>> cursor =
txn.OpenIndexCursor(object_store.id, index.id, /*key_range=*/{},
blink::mojom::IDBCursorDirection::Next);
if (!cursor.has_value()) {
return base::unexpected(cursor.error());
}
if (*key_count > kHashingThreshold) {
StatusOr<base::BlobStorage> records_digest =
CursorToSummaryValue(*cursor, /*include_primary_key=*/true, *key_count);
if (!records_digest.has_value()) {
return base::unexpected(records_digest.error());
}
contents.Set("records_digest", *std::move(records_digest));
} else {
StatusOr<base::ListValue> records =
CursorToListValue(*cursor, /*include_primary_key=*/true, *key_count);
if (!records.has_value()) {
return base::unexpected(records.error());
}
contents.Set("records", std::move(*records));
}
return contents;
}
StatusOr<base::DictValue> ObjectStoreToDictValue(
const blink::IndexedDBObjectStoreMetadata& object_store,
BackingStore::Transaction& txn) {
base::DictValue contents;
// This is technically unnecessary.
StatusOr<uint32_t> key_count =
txn.GetObjectStoreKeyCount(object_store.id, /*key_range=*/{});
if (!key_count.has_value()) {
return base::unexpected(key_count.error());
}
contents.Set("record_count", ToIntValue(*key_count));
// Print all records via Cursor.
StatusOr<std::unique_ptr<BackingStore::Cursor>> cursor =
txn.OpenObjectStoreCursor(object_store.id, /*key_range=*/{},
blink::mojom::IDBCursorDirection::Next);
if (!cursor.has_value()) {
return base::unexpected(cursor.error());
}
if (*key_count > kHashingThreshold) {
StatusOr<base::BlobStorage> records_digest = CursorToSummaryValue(
*cursor, /*include_primary_key=*/false, *key_count);
if (!records_digest.has_value()) {
return base::unexpected(records_digest.error());
}
contents.Set("records_digest", *std::move(records_digest));
} else {
StatusOr<base::ListValue> records =
CursorToListValue(*cursor, /*include_primary_key=*/false, *key_count);
if (!records.has_value()) {
return base::unexpected(records.error());
}
contents.Set("records", *std::move(records));
}
base::DictValue indexes;
indexes.reserve(object_store.indexes.size());
for (const auto& [id, index] : object_store.indexes) {
StatusOr<base::DictValue> index_dict =
IndexToDictValue(object_store, index, txn);
if (!index_dict.has_value()) {
return index_dict;
}
indexes.Set(base::NumberToString(id), std::move(*index_dict));
}
contents.Set("indexes", std::move(indexes));
return contents;
}
StatusOr<base::DictValue> DatabaseContentsToDictValue(
BackingStore::Database& db) {
base::DictValue contents;
auto txn =
db.CreateTransaction(blink::mojom::IDBTransactionDurability::Relaxed,
blink::mojom::IDBTransactionMode::ReadOnly);
// Locks are assumed to be unnecessary as this is a synchronous operation
// that won't be interrupted.
std::vector<PartitionedLock> locks;
locks.emplace_back(PartitionedLockId{.partition = 0, .key = "unused"},
base::DoNothing());
Status status = txn->Begin(std::move(locks));
if (!status.ok()) {
return base::unexpected(status);
}
base::DictValue object_stores;
object_stores.reserve(db.GetMetadata().object_stores.size());
for (const auto& [id, object_store] : db.GetMetadata().object_stores) {
StatusOr<base::DictValue> obj_store =
ObjectStoreToDictValue(object_store, *txn);
if (!obj_store.has_value()) {
return obj_store;
}
object_stores.Set(base::NumberToString(id), std::move(*obj_store));
}
contents.Set("object_stores", std::move(object_stores));
return contents;
}
} // namespace
StatusOr<base::DictValue> SnapshotDatabase(BackingStore::Database& db) {
StatusOr<base::DictValue> metadata = DatabaseMetadataToDictValue(db);
if (!metadata.has_value()) {
return metadata;
}
StatusOr<base::DictValue> contents = DatabaseContentsToDictValue(db);
if (!contents.has_value()) {
return contents;
}
base::DictValue result;
result.Set("metadata", *std::move(metadata));
result.Set("contents", *std::move(contents));
return result;
}
} // namespace content::indexed_db