Commit 1571d750 authored by Daniel Murphy's avatar Daniel Murphy Committed by Commit Bot

[DOMStorage] blink::CachedStorageArea for onion souping

As part of onion souping domstorage this adds a couple of new classes
to blink more or less duplicating the content equivalents:

CachedStorageArea matches content::LocalStorageCachedArea (with some
minor API changes, in particular around how it acquires a mojom
StorageArea). Also has improved test coverage compared to the content
version.

MockStorageArea is very similar to content::MockLevelDBWrapper (with
the difference that it doesn't implement StoragePartitionService since
it doesn't need to with the changed API of CachedStorageArea).

Bug: 781870
Change-Id: Ic64b3bf307fe275d9287669743f186c8470847bb
Reviewed-on: https://chromium-review.googlesource.com/1166218Reviewed-by: default avatarDaniel Cheng <dcheng@chromium.org>
Reviewed-by: default avatarKentaro Hara <haraken@chromium.org>
Reviewed-by: default avatarMarijn Kruisselbrink <mek@chromium.org>
Commit-Queue: Daniel Murphy <dmurph@chromium.org>
Cr-Commit-Position: refs/heads/master@{#582334}
parent d30a6822
......@@ -38,6 +38,16 @@ interface StorageAreaGetAllCallback {
// The mojo interface representing the connection to a single DOMStorage Area.
interface StorageArea {
// The quota for each storage area.
// This value is enforced in renderer processes and the browser process.
const uint32 kPerStorageAreaQuota = 10485760; // 10 MiB
// In the browser process we allow some overage to
// accommodate concurrent writes from different renderers
// that were allowed because the limit imposed in the renderer
// wasn't exceeded.
const uint32 kPerStorageAreaOverQuotaAllowance = 102400; // 100 KiB
AddObserver(associated StorageAreaObserver observer);
// Set the database entry for |key| to |value|.
......
include_rules = [
"+base/atomic_sequence_num.h",
"+base/memory/scoped_refptr.h",
"+mojo/public/cpp/bindings",
"+services/network/public/cpp/shared_url_loader_factory.h",
"+services/service_manager/public/mojom/interface_provider.mojom-blink.h",
"+third_party/blink/public/common",
......
......@@ -6,6 +6,8 @@ import("//third_party/blink/renderer/modules/modules.gni")
blink_modules_sources("storage") {
sources = [
"cached_storage_area.cc",
"cached_storage_area.h",
"dom_window_storage.cc",
"dom_window_storage.h",
"dom_window_storage_controller.cc",
......@@ -28,7 +30,11 @@ blink_modules_sources("storage") {
jumbo_source_set("unit_tests") {
testonly = true
sources = [
"cached_storage_area_test.cc",
"storage_area_map_test.cc",
"testing/fake_area_source.h",
"testing/mock_storage_area.cc",
"testing/mock_storage_area.h",
]
configs += [
......
// 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 "third_party/blink/renderer/modules/storage/cached_storage_area.h"
#include "base/metrics/histogram_macros.h"
#include "base/rand_util.h"
#include "mojo/public/cpp/bindings/strong_associated_binding.h"
#include "third_party/blink/public/platform/scheduler/web_thread_scheduler.h"
#include "third_party/blink/renderer/platform/weborigin/kurl.h"
#include "third_party/blink/renderer/platform/wtf/functional.h"
#include "third_party/blink/renderer/platform/wtf/text/string_buffer.h"
#include "third_party/blink/renderer/platform/wtf/text/unicode.h"
#include "third_party/blink/renderer/platform/wtf/text/utf8.h"
namespace blink {
namespace {
// Don't change or reorder any of the values in this enum, as these values
// are serialized on disk.
enum class StorageFormat : uint8_t { UTF16 = 0, Latin1 = 1 };
class GetAllCallback : public mojom::blink::StorageAreaGetAllCallback {
public:
static mojom::blink::StorageAreaGetAllCallbackAssociatedPtrInfo CreateAndBind(
base::OnceCallback<void(bool)> callback) {
mojom::blink::StorageAreaGetAllCallbackAssociatedPtrInfo ptr_info;
auto request = mojo::MakeRequest(&ptr_info);
mojo::MakeStrongAssociatedBinding(
base::WrapUnique(new GetAllCallback(std::move(callback))),
std::move(request));
return ptr_info;
}
private:
explicit GetAllCallback(base::OnceCallback<void(bool)> callback)
: m_callback(std::move(callback)) {}
void Complete(bool success) override { std::move(m_callback).Run(success); }
base::OnceCallback<void(bool)> m_callback;
};
// These methods are used to pack and unpack the page_url/storage_area_id into
// source strings to/from the browser.
String PackSource(const KURL& page_url, const String& storage_area_id) {
return page_url.GetString() + "\n" + storage_area_id;
}
void UnpackSource(const String& source,
KURL* page_url,
String* storage_area_id) {
Vector<String> result;
source.Split("\n", true, result);
DCHECK_EQ(result.size(), 2u);
*page_url = KURL(result[0]);
*storage_area_id = result[1];
}
} // namespace
// static
scoped_refptr<CachedStorageArea> CachedStorageArea::CreateForLocalStorage(
scoped_refptr<const SecurityOrigin> origin,
mojo::InterfacePtr<mojom::blink::StorageArea> area,
scoped_refptr<base::SingleThreadTaskRunner> ipc_runner) {
return base::AdoptRef(new CachedStorageArea(
std::move(origin), std::move(area), std::move(ipc_runner)));
}
// static
scoped_refptr<CachedStorageArea> CachedStorageArea::CreateForSessionStorage(
scoped_refptr<const SecurityOrigin> origin,
mojo::AssociatedInterfacePtr<mojom::blink::StorageArea> area,
scoped_refptr<base::SingleThreadTaskRunner> ipc_runner) {
return base::AdoptRef(new CachedStorageArea(
std::move(origin), std::move(area), std::move(ipc_runner)));
}
unsigned CachedStorageArea::GetLength() {
EnsureLoaded();
return map_->GetLength();
}
String CachedStorageArea::GetKey(unsigned index) {
EnsureLoaded();
return map_->GetKey(index);
}
String CachedStorageArea::GetItem(const String& key) {
EnsureLoaded();
return map_->GetItem(key);
}
bool CachedStorageArea::SetItem(const String& key,
const String& value,
Source* source) {
DCHECK(areas_.Contains(source));
// A quick check to reject obviously overbudget items to avoid priming the
// cache.
if ((key.length() + value.length()) * 2 >
mojom::blink::StorageArea::kPerStorageAreaQuota)
return false;
EnsureLoaded();
String old_value;
if (!map_->SetItem(key, value, &old_value))
return false;
// Determine data formats.
const FormatOption key_format = GetKeyFormat();
const FormatOption value_format = GetValueFormat();
// Ignore mutations to |key| until OnSetItemComplete.
auto ignore_add_result = ignore_key_mutations_.insert(key, 1);
if (!ignore_add_result.is_new_entry)
ignore_add_result.stored_value->value++;
base::Optional<Vector<uint8_t>> optional_old_value;
if (!old_value.IsNull())
optional_old_value = StringToUint8Vector(old_value, value_format);
KURL page_url = source->GetPageUrl();
String source_id = areas_.at(source);
blink::WebScopedVirtualTimePauser virtual_time_pauser =
source->CreateWebScopedVirtualTimePauser(
"CachedStorageArea",
WebScopedVirtualTimePauser::VirtualTaskDuration::kNonInstant);
virtual_time_pauser.PauseVirtualTime();
mojo_area_->Put(StringToUint8Vector(key, key_format),
StringToUint8Vector(value, value_format), optional_old_value,
PackSource(page_url, source_id),
WTF::Bind(&CachedStorageArea::OnSetItemComplete,
weak_factory_.GetWeakPtr(), key,
std::move(virtual_time_pauser)));
if (IsSessionStorage() && old_value != value) {
for (const auto& area : areas_) {
if (area.key != source)
area.key->EnqueueStorageEvent(key, old_value, value, page_url);
}
}
return true;
}
void CachedStorageArea::RemoveItem(const String& key, Source* source) {
DCHECK(areas_.Contains(source));
EnsureLoaded();
String old_value;
if (!map_->RemoveItem(key, &old_value))
return;
// Determine data formats.
const FormatOption key_format = GetKeyFormat();
const FormatOption value_format = GetValueFormat();
// Ignore mutations to |key| until OnRemoveItemComplete.
auto ignore_add_result = ignore_key_mutations_.insert(key, 1);
if (!ignore_add_result.is_new_entry)
ignore_add_result.stored_value->value++;
base::Optional<Vector<uint8_t>> optional_old_value;
if (should_send_old_value_on_mutations_)
optional_old_value = StringToUint8Vector(old_value, value_format);
KURL page_url = source->GetPageUrl();
String source_id = areas_.at(source);
blink::WebScopedVirtualTimePauser virtual_time_pauser =
source->CreateWebScopedVirtualTimePauser(
"CachedStorageArea",
WebScopedVirtualTimePauser::VirtualTaskDuration::kNonInstant);
virtual_time_pauser.PauseVirtualTime();
mojo_area_->Delete(StringToUint8Vector(key, key_format), optional_old_value,
PackSource(page_url, source_id),
WTF::Bind(&CachedStorageArea::OnRemoveItemComplete,
weak_factory_.GetWeakPtr(), key,
std::move(virtual_time_pauser)));
if (IsSessionStorage()) {
for (const auto& area : areas_) {
if (area.key != source)
area.key->EnqueueStorageEvent(key, old_value, String(), page_url);
}
}
}
void CachedStorageArea::Clear(Source* source) {
DCHECK(areas_.Contains(source));
bool already_empty = false;
if (IsSessionStorage()) {
EnsureLoaded();
already_empty = map_->GetLength() == 0u;
}
// No need to prime the cache in this case.
Reset();
map_ = std::make_unique<StorageAreaMap>(
mojom::blink::StorageArea::kPerStorageAreaQuota);
ignore_all_mutations_ = true;
KURL page_url = source->GetPageUrl();
String source_id = areas_.at(source);
blink::WebScopedVirtualTimePauser virtual_time_pauser =
source->CreateWebScopedVirtualTimePauser(
"CachedStorageArea",
WebScopedVirtualTimePauser::VirtualTaskDuration::kNonInstant);
virtual_time_pauser.PauseVirtualTime();
mojo_area_->DeleteAll(
PackSource(page_url, source_id),
WTF::Bind(&CachedStorageArea::OnClearComplete, weak_factory_.GetWeakPtr(),
std::move(virtual_time_pauser)));
if (IsSessionStorage() && !already_empty) {
for (const auto& area : areas_) {
if (area.key != source)
area.key->EnqueueStorageEvent(String(), String(), String(), page_url);
}
}
}
String CachedStorageArea::RegisterSource(Source* source) {
String id = String::Number(base::RandUint64());
areas_.insert(source, id);
return id;
}
CachedStorageArea::CachedStorageArea(
scoped_refptr<const SecurityOrigin> origin,
mojo::InterfacePtr<mojom::blink::StorageArea> area,
scoped_refptr<base::SingleThreadTaskRunner> ipc_runner)
: origin_(std::move(origin)),
mojo_area_(area.get()),
mojo_area_ptr_(std::move(area)),
binding_(this),
weak_factory_(this) {
mojom::blink::StorageAreaObserverAssociatedPtrInfo ptr_info;
binding_.Bind(mojo::MakeRequest(&ptr_info), std::move(ipc_runner));
mojo_area_->AddObserver(std::move(ptr_info));
}
CachedStorageArea::CachedStorageArea(
scoped_refptr<const SecurityOrigin> origin,
mojo::AssociatedInterfacePtr<mojom::blink::StorageArea> area,
scoped_refptr<base::SingleThreadTaskRunner> ipc_runner)
: origin_(std::move(origin)),
mojo_area_(area.get()),
mojo_area_associated_ptr_(std::move(area)),
binding_(this),
weak_factory_(this) {
mojom::blink::StorageAreaObserverAssociatedPtrInfo ptr_info;
binding_.Bind(mojo::MakeRequest(&ptr_info), std::move(ipc_runner));
mojo_area_->AddObserver(std::move(ptr_info));
}
CachedStorageArea::~CachedStorageArea() = default;
void CachedStorageArea::KeyAdded(const Vector<uint8_t>& key,
const Vector<uint8_t>& value,
const String& source) {
DCHECK(!IsSessionStorage());
KeyAddedOrChanged(key, value, String(), source);
}
void CachedStorageArea::KeyChanged(const Vector<uint8_t>& key,
const Vector<uint8_t>& new_value,
const Vector<uint8_t>& old_value,
const String& source) {
DCHECK(!IsSessionStorage());
KeyAddedOrChanged(
key, new_value,
Uint8VectorToString(old_value, FormatOption::kLocalStorageDetectFormat),
source);
}
void CachedStorageArea::KeyDeleted(const Vector<uint8_t>& key,
const Vector<uint8_t>& old_value,
const String& source) {
DCHECK(!IsSessionStorage());
KURL page_url;
String storage_area_id;
UnpackSource(source, &page_url, &storage_area_id);
String key_string =
Uint8VectorToString(key, FormatOption::kLocalStorageDetectFormat);
bool from_local_area = false;
for (const auto& area : areas_) {
if (area.value == storage_area_id) {
from_local_area = true;
} else {
area.key->EnqueueStorageEvent(
key_string,
Uint8VectorToString(old_value,
FormatOption::kLocalStorageDetectFormat),
String(), page_url);
}
}
if (map_ && !from_local_area) {
// This was from another process or the storage area is gone. If the former,
// remove it from our cache if we haven't already changed it and are waiting
// for the confirmation callback. In the latter case, we won't do anything
// because ignore_key_mutations_ won't be updated until the callback runs.
if (!ignore_all_mutations_ &&
ignore_key_mutations_.find(key_string) == ignore_key_mutations_.end())
map_->RemoveItem(key_string, nullptr);
}
}
void CachedStorageArea::AllDeleted(const String& source) {
KURL page_url;
String storage_area_id;
UnpackSource(source, &page_url, &storage_area_id);
bool from_local_area = false;
for (const auto& area : areas_) {
if (area.value == storage_area_id) {
from_local_area = true;
} else {
area.key->EnqueueStorageEvent(String(), String(), String(), page_url);
}
}
if (map_ && !from_local_area && !ignore_all_mutations_) {
auto old = std::move(map_);
map_ = std::make_unique<StorageAreaMap>(
mojom::blink::StorageArea::kPerStorageAreaQuota);
// We have to retain local additions which happened after this clear
// operation from another process.
auto iter = ignore_key_mutations_.begin();
while (iter != ignore_key_mutations_.end()) {
String value = old->GetItem(iter->key);
if (!value.IsNull())
map_->SetItemIgnoringQuota(iter->key, value);
++iter;
}
}
}
void CachedStorageArea::ShouldSendOldValueOnMutations(bool value) {
DCHECK(!IsSessionStorage());
should_send_old_value_on_mutations_ = value;
}
void CachedStorageArea::KeyAddedOrChanged(const Vector<uint8_t>& key,
const Vector<uint8_t>& new_value,
const String& old_value,
const String& source) {
DCHECK(!IsSessionStorage());
KURL page_url;
String storage_area_id;
UnpackSource(source, &page_url, &storage_area_id);
String key_string =
Uint8VectorToString(key, FormatOption::kLocalStorageDetectFormat);
String new_value_string =
Uint8VectorToString(new_value, FormatOption::kLocalStorageDetectFormat);
bool from_local_area = false;
for (const auto& area : areas_) {
if (area.value == storage_area_id) {
from_local_area = true;
} else {
area.key->EnqueueStorageEvent(key_string, old_value, new_value_string,
page_url);
}
}
if (map_ && !from_local_area) {
// This was from another process or the storage area is gone. If the former,
// apply it to our cache if we haven't already changed it and are waiting
// for the confirmation callback. In the latter case, we won't do anything
// because ignore_key_mutations_ won't be updated until the callback runs.
if (!ignore_all_mutations_ &&
ignore_key_mutations_.find(key_string) == ignore_key_mutations_.end()) {
// We turn off quota checking here to accommodate the over budget
// allowance that's provided in the browser process.
map_->SetItemIgnoringQuota(key_string, new_value_string);
}
}
}
void CachedStorageArea::OnSetItemComplete(const String& key,
WebScopedVirtualTimePauser,
bool success) {
if (!success) {
Reset();
return;
}
auto it = ignore_key_mutations_.find(key);
DCHECK(it != ignore_key_mutations_.end());
if (--it->value == 0)
ignore_key_mutations_.erase(it);
}
void CachedStorageArea::OnRemoveItemComplete(const String& key,
WebScopedVirtualTimePauser,
bool success) {
DCHECK(success);
auto it = ignore_key_mutations_.find(key);
DCHECK(it != ignore_key_mutations_.end());
if (--it->value == 0)
ignore_key_mutations_.erase(it);
}
void CachedStorageArea::OnClearComplete(WebScopedVirtualTimePauser,
bool success) {
DCHECK(success);
DCHECK(ignore_all_mutations_);
ignore_all_mutations_ = false;
}
void CachedStorageArea::OnGetAllComplete(bool success) {
// Since the GetAll method is synchronous, we need this asynchronously
// delivered notification to avoid applying changes to the returned array
// that we already have.
DCHECK(success);
DCHECK(ignore_all_mutations_);
ignore_all_mutations_ = false;
}
void CachedStorageArea::EnsureLoaded() {
if (map_)
return;
base::TimeTicks before = base::TimeTicks::Now();
ignore_all_mutations_ = true;
bool success = false;
Vector<mojom::blink::KeyValuePtr> data;
mojo_area_->GetAll(
GetAllCallback::CreateAndBind(WTF::Bind(
&CachedStorageArea::OnGetAllComplete, weak_factory_.GetWeakPtr())),
&success, &data);
// Determine data formats.
const FormatOption key_format = GetKeyFormat();
const FormatOption value_format = GetValueFormat();
map_ = std::make_unique<StorageAreaMap>(
mojom::blink::StorageArea::kPerStorageAreaQuota);
for (const auto& item : data) {
map_->SetItemIgnoringQuota(Uint8VectorToString(item->key, key_format),
Uint8VectorToString(item->value, value_format));
}
base::TimeDelta time_to_prime = base::TimeTicks::Now() - before;
UMA_HISTOGRAM_TIMES("LocalStorage.MojoTimeToPrime", time_to_prime);
size_t local_storage_size_kb = map_->quota_used() / 1024;
// Track localStorage size, from 0-6MB. Note that the maximum size should be
// 10MB, but we add some slop since we want to make sure the max size is
// always above what we see in practice, since histograms can't change.
UMA_HISTOGRAM_CUSTOM_COUNTS("LocalStorage.MojoSizeInKB",
local_storage_size_kb, 1, 6 * 1024, 50);
if (local_storage_size_kb < 100) {
UMA_HISTOGRAM_TIMES("LocalStorage.MojoTimeToPrimeForUnder100KB",
time_to_prime);
} else if (local_storage_size_kb < 1000) {
UMA_HISTOGRAM_TIMES("LocalStorage.MojoTimeToPrimeFor100KBTo1MB",
time_to_prime);
} else {
UMA_HISTOGRAM_TIMES("LocalStorage.MojoTimeToPrimeFor1MBTo5MB",
time_to_prime);
}
}
void CachedStorageArea::Reset() {
map_ = nullptr;
ignore_key_mutations_.clear();
ignore_all_mutations_ = false;
weak_factory_.InvalidateWeakPtrs();
}
CachedStorageArea::FormatOption CachedStorageArea::GetKeyFormat() const {
return IsSessionStorage() ? FormatOption::kSessionStorageForceUTF8
: FormatOption::kLocalStorageDetectFormat;
}
CachedStorageArea::FormatOption CachedStorageArea::GetValueFormat() const {
return IsSessionStorage() ? FormatOption::kSessionStorageForceUTF16
: FormatOption::kLocalStorageDetectFormat;
}
bool CachedStorageArea::IsSessionStorage() const {
return mojo_area_associated_ptr_.is_bound();
}
// static
String CachedStorageArea::Uint8VectorToString(const Vector<uint8_t>& input,
FormatOption format_option) {
if (input.IsEmpty())
return g_empty_string;
const size_t input_size = input.size();
String result;
bool corrupt = false;
switch (format_option) {
case FormatOption::kSessionStorageForceUTF16: {
if (input_size % sizeof(UChar) != 0) {
corrupt = true;
break;
}
StringBuffer<UChar> buffer(input_size / sizeof(UChar));
std::memcpy(buffer.Characters(), input.data(), input_size);
result = String::Adopt(buffer);
break;
}
case FormatOption::kSessionStorageForceUTF8: {
// TODO(mek): When this lived in content it used to do a "lenient"
// conversion, while this is a strict conversion. Figure out if that
// difference actually matters in practice.
result = String::FromUTF8(input.data(), input_size);
if (result.IsNull()) {
corrupt = true;
break;
}
break;
}
case FormatOption::kLocalStorageDetectFormat: {
StorageFormat format = static_cast<StorageFormat>(input[0]);
const size_t payload_size = input_size - 1;
switch (format) {
case StorageFormat::UTF16: {
if (payload_size % sizeof(UChar) != 0) {
corrupt = true;
break;
}
StringBuffer<UChar> buffer(payload_size / sizeof(UChar));
std::memcpy(buffer.Characters(), input.data() + 1, payload_size);
result = String::Adopt(buffer);
break;
}
case StorageFormat::Latin1:
result = String(reinterpret_cast<const char*>(input.data() + 1),
payload_size);
break;
default:
corrupt = true;
}
break;
}
}
if (corrupt) {
// TODO(mek): Better error recovery when corrupt (or otherwise invalid) data
// is detected.
LOCAL_HISTOGRAM_BOOLEAN("LocalStorageCachedArea.CorruptData", true);
LOG(ERROR) << "Corrupt data in domstorage";
return g_empty_string;
}
return result;
}
// static
Vector<uint8_t> CachedStorageArea::StringToUint8Vector(
const String& input,
FormatOption format_option) {
switch (format_option) {
case FormatOption::kSessionStorageForceUTF16: {
Vector<uint8_t> result(input.length() * sizeof(UChar));
input.CopyTo(reinterpret_cast<UChar*>(result.data()), 0, input.length());
return result;
}
case FormatOption::kSessionStorageForceUTF8: {
unsigned length = input.length();
if (input.Is8Bit() && input.ContainsOnlyASCII()) {
Vector<uint8_t> result(length);
std::memcpy(result.data(), input.Characters8(), length);
return result;
}
// Handle 8 bit case where it's not only ascii.
if (input.Is8Bit()) {
// This code is copied from WTF::String::Utf8(), except the vector
// doesn't have a stack-allocated capacity.
// We do this because there isn't a way to transform the CString we get
// from WTF::String::Utf8() to a Vector without an extra copy.
if (length > std::numeric_limits<unsigned>::max() / 3)
return Vector<uint8_t>();
Vector<uint8_t> buffer_vector(length * 3);
uint8_t* buffer = buffer_vector.data();
const LChar* characters = input.Characters8();
WTF::Unicode::ConversionResult result =
WTF::Unicode::ConvertLatin1ToUTF8(
&characters, characters + length,
reinterpret_cast<char**>(&buffer),
reinterpret_cast<char*>(buffer + buffer_vector.size()));
// (length * 3) should be sufficient for any conversion
DCHECK_NE(result, WTF::Unicode::kTargetExhausted);
buffer_vector.Shrink(buffer - buffer_vector.data());
return buffer_vector;
}
// TODO(dmurph): Figure out how to avoid a copy here.
// TODO(dmurph): Handle invalid UTF16 better. https://crbug.com/873280.
CString utf8 = input.Utf8(
WTF::kStrictUTF8ConversionReplacingUnpairedSurrogatesWithFFFD);
Vector<uint8_t> result(utf8.length());
std::memcpy(result.data(), utf8.data(), utf8.length());
return result;
}
case FormatOption::kLocalStorageDetectFormat: {
if (input.ContainsOnlyLatin1()) {
Vector<uint8_t> result(input.length() + 1);
result[0] = static_cast<uint8_t>(StorageFormat::Latin1);
if (input.Is8Bit()) {
std::memcpy(result.data() + 1, input.Characters8(), input.length());
} else {
for (unsigned i = 0; i < input.length(); ++i) {
result[i + 1] = input[i];
}
}
return result;
}
DCHECK(!input.Is8Bit());
Vector<uint8_t> result(input.length() * sizeof(UChar) + 1);
result[0] = static_cast<uint8_t>(StorageFormat::UTF16);
std::memcpy(result.data() + 1, input.Characters16(),
input.length() * sizeof(UChar));
return result;
}
}
NOTREACHED();
}
} // namespace blink
// 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 THIRD_PARTY_BLINK_RENDERER_MODULES_STORAGE_CACHED_STORAGE_AREA_H_
#define THIRD_PARTY_BLINK_RENDERER_MODULES_STORAGE_CACHED_STORAGE_AREA_H_
#include "mojo/public/cpp/bindings/associated_binding.h"
#include "mojo/public/cpp/bindings/associated_interface_ptr.h"
#include "mojo/public/cpp/bindings/interface_ptr.h"
#include "third_party/blink/public/mojom/dom_storage/storage_area.mojom-blink.h"
#include "third_party/blink/public/platform/web_scoped_virtual_time_pauser.h"
#include "third_party/blink/renderer/modules/modules_export.h"
#include "third_party/blink/renderer/modules/storage/storage_area_map.h"
#include "third_party/blink/renderer/platform/heap/heap_allocator.h"
#include "third_party/blink/renderer/platform/heap/persistent.h"
#include "third_party/blink/renderer/platform/weborigin/security_origin.h"
#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
namespace blink {
// An in-process implementation of LocalStorage using a LevelDB Mojo service.
// Maintains a complete cache of the origin's Map of key/value pairs for fast
// access. The cache is primed on first access and changes are written to the
// backend through the level db interface pointer. Mutations originating in
// other processes are applied to the cache via mojom::LevelDBObserver
// callbacks.
// There is one CachedStorageArea for potentially many LocalStorageArea
// objects.
class MODULES_EXPORT CachedStorageArea
: public mojom::blink::StorageAreaObserver,
public RefCounted<CachedStorageArea> {
public:
// Instances of this class are used to identify the "source" of any changes
// made to this storage area, as well as to dispatch any incoming change
// events. Change events are not sent back to the source that caused the
// change. The source passed to the various methods that modify storage
// should have been registered first by calling RegisterSource.
class Source : public GarbageCollectedMixin {
public:
virtual ~Source() {}
virtual KURL GetPageUrl() const = 0;
virtual void EnqueueStorageEvent(const String& key,
const String& old_value,
const String& new_value,
const String& url) = 0;
virtual blink::WebScopedVirtualTimePauser CreateWebScopedVirtualTimePauser(
const char* name,
WebScopedVirtualTimePauser::VirtualTaskDuration duration) = 0;
};
static scoped_refptr<CachedStorageArea> CreateForLocalStorage(
scoped_refptr<const SecurityOrigin> origin,
mojo::InterfacePtr<mojom::blink::StorageArea> area,
scoped_refptr<base::SingleThreadTaskRunner> ipc_runner);
static scoped_refptr<CachedStorageArea> CreateForSessionStorage(
scoped_refptr<const SecurityOrigin> origin,
mojo::AssociatedInterfacePtr<mojom::blink::StorageArea> area,
scoped_refptr<base::SingleThreadTaskRunner> ipc_runner);
// These correspond to blink::Storage.
unsigned GetLength();
String GetKey(unsigned index);
String GetItem(const String& key);
bool SetItem(const String& key, const String& value, Source* source);
void RemoveItem(const String& key, Source* source);
void Clear(Source* source);
// Allow this object to keep track of the Source instances corresponding to
// it, which is needed for mutation event notifications.
// Returns the (unique) id allocated for this source for testing purposes.
String RegisterSource(Source* source);
size_t memory_used() const { return map_ ? map_->quota_used() : 0; }
// Only public to allow tests to parametrize on this type.
enum class FormatOption {
kLocalStorageDetectFormat,
kSessionStorageForceUTF16,
kSessionStorageForceUTF8
};
private:
CachedStorageArea(scoped_refptr<const SecurityOrigin> origin,
mojo::InterfacePtr<mojom::blink::StorageArea> area,
scoped_refptr<base::SingleThreadTaskRunner> ipc_runner);
CachedStorageArea(
scoped_refptr<const SecurityOrigin> origin,
mojo::AssociatedInterfacePtr<mojom::blink::StorageArea> area,
scoped_refptr<base::SingleThreadTaskRunner> ipc_runner);
friend class RefCounted<CachedStorageArea>;
~CachedStorageArea() override;
friend class CachedStorageAreaTest;
friend class CachedStorageAreaStringFormatTest;
// StorageAreaObserver:
void KeyAdded(const Vector<uint8_t>& key,
const Vector<uint8_t>& value,
const String& source) override;
void KeyChanged(const Vector<uint8_t>& key,
const Vector<uint8_t>& new_value,
const Vector<uint8_t>& old_value,
const String& source) override;
void KeyDeleted(const Vector<uint8_t>& key,
const Vector<uint8_t>& old_value,
const String& source) override;
void AllDeleted(const String& source) override;
void ShouldSendOldValueOnMutations(bool value) override;
// Common helper for KeyAdded() and KeyChanged()
void KeyAddedOrChanged(const Vector<uint8_t>& key,
const Vector<uint8_t>& new_value,
const String& old_value,
const String& source);
void OnSetItemComplete(const String& key,
WebScopedVirtualTimePauser,
bool success);
void OnRemoveItemComplete(const String& key,
WebScopedVirtualTimePauser,
bool success);
void OnClearComplete(WebScopedVirtualTimePauser, bool success);
void OnGetAllComplete(bool success);
// Synchronously fetches the areas data if it hasn't been fetched already.
void EnsureLoaded();
// Resets the object back to its newly constructed state.
void Reset();
bool IsSessionStorage() const;
FormatOption GetKeyFormat() const;
FormatOption GetValueFormat() const;
static String Uint8VectorToString(const Vector<uint8_t>& input,
FormatOption format_option);
static Vector<uint8_t> StringToUint8Vector(const String& input,
FormatOption format_option);
scoped_refptr<const SecurityOrigin> origin_;
std::unique_ptr<StorageAreaMap> map_;
HashMap<String, int> ignore_key_mutations_;
bool ignore_all_mutations_ = false;
// See ShouldSendOldValueOnMutations().
bool should_send_old_value_on_mutations_ = true;
// Depending on if this is a session storage or local storage area only one of
// |mojo_area_ptr_| and |mojo_area_associated_ptr_| will be non-null. Either
// way |mojo_area_| will be equal to the non-null one.
mojom::blink::StorageArea* mojo_area_;
mojo::InterfacePtr<mojom::blink::StorageArea> mojo_area_ptr_;
mojo::AssociatedInterfacePtr<mojom::blink::StorageArea>
mojo_area_associated_ptr_;
mojo::AssociatedBinding<mojom::blink::StorageAreaObserver> binding_;
PersistentHeapHashMap<WeakMember<Source>, String> areas_;
base::WeakPtrFactory<CachedStorageArea> weak_factory_;
DISALLOW_COPY_AND_ASSIGN(CachedStorageArea);
};
} // namespace blink
#endif // THIRD_PARTY_BLINK_RENDERER_MODULES_STORAGE_CACHED_STORAGE_AREA_H_
// Copyright 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 "third_party/blink/renderer/modules/storage/cached_storage_area.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/platform/scheduler/test/fake_renderer_scheduler.h"
#include "third_party/blink/renderer/modules/storage/testing/fake_area_source.h"
#include "third_party/blink/renderer/modules/storage/testing/mock_storage_area.h"
#include "third_party/blink/renderer/platform/weborigin/kurl.h"
#include "third_party/blink/renderer/platform/weborigin/security_origin.h"
namespace blink {
using FormatOption = CachedStorageArea::FormatOption;
class CachedStorageAreaTest : public testing::Test {
public:
const scoped_refptr<SecurityOrigin> kOrigin =
SecurityOrigin::CreateFromString("http://dom_storage/");
const String kKey = "key";
const String kValue = "value";
const String kValue2 = "another value";
const KURL kPageUrl = KURL("http://dom_storage/page");
const KURL kPageUrl2 = KURL("http://dom_storage/other_page");
const String kRemoteSourceId = "1234";
const String kRemoteSource = kPageUrl2.GetString() + "\n" + kRemoteSourceId;
void SetUp() override {
if (IsSessionStorage()) {
cached_area_ = CachedStorageArea::CreateForSessionStorage(
kOrigin, mock_storage_area_.GetAssociatedInterfacePtr(),
renderer_scheduler_->IPCTaskRunner());
} else {
cached_area_ = CachedStorageArea::CreateForLocalStorage(
kOrigin, mock_storage_area_.GetInterfacePtr(),
renderer_scheduler_->IPCTaskRunner());
}
source_area_ = new FakeAreaSource(kPageUrl);
source_area_id_ = cached_area_->RegisterSource(source_area_);
source_ = kPageUrl.GetString() + "\n" + source_area_id_;
source_area2_ = new FakeAreaSource(kPageUrl2);
cached_area_->RegisterSource(source_area2_);
}
virtual bool IsSessionStorage() { return false; }
bool IsCacheLoaded() { return cached_area_->map_.get(); }
bool IsIgnoringAllMutations() { return cached_area_->ignore_all_mutations_; }
void ResetCache() { cached_area_->Reset(); }
bool IsIgnoringKeyMutations(const String& key) {
return cached_area_->ignore_key_mutations_.find(key) !=
cached_area_->ignore_key_mutations_.end();
}
static Vector<uint8_t> StringToUint8Vector(const String& input,
FormatOption format) {
return CachedStorageArea::StringToUint8Vector(input, format);
}
static String Uint8VectorToString(const Vector<uint8_t>& input,
FormatOption format) {
return CachedStorageArea::Uint8VectorToString(input, format);
}
Vector<uint8_t> KeyToUint8Vector(const String& key) {
return StringToUint8Vector(
key, IsSessionStorage() ? FormatOption::kSessionStorageForceUTF8
: FormatOption::kLocalStorageDetectFormat);
}
Vector<uint8_t> ValueToUint8Vector(const String& value) {
return StringToUint8Vector(
value, IsSessionStorage() ? FormatOption::kSessionStorageForceUTF16
: FormatOption::kLocalStorageDetectFormat);
}
String KeyFromUint8Vector(const Vector<uint8_t>& key) {
return Uint8VectorToString(
key, IsSessionStorage() ? FormatOption::kSessionStorageForceUTF8
: FormatOption::kLocalStorageDetectFormat);
}
String ValueFromUint8Vector(const Vector<uint8_t>& value) {
return Uint8VectorToString(
value, IsSessionStorage() ? FormatOption::kSessionStorageForceUTF16
: FormatOption::kLocalStorageDetectFormat);
}
protected:
std::unique_ptr<scheduler::WebThreadScheduler> renderer_scheduler_ =
std::make_unique<scheduler::FakeRendererScheduler>();
MockStorageArea mock_storage_area_;
Persistent<FakeAreaSource> source_area_;
Persistent<FakeAreaSource> source_area2_;
scoped_refptr<CachedStorageArea> cached_area_;
String source_area_id_;
String source_;
};
class CachedStorageAreaTestWithParam
: public CachedStorageAreaTest,
public testing::WithParamInterface<bool> {
public:
bool IsSessionStorage() override { return GetParam(); }
};
INSTANTIATE_TEST_CASE_P(CachedStorageAreaTest,
CachedStorageAreaTestWithParam,
::testing::Bool());
TEST_P(CachedStorageAreaTestWithParam, Basics) {
EXPECT_FALSE(IsCacheLoaded());
EXPECT_EQ(0u, cached_area_->GetLength());
EXPECT_TRUE(cached_area_->SetItem(kKey, kValue, source_area_));
EXPECT_EQ(1u, cached_area_->GetLength());
EXPECT_EQ(kKey, cached_area_->GetKey(0));
EXPECT_EQ(kValue, cached_area_->GetItem(kKey));
cached_area_->RemoveItem(kKey, source_area_);
EXPECT_EQ(0u, cached_area_->GetLength());
mock_storage_area_.Flush();
EXPECT_EQ(1u, mock_storage_area_.observer_count());
}
TEST_P(CachedStorageAreaTestWithParam, GetLength) {
// GetLength, we expect to see one call to load in the db.
EXPECT_FALSE(IsCacheLoaded());
EXPECT_EQ(0u, cached_area_->GetLength());
EXPECT_TRUE(IsCacheLoaded());
EXPECT_TRUE(mock_storage_area_.observed_get_all());
EXPECT_EQ(1u, mock_storage_area_.pending_callbacks_count());
EXPECT_TRUE(IsIgnoringAllMutations());
mock_storage_area_.CompleteAllPendingCallbacks();
mock_storage_area_.Flush();
EXPECT_FALSE(IsIgnoringAllMutations());
}
TEST_P(CachedStorageAreaTestWithParam, GetKey) {
// GetKey, expect the one call to load.
EXPECT_FALSE(IsCacheLoaded());
EXPECT_TRUE(cached_area_->GetKey(2).IsNull());
EXPECT_TRUE(IsCacheLoaded());
EXPECT_TRUE(mock_storage_area_.observed_get_all());
EXPECT_EQ(1u, mock_storage_area_.pending_callbacks_count());
}
TEST_P(CachedStorageAreaTestWithParam, GetItem) {
// GetItem, ditto.
EXPECT_FALSE(IsCacheLoaded());
EXPECT_TRUE(cached_area_->GetItem(kKey).IsNull());
EXPECT_TRUE(IsCacheLoaded());
EXPECT_TRUE(mock_storage_area_.observed_get_all());
EXPECT_EQ(1u, mock_storage_area_.pending_callbacks_count());
}
TEST_P(CachedStorageAreaTestWithParam, SetItem) {
// SetItem, we expect a call to load followed by a call to put in the db.
EXPECT_FALSE(IsCacheLoaded());
EXPECT_TRUE(cached_area_->SetItem(kKey, kValue, source_area_));
mock_storage_area_.Flush();
EXPECT_TRUE(IsCacheLoaded());
EXPECT_TRUE(mock_storage_area_.observed_get_all());
EXPECT_TRUE(mock_storage_area_.observed_put());
EXPECT_EQ(source_, mock_storage_area_.observed_source());
EXPECT_EQ(KeyToUint8Vector(kKey), mock_storage_area_.observed_key());
EXPECT_EQ(ValueToUint8Vector(kValue), mock_storage_area_.observed_value());
EXPECT_EQ(2u, mock_storage_area_.pending_callbacks_count());
EXPECT_TRUE(source_area_->events.IsEmpty());
if (IsSessionStorage()) {
ASSERT_EQ(1u, source_area2_->events.size());
EXPECT_EQ(kKey, source_area2_->events[0].key);
EXPECT_TRUE(source_area2_->events[0].old_value.IsNull());
EXPECT_EQ(kValue, source_area2_->events[0].new_value);
EXPECT_EQ(kPageUrl, source_area2_->events[0].url);
} else {
EXPECT_TRUE(source_area2_->events.IsEmpty());
}
}
TEST_P(CachedStorageAreaTestWithParam, Clear_AlreadyEmpty) {
// Clear, we expect just the one call to clear in the db since
// there's no need to load the data prior to deleting it.
// Except if we're testing session storage, in which case we also expect a
// load call first, since it needs that for event dispatching.
EXPECT_FALSE(IsCacheLoaded());
cached_area_->Clear(source_area_);
mock_storage_area_.Flush();
EXPECT_TRUE(IsCacheLoaded());
EXPECT_TRUE(mock_storage_area_.observed_delete_all());
EXPECT_EQ(source_, mock_storage_area_.observed_source());
if (IsSessionStorage()) {
EXPECT_TRUE(mock_storage_area_.observed_get_all());
EXPECT_EQ(2u, mock_storage_area_.pending_callbacks_count());
} else {
EXPECT_FALSE(mock_storage_area_.observed_get_all());
EXPECT_EQ(1u, mock_storage_area_.pending_callbacks_count());
}
// Neither should have events since area was already empty.
EXPECT_TRUE(source_area_->events.IsEmpty());
EXPECT_TRUE(source_area2_->events.IsEmpty());
}
TEST_P(CachedStorageAreaTestWithParam, Clear_WithData) {
mock_storage_area_.mutable_get_all_return_values().push_back(
mojom::blink::KeyValue::New(KeyToUint8Vector(kKey),
ValueToUint8Vector(kValue)));
EXPECT_FALSE(IsCacheLoaded());
cached_area_->Clear(source_area_);
mock_storage_area_.Flush();
EXPECT_TRUE(IsCacheLoaded());
EXPECT_TRUE(mock_storage_area_.observed_delete_all());
EXPECT_EQ(source_, mock_storage_area_.observed_source());
if (IsSessionStorage()) {
EXPECT_TRUE(mock_storage_area_.observed_get_all());
EXPECT_EQ(2u, mock_storage_area_.pending_callbacks_count());
} else {
EXPECT_FALSE(mock_storage_area_.observed_get_all());
EXPECT_EQ(1u, mock_storage_area_.pending_callbacks_count());
}
EXPECT_TRUE(source_area_->events.IsEmpty());
if (IsSessionStorage()) {
ASSERT_EQ(1u, source_area2_->events.size());
EXPECT_TRUE(source_area2_->events[0].key.IsNull());
EXPECT_TRUE(source_area2_->events[0].old_value.IsNull());
EXPECT_TRUE(source_area2_->events[0].new_value.IsNull());
EXPECT_EQ(kPageUrl, source_area2_->events[0].url);
} else {
EXPECT_TRUE(source_area2_->events.IsEmpty());
}
}
TEST_P(CachedStorageAreaTestWithParam, RemoveItem_NothingToRemove) {
// RemoveItem with nothing to remove, expect just one call to load.
EXPECT_FALSE(IsCacheLoaded());
cached_area_->RemoveItem(kKey, source_area_);
mock_storage_area_.Flush();
EXPECT_TRUE(IsCacheLoaded());
EXPECT_TRUE(mock_storage_area_.observed_get_all());
EXPECT_FALSE(mock_storage_area_.observed_delete());
EXPECT_EQ(1u, mock_storage_area_.pending_callbacks_count());
// Neither should have events since area was already empty.
EXPECT_TRUE(source_area_->events.IsEmpty());
EXPECT_TRUE(source_area2_->events.IsEmpty());
}
TEST_P(CachedStorageAreaTestWithParam, RemoveItem) {
// RemoveItem with something to remove, expect a call to load followed
// by a call to remove.
mock_storage_area_.mutable_get_all_return_values().push_back(
mojom::blink::KeyValue::New(KeyToUint8Vector(kKey),
ValueToUint8Vector(kValue)));
EXPECT_FALSE(IsCacheLoaded());
cached_area_->RemoveItem(kKey, source_area_);
mock_storage_area_.Flush();
EXPECT_TRUE(IsCacheLoaded());
EXPECT_TRUE(mock_storage_area_.observed_get_all());
EXPECT_TRUE(mock_storage_area_.observed_delete());
EXPECT_EQ(source_, mock_storage_area_.observed_source());
EXPECT_EQ(KeyToUint8Vector(kKey), mock_storage_area_.observed_key());
EXPECT_EQ(2u, mock_storage_area_.pending_callbacks_count());
EXPECT_TRUE(source_area_->events.IsEmpty());
if (IsSessionStorage()) {
ASSERT_EQ(1u, source_area2_->events.size());
EXPECT_EQ(kKey, source_area2_->events[0].key);
EXPECT_EQ(kValue, source_area2_->events[0].old_value);
EXPECT_TRUE(source_area2_->events[0].new_value.IsNull());
EXPECT_EQ(kPageUrl, source_area2_->events[0].url);
} else {
EXPECT_TRUE(source_area2_->events.IsEmpty());
}
}
TEST_P(CachedStorageAreaTestWithParam, BrowserDisconnect) {
// GetLength to prime the cache.
mock_storage_area_.mutable_get_all_return_values().push_back(
mojom::blink::KeyValue::New(KeyToUint8Vector(kKey),
ValueToUint8Vector(kValue)));
EXPECT_EQ(1u, cached_area_->GetLength());
EXPECT_TRUE(IsCacheLoaded());
mock_storage_area_.CompleteAllPendingCallbacks();
mock_storage_area_.ResetObservations();
// Now disconnect the pipe from the browser, simulating situations where the
// browser might be forced to destroy the LevelDBWrapperImpl.
mock_storage_area_.CloseAllBindings();
// Getters should still function.
EXPECT_EQ(1u, cached_area_->GetLength());
EXPECT_EQ(kValue, cached_area_->GetItem(kKey));
// And setters should also still function.
cached_area_->RemoveItem(kKey, source_area_);
EXPECT_EQ(0u, cached_area_->GetLength());
EXPECT_TRUE(cached_area_->GetItem(kKey).IsNull());
// TODO(mek): This should work for session storage too, but for some reason
// instead just hangs.
if (!IsSessionStorage()) {
// Even resetting the cache should still allow class to function properly.
ResetCache();
EXPECT_TRUE(cached_area_->GetItem(kKey).IsNull());
EXPECT_TRUE(cached_area_->SetItem(kKey, kValue, source_area_));
EXPECT_EQ(1u, cached_area_->GetLength());
EXPECT_EQ(kValue, cached_area_->GetItem(kKey));
}
}
TEST_F(CachedStorageAreaTest, MutationsAreIgnoredUntilLoadCompletion) {
mojom::blink::StorageAreaObserver* observer = cached_area_.get();
EXPECT_TRUE(cached_area_->GetItem(kKey).IsNull());
EXPECT_TRUE(IsCacheLoaded());
EXPECT_TRUE(IsIgnoringAllMutations());
// Before load completion, the mutation should be ignored.
observer->KeyAdded(KeyToUint8Vector(kKey), ValueToUint8Vector(kValue),
kRemoteSource);
EXPECT_TRUE(cached_area_->GetItem(kKey).IsNull());
// Call the load completion callback.
mock_storage_area_.CompleteOnePendingCallback(true);
mock_storage_area_.Flush();
EXPECT_FALSE(IsIgnoringAllMutations());
// Verify that mutations are now applied.
observer->KeyAdded(KeyToUint8Vector(kKey), ValueToUint8Vector(kValue),
kRemoteSource);
EXPECT_EQ(kValue, cached_area_->GetItem(kKey));
}
TEST_F(CachedStorageAreaTest, MutationsAreIgnoredUntilClearCompletion) {
cached_area_->Clear(source_area_);
mock_storage_area_.Flush();
EXPECT_TRUE(IsIgnoringAllMutations());
mock_storage_area_.CompleteOnePendingCallback(true);
mock_storage_area_.Flush();
EXPECT_FALSE(IsIgnoringAllMutations());
// Verify that calling Clear twice works as expected, the first
// completion callback should have been cancelled.
ResetCache();
cached_area_->Clear(source_area_);
mock_storage_area_.Flush();
EXPECT_TRUE(IsIgnoringAllMutations());
cached_area_->Clear(source_area_);
mock_storage_area_.Flush();
EXPECT_TRUE(IsIgnoringAllMutations());
mock_storage_area_.CompleteOnePendingCallback(true);
mock_storage_area_.Flush();
EXPECT_TRUE(IsIgnoringAllMutations());
mock_storage_area_.CompleteOnePendingCallback(true);
mock_storage_area_.Flush();
EXPECT_FALSE(IsIgnoringAllMutations());
}
TEST_F(CachedStorageAreaTest, KeyMutationsAreIgnoredUntilCompletion) {
mojom::blink::StorageAreaObserver* observer = cached_area_.get();
// SetItem
EXPECT_TRUE(cached_area_->SetItem(kKey, kValue, source_area_));
mock_storage_area_.CompleteOnePendingCallback(true); // load completion
mock_storage_area_.Flush();
EXPECT_FALSE(IsIgnoringAllMutations());
EXPECT_TRUE(IsIgnoringKeyMutations(kKey));
observer->KeyDeleted(KeyToUint8Vector(kKey), {0}, kRemoteSource);
mock_storage_area_.Flush();
EXPECT_EQ(kValue, cached_area_->GetItem(kKey));
mock_storage_area_.CompleteOnePendingCallback(true); // set completion
mock_storage_area_.Flush();
EXPECT_FALSE(IsIgnoringKeyMutations(kKey));
// RemoveItem
cached_area_->RemoveItem(kKey, source_area_);
mock_storage_area_.Flush();
EXPECT_TRUE(IsIgnoringKeyMutations(kKey));
mock_storage_area_.CompleteOnePendingCallback(true); // remove completion
mock_storage_area_.Flush();
EXPECT_FALSE(IsIgnoringKeyMutations(kKey));
// Multiple mutations to the same key.
EXPECT_TRUE(cached_area_->SetItem(kKey, kValue, source_area_));
cached_area_->RemoveItem(kKey, source_area_);
mock_storage_area_.Flush();
EXPECT_TRUE(IsIgnoringKeyMutations(kKey));
mock_storage_area_.CompleteOnePendingCallback(true); // set completion
mock_storage_area_.Flush();
EXPECT_TRUE(IsIgnoringKeyMutations(kKey));
mock_storage_area_.CompleteOnePendingCallback(true); // remove completion
mock_storage_area_.Flush();
EXPECT_FALSE(IsIgnoringKeyMutations(kKey));
// A failed set item operation should Reset the cache.
EXPECT_TRUE(cached_area_->SetItem(kKey, kValue, source_area_));
mock_storage_area_.Flush();
EXPECT_TRUE(IsIgnoringKeyMutations(kKey));
mock_storage_area_.CompleteOnePendingCallback(false);
mock_storage_area_.Flush();
EXPECT_FALSE(IsCacheLoaded());
}
TEST_F(CachedStorageAreaTest, ChangeEvents) {
mojom::blink::StorageAreaObserver* observer = cached_area_.get();
observer->KeyAdded(KeyToUint8Vector(kKey), ValueToUint8Vector(kValue),
source_);
observer->KeyChanged(KeyToUint8Vector(kKey), ValueToUint8Vector(kValue2),
ValueToUint8Vector(kValue), source_);
observer->KeyDeleted(KeyToUint8Vector(kKey), ValueToUint8Vector(kValue2),
source_);
observer->KeyAdded(KeyToUint8Vector(kKey), ValueToUint8Vector(kValue),
kRemoteSource);
observer->AllDeleted(kRemoteSource);
// Source area should have ignored all but the last two events.
ASSERT_EQ(2u, source_area_->events.size());
EXPECT_EQ(kKey, source_area_->events[0].key);
EXPECT_TRUE(source_area_->events[0].old_value.IsNull());
EXPECT_EQ(kValue, source_area_->events[0].new_value);
EXPECT_EQ(kPageUrl2, source_area_->events[0].url);
EXPECT_TRUE(source_area_->events[1].key.IsNull());
EXPECT_TRUE(source_area_->events[1].old_value.IsNull());
EXPECT_TRUE(source_area_->events[1].new_value.IsNull());
EXPECT_EQ(kPageUrl2, source_area_->events[1].url);
// Second area should not have ignored any of the events.
ASSERT_EQ(5u, source_area2_->events.size());
EXPECT_EQ(kKey, source_area2_->events[0].key);
EXPECT_TRUE(source_area2_->events[0].old_value.IsNull());
EXPECT_EQ(kValue, source_area2_->events[0].new_value);
EXPECT_EQ(kPageUrl, source_area2_->events[0].url);
EXPECT_EQ(kKey, source_area2_->events[1].key);
EXPECT_EQ(kValue, source_area2_->events[1].old_value);
EXPECT_EQ(kValue2, source_area2_->events[1].new_value);
EXPECT_EQ(kPageUrl, source_area2_->events[1].url);
EXPECT_EQ(kKey, source_area2_->events[2].key);
EXPECT_EQ(kValue2, source_area2_->events[2].old_value);
EXPECT_TRUE(source_area2_->events[2].new_value.IsNull());
EXPECT_EQ(kPageUrl, source_area2_->events[2].url);
EXPECT_EQ(kKey, source_area2_->events[3].key);
EXPECT_TRUE(source_area2_->events[3].old_value.IsNull());
EXPECT_EQ(kValue, source_area2_->events[3].new_value);
EXPECT_EQ(kPageUrl2, source_area2_->events[3].url);
EXPECT_TRUE(source_area2_->events[4].key.IsNull());
EXPECT_TRUE(source_area2_->events[4].old_value.IsNull());
EXPECT_TRUE(source_area2_->events[4].new_value.IsNull());
EXPECT_EQ(kPageUrl2, source_area2_->events[4].url);
}
namespace {
class StringEncoding : public CachedStorageAreaTest,
public testing::WithParamInterface<FormatOption> {};
INSTANTIATE_TEST_CASE_P(
CachedStorageAreaTest,
StringEncoding,
::testing::Values(FormatOption::kLocalStorageDetectFormat,
FormatOption::kSessionStorageForceUTF16,
FormatOption::kSessionStorageForceUTF8));
TEST_P(StringEncoding, RoundTrip_ASCII) {
String key("simplekey");
EXPECT_EQ(
Uint8VectorToString(StringToUint8Vector(key, GetParam()), GetParam()),
key);
}
TEST_P(StringEncoding, RoundTrip_Latin1) {
String key("Test\xf6\xb5");
EXPECT_TRUE(key.Is8Bit());
EXPECT_EQ(
Uint8VectorToString(StringToUint8Vector(key, GetParam()), GetParam()),
key);
}
TEST_P(StringEncoding, RoundTrip_UTF16) {
String key("key");
key.append(UChar(0xd83d));
key.append(UChar(0xde00));
EXPECT_EQ(
Uint8VectorToString(StringToUint8Vector(key, GetParam()), GetParam()),
key);
}
TEST_P(StringEncoding, RoundTrip_InvalidUTF16) {
String key("foo");
key.append(UChar(0xd83d));
key.append(UChar(0xde00));
key.append(UChar(0xdf01));
key.append("bar");
if (GetParam() != FormatOption::kSessionStorageForceUTF8) {
EXPECT_EQ(
Uint8VectorToString(StringToUint8Vector(key, GetParam()), GetParam()),
key);
} else {
String validKey("foo");
validKey.append(UChar(0xd83d));
validKey.append(UChar(0xde00));
validKey.append(UChar(0xfffd));
validKey.append("bar");
EXPECT_EQ(
Uint8VectorToString(StringToUint8Vector(key, GetParam()), GetParam()),
validKey);
}
}
} // namespace
TEST_F(CachedStorageAreaTest, StringEncoding_LocalStorage) {
String ascii_key("simplekey");
String non_ascii_key("key");
non_ascii_key.append(UChar(0xd83d));
non_ascii_key.append(UChar(0xde00));
EXPECT_EQ(
StringToUint8Vector(ascii_key, FormatOption::kLocalStorageDetectFormat)
.size(),
ascii_key.length() + 1);
EXPECT_EQ(StringToUint8Vector(non_ascii_key,
FormatOption::kLocalStorageDetectFormat)
.size(),
non_ascii_key.length() * 2 + 1);
}
TEST_F(CachedStorageAreaTest, StringEncoding_UTF8) {
String ascii_key("simplekey");
String non_ascii_key("key");
non_ascii_key.append(UChar(0xd83d));
non_ascii_key.append(UChar(0xde00));
EXPECT_EQ(
StringToUint8Vector(ascii_key, FormatOption::kSessionStorageForceUTF8)
.size(),
ascii_key.length());
EXPECT_EQ(
StringToUint8Vector(non_ascii_key, FormatOption::kSessionStorageForceUTF8)
.size(),
7u);
}
TEST_F(CachedStorageAreaTest, StringEncoding_UTF16) {
String ascii_key("simplekey");
String non_ascii_key("key");
non_ascii_key.append(UChar(0xd83d));
non_ascii_key.append(UChar(0xde00));
EXPECT_EQ(
StringToUint8Vector(ascii_key, FormatOption::kSessionStorageForceUTF16)
.size(),
ascii_key.length() * 2);
EXPECT_EQ(StringToUint8Vector(non_ascii_key,
FormatOption::kSessionStorageForceUTF16)
.size(),
non_ascii_key.length() * 2);
}
} // namespace blink
// 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 THIRD_PARTY_BLINK_RENDERER_MODULES_STORAGE_TESTING_FAKE_AREA_SOURCE_H_
#define THIRD_PARTY_BLINK_RENDERER_MODULES_STORAGE_TESTING_FAKE_AREA_SOURCE_H_
#include "third_party/blink/public/platform/web_scoped_virtual_time_pauser.h"
#include "third_party/blink/renderer/modules/storage/cached_storage_area.h"
#include "third_party/blink/renderer/platform/weborigin/kurl.h"
namespace blink {
class FakeAreaSource : public GarbageCollectedFinalized<FakeAreaSource>,
public CachedStorageArea::Source {
USING_GARBAGE_COLLECTED_MIXIN(FakeAreaSource);
public:
explicit FakeAreaSource(const KURL& page_url) : page_url_(page_url) {}
KURL GetPageUrl() const override { return page_url_; }
void EnqueueStorageEvent(const String& key,
const String& old_value,
const String& new_value,
const String& url) override {
events.push_back(Event{key, old_value, new_value, url});
}
blink::WebScopedVirtualTimePauser CreateWebScopedVirtualTimePauser(
const char* name,
WebScopedVirtualTimePauser::VirtualTaskDuration duration) override {
return blink::WebScopedVirtualTimePauser();
}
struct Event {
String key, old_value, new_value, url;
};
Vector<Event> events;
private:
KURL page_url_;
};
} // namespace blink
#endif // THIRD_PARTY_BLINK_RENDERER_MODULES_STORAGE_TESTING_FAKE_AREA_SOURCE_H_
// Copyright 2017 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 "third_party/blink/renderer/modules/storage/testing/mock_storage_area.h"
#include "third_party/blink/renderer/platform/wtf/functional.h"
namespace blink {
MockStorageArea::MockStorageArea() = default;
MockStorageArea::~MockStorageArea() = default;
mojom::blink::StorageAreaPtr MockStorageArea::GetInterfacePtr() {
mojom::blink::StorageAreaPtr result;
bindings_.AddBinding(this, MakeRequest(&result));
return result;
}
mojom::blink::StorageAreaAssociatedPtr
MockStorageArea::GetAssociatedInterfacePtr() {
mojom::blink::StorageAreaAssociatedPtr result;
associated_bindings_.AddBinding(
this, MakeRequestAssociatedWithDedicatedPipe(&result));
return result;
}
void MockStorageArea::AddObserver(
mojom::blink::StorageAreaObserverAssociatedPtrInfo observer) {
++observer_count_;
}
void MockStorageArea::Put(
const Vector<uint8_t>& key,
const Vector<uint8_t>& value,
const base::Optional<Vector<uint8_t>>& client_old_value,
const String& source,
PutCallback callback) {
observed_put_ = true;
observed_key_ = key;
observed_value_ = value;
observed_source_ = source;
pending_callbacks_.push_back(std::move(callback));
}
void MockStorageArea::Delete(
const Vector<uint8_t>& key,
const base::Optional<Vector<uint8_t>>& client_old_value,
const String& source,
DeleteCallback callback) {
observed_delete_ = true;
observed_key_ = key;
observed_source_ = source;
pending_callbacks_.push_back(std::move(callback));
}
void MockStorageArea::DeleteAll(const String& source,
DeleteAllCallback callback) {
observed_delete_all_ = true;
observed_source_ = source;
pending_callbacks_.push_back(std::move(callback));
}
void MockStorageArea::Get(const Vector<uint8_t>& key, GetCallback callback) {
NOTREACHED();
}
void MockStorageArea::GetAll(
mojom::blink::StorageAreaGetAllCallbackAssociatedPtrInfo complete_callback,
GetAllCallback callback) {
mojom::blink::StorageAreaGetAllCallbackAssociatedPtr complete_ptr;
complete_ptr.Bind(std::move(complete_callback));
pending_callbacks_.push_back(
WTF::Bind(&mojom::blink::StorageAreaGetAllCallback::Complete,
std::move(complete_ptr)));
observed_get_all_ = true;
std::move(callback).Run(true, std::move(get_all_return_values_));
}
} // namespace blink
// Copyright 2017 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 THIRD_PARTY_BLINK_RENDERER_MODULES_STORAGE_TESTING_MOCK_STORAGE_AREA_H_
#define THIRD_PARTY_BLINK_RENDERER_MODULES_STORAGE_TESTING_MOCK_STORAGE_AREA_H_
#include "mojo/public/cpp/bindings/associated_binding_set.h"
#include "mojo/public/cpp/bindings/binding_set.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/mojom/dom_storage/storage_area.mojom-blink.h"
#include "third_party/blink/renderer/platform/wtf/deque.h"
namespace blink {
// Mock StorageArea that records all read and write events.
class MockStorageArea : public mojom::blink::StorageArea {
public:
using ResultCallback = base::OnceCallback<void(bool)>;
MockStorageArea();
~MockStorageArea() override;
mojom::blink::StorageAreaPtr GetInterfacePtr();
mojom::blink::StorageAreaAssociatedPtr GetAssociatedInterfacePtr();
// StorageArea implementation:
void AddObserver(
mojom::blink::StorageAreaObserverAssociatedPtrInfo observer) override;
void Put(const Vector<uint8_t>& key,
const Vector<uint8_t>& value,
const base::Optional<Vector<uint8_t>>& client_old_value,
const String& source,
PutCallback callback) override;
void Delete(const Vector<uint8_t>& key,
const base::Optional<Vector<uint8_t>>& client_old_value,
const String& source,
DeleteCallback callback) override;
void DeleteAll(const String& source, DeleteAllCallback callback) override;
void Get(const Vector<uint8_t>& key, GetCallback callback) override;
void GetAll(mojom::blink::StorageAreaGetAllCallbackAssociatedPtrInfo
complete_callback,
GetAllCallback callback) override;
// Methods and members for use by test fixtures.
bool HasBindings() {
return !bindings_.empty() || !associated_bindings_.empty();
}
void ResetObservations() {
observed_get_all_ = false;
observed_put_ = false;
observed_delete_ = false;
observed_delete_all_ = false;
observed_key_.clear();
observed_value_.clear();
observed_source_ = String();
}
void CompleteAllPendingCallbacks() {
while (!pending_callbacks_.empty())
CompleteOnePendingCallback(true);
}
void CompleteOnePendingCallback(bool success) {
ASSERT_TRUE(!pending_callbacks_.empty());
std::move(pending_callbacks_.front()).Run(success);
pending_callbacks_.pop_front();
}
void Flush() {
bindings_.FlushForTesting();
associated_bindings_.FlushForTesting();
}
void CloseAllBindings() {
bindings_.CloseAllBindings();
associated_bindings_.CloseAllBindings();
}
size_t pending_callbacks_count() const { return pending_callbacks_.size(); }
bool observed_get_all() const { return observed_get_all_; }
bool observed_put() const { return observed_put_; }
bool observed_delete() const { return observed_delete_; }
bool observed_delete_all() const { return observed_delete_all_; }
const Vector<uint8_t>& observed_key() const { return observed_key_; }
const Vector<uint8_t>& observed_value() const { return observed_value_; }
const String& observed_source() const { return observed_source_; }
size_t observer_count() const { return observer_count_; }
Vector<mojom::blink::KeyValuePtr>& mutable_get_all_return_values() {
return get_all_return_values_;
}
private:
Deque<ResultCallback> pending_callbacks_;
bool observed_get_all_ = false;
bool observed_put_ = false;
bool observed_delete_ = false;
bool observed_delete_all_ = false;
Vector<uint8_t> observed_key_;
Vector<uint8_t> observed_value_;
String observed_source_;
size_t observer_count_ = 0;
Vector<mojom::blink::KeyValuePtr> get_all_return_values_;
mojo::BindingSet<mojom::blink::StorageArea> bindings_;
mojo::AssociatedBindingSet<mojom::blink::StorageArea> associated_bindings_;
};
} // namespace blink
#endif // THIRD_PARTY_BLINK_RENDERER_MODULES_STORAGE_TESTING_MOCK_STORAGE_AREA_H_
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment