[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