blob: ab074375a10ddf7e658a35e0a80fa527636c37cc [file] [log] [blame]
// 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/numerics/safe_conversions.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,
InspectorEventListener* listener) {
return base::AdoptRef(new CachedStorageArea(
std::move(origin), std::move(area), std::move(ipc_runner), listener));
}
// static
scoped_refptr<CachedStorageArea> CachedStorageArea::CreateForSessionStorage(
scoped_refptr<const SecurityOrigin> origin,
mojo::AssociatedInterfacePtr<mojom::blink::StorageArea> area,
scoped_refptr<base::SingleThreadTaskRunner> ipc_runner,
InspectorEventListener* listener) {
return base::AdoptRef(new CachedStorageArea(
std::move(origin), std::move(area), std::move(ipc_runner), listener));
}
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)
EnqueueStorageEvent(key, old_value, value, page_url, source_id);
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())
EnqueueStorageEvent(key, old_value, String(), page_url, source_id);
}
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)
EnqueueStorageEvent(String(), String(), String(), page_url, source_id);
}
String CachedStorageArea::RegisterSource(Source* source) {
String id = String::Number(base::RandUint64());
areas_->insert(source, id);
return id;
}
// LocalStorage constructor.
CachedStorageArea::CachedStorageArea(
scoped_refptr<const SecurityOrigin> origin,
mojo::InterfacePtr<mojom::blink::StorageArea> area,
scoped_refptr<base::SingleThreadTaskRunner> ipc_runner,
InspectorEventListener* listener)
: origin_(std::move(origin)),
inspector_event_listener_(listener),
mojo_area_(area.get()),
mojo_area_ptr_(std::move(area)),
binding_(this),
areas_(new HeapHashMap<WeakMember<Source>, String>),
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));
}
// SessionStorage constructor.
CachedStorageArea::CachedStorageArea(
scoped_refptr<const SecurityOrigin> origin,
mojo::AssociatedInterfacePtr<mojom::blink::StorageArea> area,
scoped_refptr<base::SingleThreadTaskRunner> ipc_runner,
InspectorEventListener* listener)
: origin_(std::move(origin)),
inspector_event_listener_(listener),
mojo_area_(area.get()),
mojo_area_associated_ptr_(std::move(area)),
binding_(this),
areas_(new HeapHashMap<WeakMember<Source>, String>),
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;
String old_value_string =
Uint8VectorToString(old_value, FormatOption::kLocalStorageDetectFormat);
for (const auto& area : *areas_) {
if (area.value == storage_area_id) {
from_local_area = true;
break;
}
}
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);
}
EnqueueStorageEvent(key_string, old_value_string, String(), page_url,
storage_area_id);
}
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;
break;
}
}
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;
}
}
EnqueueStorageEvent(String(), String(), String(), page_url, storage_area_id);
}
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;
break;
}
}
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);
}
}
EnqueueStorageEvent(key_string, old_value, new_value_string, page_url,
storage_area_id);
}
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",
base::saturated_cast<base::Histogram::Sample>(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();
}
void CachedStorageArea::EnqueueStorageEvent(const String& key,
const String& old_value,
const String& new_value,
const String& url,
const String& storage_area_id) {
HeapVector<Member<Source>, 1> areas_to_remove_;
for (const auto& area : *areas_) {
if (area.value != storage_area_id) {
bool keep = area.key->EnqueueStorageEvent(key, old_value, new_value, url);
if (!keep)
areas_to_remove_.push_back(area.key);
}
}
areas_->RemoveAll(areas_to_remove_);
inspector_event_listener_->DidDispatchStorageEvent(origin_.get(), key,
old_value, new_value);
}
// static
String CachedStorageArea::Uint8VectorToString(const Vector<uint8_t>& input,
FormatOption format_option) {
if (input.IsEmpty())
return g_empty_string;
const wtf_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 wtf_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.ContainsOnlyASCIIOrEmpty()) {
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(
static_cast<wtf_size_t>(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.ContainsOnlyLatin1OrEmpty()) {
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