[SessionStorage] Metadata parsing and writing for mojo version.
Bug: 716490
Change-Id: If2b0672af149abf44dd475a2196e7b727cb450a7
Reviewed-on: https://chromium-review.googlesource.com/986880
Reviewed-by: John Abd-El-Malek <[email protected]>
Reviewed-by: Marijn Kruisselbrink <[email protected]>
Commit-Queue: Daniel Murphy <[email protected]>
Cr-Commit-Position: refs/heads/master@{#550454}
diff --git a/content/browser/BUILD.gn b/content/browser/BUILD.gn
index a967e1b..e267363 100644
--- a/content/browser/BUILD.gn
+++ b/content/browser/BUILD.gn
@@ -688,6 +688,8 @@
"dom_storage/session_storage_database.h",
"dom_storage/session_storage_database_adapter.cc",
"dom_storage/session_storage_database_adapter.h",
+ "dom_storage/session_storage_metadata.cc",
+ "dom_storage/session_storage_metadata.h",
"dom_storage/session_storage_namespace_impl.cc",
"dom_storage/session_storage_namespace_impl.h",
"download/blob_download_url_loader_factory_getter.cc",
diff --git a/content/browser/dom_storage/session_storage_metadata.cc b/content/browser/dom_storage/session_storage_metadata.cc
new file mode 100644
index 0000000..87af79b7
--- /dev/null
+++ b/content/browser/dom_storage/session_storage_metadata.cc
@@ -0,0 +1,380 @@
+// Copyright 2018 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 "content/browser/dom_storage/session_storage_metadata.h"
+
+#include "base/macros.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/stringprintf.h"
+#include "components/services/leveldb/public/cpp/util.h"
+#include "content/common/dom_storage/dom_storage_namespace_ids.h"
+#include "url/gurl.h"
+
+namespace content {
+namespace {
+using leveldb::mojom::BatchedOperation;
+using leveldb::mojom::BatchOperationType;
+using leveldb::mojom::BatchedOperationPtr;
+
+// Example layout of the database:
+// | key | value |
+// |----------------------------------------|--------------------|
+// | map-1-a | b (a = b in map 1) |
+// | ... | |
+// | namespace-<36 char guid 1>-origin1 | 1 (mapid) |
+// | namespace-<36 char guid 1>-origin2 | 2 |
+// | namespace-<36 char guid 2>-origin1 | 1 (shallow copy) |
+// | namespace-<36 char guid 2>-origin2 | 2 (shallow copy) |
+// | namespace-<36 char guid 3>-origin1 | 3 (deep copy) |
+// | namespace-<36 char guid 3>-origin2 | 2 (shallow copy) |
+// | next-map-id | 4 |
+// | version | 1 |
+// Example area key: namespace-dabc53e1_8291_4de5_824f_dab8aa69c846-origin2
+//
+// Note: All number values are string conversions of numbers.
+
+// This is "map-" (without the quotes).
+constexpr const uint8_t kMapIdPrefixBytes[] = {'m', 'a', 'p', '-'};
+
+constexpr const size_t kNamespacePrefixLength =
+ arraysize(SessionStorageMetadata::kNamespacePrefixBytes);
+constexpr const uint8_t kNamespaceOriginSeperatorByte = '-';
+constexpr const size_t kNamespaceOriginSeperatorLength = 1;
+constexpr const size_t kPrefixBeforeOriginLength =
+ kNamespacePrefixLength + kSessionStorageNamespaceIdLength +
+ kNamespaceOriginSeperatorLength;
+
+bool ValueToNumber(const std::vector<uint8_t>& value, int64_t* out) {
+ return base::StringToInt64(leveldb::Uint8VectorToStringPiece(value), out);
+}
+
+std::vector<uint8_t> NumberToValue(int64_t map_number) {
+ return leveldb::StdStringToUint8Vector(base::NumberToString(map_number));
+}
+} // namespace
+
+constexpr const int64_t SessionStorageMetadata::kInvalidDatabaseVersion;
+constexpr const int64_t SessionStorageMetadata::kInvalidMapId;
+constexpr const uint8_t SessionStorageMetadata::kDatabaseVersionBytes[];
+constexpr const uint8_t SessionStorageMetadata::kNamespacePrefixBytes[];
+constexpr const uint8_t SessionStorageMetadata::kNextMapIdKeyBytes[];
+
+SessionStorageMetadata::MapData::MapData(int64_t map_number)
+ : number_as_bytes_(NumberToValue(map_number)),
+ key_prefix_(SessionStorageMetadata::GetMapPrefix(number_as_bytes_)) {}
+SessionStorageMetadata::MapData::~MapData() = default;
+
+SessionStorageMetadata::SessionStorageMetadata() {}
+
+SessionStorageMetadata::~SessionStorageMetadata() {}
+
+std::vector<leveldb::mojom::BatchedOperationPtr>
+SessionStorageMetadata::SetupNewDatabase(int64_t version) {
+ database_version_ = version;
+ next_map_id_ = 0;
+ next_map_id_from_namespaces_ = 0;
+ namespace_origin_map_.clear();
+
+ std::vector<leveldb::mojom::BatchedOperationPtr> operations;
+ operations.reserve(2);
+ operations.push_back(BatchedOperation::New(
+ BatchOperationType::PUT_KEY,
+ std::vector<uint8_t>(std::begin(kDatabaseVersionBytes),
+ std::end(kDatabaseVersionBytes)),
+ DatabaseVersionAsVector()));
+ operations.push_back(
+ BatchedOperation::New(BatchOperationType::PUT_KEY,
+ std::vector<uint8_t>(std::begin(kNextMapIdKeyBytes),
+ std::end(kNextMapIdKeyBytes)),
+ NumberToValue(next_map_id_)));
+ return operations;
+}
+
+bool SessionStorageMetadata::ParseDatabaseVersion(
+ const std::vector<uint8_t>& value) {
+ if (!ValueToNumber(value, &database_version_)) {
+ database_version_ = kInvalidDatabaseVersion;
+ return false;
+ }
+ return true;
+}
+
+bool SessionStorageMetadata::ParseNamespaces(
+ std::vector<leveldb::mojom::KeyValuePtr> values) {
+ namespace_origin_map_.clear();
+ next_map_id_from_namespaces_ = 0;
+ // Since the data is ordered, all namespace data is in one spot. This keeps a
+ // reference to the last namespace data map to be more efficient.
+ std::string last_namespace_id;
+ std::map<url::Origin, scoped_refptr<MapData>>* last_namespace = nullptr;
+ std::map<int64_t, scoped_refptr<MapData>> maps;
+ bool error = false;
+ for (const leveldb::mojom::KeyValuePtr& key_value : values) {
+ size_t key_size = key_value->key.size();
+ if (key_size <= kNamespacePrefixLength) {
+ error = true;
+ break;
+ }
+
+ base::StringPiece key_as_string =
+ leveldb::Uint8VectorToStringPiece(key_value->key);
+
+ // The key must start with 'namespace-'.
+ if (!key_as_string.starts_with(base::StringPiece(
+ reinterpret_cast<const char*>(kNamespacePrefixBytes),
+ kNamespacePrefixLength))) {
+ error = true;
+ break;
+ }
+
+ // Old databases have a dummy 'namespace-' entry.
+ if (key_size == kNamespacePrefixLength)
+ continue;
+
+ // Check that we have a prefix of 'namespace-<guid>-'.
+ if (key_size < kPrefixBeforeOriginLength ||
+ key_as_string[kPrefixBeforeOriginLength - 1] !=
+ static_cast<const char>(kNamespaceOriginSeperatorByte)) {
+ error = true;
+ break;
+ }
+
+ // Old databases have a dummy 'namespace-<guid>-' entry.
+ if (key_size == kPrefixBeforeOriginLength)
+ continue;
+
+ base::StringPiece namespace_id = key_as_string.substr(
+ kNamespacePrefixLength, kSessionStorageNamespaceIdLength);
+
+ base::StringPiece origin_str =
+ key_as_string.substr(kPrefixBeforeOriginLength);
+
+ int64_t map_number;
+ if (!ValueToNumber(key_value->value, &map_number)) {
+ error = true;
+ break;
+ }
+
+ if (map_number >= next_map_id_from_namespaces_)
+ next_map_id_from_namespaces_ = map_number + 1;
+
+ auto origin_gurl = GURL(origin_str);
+ if (!origin_gurl.is_valid()) {
+ error = true;
+ break;
+ }
+
+ auto origin = url::Origin::Create(origin_gurl);
+ if (namespace_id != last_namespace_id) {
+ last_namespace_id = namespace_id.as_string();
+ DCHECK(namespace_origin_map_.find(last_namespace_id) ==
+ namespace_origin_map_.end());
+ last_namespace = &(namespace_origin_map_[last_namespace_id]);
+ }
+ auto map_it = maps.find(map_number);
+ if (map_it == maps.end()) {
+ map_it = maps.emplace(std::piecewise_construct,
+ std::forward_as_tuple(map_number),
+ std::forward_as_tuple(new MapData(map_number)))
+ .first;
+ }
+ map_it->second->IncReferenceCount();
+
+ last_namespace->emplace(std::make_pair(std::move(origin), map_it->second));
+ }
+ if (error) {
+ namespace_origin_map_.clear();
+ next_map_id_from_namespaces_ = 0;
+ return false;
+ }
+ if (next_map_id_ == 0 || next_map_id_ < next_map_id_from_namespaces_)
+ next_map_id_ = next_map_id_from_namespaces_;
+ return true;
+}
+
+void SessionStorageMetadata::ParseNextMapId(
+ const std::vector<uint8_t>& map_id) {
+ if (!ValueToNumber(map_id, &next_map_id_))
+ next_map_id_ = next_map_id_from_namespaces_;
+ if (next_map_id_ < next_map_id_from_namespaces_)
+ next_map_id_ = next_map_id_from_namespaces_;
+}
+
+std::vector<uint8_t> SessionStorageMetadata::DatabaseVersionAsVector() const {
+ DCHECK_NE(database_version_, kInvalidDatabaseVersion);
+ return NumberToValue(database_version_);
+}
+
+SessionStorageMetadata::MapData* SessionStorageMetadata::RegisterNewMap(
+ NamespaceEntry namespace_entry,
+ const url::Origin& origin,
+ std::vector<leveldb::mojom::BatchedOperationPtr>* save_operations) {
+ auto new_map_data = base::MakeRefCounted<MapData>(next_map_id_);
+ ++next_map_id_;
+
+ save_operations->push_back(BatchedOperation::New(
+ BatchOperationType::PUT_KEY,
+ std::vector<uint8_t>(
+ SessionStorageMetadata::kNextMapIdKeyBytes,
+ std::end(SessionStorageMetadata::kNextMapIdKeyBytes)),
+ NumberToValue(next_map_id_)));
+
+ std::map<url::Origin, scoped_refptr<MapData>>& namespace_origins =
+ namespace_entry->second;
+ auto namespace_it = namespace_origins.find(origin);
+ if (namespace_it != namespace_origins.end()) {
+ // Check the old map doesn't have the same number as the new map.
+ DCHECK(namespace_it->second->MapNumberAsBytes() !=
+ new_map_data->MapNumberAsBytes());
+ DCHECK_GT(namespace_it->second->ReferenceCount(), 1)
+ << "A new map should never be registered for an area that has a "
+ "single-refcount map.";
+ // There was already an area key here, so decrement that map reference.
+ namespace_it->second->DecReferenceCount();
+ namespace_it->second = new_map_data;
+ } else {
+ namespace_origins.emplace(std::make_pair(origin, new_map_data));
+ }
+ new_map_data->IncReferenceCount();
+
+ save_operations->push_back(BatchedOperation::New(
+ BatchOperationType::PUT_KEY, GetAreaKey(namespace_entry->first, origin),
+ new_map_data->MapNumberAsBytes()));
+
+ return new_map_data.get();
+}
+
+void SessionStorageMetadata::RegisterShallowClonedNamespace(
+ NamespaceEntry source_namespace,
+ NamespaceEntry destination_namespace,
+ std::vector<leveldb::mojom::BatchedOperationPtr>* save_operations) {
+ std::map<url::Origin, scoped_refptr<MapData>>& source_origins =
+ source_namespace->second;
+ std::map<url::Origin, scoped_refptr<MapData>>& destination_origins =
+ destination_namespace->second;
+ DCHECK_EQ(0ul, destination_origins.size());
+
+ save_operations->reserve(save_operations->size() + source_origins.size());
+ for (const auto& origin_map_pair : source_origins) {
+ destination_origins.emplace(std::piecewise_construct,
+ std::forward_as_tuple(origin_map_pair.first),
+ std::forward_as_tuple(origin_map_pair.second));
+ origin_map_pair.second->IncReferenceCount();
+
+ save_operations->push_back(BatchedOperation::New(
+ BatchOperationType::PUT_KEY,
+ GetAreaKey(destination_namespace->first, origin_map_pair.first),
+ origin_map_pair.second->MapNumberAsBytes()));
+ }
+}
+
+void SessionStorageMetadata::DeleteNamespace(
+ const std::string& namespace_id,
+ std::vector<BatchedOperationPtr>* delete_operations) {
+ auto it = namespace_origin_map_.find(namespace_id);
+ if (it == namespace_origin_map_.end())
+ return;
+
+ delete_operations->push_back(
+ BatchedOperation::New(BatchOperationType::DELETE_PREFIXED_KEY,
+ GetNamespacePrefix(namespace_id), base::nullopt));
+
+ const std::map<url::Origin, scoped_refptr<MapData>>& origins = it->second;
+ for (const auto& origin_map_pair : origins) {
+ MapData* map_data = origin_map_pair.second.get();
+ DCHECK_GT(map_data->ReferenceCount(), 0);
+ map_data->DecReferenceCount();
+ if (map_data->ReferenceCount() == 0) {
+ delete_operations->push_back(
+ BatchedOperation::New(BatchOperationType::DELETE_PREFIXED_KEY,
+ map_data->KeyPrefix(), base::nullopt));
+ }
+ }
+
+ namespace_origin_map_.erase(it);
+}
+
+void SessionStorageMetadata::DeleteArea(
+ const std::string& namespace_id,
+ const url::Origin& origin,
+ std::vector<BatchedOperationPtr>* delete_operations) {
+ NamespaceEntry ns_entry = namespace_origin_map_.find(namespace_id);
+ if (ns_entry == namespace_origin_map_.end())
+ return;
+
+ auto origin_map_it = ns_entry->second.find(origin);
+ if (origin_map_it == ns_entry->second.end())
+ return;
+
+ MapData* map_data = origin_map_it->second.get();
+
+ delete_operations->push_back(
+ BatchedOperation::New(BatchOperationType::DELETE_KEY,
+ GetAreaKey(namespace_id, origin), base::nullopt));
+
+ DCHECK_GT(map_data->ReferenceCount(), 0);
+ map_data->DecReferenceCount();
+ if (map_data->ReferenceCount() == 0) {
+ delete_operations->push_back(
+ BatchedOperation::New(BatchOperationType::DELETE_PREFIXED_KEY,
+ map_data->KeyPrefix(), base::nullopt));
+ }
+ ns_entry->second.erase(origin_map_it);
+}
+
+SessionStorageMetadata::NamespaceEntry
+SessionStorageMetadata::GetOrCreateNamespaceEntry(
+ const std::string& namespace_id) {
+ // Note: if the entry exists, emplace will return the existing entry and NOT
+ // insert a new entry.
+ return namespace_origin_map_
+ .emplace(std::piecewise_construct, std::forward_as_tuple(namespace_id),
+ std::forward_as_tuple())
+ .first;
+}
+
+// static
+std::vector<uint8_t> SessionStorageMetadata::GetNamespacePrefix(
+ const std::string& namespace_id) {
+ std::vector<uint8_t> namespace_prefix(
+ SessionStorageMetadata::kNamespacePrefixBytes,
+ std::end(SessionStorageMetadata::kNamespacePrefixBytes));
+ namespace_prefix.insert(namespace_prefix.end(), namespace_id.data(),
+ namespace_id.data() + namespace_id.size());
+ namespace_prefix.push_back(kNamespaceOriginSeperatorByte);
+ return namespace_prefix;
+}
+
+// static
+std::vector<uint8_t> SessionStorageMetadata::GetAreaKey(
+ const std::string& namespace_id,
+ const url::Origin& origin) {
+ std::vector<uint8_t> area_key(
+ SessionStorageMetadata::kNamespacePrefixBytes,
+ std::end(SessionStorageMetadata::kNamespacePrefixBytes));
+ area_key.insert(area_key.end(), namespace_id.begin(), namespace_id.end());
+ area_key.push_back(kNamespaceOriginSeperatorByte);
+ std::string origin_str = origin.GetURL().spec();
+ area_key.insert(area_key.end(), origin_str.data(),
+ origin_str.data() + origin_str.size());
+ return area_key;
+}
+
+// static
+std::vector<uint8_t> SessionStorageMetadata::GetMapPrefix(int64_t map_number) {
+ return GetMapPrefix(NumberToValue(map_number));
+}
+
+// static
+std::vector<uint8_t> SessionStorageMetadata::GetMapPrefix(
+ const std::vector<uint8_t>& map_number_as_bytes) {
+ std::vector<uint8_t> map_prefix(kMapIdPrefixBytes,
+ std::end(kMapIdPrefixBytes));
+ map_prefix.insert(map_prefix.end(), map_number_as_bytes.begin(),
+ map_number_as_bytes.end());
+ map_prefix.push_back(kNamespaceOriginSeperatorByte);
+ return map_prefix;
+}
+
+} // namespace content
diff --git a/content/browser/dom_storage/session_storage_metadata.h b/content/browser/dom_storage/session_storage_metadata.h
new file mode 100644
index 0000000..0b3ea98
--- /dev/null
+++ b/content/browser/dom_storage/session_storage_metadata.h
@@ -0,0 +1,157 @@
+// Copyright 2018 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.
+#ifndef CONTENT_BROWSER_DOM_STORAGE_SESSION_STORAGE_METADATA_H_
+#define CONTENT_BROWSER_DOM_STORAGE_SESSION_STORAGE_METADATA_H_
+
+#include <stdint.h>
+#include <map>
+#include <string>
+#include <vector>
+
+#include "base/memory/ref_counted.h"
+#include "components/services/leveldb/public/interfaces/leveldb.mojom.h"
+#include "content/common/content_export.h"
+#include "url/origin.h"
+
+namespace content {
+
+// Holds the metadata information for a session storage database. This includes
+// logic for parsing and saving database content.
+class CONTENT_EXPORT SessionStorageMetadata {
+ public:
+ static constexpr const int64_t kInvalidDatabaseVersion = -1;
+ static constexpr const int64_t kInvalidMapId = -1;
+
+ static constexpr const uint8_t kDatabaseVersionBytes[] = {'v', 'e', 'r', 's',
+ 'i', 'o', 'n'};
+
+ static constexpr const uint8_t kNamespacePrefixBytes[] = {
+ 'n', 'a', 'm', 'e', 's', 'p', 'a', 'c', 'e', '-'};
+
+ // This is "next-map-id" (without the quotes).
+ static constexpr const uint8_t kNextMapIdKeyBytes[] = {
+ 'n', 'e', 'x', 't', '-', 'm', 'a', 'p', '-', 'i', 'd'};
+
+ // Represents a map which can be shared by multiple areas.
+ // The |DeleteNamespace| and |DeleteArea| methods can destroy any MapData
+ // objects who are no longer referenced by another namespace.
+ class CONTENT_EXPORT MapData : public base::RefCounted<MapData> {
+ public:
+ explicit MapData(int64_t map_number);
+
+ // The number of namespaces that reference this map.
+ int ReferenceCount() const { return reference_count_; }
+
+ // The key prefix for the map data (e.g. "map-2-").
+ const std::vector<uint8_t>& KeyPrefix() const { return key_prefix_; }
+
+ private:
+ friend class base::RefCounted<MapData>;
+ friend class SessionStorageMetadata;
+ ~MapData();
+
+ const std::vector<uint8_t>& MapNumberAsBytes() const {
+ return number_as_bytes_;
+ }
+ void IncReferenceCount() { ++reference_count_; }
+ void DecReferenceCount() { --reference_count_; }
+
+ // The map number as bytes (e.g. "2"). These bytes are the string
+ // representation of the map number.
+ std::vector<uint8_t> number_as_bytes_;
+ std::vector<uint8_t> key_prefix_;
+ int reference_count_ = 0;
+ };
+
+ using NamespaceOriginMap =
+ std::map<std::string, std::map<url::Origin, scoped_refptr<MapData>>>;
+ using NamespaceEntry = NamespaceOriginMap::iterator;
+
+ SessionStorageMetadata();
+ ~SessionStorageMetadata();
+
+ // For a new database, this sets the database version, clears the metadata,
+ // and returns the operations to save to disk.
+ std::vector<leveldb::mojom::BatchedOperationPtr> SetupNewDatabase(
+ int64_t version);
+
+ // This parses the database version from the bytes that were stored on
+ // disk.
+ bool ParseDatabaseVersion(const std::vector<uint8_t>& value);
+ // Parses all namespaces and maps, and stores all metadata locally.
+ // This invalidates all NamespaceEntry and MapData objects.
+ // If there is a parsing error, the namespaces will be cleared.
+ bool ParseNamespaces(std::vector<leveldb::mojom::KeyValuePtr> values);
+
+ // Parses the next map id from the given bytes. If that fails, then it uses
+ // the next available id from parsing the namespaces. This call is not
+ // necessary on new databases.
+ void ParseNextMapId(const std::vector<uint8_t>& map_id);
+
+ int64_t NextMapId() const { return next_map_id_; }
+ int64_t DatabaseVersion() const { return database_version_; }
+ std::vector<uint8_t> DatabaseVersionAsVector() const;
+
+ // Creates new map data for the given namespace-origin area. If the area
+ // entry exists, then it will decrement the refcount of the old map. The
+ // |save_operations| save the new or modified area entry, as well as saving
+ // the next available map id.
+ // Note: It is invalid to call this method for an area that has a map with
+ // only one reference.
+ MapData* RegisterNewMap(
+ NamespaceEntry namespace_entry,
+ const url::Origin& origin,
+ std::vector<leveldb::mojom::BatchedOperationPtr>* save_operations);
+
+ // Registers an origin-map in the |destination_namespace| from every
+ // origin-map in the |source_namespace|. The |destination_namespace| must have
+ // no origin-maps. All maps in the destination namespace are the same maps as
+ // the source namespace. All database operations to save the namespace origin
+ // metadata are put in |save_operations|.
+ void RegisterShallowClonedNamespace(
+ NamespaceEntry source_namespace,
+ NamespaceEntry destination_namespace,
+ std::vector<leveldb::mojom::BatchedOperationPtr>* save_operations);
+
+ // Deletes the given namespace any any maps that no longer have any
+ // references. This will invalidate all NamespaceEntry objects for the
+ // |namespace_id|, and can invalidate any MapData objects whose reference
+ // count hits zero.
+ void DeleteNamespace(
+ const std::string& namespace_id,
+ std::vector<leveldb::mojom::BatchedOperationPtr>* delete_operations);
+
+ // This removes the metadata entry for this namespace-origin area. If the map
+ // at this entry isn't reference by any other area (refcount hits 0), then
+ // this will delete that map on disk and invalidate that MapData.
+ void DeleteArea(
+ const std::string& namespace_id,
+ const url::Origin& origin,
+ std::vector<leveldb::mojom::BatchedOperationPtr>* delete_operations);
+
+ NamespaceEntry GetOrCreateNamespaceEntry(const std::string& namespace_id);
+
+ const NamespaceOriginMap& namespace_origin_map() const {
+ return namespace_origin_map_;
+ }
+
+ private:
+ static std::vector<uint8_t> GetNamespacePrefix(
+ const std::string& namespace_id);
+ static std::vector<uint8_t> GetAreaKey(const std::string& namespace_id,
+ const url::Origin& origin);
+ static std::vector<uint8_t> GetMapPrefix(int64_t map_number);
+ static std::vector<uint8_t> GetMapPrefix(
+ const std::vector<uint8_t>& map_number_as_bytes);
+
+ int64_t database_version_ = kInvalidDatabaseVersion;
+ int64_t next_map_id_ = kInvalidMapId;
+ int64_t next_map_id_from_namespaces_ = 0;
+
+ NamespaceOriginMap namespace_origin_map_;
+};
+
+} // namespace content
+
+#endif // CONTENT_BROWSER_DOM_STORAGE_SESSION_STORAGE_METADATA_H_
diff --git a/content/browser/dom_storage/session_storage_metadata_unittest.cc b/content/browser/dom_storage/session_storage_metadata_unittest.cc
new file mode 100644
index 0000000..96968cac
--- /dev/null
+++ b/content/browser/dom_storage/session_storage_metadata_unittest.cc
@@ -0,0 +1,348 @@
+// Copyright 2018 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 "content/browser/dom_storage/session_storage_metadata.h"
+
+#include "base/bind.h"
+#include "base/guid.h"
+#include "base/memory/ptr_util.h"
+#include "base/run_loop.h"
+#include "base/stl_util.h"
+#include "components/services/leveldb/public/cpp/util.h"
+#include "content/public/test/test_browser_thread_bundle.h"
+#include "content/test/fake_leveldb_database.h"
+#include "mojo/public/cpp/bindings/strong_associated_binding.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "url/gurl.h"
+#include "url/origin.h"
+
+namespace content {
+namespace {
+using leveldb::StdStringToUint8Vector;
+using leveldb::Uint8VectorToStdString;
+using leveldb::mojom::DatabaseError;
+
+void GetCallback(std::vector<uint8_t>* value_out,
+ DatabaseError error,
+ const std::vector<uint8_t>& value) {
+ *value_out = value;
+}
+
+void ErrorCallback(DatabaseError* error_out, DatabaseError error) {
+ *error_out = error;
+}
+
+void GetAllCallback(std::vector<leveldb::mojom::KeyValuePtr>* values_out,
+ DatabaseError error,
+ std::vector<leveldb::mojom::KeyValuePtr> values) {
+ *values_out = std::move(values);
+}
+
+class SessionStorageMetadataTest : public testing::Test {
+ public:
+ SessionStorageMetadataTest()
+ : test_namespace1_id_(base::GenerateGUID()),
+ test_namespace2_id_(base::GenerateGUID()),
+ test_namespace3_id_(base::GenerateGUID()),
+ test_origin1_(url::Origin::Create(GURL("http://host1:1/"))),
+ test_origin2_(url::Origin::Create(GURL("http://host2:2/"))),
+ database_(&mock_data_) {
+ next_map_id_key_ = std::vector<uint8_t>(
+ std::begin(SessionStorageMetadata::kNextMapIdKeyBytes),
+ std::end(SessionStorageMetadata::kNextMapIdKeyBytes));
+ database_version_key_ = std::vector<uint8_t>(
+ std::begin(SessionStorageMetadata::kDatabaseVersionBytes),
+ std::end(SessionStorageMetadata::kDatabaseVersionBytes));
+ namespaces_prefix_key_ = std::vector<uint8_t>(
+ std::begin(SessionStorageMetadata::kNamespacePrefixBytes),
+ std::end(SessionStorageMetadata::kNamespacePrefixBytes));
+ }
+ ~SessionStorageMetadataTest() override {}
+
+ void ReadMetadataFromDatabase(SessionStorageMetadata* metadata) {
+ std::vector<uint8_t> value;
+ database_.Get(database_version_key_, base::BindOnce(&GetCallback, &value));
+ EXPECT_TRUE(metadata->ParseDatabaseVersion(value));
+ database_.Get(next_map_id_key_, base::BindOnce(&GetCallback, &value));
+ metadata->ParseNextMapId(value);
+ std::vector<leveldb::mojom::KeyValuePtr> values;
+ database_.GetPrefixed(namespaces_prefix_key_,
+ base::BindOnce(&GetAllCallback, &values));
+ EXPECT_TRUE(metadata->ParseNamespaces(std::move(values)));
+ }
+
+ void SetupTestData() {
+ // | key | value |
+ // |----------------------------------------|--------------------|
+ // | map-1-key1 | data1 |
+ // | map-3-key1 | data3 |
+ // | map-4-key1 | data4 |
+ // | namespace-<guid 1>-http://host1:1/ | 1 |
+ // | namespace-<guid 1>-http://host2:2/ | 3 |
+ // | namespace-<guid 2>-http://host1:1/ | 1 |
+ // | namespace-<guid 2>-http://host2:2/ | 4 |
+ // | next-map-id | 5 |
+ // | version | 1 |
+ mock_data_[StdStringToUint8Vector(
+ std::string("namespace-") + test_namespace1_id_ + "-" +
+ test_origin1_.GetURL().spec())] = StdStringToUint8Vector("1");
+ mock_data_[StdStringToUint8Vector(
+ std::string("namespace-") + test_namespace1_id_ + "-" +
+ test_origin2_.GetURL().spec())] = StdStringToUint8Vector("3");
+ mock_data_[StdStringToUint8Vector(
+ std::string("namespace-") + test_namespace2_id_ + "-" +
+ test_origin1_.GetURL().spec())] = StdStringToUint8Vector("1");
+ mock_data_[StdStringToUint8Vector(
+ std::string("namespace-") + test_namespace2_id_ + "-" +
+ test_origin2_.GetURL().spec())] = StdStringToUint8Vector("4");
+
+ mock_data_[next_map_id_key_] = StdStringToUint8Vector("5");
+
+ mock_data_[StdStringToUint8Vector("map-1-key1")] =
+ StdStringToUint8Vector("data1");
+ mock_data_[StdStringToUint8Vector("map-3-key1")] =
+ StdStringToUint8Vector("data3");
+ mock_data_[StdStringToUint8Vector("map-4-key1")] =
+ StdStringToUint8Vector("data4");
+
+ mock_data_[database_version_key_] = StdStringToUint8Vector("1");
+ }
+
+ protected:
+ std::string test_namespace1_id_;
+ std::string test_namespace2_id_;
+ std::string test_namespace3_id_;
+ url::Origin test_origin1_;
+ url::Origin test_origin2_;
+ TestBrowserThreadBundle thread_bundle_;
+ std::map<std::vector<uint8_t>, std::vector<uint8_t>> mock_data_;
+ FakeLevelDBDatabase database_;
+
+ std::vector<uint8_t> database_version_key_;
+ std::vector<uint8_t> next_map_id_key_;
+ std::vector<uint8_t> namespaces_prefix_key_;
+};
+
+TEST_F(SessionStorageMetadataTest, SaveNewMetadata) {
+ SessionStorageMetadata metadata;
+ std::vector<leveldb::mojom::BatchedOperationPtr> operations =
+ metadata.SetupNewDatabase(1);
+
+ DatabaseError error;
+ database_.Write(std::move(operations),
+ base::BindOnce(&ErrorCallback, &error));
+ EXPECT_EQ(DatabaseError::OK, error);
+
+ EXPECT_EQ(StdStringToUint8Vector("1"), mock_data_[database_version_key_]);
+ EXPECT_EQ(StdStringToUint8Vector("0"), mock_data_[next_map_id_key_]);
+}
+
+TEST_F(SessionStorageMetadataTest, LoadingData) {
+ SetupTestData();
+ SessionStorageMetadata metadata;
+ ReadMetadataFromDatabase(&metadata);
+
+ EXPECT_EQ(1, metadata.DatabaseVersion());
+ EXPECT_EQ(5, metadata.NextMapId());
+ EXPECT_EQ(2ul, metadata.namespace_origin_map().size());
+
+ // Namespace 1 should have 2 origins, referencing map 1 and 3. Map 1 is shared
+ // between namespace 1 and namespace 2.
+ auto entry = metadata.GetOrCreateNamespaceEntry(test_namespace1_id_);
+ EXPECT_EQ(test_namespace1_id_, entry->first);
+ EXPECT_EQ(2ul, entry->second.size());
+ EXPECT_EQ(StdStringToUint8Vector("map-1-"),
+ entry->second[test_origin1_]->KeyPrefix());
+ EXPECT_EQ(2, entry->second[test_origin1_]->ReferenceCount());
+ EXPECT_EQ(StdStringToUint8Vector("map-3-"),
+ entry->second[test_origin2_]->KeyPrefix());
+ EXPECT_EQ(1, entry->second[test_origin2_]->ReferenceCount());
+
+ // Namespace 2 is the same, except the second origin references map 4.
+ entry = metadata.GetOrCreateNamespaceEntry(test_namespace2_id_);
+ EXPECT_EQ(test_namespace2_id_, entry->first);
+ EXPECT_EQ(2ul, entry->second.size());
+ EXPECT_EQ(StdStringToUint8Vector("map-1-"),
+ entry->second[test_origin1_]->KeyPrefix());
+ EXPECT_EQ(2, entry->second[test_origin1_]->ReferenceCount());
+ EXPECT_EQ(StdStringToUint8Vector("map-4-"),
+ entry->second[test_origin2_]->KeyPrefix());
+ EXPECT_EQ(1, entry->second[test_origin2_]->ReferenceCount());
+}
+
+TEST_F(SessionStorageMetadataTest, SaveNewMap) {
+ SetupTestData();
+ SessionStorageMetadata metadata;
+ ReadMetadataFromDatabase(&metadata);
+
+ std::vector<leveldb::mojom::BatchedOperationPtr> operations;
+ auto ns1_entry = metadata.GetOrCreateNamespaceEntry(test_namespace1_id_);
+ auto* map_data =
+ metadata.RegisterNewMap(ns1_entry, test_origin1_, &operations);
+ ASSERT_TRUE(map_data);
+
+ // Verify in-memory metadata is correct.
+ EXPECT_EQ(StdStringToUint8Vector("map-5-"),
+ ns1_entry->second[test_origin1_]->KeyPrefix());
+ EXPECT_EQ(1, ns1_entry->second[test_origin1_]->ReferenceCount());
+ EXPECT_EQ(1, metadata.GetOrCreateNamespaceEntry(test_namespace2_id_)
+ ->second[test_origin1_]
+ ->ReferenceCount());
+
+ DatabaseError error;
+ database_.Write(std::move(operations),
+ base::BindOnce(&ErrorCallback, &error));
+ EXPECT_EQ(DatabaseError::OK, error);
+
+ // Verify metadata was written to disk.
+ EXPECT_EQ(StdStringToUint8Vector("6"), mock_data_[next_map_id_key_]);
+ EXPECT_EQ(StdStringToUint8Vector("5"),
+ mock_data_[StdStringToUint8Vector(std::string("namespace-") +
+ test_namespace1_id_ + "-" +
+ test_origin1_.GetURL().spec())]);
+}
+
+TEST_F(SessionStorageMetadataTest, ShallowCopies) {
+ SetupTestData();
+ SessionStorageMetadata metadata;
+ ReadMetadataFromDatabase(&metadata);
+
+ auto ns1_entry = metadata.GetOrCreateNamespaceEntry(test_namespace1_id_);
+ auto ns3_entry = metadata.GetOrCreateNamespaceEntry(test_namespace3_id_);
+
+ std::vector<leveldb::mojom::BatchedOperationPtr> operations;
+ metadata.RegisterShallowClonedNamespace(ns1_entry, ns3_entry, &operations);
+
+ DatabaseError error;
+ database_.Write(std::move(operations),
+ base::BindOnce(&ErrorCallback, &error));
+ EXPECT_EQ(DatabaseError::OK, error);
+
+ // Verify in-memory metadata is correct.
+ EXPECT_EQ(StdStringToUint8Vector("map-1-"),
+ ns3_entry->second[test_origin1_]->KeyPrefix());
+ EXPECT_EQ(StdStringToUint8Vector("map-3-"),
+ ns3_entry->second[test_origin2_]->KeyPrefix());
+ EXPECT_EQ(ns1_entry->second[test_origin1_].get(),
+ ns3_entry->second[test_origin1_].get());
+ EXPECT_EQ(ns1_entry->second[test_origin2_].get(),
+ ns3_entry->second[test_origin2_].get());
+ EXPECT_EQ(3, ns3_entry->second[test_origin1_]->ReferenceCount());
+ EXPECT_EQ(2, ns3_entry->second[test_origin2_]->ReferenceCount());
+
+ // Verify metadata was written to disk.
+ EXPECT_EQ(StdStringToUint8Vector("1"),
+ mock_data_[StdStringToUint8Vector(std::string("namespace-") +
+ test_namespace3_id_ + "-" +
+ test_origin1_.GetURL().spec())]);
+ EXPECT_EQ(StdStringToUint8Vector("3"),
+ mock_data_[StdStringToUint8Vector(std::string("namespace-") +
+ test_namespace3_id_ + "-" +
+ test_origin2_.GetURL().spec())]);
+}
+
+TEST_F(SessionStorageMetadataTest, DeleteNamespace) {
+ SetupTestData();
+ SessionStorageMetadata metadata;
+ ReadMetadataFromDatabase(&metadata);
+
+ std::vector<leveldb::mojom::BatchedOperationPtr> operations;
+ metadata.DeleteNamespace(test_namespace1_id_, &operations);
+ DatabaseError error;
+ database_.Write(std::move(operations),
+ base::BindOnce(&ErrorCallback, &error));
+ EXPECT_EQ(DatabaseError::OK, error);
+
+ EXPECT_FALSE(
+ base::ContainsKey(metadata.namespace_origin_map(), test_namespace1_id_));
+
+ // Verify in-memory metadata is correct.
+ auto ns2_entry = metadata.GetOrCreateNamespaceEntry(test_namespace2_id_);
+ EXPECT_EQ(1, ns2_entry->second[test_origin1_]->ReferenceCount());
+ EXPECT_EQ(1, ns2_entry->second[test_origin2_]->ReferenceCount());
+
+ // Verify metadata and data was deleted from disk.
+ EXPECT_FALSE(base::ContainsKey(
+ mock_data_,
+ StdStringToUint8Vector(std::string("namespace-") + test_namespace1_id_ +
+ "-" + test_origin1_.GetURL().spec())));
+ EXPECT_FALSE(base::ContainsKey(
+ mock_data_,
+ StdStringToUint8Vector(std::string("namespace-") + test_namespace1_id_ +
+ "-" + test_origin2_.GetURL().spec())));
+ EXPECT_FALSE(
+ base::ContainsKey(mock_data_, StdStringToUint8Vector("map-3-key1")));
+ EXPECT_TRUE(
+ base::ContainsKey(mock_data_, StdStringToUint8Vector("map-1-key1")));
+}
+
+TEST_F(SessionStorageMetadataTest, DeleteArea) {
+ SetupTestData();
+ SessionStorageMetadata metadata;
+ ReadMetadataFromDatabase(&metadata);
+
+ // First delete an area with a shared map.
+ std::vector<leveldb::mojom::BatchedOperationPtr> operations;
+ metadata.DeleteArea(test_namespace1_id_, test_origin1_, &operations);
+ DatabaseError error;
+ database_.Write(std::move(operations),
+ base::BindOnce(&ErrorCallback, &error));
+ EXPECT_EQ(DatabaseError::OK, error);
+
+ // Verify in-memory metadata is correct.
+ auto ns1_entry = metadata.GetOrCreateNamespaceEntry(test_namespace1_id_);
+ auto ns2_entry = metadata.GetOrCreateNamespaceEntry(test_namespace2_id_);
+ EXPECT_FALSE(base::ContainsKey(ns1_entry->second, test_origin1_));
+ EXPECT_EQ(1, ns1_entry->second[test_origin2_]->ReferenceCount());
+ EXPECT_EQ(1, ns2_entry->second[test_origin1_]->ReferenceCount());
+ EXPECT_EQ(1, ns2_entry->second[test_origin2_]->ReferenceCount());
+
+ // Verify only the applicable data was deleted.
+ EXPECT_FALSE(base::ContainsKey(
+ mock_data_,
+ StdStringToUint8Vector(std::string("namespace-") + test_namespace1_id_ +
+ "-" + test_origin1_.GetURL().spec())));
+ EXPECT_TRUE(base::ContainsKey(
+ mock_data_,
+ StdStringToUint8Vector(std::string("namespace-") + test_namespace1_id_ +
+ "-" + test_origin2_.GetURL().spec())));
+ EXPECT_TRUE(
+ base::ContainsKey(mock_data_, StdStringToUint8Vector("map-1-key1")));
+ EXPECT_TRUE(
+ base::ContainsKey(mock_data_, StdStringToUint8Vector("map-4-key1")));
+
+ // Now delete an area with a unique map.
+ operations.clear();
+ metadata.DeleteArea(test_namespace2_id_, test_origin2_, &operations);
+ database_.Write(std::move(operations),
+ base::BindOnce(&ErrorCallback, &error));
+ EXPECT_EQ(DatabaseError::OK, error);
+
+ // Verify in-memory metadata is correct.
+ EXPECT_FALSE(base::ContainsKey(ns1_entry->second, test_origin1_));
+ EXPECT_EQ(1, ns1_entry->second[test_origin2_]->ReferenceCount());
+ EXPECT_EQ(1, ns2_entry->second[test_origin1_]->ReferenceCount());
+ EXPECT_FALSE(base::ContainsKey(ns2_entry->second, test_origin2_));
+
+ // Verify only the applicable data was deleted.
+ EXPECT_TRUE(base::ContainsKey(
+ mock_data_,
+ StdStringToUint8Vector(std::string("namespace-") + test_namespace2_id_ +
+ "-" + test_origin1_.GetURL().spec())));
+ EXPECT_FALSE(base::ContainsKey(
+ mock_data_,
+ StdStringToUint8Vector(std::string("namespace-") + test_namespace2_id_ +
+ "-" + test_origin2_.GetURL().spec())));
+ EXPECT_TRUE(
+ base::ContainsKey(mock_data_, StdStringToUint8Vector("map-1-key1")));
+ EXPECT_TRUE(
+ base::ContainsKey(mock_data_, StdStringToUint8Vector("map-3-key1")));
+ EXPECT_FALSE(
+ base::ContainsKey(mock_data_, StdStringToUint8Vector("map-4-key1")));
+}
+
+} // namespace
+
+} // namespace content