diff options
author | marja@chromium.org <marja@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-04-27 12:22:55 +0000 |
---|---|---|
committer | marja@chromium.org <marja@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-04-27 12:22:55 +0000 |
commit | 99e258895f99dcbaeeb979b9da84983e7598b8b5 (patch) | |
tree | 5a9f31d14e2814a3070341e36e772096a62e259b | |
parent | 091f26196c6a0ca3cb6cebefec154048b01e600e (diff) | |
download | chromium_src-99e258895f99dcbaeeb979b9da84983e7598b8b5.zip chromium_src-99e258895f99dcbaeeb979b9da84983e7598b8b5.tar.gz chromium_src-99e258895f99dcbaeeb979b9da84983e7598b8b5.tar.bz2 |
Add dom_storage::SessionStorageDatabase.
BUG=104292
TEST=SessionStorageDatabaseTest.*
TBR=tony@chromium.org
Review URL: http://codereview.chromium.org/10176005
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@134259 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r-- | webkit/dom_storage/session_storage_database.cc | 658 | ||||
-rw-r--r-- | webkit/dom_storage/session_storage_database.h | 202 | ||||
-rw-r--r-- | webkit/dom_storage/session_storage_database_unittest.cc | 858 | ||||
-rw-r--r-- | webkit/dom_storage/webkit_dom_storage.gypi | 3 | ||||
-rw-r--r-- | webkit/tools/test_shell/test_shell.gypi | 1 |
5 files changed, 1722 insertions, 0 deletions
diff --git a/webkit/dom_storage/session_storage_database.cc b/webkit/dom_storage/session_storage_database.cc new file mode 100644 index 0000000..e66d477 --- /dev/null +++ b/webkit/dom_storage/session_storage_database.cc @@ -0,0 +1,658 @@ +// Copyright (c) 2012 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 "webkit/dom_storage/session_storage_database.h" + +#include "base/file_util.h" +#include "base/logging.h" +#include "base/stringprintf.h" +#include "base/string_number_conversions.h" +#include "base/utf_string_conversions.h" +#include "googleurl/src/gurl.h" +#include "third_party/leveldatabase/src/include/leveldb/db.h" +#include "third_party/leveldatabase/src/include/leveldb/iterator.h" +#include "third_party/leveldatabase/src/include/leveldb/status.h" +#include "third_party/leveldatabase/src/include/leveldb/options.h" +#include "third_party/leveldatabase/src/include/leveldb/write_batch.h" + +// Layout of the database: +// | key | value | +// ----------------------------------------------------------------------- +// | map-1 | 2 (refcount, start of map-1-* keys)| +// | map-1-a | b (a = b in map 1) | +// | ... | | +// | namespace- | dummy (start of namespace-* keys) | +// | namespace-1 (1 = namespace id) | dummy (start of namespace-1-* keys)| +// | namespace-1-origin1 | 1 (mapid) | +// | namespace-1-origin2 | 2 | +// | namespace-2 | dummy | +// | namespace-2-origin1 | 1 (shallow copy) | +// | namespace-2-origin2 | 2 (shallow copy) | +// | namespace-3 | dummy | +// | namespace-3-origin1 | 3 (deep copy) | +// | namespace-3-origin2 | 2 (shallow copy) | +// | next-namespace-id | 4 | +// | next-map-id | 4 | + +namespace dom_storage { + +SessionStorageDatabase::SessionStorageDatabase(const FilePath& file_path) + : file_path_(file_path), + db_error_(false), + is_inconsistent_(false), + namespace_offset_(0) { } + +SessionStorageDatabase::~SessionStorageDatabase() { +} + +void SessionStorageDatabase::ReadAreaValues(int64 namespace_id, + const GURL& origin, + ValuesMap* result) { + // We don't create a database if it doesn't exist. In that case, there is + // nothing to be added to the result. + if (!LazyOpen(false)) + return; + std::string map_id; + bool exists; + if (!GetMapForArea(namespace_id, origin, &exists, &map_id)) + return; + if (exists) + ReadMap(map_id, result, false); +} + +bool SessionStorageDatabase::CommitAreaChanges(int64 namespace_id, + const GURL& origin, + bool clear_all_first, + const ValuesMap& changes) { + // Even if |changes| is empty, we need to write the appropriate placeholders + // in the database, so that it can be later shallow-copied succssfully. + if (!LazyOpen(true)) + return false; + + leveldb::WriteBatch batch; + // Ensure that the keys "namespace-" "namespace-N" (see the schema above) + // exist. + const bool kOkIfExists = true; + if (!CreateNamespace(namespace_id, kOkIfExists, &batch)) + return false; + + std::string map_id; + bool exists; + if (!GetMapForArea(namespace_id, origin, &exists, &map_id)) + return false; + if (exists) { + // We shouldn't write data into a shallow copy. If this is a shallow copy, + // it's a caller error (not an inconsistency in the database). + int64 ref_count; + if (!GetMapRefCount(map_id, &ref_count)) + return false; + if (!CallerErrorCheck(ref_count == 1)) + return false; + + if (clear_all_first) { + if (!ClearMap(map_id, &batch)) + return false; + } + } else { + // Map doesn't exist, create it now if needed. + if (!changes.empty()) { + if (!CreateMapForArea(namespace_id, origin, &map_id, &batch)) + return false; + } + } + + WriteValuesToMap(map_id, changes, &batch); + + leveldb::Status s = db_->Write(leveldb::WriteOptions(), &batch); + return DatabaseErrorCheck(s.ok()); +} + +bool SessionStorageDatabase::CloneNamespace(int64 namespace_id, + int64 new_namespace_id) { + // Go through all origins in the namespace |namespace_id|, create placeholders + // for them in |new_namespace_id|, and associate them with the existing maps. + + // Example, data before shallow copy: + // | map-1 | 1 (refcount) | + // | map-1-a | b | + // | namespace-1 (1 = namespace id) | dummy | + // | namespace-1-origin1 | 1 (mapid) | + + // Example, data after shallow copy: + // | map-1 | 2 (inc. refcount) | + // | map-1-a | b | + // | namespace-1 (1 = namespace id) | dummy | + // | namespace-1-origin1 | 1 (mapid) | + // | namespace-2 | dummy | + // | namespace-2-origin1 | 1 (mapid) << references the same map + + if (!LazyOpen(true)) + return false; + + leveldb::WriteBatch batch; + const bool kOkIfExists = false; + if (!CreateNamespace(new_namespace_id, kOkIfExists, &batch)) + return false; + + std::map<std::string, std::string> areas; + if (!GetAreasInNamespace(namespace_id, &areas)) + return false; + + for (std::map<std::string, std::string>::const_iterator it = areas.begin(); + it != areas.end(); ++it) { + const std::string& origin = it->first; + const std::string& map_id = it->second; + if (!IncreaseMapRefCount(map_id, &batch)) + return false; + AddAreaToNamespace(new_namespace_id, origin, map_id, &batch); + } + leveldb::Status s = db_->Write(leveldb::WriteOptions(), &batch); + return DatabaseErrorCheck(s.ok()); +} + +bool SessionStorageDatabase::DeepCopyArea(int64 namespace_id, + const GURL& origin) { + // Example, data before deep copy: + // | namespace-1 (1 = namespace id) | dummy | + // | namespace-1-origin1 | 1 (mapid) | + // | namespace-2 | dummy | + // | namespace-2-origin1 | 1 (mapid) << references the same map + // | map-1 | 2 (refcount) | + // | map-1-a | b | + + // Example, data after deep copy copy: + // | namespace-1 (1 = namespace id) | dummy | + // | namespace-1-origin1 | 1 (mapid) | + // | namespace-2 | dummy | + // | namespace-2-origin1 | 2 (mapid) << references the new map + // | map-1 | 1 (dec. refcount) | + // | map-1-a | b | + // | map-2 | 1 (refcount) | + // | map-2-a | b | + + if (!LazyOpen(true)) + return false; + + std::string old_map_id; + bool exists; + if (!GetMapForArea(namespace_id, origin, &exists, &old_map_id)) + return false; + + // If the area doesn't exist, or if it's not a shallow copy, it's a caller + // error. + if (!CallerErrorCheck(exists)) + return false; + int64 ref_count; + if (!GetMapRefCount(old_map_id, &ref_count)) + return false; + if (!CallerErrorCheck(ref_count > 1)) + return false; + + leveldb::WriteBatch batch; + std::string new_map_id; + if (!CreateMapForArea(namespace_id, origin, &new_map_id, &batch)) + return false; + + // Copy the values in the map. + ValuesMap values; + if (!ReadMap(old_map_id, &values, false)) + return false; + WriteValuesToMap(new_map_id, values, &batch); + + if (!DecreaseMapRefCount(old_map_id, 1, &batch)) + return false; + + leveldb::Status s = db_->Write(leveldb::WriteOptions(), &batch); + return DatabaseErrorCheck(s.ok()); +} + +bool SessionStorageDatabase::DeleteArea(int64 namespace_id, + const GURL& origin) { + if (!LazyOpen(false)) { + // No need to create the database if it doesn't exist. + return true; + } + leveldb::WriteBatch batch; + if (!DeleteArea(namespace_id, origin.spec(), &batch)) + return false; + leveldb::Status s = db_->Write(leveldb::WriteOptions(), &batch); + return DatabaseErrorCheck(s.ok()); +} + +bool SessionStorageDatabase::DeleteNamespace(int64 namespace_id) { + if (!LazyOpen(false)) { + // No need to create the database if it doesn't exist. + return true; + } + // Itereate through the areas in the namespace. + leveldb::WriteBatch batch; + std::map<std::string, std::string> areas; + if (!GetAreasInNamespace(namespace_id, &areas)) + return false; + for (std::map<std::string, std::string>::const_iterator it = areas.begin(); + it != areas.end(); ++it) { + const std::string& origin = it->first; + if (!DeleteArea(namespace_id, origin, &batch)) + return false; + } + batch.Delete(NamespaceStartKey(namespace_id, namespace_offset_)); + leveldb::Status s = db_->Write(leveldb::WriteOptions(), &batch); + return DatabaseErrorCheck(s.ok()); +} + +bool SessionStorageDatabase::LazyOpen(bool create_if_needed) { + base::AutoLock auto_lock(db_lock_); + if (db_error_ || is_inconsistent_) { + // Don't try to open a database that we know has failed already. + return false; + } + if (IsOpen()) + return true; + + if (!create_if_needed && + (!file_util::PathExists(file_path_) || + file_util::IsDirectoryEmpty(file_path_))) { + // If the directory doesn't exist already and we haven't been asked to + // create a file on disk, then we don't bother opening the database. This + // means we wait until we absolutely need to put something onto disk before + // we do so. + return false; + } + + leveldb::DB* db; + leveldb::Status s = TryToOpen(file_path_, &db); + if (!s.ok()) { + LOG(WARNING) << "Failed to open leveldb in " << file_path_.value() + << ", error: " << s.ToString(); + DCHECK(db == NULL); + + // Clear the directory and try again. + file_util::Delete(file_path_, true); + s = TryToOpen(file_path_, &db); + if (!s.ok()) { + LOG(WARNING) << "Failed to open leveldb in " << file_path_.value() + << ", error: " << s.ToString(); + DCHECK(db == NULL); + db_error_ = true; + return false; + } + } + db_.reset(db); + + return GetNextNamespaceId(&namespace_offset_); +} + +leveldb::Status SessionStorageDatabase::TryToOpen(const FilePath& file_path, + leveldb::DB** db) { + leveldb::Options options; + // The directory exists but a valid leveldb database might not exist inside it + // (e.g., a subset of the needed files might be missing). Handle this + // situation gracefully by creating the database now. + options.create_if_missing = true; +#if defined(OS_WIN) + return leveldb::DB::Open(options, WideToUTF8(file_path.value()), db); +#elif defined(OS_POSIX) + return leveldb::DB::Open(options, file_path.value(), db); +#endif +} + +bool SessionStorageDatabase::IsOpen() const { + return db_.get() != NULL; +} + +bool SessionStorageDatabase::CallerErrorCheck(bool ok) const { + DCHECK(ok); + return ok; +} + +bool SessionStorageDatabase::ConsistencyCheck(bool ok) { + if (ok) + return true; + base::AutoLock auto_lock(db_lock_); + DCHECK(false); + is_inconsistent_ = true; + // We cannot recover the database during this run, e.g., the upper layer can + // have a different understanding of the database state (shallow and deep + // copies). + // TODO(marja): Error handling. + return false; +} + +bool SessionStorageDatabase::DatabaseErrorCheck(bool ok) { + if (ok) + return true; + base::AutoLock auto_lock(db_lock_); + db_error_ = true; + // TODO(marja): Error handling. + return false; +} + +bool SessionStorageDatabase::CreateNamespace(int64 namespace_id, + bool ok_if_exists, + leveldb::WriteBatch* batch) { + std::string namespace_prefix = NamespacePrefix(); + std::string dummy; + leveldb::Status s = db_->Get(leveldb::ReadOptions(), namespace_prefix, + &dummy); + if (!DatabaseErrorCheck(s.ok() || s.IsNotFound())) + return false; + if (s.IsNotFound()) + batch->Put(namespace_prefix, ""); + + std::string namespace_start_key = + NamespaceStartKey(namespace_id, namespace_offset_); + s = db_->Get(leveldb::ReadOptions(), namespace_start_key, &dummy); + if (!DatabaseErrorCheck(s.ok() || s.IsNotFound())) + return false; + if (s.IsNotFound()) { + batch->Put(namespace_start_key, ""); + return UpdateNextNamespaceId(namespace_id, batch); + } + return CallerErrorCheck(ok_if_exists); +} + +bool SessionStorageDatabase::GetNextNamespaceId(int64* next_namespace_id) { + std::string next_namespace_id_string; + leveldb::Status s = db_->Get(leveldb::ReadOptions(), NextNamespaceIdKey(), + &next_namespace_id_string); + if (!DatabaseErrorCheck(s.ok() || s.IsNotFound())) + return false; + if (s.IsNotFound()) { + *next_namespace_id = 0; + return true; + } + bool conversion_ok = + base::StringToInt64(next_namespace_id_string, next_namespace_id); + return ConsistencyCheck(conversion_ok); +} + +bool SessionStorageDatabase::UpdateNextNamespaceId(int64 namespace_id, + leveldb::WriteBatch* batch) { + int64 next_namespace_id; + if (!GetNextNamespaceId(&next_namespace_id)) + return false; + if (next_namespace_id < namespace_id + namespace_offset_ + 1) { + next_namespace_id = namespace_id + namespace_offset_ + 1; + batch->Put(NextNamespaceIdKey(), base::Int64ToString(next_namespace_id)); + } + return true; +} + +bool SessionStorageDatabase::GetAreasInNamespace( + int64 namespace_id, + std::map<std::string, std::string>* areas) { + return GetAreasInNamespace(NamespaceIdStr(namespace_id, namespace_offset_), + areas); +} + +bool SessionStorageDatabase::GetAreasInNamespace( + const std::string& namespace_id_str, + std::map<std::string, std::string>* areas) { + std::string namespace_start_key = NamespaceStartKey(namespace_id_str); + scoped_ptr<leveldb::Iterator> it(db_->NewIterator(leveldb::ReadOptions())); + it->Seek(namespace_start_key); + if (it->status().IsNotFound()) { + // The namespace_start_key is not found when the namespace doesn't contain + // any areas. We don't need to do anything. + return true; + } + if (!DatabaseErrorCheck(it->status().ok())) + return false; + + // Skip the dummy entry "namespace-<namespaceid>" and iterate the origins. + for (it->Next(); it->Valid(); it->Next()) { + std::string key = it->key().ToString(); + if (key.find(namespace_start_key) != 0) { + // Iterated past the origins for this namespace. + break; + } + size_t second_dash = key.find('-', namespace_start_key.length()); + if (!ConsistencyCheck(second_dash != std::string::npos)) + return false; + std::string origin = key.substr(second_dash + 1); + std::string map_id = it->value().ToString(); + (*areas)[origin] = map_id; + } + return true; +} + +void SessionStorageDatabase::AddAreaToNamespace(int64 namespace_id, + const std::string& origin, + const std::string& map_id, + leveldb::WriteBatch* batch) { + std::string namespace_key = NamespaceKey( + NamespaceIdStr(namespace_id, namespace_offset_), origin); + batch->Put(namespace_key, map_id); +} + +bool SessionStorageDatabase::DeleteArea(int64 namespace_id, + const std::string& origin, + leveldb::WriteBatch* batch) { + return DeleteArea(NamespaceIdStr(namespace_id, namespace_offset_), + origin, batch); +} + +bool SessionStorageDatabase::DeleteArea(const std::string& namespace_id_str, + const std::string& origin, + leveldb::WriteBatch* batch) { + std::string map_id; + bool exists; + if (!GetMapForArea(namespace_id_str, origin, &exists, &map_id)) + return false; + if (!exists) + return true; // Nothing to delete. + if (!DecreaseMapRefCount(map_id, 1, batch)) + return false; + std::string namespace_key = NamespaceKey(namespace_id_str, origin); + batch->Delete(namespace_key); + return true; +} + +bool SessionStorageDatabase::GetMapForArea(int64 namespace_id, + const GURL& origin, + bool* exists, + std::string* map_id) { + return GetMapForArea( + base::Int64ToString(namespace_id + namespace_offset_), + origin.spec(), exists, map_id); +} + +bool SessionStorageDatabase::GetMapForArea(const std::string& namespace_id_str, + const std::string& origin, + bool* exists, std::string* map_id) { + std::string namespace_key = NamespaceKey(namespace_id_str, origin); + leveldb::Status s = db_->Get(leveldb::ReadOptions(), namespace_key, map_id); + if (s.IsNotFound()) { + *exists = false; + return true; + } + *exists = true; + return DatabaseErrorCheck(s.ok()); +} + +bool SessionStorageDatabase::CreateMapForArea(int64 namespace_id, + const GURL& origin, + std::string* map_id, + leveldb::WriteBatch* batch) { + std::string next_map_id_key = NextMapIdKey(); + leveldb::Status s = db_->Get(leveldb::ReadOptions(), next_map_id_key, map_id); + if (!DatabaseErrorCheck(s.ok() || s.IsNotFound())) + return false; + int64 next_map_id = 0; + if (s.IsNotFound()) { + *map_id = "0"; + } else { + bool conversion_ok = base::StringToInt64(*map_id, &next_map_id); + if (!ConsistencyCheck(conversion_ok)) + return false; + } + batch->Put(next_map_id_key, base::Int64ToString(++next_map_id)); + std::string namespace_key = + NamespaceKey(namespace_id, namespace_offset_, origin); + batch->Put(namespace_key, *map_id); + batch->Put(MapRefCountKey(*map_id), "1"); + return true; +} + +bool SessionStorageDatabase::ReadMap(const std::string& map_id, + ValuesMap* result, + bool only_keys) { + scoped_ptr<leveldb::Iterator> it(db_->NewIterator(leveldb::ReadOptions())); + std::string map_start_key = MapRefCountKey(map_id); + it->Seek(map_start_key); + // The map needs to exist, otherwise we have a stale map_id in the database. + if (!ConsistencyCheck(!it->status().IsNotFound())) + return false; + if (!DatabaseErrorCheck(it->status().ok())) + return false; + const int kPrefixLength = std::string(MapPrefix()).length(); + // Skip the dummy entry "map-<mapid>". + for (it->Next(); it->Valid(); it->Next()) { + // Key is of the form "map-<mapid>-<key>". + std::string key = it->key().ToString(); + size_t second_dash = key.find('-', kPrefixLength); + if (second_dash == std::string::npos || + key.substr(kPrefixLength, second_dash - kPrefixLength) != map_id) { + // Iterated beyond the keys in this map. + break; + } + string16 key16 = UTF8ToUTF16(key.substr(second_dash + 1)); + if (only_keys) { + (*result)[key16] = NullableString16(true); + } else { + // Convert the raw data stored in std::string (it->value()) to raw data + // stored in string16. + size_t len = it->value().size() / sizeof(char16); + const char16* data_ptr = + reinterpret_cast<const char16*>(it->value().data()); + (*result)[key16] = NullableString16(string16(data_ptr, len), false); + } + } + return true; +} + +void SessionStorageDatabase::WriteValuesToMap(const std::string& map_id, + const ValuesMap& values, + leveldb::WriteBatch* batch) { + for (ValuesMap::const_iterator it = values.begin(); it != values.end(); + ++it) { + NullableString16 value = it->second; + std::string key = MapKey(map_id, UTF16ToUTF8(it->first)); + if (value.is_null()) { + batch->Delete(key); + } else { + // Convert the raw data stored in string16 to raw data stored in + // std::string. + const char* data = reinterpret_cast<const char*>(value.string().data()); + size_t size = value.string().size() * 2; + batch->Put(key, leveldb::Slice(data, size)); + } + } +} + +bool SessionStorageDatabase::GetMapRefCount(const std::string& map_id, + int64* ref_count) { + std::string ref_count_string; + leveldb::Status s = db_->Get(leveldb::ReadOptions(), + MapRefCountKey(map_id), &ref_count_string); + if (!ConsistencyCheck(s.ok())) + return false; + bool conversion_ok = base::StringToInt64(ref_count_string, ref_count); + return ConsistencyCheck(conversion_ok); +} + +bool SessionStorageDatabase::IncreaseMapRefCount(const std::string& map_id, + leveldb::WriteBatch* batch) { + // Increase the ref count for the map. + int64 old_ref_count; + if (!GetMapRefCount(map_id, &old_ref_count)) + return false; + batch->Put(MapRefCountKey(map_id), base::Int64ToString(++old_ref_count)); + return true; +} + +bool SessionStorageDatabase::DecreaseMapRefCount(const std::string& map_id, + int decrease, + leveldb::WriteBatch* batch) { + // Decrease the ref count for the map. + int64 ref_count; + if (!GetMapRefCount(map_id, &ref_count)) + return false; + if (!ConsistencyCheck(decrease <= ref_count)) + return false; + ref_count -= decrease; + if (ref_count > 0) { + batch->Put(MapRefCountKey(map_id), base::Int64ToString(ref_count)); + } else { + // Clear all keys in the map. + if (!ClearMap(map_id, batch)) + return false; + batch->Delete(MapRefCountKey(map_id)); + } + return true; +} + +bool SessionStorageDatabase::ClearMap(const std::string& map_id, + leveldb::WriteBatch* batch) { + ValuesMap values; + if (!ReadMap(map_id, &values, true)) + return false; + for (ValuesMap::const_iterator it = values.begin(); it != values.end(); ++it) + batch->Delete(MapKey(map_id, UTF16ToUTF8(it->first))); + return true; +} + +std::string SessionStorageDatabase::NamespaceStartKey( + const std::string& namespace_id_str) { + return base::StringPrintf("namespace-%s", namespace_id_str.c_str()); +} + +std::string SessionStorageDatabase::NamespaceStartKey(int64 namespace_id, + int64 namespace_offset) { + return NamespaceStartKey(NamespaceIdStr(namespace_id, namespace_offset)); +} + +std::string SessionStorageDatabase::NamespaceKey( + const std::string& namespace_id_str, const std::string& origin) { + return base::StringPrintf("namespace-%s-%s", namespace_id_str.c_str(), + origin.c_str()); +} + +std::string SessionStorageDatabase::NamespaceKey( + int64 namespace_id, int64 namespace_offset, const GURL& origin) { + return NamespaceKey(NamespaceIdStr(namespace_id, namespace_offset), + origin.spec()); +} + +std::string SessionStorageDatabase::NamespaceIdStr(int64 namespace_id, + int64 namespace_offset) { + return base::Int64ToString(namespace_id + namespace_offset); +} + +const char* SessionStorageDatabase::NamespacePrefix() { + return "namespace-"; +} + +std::string SessionStorageDatabase::MapRefCountKey(const std::string& map_id) { + return base::StringPrintf("map-%s", map_id.c_str()); +} + +std::string SessionStorageDatabase::MapKey(const std::string& map_id, + const std::string& key) { + return base::StringPrintf("map-%s-%s", map_id.c_str(), key.c_str()); +} + +const char* SessionStorageDatabase::MapPrefix() { + return "map-"; +} + +const char* SessionStorageDatabase::NextNamespaceIdKey() { + return "next-namespace-id"; +} + +const char* SessionStorageDatabase::NextMapIdKey() { + return "next-map-id"; +} + +} // namespace dom_storage diff --git a/webkit/dom_storage/session_storage_database.h b/webkit/dom_storage/session_storage_database.h new file mode 100644 index 0000000..5230f37 --- /dev/null +++ b/webkit/dom_storage/session_storage_database.h @@ -0,0 +1,202 @@ +// Copyright (c) 2012 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 WEBKIT_DOM_STORAGE_SESSION_STORAGE_DATABASE_H_ +#define WEBKIT_DOM_STORAGE_SESSION_STORAGE_DATABASE_H_ +#pragma once + +#include <map> +#include <string> + +#include "base/file_path.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "base/synchronization/lock.h" +#include "third_party/leveldatabase/src/include/leveldb/status.h" +#include "webkit/dom_storage/dom_storage_types.h" + +class GURL; + +namespace leveldb { +class DB; +class WriteBatch; +} // namespace leveldb + +namespace dom_storage { + +// SessionStorageDatabase holds the data from multiple namespaces and multiple +// origins. All DomStorageAreas for session storage share the same +// SessionStorageDatabase. +class SessionStorageDatabase : + public base::RefCountedThreadSafe<SessionStorageDatabase> { + public: + explicit SessionStorageDatabase(const FilePath& file_path); + + // Reads the (key, value) pairs for |namespace_id| and |origin|. |result| is + // assumed to be empty and any duplicate keys will be overwritten. If the + // database exists on disk then it will be opened. If it does not exist then + // it will not be created and |result| will be unmodified. + void ReadAreaValues(int64 namespace_id, + const GURL& origin, + ValuesMap* result); + + // Updates the data for |namespace_id| and |origin|. Will remove all keys + // before updating the database if |clear_all_first| is set. Then all entries + // in |changes| will be examined - keys mapped to a null NullableString16 will + // be removed and all others will be inserted/updated as appropriate. + bool CommitAreaChanges(int64 namespace_id, + const GURL& origin, + bool clear_all_first, + const ValuesMap& changes); + + // Creates shallow copies of the areas for |namespace_id| and associates them + // with |new_namespace_id|. + bool CloneNamespace(int64 namespace_id, int64 new_namespace_id); + + // Creates a deep copy of the area for |namespace_id| and |origin|. + bool DeepCopyArea(int64 namespace_id, const GURL& origin); + + // Deletes the data for |namespace_id| and |origin|. + bool DeleteArea(int64 namespace_id, const GURL& origin); + + // Deletes the data for |namespace_id|. + bool DeleteNamespace(int64 namespace_id); + + private: + friend class base::RefCountedThreadSafe<SessionStorageDatabase>; + friend class SessionStorageDatabaseTest; + + ~SessionStorageDatabase(); + + bool LazyOpen(bool create_if_needed); + leveldb::Status TryToOpen(const FilePath& file_path, leveldb::DB** db); + bool IsOpen() const; + + // Helpers for checking caller erros, invariants and database errors. All + // these return |ok|, for chaining. + bool CallerErrorCheck(bool ok) const; + bool ConsistencyCheck(bool ok); + bool DatabaseErrorCheck(bool ok); + + // Helper functions. All return true if the operation succeeded, and false if + // it failed (a database error or a consistency error). If the return type is + // void, the operation cannot fail. If they return false, ConsistencyCheck or + // DatabaseErrorCheck have already been called. + + // Creates a namespace for |namespace_id| and updates the next namespace id if + // needed. If |ok_if_exists| is false, checks that the namespace didn't exist + // before. + bool CreateNamespace(int64 namespace_id, + bool ok_if_exists, + leveldb::WriteBatch* batch); + // Reads the next namespace id. + bool GetNextNamespaceId(int64* next_namespace_id); + bool UpdateNextNamespaceId(int64 namespace_id, + leveldb::WriteBatch* batch); + // Reads the areas assoiated with |namespace_id| and puts the (origin, map_id) + // pairs into |areas|. + bool GetAreasInNamespace(int64 namespace_id, + std::map<std::string, std::string>* areas); + bool GetAreasInNamespace(const std::string& namespace_id_str, + std::map<std::string, std::string>* areas); + + // Adds an association between |origin| and |map_id| into the namespace + // |namespace_id|. + void AddAreaToNamespace(int64 namespace_id, + const std::string& origin, + const std::string& map_id, + leveldb::WriteBatch* batch); + + // Helpers for deleting data for |namespace_id| and |origin|. + bool DeleteArea(int64 namespace_id, + const std::string& origin, + leveldb::WriteBatch* batch); + bool DeleteArea(const std::string& namespace_id_str, + const std::string& origin, + leveldb::WriteBatch* batch); + + // Retrieves the map id for |namespace_id| and |origin|. It's not an error if + // the map doesn't exist. + bool GetMapForArea(int64 namespace_id, + const GURL& origin, + bool* exists, + std::string* map_id); + bool GetMapForArea(const std::string& namespace_id_str, + const std::string& origin, + bool* exists, + std::string* map_id); + + // Creates a new map for |namespace_id| and |origin|. |map_id| will hold the + // id of the created map. If there is a map for |namespace_id| and |origin|, + // this just overwrites the map id. The caller is responsible for decreasing + // the ref count. + bool CreateMapForArea(int64 namespace_id, + const GURL& origin, + std::string* map_id, + leveldb::WriteBatch* batch); + // Reads the contents of the map |map_id| into |result|. If |only_keys| is + // true, only keys are aread from the database and the values in |result| will + // be empty. + bool ReadMap(const std::string& map_id, + ValuesMap* result, + bool only_keys); + // Writes |values| into the map |map_id|. + void WriteValuesToMap(const std::string& map_id, + const ValuesMap& values, + leveldb::WriteBatch* batch); + + bool GetMapRefCount(const std::string& map_id, int64* ref_count); + bool IncreaseMapRefCount(const std::string& map_id, + leveldb::WriteBatch* batch); + // Decreases the ref count of a map by |decrease|. If the ref count goes to 0, + // deletes the map. + bool DecreaseMapRefCount(const std::string& map_id, + int decrease, + leveldb::WriteBatch* batch); + + // Deletes all values in |map_id|. + bool ClearMap(const std::string& map_id, leveldb::WriteBatch* batch); + + // Helper functions for creating the keys needed for the schema. + static std::string NamespaceStartKey(const std::string& namespace_id_str); + static std::string NamespaceStartKey(int64 namespace_id, + int64 namespace_offset); + static std::string NamespaceKey(const std::string& namespace_id_str, + const std::string& origin); + static std::string NamespaceKey(int64 namespace_id, + int64 namespace_offset, + const GURL& origin); + static std::string NamespaceIdStr(int64 namespace_id, int64 namespace_offset); + static const char* NamespacePrefix(); + static std::string MapRefCountKey(const std::string& map_id); + static std::string MapKey(const std::string& map_id, const std::string& key); + static const char* MapPrefix(); + static const char* NextNamespaceIdKey(); + static const char* NextMapIdKey(); + + scoped_ptr<leveldb::DB> db_; + FilePath file_path_; + + // For protecting the database opening code. + base::Lock db_lock_; + + // True if a database error has occurred (e.g., cannot read data). + bool db_error_; + // True if the database is in an inconsistent state. + bool is_inconsistent_; + + // On startup, we read the next ununsed namespace id from the database. It + // will be the offset for namespace ids. The actual id of a namespace in the + // database will be: id passed to the API function + namespace_offset_. The + // namespace ids which are handled as int64 (named namespace_id) don't contain + // the offset yet. The namespaces ids which are handled as strings (named + // namesapce_id_str) contain the offset. + int64 namespace_offset_; + + DISALLOW_COPY_AND_ASSIGN(SessionStorageDatabase); +}; + +} // namespace dom_storage + +#endif // WEBKIT_DOM_STORAGE_SESSION_STORAGE_DATABASE_H_ diff --git a/webkit/dom_storage/session_storage_database_unittest.cc b/webkit/dom_storage/session_storage_database_unittest.cc new file mode 100644 index 0000000..c047009 --- /dev/null +++ b/webkit/dom_storage/session_storage_database_unittest.cc @@ -0,0 +1,858 @@ +// Copyright (c) 2012 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 "webkit/dom_storage/session_storage_database.h" + +#include <algorithm> +#include <map> +#include <string> + +#include "base/file_util.h" +#include "base/logging.h" +#include "base/scoped_temp_dir.h" +#include "base/string_number_conversions.h" +#include "base/utf_string_conversions.h" +#include "googleurl/src/gurl.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/leveldatabase/src/include/leveldb/db.h" +#include "third_party/leveldatabase/src/include/leveldb/iterator.h" +#include "third_party/leveldatabase/src/include/leveldb/options.h" +#include "webkit/dom_storage/dom_storage_types.h" + +namespace dom_storage { + +class SessionStorageDatabaseTest : public testing::Test { + public: + SessionStorageDatabaseTest(); + virtual ~SessionStorageDatabaseTest(); + virtual void SetUp() OVERRIDE; + + protected: + typedef std::map<std::string, std::string> DataMap; + + // Helpers. + static bool IsNamespaceKey(const std::string& key, + int64* namespace_id); + static bool IsNamespaceOriginKey(const std::string& key, + int64* namespace_id); + static bool IsMapRefCountKey(const std::string& key, + int64* map_id); + static bool IsMapValueKey(const std::string& key, + int64* map_id); + void ResetDatabase(); + void ReadData(DataMap* data) const; + void CheckDatabaseConsistency() const; + void CheckEmptyDatabase() const; + void DumpData() const; + void CheckAreaData(int64 namespace_id, + const GURL& origin, + const ValuesMap& reference) const; + void CompareValuesMaps(const ValuesMap& map1, const ValuesMap& map2) const; + std::string GetMapForArea(int64 namespace_id, + const GURL& origin) const; + int64 GetMapRefCount(const std::string& map_id) const; + int64 NextNamespaceId() const; + + ScopedTempDir temp_dir_; + scoped_refptr<SessionStorageDatabase> db_; + + // Test data. + const GURL kOrigin1; + const GURL kOrigin2; + const string16 kKey1; + const string16 kKey2; + const string16 kKey3; + const NullableString16 kValue1; + const NullableString16 kValue2; + const NullableString16 kValue3; + const NullableString16 kValue4; + const NullableString16 kValueNull; + + DISALLOW_COPY_AND_ASSIGN(SessionStorageDatabaseTest); +}; + +SessionStorageDatabaseTest::SessionStorageDatabaseTest() + : kOrigin1("http://www.origin1.com"), + kOrigin2("http://www.origin2.com"), + kKey1(ASCIIToUTF16("key1")), + kKey2(ASCIIToUTF16("key2")), + kKey3(ASCIIToUTF16("key3")), + kValue1(NullableString16(ASCIIToUTF16("value1"), false)), + kValue2(NullableString16(ASCIIToUTF16("value2"), false)), + kValue3(NullableString16(ASCIIToUTF16("value3"), false)), + kValue4(NullableString16(ASCIIToUTF16("value4"), false)), + kValueNull(NullableString16(true)) { } + +SessionStorageDatabaseTest::~SessionStorageDatabaseTest() { } + +void SessionStorageDatabaseTest::SetUp() { + ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); + ResetDatabase(); +} + +void SessionStorageDatabaseTest::ResetDatabase() { + db_ = new SessionStorageDatabase(temp_dir_.path()); + ASSERT_TRUE(db_->LazyOpen(true)); +} + +// static +bool SessionStorageDatabaseTest::IsNamespaceKey(const std::string& key, + int64* namespace_id) { + std::string namespace_prefix = SessionStorageDatabase::NamespacePrefix(); + if (key.find(namespace_prefix) != 0) + return false; + if (key == namespace_prefix) + return false; + + size_t second_dash = key.find('-', namespace_prefix.length()); + if (second_dash != std::string::npos) + return false; + + // Key is of the form "namespace-<namespaceid>". + std::string namespace_id_str = key.substr(namespace_prefix.length()); + bool conversion_ok = base::StringToInt64(namespace_id_str, namespace_id); + EXPECT_TRUE(conversion_ok); + return true; +} + +// static +bool SessionStorageDatabaseTest::IsNamespaceOriginKey(const std::string& key, + int64* namespace_id) { + std::string namespace_prefix = SessionStorageDatabase::NamespacePrefix(); + if (key.find(namespace_prefix) != 0) + return false; + size_t second_dash = key.find('-', namespace_prefix.length()); + if (second_dash == std::string::npos) + return false; + // Key is of the form "namespace-<namespaceid>-<origin>", and the value + // is the map id. + std::string namespace_id_str = + key.substr(namespace_prefix.length(), + second_dash - namespace_prefix.length()); + bool conversion_ok = base::StringToInt64(namespace_id_str, namespace_id); + EXPECT_TRUE(conversion_ok); + return true; +} + +// static +bool SessionStorageDatabaseTest::IsMapRefCountKey(const std::string& key, + int64* map_id) { + std::string map_prefix = SessionStorageDatabase::MapPrefix(); + if (key.find(map_prefix) != 0) + return false; + size_t second_dash = key.find('-', map_prefix.length()); + if (second_dash != std::string::npos) + return false; + // Key is of the form "map-<mapid>" and the value is the ref count. + std::string map_id_str = key.substr(map_prefix.length(), second_dash); + bool conversion_ok = base::StringToInt64(map_id_str, map_id); + EXPECT_TRUE(conversion_ok); + return true; +} + +// static +bool SessionStorageDatabaseTest::IsMapValueKey(const std::string& key, + int64* map_id) { + std::string map_prefix = SessionStorageDatabase::MapPrefix(); + if (key.find(map_prefix) != 0) + return false; + size_t second_dash = key.find('-', map_prefix.length()); + if (second_dash == std::string::npos) + return false; + // Key is of the form "map-<mapid>-key". + std::string map_id_str = + key.substr(map_prefix.length(), second_dash - map_prefix.length()); + bool conversion_ok = base::StringToInt64(map_id_str, map_id); + EXPECT_TRUE(conversion_ok); + return true; +} + +void SessionStorageDatabaseTest::ReadData(DataMap* data) const { + leveldb::DB* leveldb = db_->db_.get(); + scoped_ptr<leveldb::Iterator> it( + leveldb->NewIterator(leveldb::ReadOptions())); + for (it->SeekToFirst(); it->Valid(); it->Next()) { + (*data)[it->key().ToString()] = it->value().ToString(); + } +} + +void SessionStorageDatabaseTest::CheckDatabaseConsistency() const { + DataMap data; + ReadData(&data); + // Empty db is ok. + if (data.empty()) + return; + + // For detecting rubbish keys. + size_t valid_keys = 0; + + std::string next_namespace_id_key = + SessionStorageDatabase::NextNamespaceIdKey(); + std::string next_map_id_key = SessionStorageDatabase::NextMapIdKey(); + // Check the namespace start key. + if (data.find(SessionStorageDatabase::NamespacePrefix()) == data.end()) { + // If there is no namespace start key, the database may contain only counter + // keys. + for (DataMap::const_iterator it = data.begin(); it != data.end(); ++it) { + ASSERT_TRUE(it->first == next_namespace_id_key || + it->first == next_map_id_key); + } + return; + } + ++valid_keys; + + // Iterate the "namespace-" keys. + std::set<int64> found_namespace_ids; + int64 max_namespace_id = -1; + std::map<int64, int64> expected_map_refcounts; + int64 max_map_id = -1; + + for (DataMap::const_iterator it = data.begin(); it != data.end(); ++it) { + int64 namespace_id; + std::string origin; + if (IsNamespaceKey(it->first, &namespace_id)) { + ASSERT_GT(namespace_id, 0); + found_namespace_ids.insert(namespace_id); + max_namespace_id = std::max(namespace_id, max_namespace_id); + ++valid_keys; + } else if (IsNamespaceOriginKey( + it->first, &namespace_id)) { + // Check that the corresponding "namespace-<namespaceid>" key exists. It + // has been read by now, since the keys are stored in order. + ASSERT_TRUE(found_namespace_ids.find(namespace_id) != + found_namespace_ids.end()); + int64 map_id; + bool conversion_ok = base::StringToInt64(it->second, &map_id); + ASSERT_TRUE(conversion_ok); + ASSERT_GE(map_id, 0); + ++expected_map_refcounts[map_id]; + max_map_id = std::max(map_id, max_map_id); + ++valid_keys; + } + } + if (max_namespace_id != -1) { + // The database contains namespaces. + ASSERT_TRUE(data.find(next_namespace_id_key) != data.end()); + int64 next_namespace_id; + bool conversion_ok = + base::StringToInt64(data[next_namespace_id_key], &next_namespace_id); + ASSERT_TRUE(conversion_ok); + ASSERT_GT(next_namespace_id, max_namespace_id); + } + if (max_map_id != -1) { + // The database contains maps. + ASSERT_TRUE(data.find(next_map_id_key) != data.end()); + int64 next_map_id; + bool conversion_ok = + base::StringToInt64(data[next_map_id_key], &next_map_id); + ASSERT_TRUE(conversion_ok); + ASSERT_GT(next_map_id, max_map_id); + } + + // Iterate the "map-" keys. + std::set<int64> found_map_ids; + for (DataMap::const_iterator it = data.begin(); it != data.end(); ++it) { + int64 map_id; + if (IsMapRefCountKey(it->first, &map_id)) { + int64 ref_count; + bool conversion_ok = base::StringToInt64(it->second, &ref_count); + ASSERT_TRUE(conversion_ok); + // Check that the map is not stale. + ASSERT_GT(ref_count, 0); + ASSERT_TRUE(expected_map_refcounts.find(map_id) != + expected_map_refcounts.end()); + ASSERT_EQ(expected_map_refcounts[map_id], ref_count); + // Mark the map as existing. + expected_map_refcounts.erase(map_id); + found_map_ids.insert(map_id); + ++valid_keys; + } else if (IsMapValueKey(it->first, &map_id)) { + ASSERT_TRUE(found_map_ids.find(map_id) != found_map_ids.end()); + ++valid_keys; + } + } + // Check that all maps referred to exist. + ASSERT_TRUE(expected_map_refcounts.empty()); + + // Count valid keys. + if (data.find(next_namespace_id_key) != data.end()) + ++valid_keys; + + if (data.find(next_map_id_key) != data.end()) + ++valid_keys; + + ASSERT_EQ(data.size(), valid_keys); +} + +void SessionStorageDatabaseTest::CheckEmptyDatabase() const { + DataMap data; + ReadData(&data); + size_t valid_keys = 0; + if (data.find(SessionStorageDatabase::NamespacePrefix()) != data.end()) + ++valid_keys; + if (data.find(SessionStorageDatabase::NextNamespaceIdKey()) != data.end()) + ++valid_keys; + if (data.find(SessionStorageDatabase::NextMapIdKey()) != data.end()) + ++valid_keys; + EXPECT_EQ(valid_keys, data.size()); +} + +void SessionStorageDatabaseTest::DumpData() const { + LOG(WARNING) << "---- Session storage contents"; + scoped_ptr<leveldb::Iterator> it( + db_->db_->NewIterator(leveldb::ReadOptions())); + for (it->SeekToFirst(); it->Valid(); it->Next()) { + int64 dummy_map_id; + if (IsMapValueKey(it->key().ToString(), &dummy_map_id)) { + // Convert the value back to string16. + string16 value; + size_t len = it->value().size() / sizeof(char16); + value.resize(len); + value.assign(reinterpret_cast<const char16*>(it->value().data()), len); + LOG(WARNING) << it->key().ToString() << ": " << value; + } else { + LOG(WARNING) << it->key().ToString() << ": " << it->value().ToString(); + } + } + LOG(WARNING) << "----"; +} + +void SessionStorageDatabaseTest::CheckAreaData( + int64 namespace_id, const GURL& origin, const ValuesMap& reference) const { + ValuesMap values; + db_->ReadAreaValues(namespace_id, origin, &values); + CompareValuesMaps(values, reference); +} + +void SessionStorageDatabaseTest::CompareValuesMaps( + const ValuesMap& map1, + const ValuesMap& map2) const { + ASSERT_EQ(map2.size(), map1.size()); + for (ValuesMap::const_iterator it = map1.begin(); it != map1.end(); ++it) { + string16 key = it->first; + ASSERT_TRUE(map2.find(key) != map2.end()); + NullableString16 val1 = it->second; + NullableString16 val2 = map2.find(key)->second; + EXPECT_EQ(val2.is_null(), val1.is_null()); + EXPECT_EQ(val2.string(), val1.string()); + } +} + +std::string SessionStorageDatabaseTest::GetMapForArea( + int64 namespace_id, const GURL& origin) const { + bool exists; + std::string map_id; + EXPECT_TRUE(db_->GetMapForArea(namespace_id, origin, + &exists, &map_id)); + EXPECT_TRUE(exists); + return map_id; +} + +int64 SessionStorageDatabaseTest::GetMapRefCount( + const std::string& map_id) const { + int64 ref_count; + EXPECT_TRUE(db_->GetMapRefCount(map_id, &ref_count)); + return ref_count; +} + +int64 SessionStorageDatabaseTest::NextNamespaceId() const { + int64 next_namespace_id; + EXPECT_TRUE(db_->GetNextNamespaceId(&next_namespace_id)); + return next_namespace_id; +} + +TEST_F(SessionStorageDatabaseTest, EmptyDatabaseSanityCheck) { + // An empty database should be valid. + CheckDatabaseConsistency(); +} + +TEST_F(SessionStorageDatabaseTest, WriteDataForOneOrigin) { + // Keep track on what the values should look like. + ValuesMap reference; + // Write data. + { + ValuesMap changes; + changes[kKey1] = kValue1; + changes[kKey2] = kValue2; + changes[kKey3] = kValue3; + reference[kKey1] = kValue1; + reference[kKey2] = kValue2; + reference[kKey3] = kValue3; + EXPECT_TRUE(db_->CommitAreaChanges(1, kOrigin1, false, changes)); + } + CheckDatabaseConsistency(); + CheckAreaData(1, kOrigin1, reference); + + // Overwrite and delete values. + { + ValuesMap changes; + changes[kKey1] = kValue4; + changes[kKey3] = kValueNull; + reference[kKey1] = kValue4; + reference.erase(kKey3); + EXPECT_TRUE(db_->CommitAreaChanges(1, kOrigin1, false, changes)); + } + CheckDatabaseConsistency(); + CheckAreaData(1, kOrigin1, reference); + + // Clear data before writing. + { + ValuesMap changes; + changes[kKey2] = kValue2; + reference.erase(kKey1); + reference[kKey2] = kValue2; + EXPECT_TRUE(db_->CommitAreaChanges(1, kOrigin1, true, changes)); + } + CheckDatabaseConsistency(); + CheckAreaData(1, kOrigin1, reference); +} + +TEST_F(SessionStorageDatabaseTest, WriteDataForTwoOrigins) { + // Write data. + ValuesMap data1; + data1[kKey1] = kValue1; + data1[kKey2] = kValue2; + data1[kKey3] = kValue3; + EXPECT_TRUE(db_->CommitAreaChanges(1, kOrigin1, false, data1)); + + ValuesMap data2; + data2[kKey1] = kValue4; + data2[kKey2] = kValue1; + data2[kKey3] = kValue2; + EXPECT_TRUE(db_->CommitAreaChanges(1, kOrigin2, false, data2)); + + CheckDatabaseConsistency(); + CheckAreaData(1, kOrigin1, data1); + CheckAreaData(1, kOrigin2, data2); +} + +TEST_F(SessionStorageDatabaseTest, WriteDataForTwoNamespaces) { + // Write data. + ValuesMap data11; + data11[kKey1] = kValue1; + data11[kKey2] = kValue2; + data11[kKey3] = kValue3; + EXPECT_TRUE(db_->CommitAreaChanges(1, kOrigin1, false, data11)); + ValuesMap data12; + data12[kKey2] = kValue4; + data12[kKey3] = kValue3; + EXPECT_TRUE(db_->CommitAreaChanges(1, kOrigin2, false, data12)); + ValuesMap data21; + data21[kKey1] = kValue2; + data21[kKey2] = kValue4; + EXPECT_TRUE(db_->CommitAreaChanges(2, kOrigin1, false, data21)); + ValuesMap data22; + data22[kKey2] = kValue1; + data22[kKey3] = kValue2; + EXPECT_TRUE(db_->CommitAreaChanges(2, kOrigin2, false, data22)); + CheckDatabaseConsistency(); + CheckAreaData(1, kOrigin1, data11); + CheckAreaData(1, kOrigin2, data12); + CheckAreaData(2, kOrigin1, data21); + CheckAreaData(2, kOrigin2, data22); +} + +TEST_F(SessionStorageDatabaseTest, ShallowCopy) { + // Write data for a namespace, for 2 origins. + ValuesMap data1; + data1[kKey1] = kValue1; + data1[kKey2] = kValue2; + data1[kKey3] = kValue3; + ASSERT_TRUE(db_->CommitAreaChanges(1, kOrigin1, false, data1)); + ValuesMap data2; + data2[kKey1] = kValue2; + data2[kKey3] = kValue1; + ASSERT_TRUE(db_->CommitAreaChanges(1, kOrigin2, false, data2)); + // Make a shallow copy. + EXPECT_TRUE(db_->CloneNamespace(1, 5)); + // Now both namespaces should have the same data. + CheckDatabaseConsistency(); + CheckAreaData(1, kOrigin1, data1); + CheckAreaData(1, kOrigin2, data2); + CheckAreaData(5, kOrigin1, data1); + CheckAreaData(5, kOrigin2, data2); + // Both the namespaces refer to the same maps. + EXPECT_EQ(GetMapForArea(1, kOrigin1), GetMapForArea(5, kOrigin1)); + EXPECT_EQ(GetMapForArea(1, kOrigin2), GetMapForArea(5, kOrigin2)); + EXPECT_EQ(2, GetMapRefCount(GetMapForArea(1, kOrigin1))); + EXPECT_EQ(2, GetMapRefCount(GetMapForArea(1, kOrigin2))); +} + +TEST_F(SessionStorageDatabaseTest, DeepCopy) { + ValuesMap data1; + data1[kKey1] = kValue1; + data1[kKey2] = kValue2; + ASSERT_TRUE(db_->CommitAreaChanges(1, kOrigin1, false, data1)); + EXPECT_TRUE(db_->CloneNamespace(1, 5)); + + // Make the shallow copy deep. + EXPECT_TRUE(db_->DeepCopyArea(5, kOrigin1)); + + // Write data into the deep copy. + ValuesMap changes; + ValuesMap reference; + changes[kKey1] = kValueNull; + changes[kKey2] = kValue4; + changes[kKey3] = kValue4; + reference[kKey2] = kValue4; + reference[kKey3] = kValue4; + EXPECT_TRUE(db_->CommitAreaChanges(5, kOrigin1, false, changes)); + + // Values in the original namespace were not changed. + CheckAreaData(1, kOrigin1, data1); + // But values in the deep copy were. + CheckAreaData(5, kOrigin1, reference); + + // The namespaces no longer refer to the same map. + EXPECT_NE(GetMapForArea(1, kOrigin1), GetMapForArea(5, kOrigin1)); + EXPECT_EQ(1, GetMapRefCount(GetMapForArea(1, kOrigin1))); + EXPECT_EQ(1, GetMapRefCount(GetMapForArea(5, kOrigin1))); +} + +TEST_F(SessionStorageDatabaseTest, ManyShallowCopies) { + // Write data for a namespace, for 2 origins. + ValuesMap data1; + data1[kKey1] = kValue1; + data1[kKey2] = kValue2; + data1[kKey3] = kValue3; + ASSERT_TRUE(db_->CommitAreaChanges(1, kOrigin1, false, data1)); + ValuesMap data2; + data2[kKey1] = kValue2; + data2[kKey3] = kValue1; + ASSERT_TRUE(db_->CommitAreaChanges(1, kOrigin2, false, data2)); + + // Make a two shallow copies. + EXPECT_TRUE(db_->CloneNamespace(1, 5)); + EXPECT_TRUE(db_->CloneNamespace(1, 6)); + + // Make a shallow copy of a shallow copy. + EXPECT_TRUE(db_->CloneNamespace(6, 7)); + + // Now all namespaces should have the same data. + CheckDatabaseConsistency(); + CheckAreaData(1, kOrigin1, data1); + CheckAreaData(5, kOrigin1, data1); + CheckAreaData(6, kOrigin1, data1); + CheckAreaData(7, kOrigin1, data1); + CheckAreaData(1, kOrigin2, data2); + CheckAreaData(5, kOrigin2, data2); + CheckAreaData(6, kOrigin2, data2); + CheckAreaData(7, kOrigin2, data2); + + // All namespaces refer to the same maps. + EXPECT_EQ(GetMapForArea(1, kOrigin1), GetMapForArea(5, kOrigin1)); + EXPECT_EQ(GetMapForArea(1, kOrigin2), GetMapForArea(5, kOrigin2)); + EXPECT_EQ(GetMapForArea(1, kOrigin1), GetMapForArea(6, kOrigin1)); + EXPECT_EQ(GetMapForArea(1, kOrigin2), GetMapForArea(6, kOrigin2)); + EXPECT_EQ(GetMapForArea(1, kOrigin1), GetMapForArea(7, kOrigin1)); + EXPECT_EQ(GetMapForArea(1, kOrigin2), GetMapForArea(7, kOrigin2)); + + // Check the ref counts. + EXPECT_EQ(4, GetMapRefCount(GetMapForArea(1, kOrigin1))); + EXPECT_EQ(4, GetMapRefCount(GetMapForArea(1, kOrigin2))); +} + +TEST_F(SessionStorageDatabaseTest, DisassociateShallowCopy) { + ValuesMap data1; + data1[kKey1] = kValue1; + data1[kKey2] = kValue2; + ASSERT_TRUE(db_->CommitAreaChanges(1, kOrigin1, false, data1)); + EXPECT_TRUE(db_->CloneNamespace(1, 5)); + + // Disassoaciate the shallow copy. + EXPECT_TRUE(db_->DeleteArea(5, kOrigin1)); + CheckDatabaseConsistency(); + + // Now new data can be written to that map. + ValuesMap reference; + ValuesMap changes; + changes[kKey1] = kValueNull; + changes[kKey2] = kValue4; + changes[kKey3] = kValue4; + reference[kKey2] = kValue4; + reference[kKey3] = kValue4; + EXPECT_TRUE(db_->CommitAreaChanges(5, kOrigin1, false, changes)); + + // Values in the original map were not changed. + CheckAreaData(1, kOrigin1, data1); + + // But values in the disassociated map were. + CheckAreaData(5, kOrigin1, reference); +} + +TEST_F(SessionStorageDatabaseTest, DeleteNamespace) { + ValuesMap data1; + data1[kKey1] = kValue1; + data1[kKey2] = kValue2; + data1[kKey3] = kValue3; + ASSERT_TRUE(db_->CommitAreaChanges(1, kOrigin1, false, data1)); + ValuesMap data2; + data2[kKey2] = kValue4; + data2[kKey3] = kValue3; + ASSERT_TRUE(db_->CommitAreaChanges(1, kOrigin2, false, data2)); + EXPECT_TRUE(db_->DeleteNamespace(1)); + CheckDatabaseConsistency(); + CheckEmptyDatabase(); +} + +TEST_F(SessionStorageDatabaseTest, DeleteNamespaceWithShallowCopy) { + // Write data for a namespace, for 2 origins. + ValuesMap data1; + data1[kKey1] = kValue1; + data1[kKey2] = kValue2; + data1[kKey3] = kValue3; + ASSERT_TRUE(db_->CommitAreaChanges(1, kOrigin1, false, data1)); + ValuesMap data2; + data2[kKey1] = kValue2; + data2[kKey3] = kValue1; + ASSERT_TRUE(db_->CommitAreaChanges(1, kOrigin2, false, data2)); + + // Make a shallow copy and delete the original namespace. + EXPECT_TRUE(db_->CloneNamespace(1, 5));; + EXPECT_TRUE(db_->DeleteNamespace(1)); + + // The original namespace has no data. + CheckDatabaseConsistency(); + CheckAreaData(1, kOrigin1, ValuesMap()); + CheckAreaData(1, kOrigin2, ValuesMap()); + // But the copy persists. + CheckAreaData(5, kOrigin1, data1); + CheckAreaData(5, kOrigin2, data2); +} + +TEST_F(SessionStorageDatabaseTest, DeleteArea) { + // Write data for a namespace, for 2 origins. + ValuesMap data1; + data1[kKey1] = kValue1; + data1[kKey2] = kValue2; + data1[kKey3] = kValue3; + ASSERT_TRUE(db_->CommitAreaChanges(1, kOrigin1, false, data1)); + ValuesMap data2; + data2[kKey1] = kValue2; + data2[kKey3] = kValue1; + ASSERT_TRUE(db_->CommitAreaChanges(1, kOrigin2, false, data2)); + + EXPECT_TRUE(db_->DeleteArea(1, kOrigin2)); + CheckDatabaseConsistency(); + // The data for the non-deleted origin persists. + CheckAreaData(1, kOrigin1, data1); + // The data for the deleted origin is gone. + CheckAreaData(1, kOrigin2, ValuesMap()); +} + +TEST_F(SessionStorageDatabaseTest, DeleteAreaWithShallowCopy) { + // Write data for a namespace, for 2 origins. + ValuesMap data1; + data1[kKey1] = kValue1; + data1[kKey2] = kValue2; + data1[kKey3] = kValue3; + ASSERT_TRUE(db_->CommitAreaChanges(1, kOrigin1, false, data1)); + ValuesMap data2; + data2[kKey1] = kValue2; + data2[kKey3] = kValue1; + ASSERT_TRUE(db_->CommitAreaChanges(1, kOrigin2, false, data2)); + + // Make a shallow copy and delete an origin from the original namespace. + EXPECT_TRUE(db_->CloneNamespace(1, 5)); + EXPECT_TRUE(db_->DeleteArea(1, kOrigin1)); + CheckDatabaseConsistency(); + + // The original namespace has data for only the non-deleted origin. + CheckAreaData(1, kOrigin1, ValuesMap()); + CheckAreaData(1, kOrigin2, data2); + // But the copy persists. + CheckAreaData(5, kOrigin1, data1); + CheckAreaData(5, kOrigin2, data2); +} + +TEST_F(SessionStorageDatabaseTest, WriteRawBytes) { + // Write data which is not valid utf8 and contains null bytes. + unsigned char raw_data[10] = {255, 0, 0, 0, 1, 2, 3, 4, 5, 0}; + ValuesMap changes; + string16 string_with_raw_data; + string_with_raw_data.assign(reinterpret_cast<char16*>(raw_data), 5); + changes[kKey1] = NullableString16(string_with_raw_data, false); + EXPECT_TRUE(db_->CommitAreaChanges(1, kOrigin1, false, changes)); + CheckDatabaseConsistency(); + ValuesMap values; + db_->ReadAreaValues(1, kOrigin1, &values); + const unsigned char* data = + reinterpret_cast<const unsigned char*>(values[kKey1].string().data()); + for (int i = 0; i < 10; ++i) + EXPECT_EQ(raw_data[i], data[i]); +} + +TEST_F(SessionStorageDatabaseTest, NextNamespaceId) { + // Create namespaces, check the next namespace id. + ValuesMap data1; + data1[kKey1] = kValue1; + data1[kKey2] = kValue2; + ASSERT_TRUE(db_->CommitAreaChanges(10, kOrigin1, false, data1)); + EXPECT_EQ(10 + 1, NextNamespaceId()); + ASSERT_TRUE(db_->CommitAreaChanges(343, kOrigin1, false, data1)); + EXPECT_EQ(343 + 1, NextNamespaceId()); + ASSERT_TRUE(db_->CommitAreaChanges(99, kOrigin1, false, data1)); + EXPECT_EQ(343 + 1, NextNamespaceId()); + + // Close the database and recreate it. + ResetDatabase(); + + // The next namespace id is persisted. + EXPECT_EQ(344, NextNamespaceId()); + + // Create more namespaces. + EXPECT_TRUE(db_->CommitAreaChanges(1, kOrigin1, false, data1)); + EXPECT_EQ(344 + 1 + 1, NextNamespaceId()); + + EXPECT_TRUE(db_->CommitAreaChanges(959, kOrigin1, false, data1)); + EXPECT_EQ(344 + 959 + 1, NextNamespaceId()); +} + +TEST_F(SessionStorageDatabaseTest, NamespaceOffset) { + // Create a namespace with id 1. + ValuesMap data1; + data1[kKey1] = kValue1; + data1[kKey2] = kValue2; + ASSERT_TRUE(db_->CommitAreaChanges(1, kOrigin1, false, data1)); + + // Close the database and recreate it. + ResetDatabase(); + + // Create another namespace with id 1. + ValuesMap data2; + data2[kKey1] = kValue3; + EXPECT_TRUE(db_->CommitAreaChanges(1, kOrigin1, false, data2)); + + // Now the values for namespace 1 are the new ones. + CheckAreaData(1, kOrigin1, data2); + + // The values for the old namespace 1 are still accessible via id -1. + CheckAreaData(-1, kOrigin1, data1); +} + +TEST_F(SessionStorageDatabaseTest, NamespaceOffsetCloneNamespace) { + // Create a namespace with id 1. + ValuesMap data1; + data1[kKey1] = kValue1; + data1[kKey2] = kValue2; + ASSERT_TRUE(db_->CommitAreaChanges(1, kOrigin1, false, data1)); + + // Close the database and recreate it. + ResetDatabase(); + + // Create another namespace with id 1. + ValuesMap data2; + data2[kKey1] = kValue3; + EXPECT_TRUE(db_->CommitAreaChanges(1, kOrigin1, false, data2)); + + // Make a shallow copy of the newly created namespace. + EXPECT_TRUE(db_->CloneNamespace(1, 20)); + + // The clone contains values from the newly created namespace. + CheckAreaData(20, kOrigin1, data2); + CheckAreaData(1, kOrigin1, data2); + + // The values for the old namespace 1 are still accessible via id -1. + CheckAreaData(-1, kOrigin1, data1); + + // Close the database and recreate it. + ResetDatabase(); + + // The namespace and the clone are still accessible. + CheckAreaData(-1, kOrigin1, data2); + CheckAreaData(-20, kOrigin1, data2); + CheckAreaData(-22, kOrigin1, data1); +} + +TEST_F(SessionStorageDatabaseTest, NamespaceOffsetDeepCopyArea) { + // Create a namespace with id 1. + ValuesMap data1; + data1[kKey1] = kValue1; + data1[kKey2] = kValue2; + ASSERT_TRUE(db_->CommitAreaChanges(1, kOrigin1, false, data1)); + + // Close the database and recreate it. + ResetDatabase(); + + // Create another namespace with id 1. + ValuesMap data2; + data2[kKey1] = kValue3; + EXPECT_TRUE(db_->CommitAreaChanges(1, kOrigin1, false, data2)); + + // Make a shallow copy of the newly created namespace. + EXPECT_TRUE(db_->CloneNamespace(1, 20)); + // And make it deep. + EXPECT_TRUE(db_->DeepCopyArea(20, kOrigin1)); + + // Now the values can be altered in the deep copy. + ValuesMap data3; + data3[kKey1] = kValue2; + EXPECT_TRUE(db_->CommitAreaChanges(20, kOrigin1, false, data3)); + + CheckAreaData(20, kOrigin1, data3); + CheckAreaData(1, kOrigin1, data2); + + // The values for the old namespace 1 are still accessible via id -1. + CheckAreaData(-1, kOrigin1, data1); + + // Close the database and recreate it. + ResetDatabase(); + + // The namespace and the deep copy are still accessible. + CheckAreaData(-1, kOrigin1, data3); + CheckAreaData(-20, kOrigin1, data2); + CheckAreaData(-22, kOrigin1, data1); +} + +TEST_F(SessionStorageDatabaseTest, NamespaceOffsetDeleteArea) { + // Create a namespace with id 1. + ValuesMap data1; + data1[kKey1] = kValue1; + data1[kKey2] = kValue2; + ASSERT_TRUE(db_->CommitAreaChanges(1, kOrigin1, false, data1)); + + // Close the database and recreate it. + ResetDatabase(); + + // Create another namespace with id 1. + ValuesMap data2; + data2[kKey1] = kValue3; + EXPECT_TRUE(db_->CommitAreaChanges(1, kOrigin1, false, data2)); + + // Delete kOrigin1 from the newly created namespace. + EXPECT_TRUE(db_->DeleteArea(1, kOrigin1)); + + // Namespace 1 is empty. + CheckAreaData(1, kOrigin1, ValuesMap()); + + // The values for the old namespace 1 are still accessible via id -1. + CheckAreaData(-1, kOrigin1, data1); +} + +TEST_F(SessionStorageDatabaseTest, NamespaceOffsetDeleteNamespace) { + // Create a namespace with id 1. + ValuesMap data1; + data1[kKey1] = kValue1; + data1[kKey2] = kValue2; + ASSERT_TRUE(db_->CommitAreaChanges(1, kOrigin1, false, data1)); + + // Close the database and recreate it. + ResetDatabase(); + + // Create another namespace with id 1. + ValuesMap data2; + data2[kKey1] = kValue3; + EXPECT_TRUE(db_->CommitAreaChanges(1, kOrigin1, false, data2)); + + // Delete the newly created namespace. + EXPECT_TRUE(db_->DeleteNamespace(1)); + + // Namespace 1 is empty. + CheckAreaData(1, kOrigin1, ValuesMap()); + + // The values for the old namespace 1 are still accessible via id -1. + CheckAreaData(-1, kOrigin1, data1); +} + +} // namespace dom_storage diff --git a/webkit/dom_storage/webkit_dom_storage.gypi b/webkit/dom_storage/webkit_dom_storage.gypi index 65df554..82f6670 100644 --- a/webkit/dom_storage/webkit_dom_storage.gypi +++ b/webkit/dom_storage/webkit_dom_storage.gypi @@ -11,6 +11,7 @@ 'dependencies': [ '<(DEPTH)/base/base.gyp:base', '<(DEPTH)/sql/sql.gyp:sql', + '<(DEPTH)/third_party/leveldatabase/leveldatabase.gyp:leveldatabase', '<(DEPTH)/third_party/sqlite/sqlite.gyp:sqlite', '<(DEPTH)/webkit/support/webkit_support.gyp:quota', ], @@ -32,6 +33,8 @@ 'dom_storage_task_runner.cc', 'dom_storage_task_runner.h', 'dom_storage_types.h', + 'session_storage_database.cc', + 'session_storage_database.h' ], 'conditions': [ ['inside_chromium_build==0', { diff --git a/webkit/tools/test_shell/test_shell.gypi b/webkit/tools/test_shell/test_shell.gypi index f11ec3f..e706d04 100644 --- a/webkit/tools/test_shell/test_shell.gypi +++ b/webkit/tools/test_shell/test_shell.gypi @@ -407,6 +407,7 @@ '../../dom_storage/dom_storage_context_unittest.cc', '../../dom_storage/dom_storage_database_unittest.cc', '../../dom_storage/dom_storage_map_unittest.cc', + '../../dom_storage/session_storage_database_unittest.cc', '../../fileapi/file_system_database_test_helper.cc', '../../fileapi/file_system_database_test_helper.h', '../../fileapi/file_system_directory_database_unittest.cc', |