blob: 1ec45264e7f46f2d9cf080561dbd7fde9629aafb [file] [log] [blame]
/*
* Copyright (C) 2011 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "third_party/blink/renderer/bindings/modules/v8/v8_binding_for_modules.h"
#include "third_party/blink/public/common/indexeddb/indexeddb_key.h"
#include "third_party/blink/public/mojom/indexeddb/indexeddb.mojom-blink.h"
#include "third_party/blink/renderer/bindings/core/v8/serialization/serialized_script_value.h"
#include "third_party/blink/renderer/bindings/core/v8/serialization/serialized_script_value_factory.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_array_buffer.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_array_buffer_view.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_blob.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_dom_string_list.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_file.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_uint8_array.h"
#include "third_party/blink/renderer/bindings/modules/v8/to_v8_for_modules.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_idb_cursor.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_idb_cursor_with_value.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_idb_database.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_idb_index.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_idb_key_range.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_idb_object_store.h"
#include "third_party/blink/renderer/core/typed_arrays/dom_array_buffer.h"
#include "third_party/blink/renderer/core/typed_arrays/dom_array_buffer_view.h"
#include "third_party/blink/renderer/modules/indexeddb/idb_key.h"
#include "third_party/blink/renderer/modules/indexeddb/idb_key_path.h"
#include "third_party/blink/renderer/modules/indexeddb/idb_key_range.h"
#include "third_party/blink/renderer/modules/indexeddb/idb_tracing.h"
#include "third_party/blink/renderer/modules/indexeddb/idb_value.h"
#include "third_party/blink/renderer/platform/wtf/math_extras.h"
#include "third_party/blink/renderer/platform/wtf/shared_buffer.h"
#include "third_party/blink/renderer/platform/wtf/vector.h"
namespace blink {
static v8::Local<v8::Value> DeserializeIDBValueData(v8::Isolate*,
const IDBValue*);
static v8::Local<v8::Value> DeserializeIDBValueArray(
v8::Isolate*,
v8::Local<v8::Object> creation_context,
const Vector<std::unique_ptr<IDBValue>>&);
v8::Local<v8::Value> ToV8(const IDBKeyPath& value,
v8::Local<v8::Object> creation_context,
v8::Isolate* isolate) {
switch (value.GetType()) {
case mojom::IDBKeyPathType::Null:
return v8::Null(isolate);
case mojom::IDBKeyPathType::String:
return V8String(isolate, value.GetString());
case mojom::IDBKeyPathType::Array:
return ToV8(value.Array(), creation_context, isolate);
}
NOTREACHED();
return v8::Undefined(isolate);
}
v8::Local<v8::Value> ToV8(const IDBKey* key,
v8::Local<v8::Object> creation_context,
v8::Isolate* isolate) {
if (!key) {
// The IndexedDB spec requires that absent keys appear as attribute
// values as undefined, rather than the more typical (for DOM) null.
// This appears on the |upper| and |lower| attributes of IDBKeyRange.
// Spec: http://www.w3.org/TR/IndexedDB/#idl-def-IDBKeyRange
return v8::Local<v8::Value>();
}
v8::Local<v8::Context> context = isolate->GetCurrentContext();
switch (key->GetType()) {
case mojom::IDBKeyType::Invalid:
case mojom::IDBKeyType::Min:
NOTREACHED();
return v8::Local<v8::Value>();
case mojom::IDBKeyType::None:
return v8::Null(isolate);
case mojom::IDBKeyType::Number:
return v8::Number::New(isolate, key->Number());
case mojom::IDBKeyType::String:
return V8String(isolate, key->GetString());
case mojom::IDBKeyType::Binary:
// https://w3c.github.io/IndexedDB/#convert-a-value-to-a-key
return ToV8(DOMArrayBuffer::Create(key->Binary()), creation_context,
isolate);
case mojom::IDBKeyType::Date:
return v8::Date::New(context, key->Date()).ToLocalChecked();
case mojom::IDBKeyType::Array: {
v8::Local<v8::Array> array = v8::Array::New(isolate, key->Array().size());
for (wtf_size_t i = 0; i < key->Array().size(); ++i) {
v8::Local<v8::Value> value =
ToV8(key->Array()[i].get(), creation_context, isolate);
if (value.IsEmpty())
value = v8::Undefined(isolate);
bool created_property;
if (!array->CreateDataProperty(context, i, value)
.To(&created_property) ||
!created_property)
return v8::Local<v8::Value>();
}
return array;
}
}
NOTREACHED();
return v8::Local<v8::Value>();
}
// IDBAny is a variant type used to hold the values produced by the |result|
// attribute of IDBRequest and (as a convenience) the |source| attribute of
// IDBRequest and IDBCursor.
// TODO(jsbell): Replace the use of IDBAny for |source| attributes (which are
// ScriptWrappable types) using unions per IDL.
v8::Local<v8::Value> ToV8(const IDBAny* impl,
v8::Local<v8::Object> creation_context,
v8::Isolate* isolate) {
if (!impl)
return v8::Null(isolate);
switch (impl->GetType()) {
case IDBAny::kUndefinedType:
return v8::Undefined(isolate);
case IDBAny::kNullType:
return v8::Null(isolate);
case IDBAny::kIDBCursorType:
return ToV8(impl->IdbCursor(), creation_context, isolate);
case IDBAny::kIDBCursorWithValueType:
return ToV8(impl->IdbCursorWithValue(), creation_context, isolate);
case IDBAny::kIDBDatabaseType:
return ToV8(impl->IdbDatabase(), creation_context, isolate);
case IDBAny::kIDBValueType:
return DeserializeIDBValue(isolate, creation_context, impl->Value());
case IDBAny::kIDBValueArrayType:
return DeserializeIDBValueArray(isolate, creation_context,
impl->Values());
case IDBAny::kIntegerType:
return v8::Number::New(isolate, impl->Integer());
case IDBAny::kKeyType:
return ToV8(impl->Key(), creation_context, isolate);
}
NOTREACHED();
return v8::Undefined(isolate);
}
// Convert a simple (non-Array) script value to an Indexed DB key. If the
// conversion fails due to a detached buffer, an exception is thrown. If
// the value can't be converted into a key, an 'Invalid' key is returned. This
// is used to implement part of the following spec algorithm:
// https://w3c.github.io/IndexedDB/#convert-value-to-key
// A V8 exception may be thrown on bad data or by script's getters; if so,
// callers should not make further V8 calls.
static std::unique_ptr<IDBKey> CreateIDBKeyFromSimpleValue(
v8::Isolate* isolate,
v8::Local<v8::Value> value,
ExceptionState& exception_state) {
DCHECK(!value->IsArray());
if (value->IsNumber() && !std::isnan(value.As<v8::Number>()->Value()))
return IDBKey::CreateNumber(value.As<v8::Number>()->Value());
if (value->IsString())
return IDBKey::CreateString(ToCoreString(value.As<v8::String>()));
if (value->IsDate() && !std::isnan(value.As<v8::Date>()->ValueOf()))
return IDBKey::CreateDate(value.As<v8::Date>()->ValueOf());
if (value->IsArrayBuffer()) {
DOMArrayBuffer* buffer = V8ArrayBuffer::ToImpl(value.As<v8::Object>());
if (buffer->IsDetached()) {
exception_state.ThrowTypeError("The ArrayBuffer is detached.");
return IDBKey::CreateInvalid();
}
const char* start = static_cast<const char*>(buffer->Data());
size_t length = buffer->ByteLength();
return IDBKey::CreateBinary(SharedBuffer::Create(start, length));
}
if (value->IsArrayBufferView()) {
DOMArrayBufferView* view =
V8ArrayBufferView::ToImpl(value.As<v8::Object>());
if (view->buffer()->IsDetached()) {
exception_state.ThrowTypeError("The viewed ArrayBuffer is detached.");
return IDBKey::CreateInvalid();
}
const char* start = static_cast<const char*>(view->BaseAddress());
size_t length = view->byteLength();
return IDBKey::CreateBinary(SharedBuffer::Create(start, length));
}
return IDBKey::CreateInvalid();
}
// Convert a script value to an Indexed DB key. If the result cannot be
// converted, an 'Invalid' key is returned. If an array is being
// converted, and a potential subkey does not exist, then the array is
// returned but with an 'Invalid' entry; this is used for "multi-entry"
// indexes where an array with invalid members is permitted will later be
// sanitized. This is used to implement both of the following spec algorithms:
// https://w3c.github.io/IndexedDB/#convert-value-to-key
// https://w3c.github.io/IndexedDB/#convert-a-value-to-a-multientry-key
// A V8 exception may be thrown on bad data or by script's getters; if so,
// callers should not make further V8 calls.
static std::unique_ptr<IDBKey> CreateIDBKeyFromValue(
v8::Isolate* isolate,
v8::Local<v8::Value> value,
ExceptionState& exception_state) {
// Simple case:
if (!value->IsArray())
return CreateIDBKeyFromSimpleValue(isolate, value, exception_state);
// Recursion is done on the heap rather than the stack.
struct Record {
Record(v8::Local<v8::Array> array)
: array(std::move(array)), length(array->Length()) {
subkeys.ReserveInitialCapacity(length);
}
Record(const Record&) = delete;
Record& operator=(const Record&) = delete;
// Array being converted.
v8::Local<v8::Array> array;
// Length of |array|. Snapshotted (per spec), since getters may alter it.
uint32_t length;
// Converted sub-keys.
IDBKey::KeyArray subkeys;
};
// Recursion stack.
Vector<std::unique_ptr<Record>> stack;
// Tracks seen arrays, to detect circular references and abort (per spec).
Vector<v8::Local<v8::Array>> seen;
// Initial state.
{
v8::Local<v8::Array> array = value.As<v8::Array>();
if (array->Length() > IndexedDBKey::kMaximumArraySize)
return IDBKey::CreateInvalid();
stack.push_back(std::make_unique<Record>(array));
seen.push_back(array);
}
v8::Local<v8::Context> context = isolate->GetCurrentContext();
v8::TryCatch try_block(isolate);
// Process stack - will return when complete.
while (true) {
DCHECK(!stack.IsEmpty());
Record* top = stack.back().get();
const wtf_size_t item_index = top->subkeys.size();
// Done this array?
if (item_index == top->length) {
std::unique_ptr<IDBKey> key =
IDBKey::CreateArray(std::move(top->subkeys));
seen.pop_back();
stack.pop_back();
if (stack.IsEmpty())
return key;
top = stack.back().get();
top->subkeys.push_back(std::move(key));
continue;
}
// Process next item from current array.
bool has_own_property;
if (!top->array->HasOwnProperty(context, item_index)
.To(&has_own_property)) {
exception_state.RethrowV8Exception(try_block.Exception());
return IDBKey::CreateInvalid();
}
if (!has_own_property)
return IDBKey::CreateInvalid();
v8::Local<v8::Value> item;
if (!top->array->Get(context, item_index).ToLocal(&item)) {
exception_state.RethrowV8Exception(try_block.Exception());
return IDBKey::CreateInvalid();
}
if (!item->IsArray()) {
// A non-array: convert it directly.
auto key = CreateIDBKeyFromSimpleValue(isolate, item, exception_state);
if (exception_state.HadException()) {
DCHECK(!try_block.HasCaught());
return IDBKey::CreateInvalid();
}
top->subkeys.push_back(std::move(key));
} else {
// A sub-array; push onto the stack and start processing it.
v8::Local<v8::Array> array = item.As<v8::Array>();
if (seen.Contains(array) || stack.size() >= IndexedDBKey::kMaximumDepth ||
array->Length() > IndexedDBKey::kMaximumArraySize) {
return IDBKey::CreateInvalid();
}
stack.push_back(std::make_unique<Record>(array));
seen.push_back(array);
}
}
}
// Indexed DB key paths should apply to explicitly copied properties (that
// will be "own" properties when deserialized) as well as the following.
// http://www.w3.org/TR/IndexedDB/#key-path-construct
static bool IsImplicitProperty(v8::Isolate* isolate,
v8::Local<v8::Value> value,
const String& name) {
if (value->IsString() && name == "length")
return true;
if (value->IsArray() && name == "length")
return true;
if (V8Blob::HasInstance(value, isolate))
return name == "size" || name == "type";
if (V8File::HasInstance(value, isolate))
return name == "name" || name == "lastModified" ||
name == "lastModifiedDate";
return false;
}
// Assumes a valid key path.
static Vector<String> ParseKeyPath(const String& key_path) {
Vector<String> elements;
IDBKeyPathParseError error;
IDBParseKeyPath(key_path, elements, error);
DCHECK_EQ(error, kIDBKeyPathParseErrorNone);
return elements;
}
// Evaluate a key path string against a value and return a key. It must be
// called repeatedly for array-type key paths. Failure in the evaluation steps
// (per spec) is represented by returning nullptr. Failure to convert the result
// to a key is representing by returning an 'Invalid' key. (An array with
// invalid members will be returned if needed.)
// https://w3c.github.io/IndexedDB/#evaluate-a-key-path-on-a-value
// A V8 exception may be thrown on bad data or by script's getters; if so,
// callers should not make further V8 calls.
static std::unique_ptr<IDBKey> CreateIDBKeyFromValueAndKeyPath(
v8::Isolate* isolate,
v8::Local<v8::Value> v8_value,
const String& key_path,
ExceptionState& exception_state) {
Vector<String> key_path_elements = ParseKeyPath(key_path);
DCHECK(isolate->InContext());
v8::HandleScope handle_scope(isolate);
v8::Local<v8::Context> context = isolate->GetCurrentContext();
v8::TryCatch block(isolate);
for (wtf_size_t i = 0; i < key_path_elements.size(); ++i) {
const String& element = key_path_elements[i];
// Special cases from https://w3c.github.io/IndexedDB/#key-path-construct
// These access special or non-own properties directly, to avoid side
// effects.
if (v8_value->IsString() && element == "length") {
int32_t length = v8_value.As<v8::String>()->Length();
v8_value = v8::Number::New(isolate, length);
continue;
}
if (v8_value->IsArray() && element == "length") {
int32_t length = v8_value.As<v8::Array>()->Length();
v8_value = v8::Number::New(isolate, length);
continue;
}
if (!v8_value->IsObject())
return nullptr;
v8::Local<v8::Object> object = v8_value.As<v8::Object>();
if (V8Blob::HasInstance(object, isolate)) {
if (element == "size") {
v8_value = v8::Number::New(isolate, V8Blob::ToImpl(object)->size());
continue;
}
if (element == "type") {
v8_value = V8String(isolate, V8Blob::ToImpl(object)->type());
continue;
}
// Fall through.
}
if (V8File::HasInstance(object, isolate)) {
if (element == "name") {
v8_value = V8String(isolate, V8File::ToImpl(object)->name());
continue;
}
if (element == "lastModified") {
v8_value =
v8::Number::New(isolate, V8File::ToImpl(object)->lastModified());
continue;
}
if (element == "lastModifiedDate") {
v8_value = V8File::ToImpl(object)
->lastModifiedDate(ScriptState::From(context))
.V8Value();
continue;
}
// Fall through.
}
v8::Local<v8::String> key = V8String(isolate, element);
bool has_own_property;
if (!object->HasOwnProperty(context, key).To(&has_own_property)) {
exception_state.RethrowV8Exception(block.Exception());
return nullptr;
}
if (!has_own_property)
return nullptr;
if (!object->Get(context, key).ToLocal(&v8_value)) {
exception_state.RethrowV8Exception(block.Exception());
return nullptr;
}
}
return CreateIDBKeyFromValue(isolate, v8_value, exception_state);
}
// Evaluate a key path against a value and return a key. For string-type
// paths, nullptr is returned if evaluation of the path fails, and an
// 'Invalid' key if evaluation succeeds but conversion fails. For array-type
// paths, nullptr is returned if evaluation of any sub-path fails, otherwise
// an array key is returned (with potentially 'Invalid' members).
// https://w3c.github.io/IndexedDB/#evaluate-a-key-path-on-a-value
// A V8 exception may be thrown on bad data or by script's getters; if so,
// callers should not make further V8 calls.
static std::unique_ptr<IDBKey> CreateIDBKeyFromValueAndKeyPath(
v8::Isolate* isolate,
v8::Local<v8::Value> value,
const IDBKeyPath& key_path,
ExceptionState& exception_state) {
DCHECK(!key_path.IsNull());
v8::HandleScope handle_scope(isolate);
if (key_path.GetType() == mojom::IDBKeyPathType::Array) {
const Vector<String>& array = key_path.Array();
IDBKey::KeyArray result;
result.ReserveInitialCapacity(array.size());
for (const String& path : array) {
auto key = CreateIDBKeyFromValueAndKeyPath(isolate, value, path,
exception_state);
if (exception_state.HadException())
return nullptr;
// Evaluation of path failed - overall failure.
if (!key)
return nullptr;
result.emplace_back(std::move(key));
}
// An array key is always returned, even if members may be invalid.
return IDBKey::CreateArray(std::move(result));
}
DCHECK_EQ(key_path.GetType(), mojom::IDBKeyPathType::String);
return CreateIDBKeyFromValueAndKeyPath(isolate, value, key_path.GetString(),
exception_state);
}
// Evaluate an index's key path against a value and return a key. This
// handles the special case for indexes where a compound key path
// may result in "holes", depending on the store's properties.
// Otherwise, nullptr is returned.
// https://w3c.github.io/IndexedDB/#evaluate-a-key-path-on-a-value
// A V8 exception may be thrown on bad data or by script's getters; if so,
// callers should not make further V8 calls.
static std::unique_ptr<IDBKey> CreateIDBKeyFromValueAndKeyPaths(
v8::Isolate* isolate,
v8::Local<v8::Value> value,
const IDBKeyPath& store_key_path,
const IDBKeyPath& index_key_path,
ExceptionState& exception_state) {
DCHECK(!index_key_path.IsNull());
v8::HandleScope handle_scope(isolate);
if (index_key_path.GetType() == mojom::IDBKeyPathType::Array) {
const Vector<String>& array = index_key_path.Array();
const bool uses_inline_keys =
store_key_path.GetType() == mojom::IDBKeyPathType::String;
IDBKey::KeyArray result;
result.ReserveInitialCapacity(array.size());
for (const String& path : array) {
auto key = CreateIDBKeyFromValueAndKeyPath(isolate, value, path,
exception_state);
if (exception_state.HadException())
return nullptr;
if (!key && uses_inline_keys && store_key_path.GetString() == path) {
// Compound keys that include the store's inline primary key which
// will be generated lazily are represented as "holes".
key = IDBKey::CreateNone();
} else if (!key) {
// Key path evaluation failed.
return nullptr;
} else if (!key->IsValid()) {
// An Invalid key is returned if not valid in this case (but not the
// other CreateIDBKeyFromValueAndKeyPath function) because:
// * Invalid members are only allowed for multi-entry arrays.
// * Array key paths can't be multi-entry.
return IDBKey::CreateInvalid();
}
result.emplace_back(std::move(key));
}
return IDBKey::CreateArray(std::move(result));
}
DCHECK_EQ(index_key_path.GetType(), mojom::IDBKeyPathType::String);
return CreateIDBKeyFromValueAndKeyPath(
isolate, value, index_key_path.GetString(), exception_state);
}
// Deserialize just the value data & blobInfo from the given IDBValue.
//
// Primary key injection is performed in deserializeIDBValue() below.
static v8::Local<v8::Value> DeserializeIDBValueData(v8::Isolate* isolate,
const IDBValue* value) {
DCHECK(isolate->InContext());
if (!value || value->IsNull())
return v8::Null(isolate);
scoped_refptr<SerializedScriptValue> serialized_value =
value->CreateSerializedValue();
serialized_value->FileSystemAccessTokens() =
std::move(const_cast<IDBValue*>(value)->FileSystemAccessTokens());
SerializedScriptValue::DeserializeOptions options;
options.blob_info = &value->BlobInfo();
// deserialize() returns null when serialization fails. This is sub-optimal
// because IndexedDB values can be null, so an application cannot distinguish
// between a de-serialization failure and a legitimately stored null value.
//
// TODO(crbug.com/703704): Ideally, SerializedScriptValue should return an
// empty handle on serialization errors, which should be handled by higher
// layers. For example, IndexedDB could throw an exception, abort the
// transaction, or close the database connection.
return serialized_value->Deserialize(isolate, options);
}
// Deserialize the entire IDBValue.
//
// On top of deserializeIDBValueData(), this handles the special case of having
// to inject a key into the de-serialized value. See injectV8KeyIntoV8Value()
// for details.
v8::Local<v8::Value> DeserializeIDBValue(v8::Isolate* isolate,
v8::Local<v8::Object> creation_context,
const IDBValue* value) {
DCHECK(isolate->InContext());
if (!value || value->IsNull())
return v8::Null(isolate);
v8::Local<v8::Value> v8_value = DeserializeIDBValueData(isolate, value);
if (value->PrimaryKey()) {
v8::Local<v8::Value> key =
ToV8(value->PrimaryKey(), creation_context, isolate);
if (key.IsEmpty())
return v8::Local<v8::Value>();
InjectV8KeyIntoV8Value(isolate, key, v8_value, value->KeyPath());
// TODO(crbug.com/703704): Throw an error here or at a higher layer if
// injectV8KeyIntoV8Value() returns false, which means that the serialized
// value got corrupted while on disk.
}
return v8_value;
}
static v8::Local<v8::Value> DeserializeIDBValueArray(
v8::Isolate* isolate,
v8::Local<v8::Object> creation_context,
const Vector<std::unique_ptr<IDBValue>>& values) {
DCHECK(isolate->InContext());
v8::Local<v8::Context> context = isolate->GetCurrentContext();
v8::Local<v8::Array> array = v8::Array::New(isolate, values.size());
for (wtf_size_t i = 0; i < values.size(); ++i) {
v8::Local<v8::Value> v8_value =
DeserializeIDBValue(isolate, creation_context, values[i].get());
if (v8_value.IsEmpty())
v8_value = v8::Undefined(isolate);
bool created_property;
if (!array->CreateDataProperty(context, i, v8_value)
.To(&created_property) ||
!created_property)
return v8::Local<v8::Value>();
}
return array;
}
// Injects a primary key into a deserialized V8 value.
//
// In general, the value stored in IndexedDB is the serialized version of a
// value passed to the API. However, the specification has a special case of
// object stores that specify a key path and have a key generator. In this case,
// the conceptual description in the spec states that the key produced by the
// key generator is injected into the value before it is written to IndexedDB.
//
// We cannot implement the spec's conceptual description. We need to assign
// primary keys in the browser process, to ensure that multiple renderer
// processes talking to the same database receive sequential keys. At the same
// time, we want the value serialization code to live in the renderer process,
// because this type of code is a likely victim to security exploits.
//
// We handle this special case by serializing and writing values without the
// corresponding keys. At read time, we obtain the keys and the values
// separately, and we inject the keys into values.
bool InjectV8KeyIntoV8Value(v8::Isolate* isolate,
v8::Local<v8::Value> key,
v8::Local<v8::Value> value,
const IDBKeyPath& key_path) {
IDB_TRACE("injectIDBV8KeyIntoV8Value");
DCHECK(isolate->InContext());
DCHECK_EQ(key_path.GetType(), mojom::IDBKeyPathType::String);
Vector<String> key_path_elements = ParseKeyPath(key_path.GetString());
// The conbination of a key generator and an empty key path is forbidden by
// spec.
if (!key_path_elements.size()) {
NOTREACHED();
return false;
}
v8::HandleScope handle_scope(isolate);
v8::Local<v8::Context> context = isolate->GetCurrentContext();
// For an object o = {} which should have keypath 'a.b.c' and key k, this
// populates o to be {a:{b:{}}}. This is only applied to deserialized
// values, so we can assume that there are no getters/setters on the
// object itself (though there might be on the prototype chain).
//
// Previous versions of this code assumed that the deserialized value meets
// the constraints checked by the serialization validation code. For example,
// given a keypath of a.b.c, the code assumed that the de-serialized value
// cannot possibly be {a:{b:42}}. This is not a safe assumption.
//
// IndexedDB's backing store (LevelDB) does use CRC32C to protect against disk
// errors. However, this does not prevent corruption caused by bugs in the
// higher level code writing invalid values. The following cases are
// interesting here.
//
// (1) Deserialization failures, which are currently handled by returning
// null. Disk errors aside, deserialization errors can also occur when a user
// switches channels and receives an older build which does not support the
// serialization format used by the previous (more recent) build that the user
// had.
//
// (2) Bugs that write a value which is incompatible with the primary key
// injection required by the object store. The simplest example is writing
// numbers or booleans to an object store with an auto-incrementing primary
// keys.
for (wtf_size_t i = 0; i < key_path_elements.size() - 1; ++i) {
if (!value->IsObject())
return false;
const String& key_path_element = key_path_elements[i];
DCHECK(!IsImplicitProperty(isolate, value, key_path_element));
v8::Local<v8::Object> object = value.As<v8::Object>();
v8::Local<v8::String> property = V8String(isolate, key_path_element);
bool has_own_property;
if (!object->HasOwnProperty(context, property).To(&has_own_property))
return false;
if (has_own_property) {
if (!object->Get(context, property).ToLocal(&value))
return false;
} else {
value = v8::Object::New(isolate);
bool created_property;
if (!object->CreateDataProperty(context, property, value)
.To(&created_property) ||
!created_property)
return false;
}
}
// Implicit properties don't need to be set. The caller is not required to
// be aware of this, so this is an expected no-op. The caller can verify
// that the value is correct via assertPrimaryKeyValidOrInjectable.
if (IsImplicitProperty(isolate, value, key_path_elements.back()))
return true;
// If the key path does not point to an implicit property, the value must be
// an object. Previous code versions DCHECKed this, which is unsafe in the
// event of database corruption or version skew in the serialization format.
if (!value->IsObject())
return false;
v8::Local<v8::Object> object = value.As<v8::Object>();
v8::Local<v8::String> property = V8String(isolate, key_path_elements.back());
bool created_property;
if (!object->CreateDataProperty(context, property, key).To(&created_property))
return false;
return created_property;
}
// Verify that an value can have an generated key inserted at the location
// specified by the key path (by injectV8KeyIntoV8Value) when the object is
// later deserialized.
bool CanInjectIDBKeyIntoScriptValue(v8::Isolate* isolate,
const ScriptValue& script_value,
const IDBKeyPath& key_path) {
IDB_TRACE("canInjectIDBKeyIntoScriptValue");
DCHECK_EQ(key_path.GetType(), mojom::IDBKeyPathType::String);
Vector<String> key_path_elements = ParseKeyPath(key_path.GetString());
if (!key_path_elements.size())
return false;
v8::Local<v8::Value> current(script_value.V8Value());
if (!current->IsObject())
return false;
v8::Local<v8::Context> context = isolate->GetCurrentContext();
for (wtf_size_t i = 0; i < key_path_elements.size(); ++i) {
const String& key_path_element = key_path_elements[i];
// Can't overwrite properties like array or string length.
if (IsImplicitProperty(isolate, current, key_path_element))
return false;
// Can't set properties on non-objects.
if (!current->IsObject())
return false;
v8::Local<v8::Object> object = current.As<v8::Object>();
v8::Local<v8::String> property = V8String(isolate, key_path_element);
// If the value lacks an "own" property, it can be added - either as
// an intermediate object or as the final value.
bool has_own_property;
if (!object->HasOwnProperty(context, property).To(&has_own_property))
return false;
if (!has_own_property)
return true;
// Otherwise, get it and keep traversing.
if (!object->Get(context, property).ToLocal(&current))
return false;
}
return true;
}
ScriptValue DeserializeScriptValue(ScriptState* script_state,
SerializedScriptValue* serialized_value,
const Vector<WebBlobInfo>* blob_info) {
v8::Isolate* isolate = script_state->GetIsolate();
v8::HandleScope handle_scope(isolate);
if (!serialized_value)
return ScriptValue::CreateNull(script_state->GetIsolate());
SerializedScriptValue::DeserializeOptions options;
options.blob_info = blob_info;
return ScriptValue(isolate, serialized_value->Deserialize(isolate, options));
}
SQLValue NativeValueTraits<SQLValue>::NativeValue(
v8::Isolate* isolate,
v8::Local<v8::Value> value,
ExceptionState& exception_state) {
if (value.IsEmpty() || value->IsNull())
return SQLValue();
if (value->IsNumber())
return SQLValue(value.As<v8::Number>()->Value());
V8StringResource<> string_value(value);
if (!string_value.Prepare(exception_state))
return SQLValue();
return SQLValue(string_value);
}
std::unique_ptr<IDBKey> NativeValueTraits<std::unique_ptr<IDBKey>>::NativeValue(
v8::Isolate* isolate,
v8::Local<v8::Value> value,
ExceptionState& exception_state) {
return CreateIDBKeyFromValue(isolate, value, exception_state);
}
std::unique_ptr<IDBKey> NativeValueTraits<std::unique_ptr<IDBKey>>::NativeValue(
v8::Isolate* isolate,
v8::Local<v8::Value> value,
ExceptionState& exception_state,
const IDBKeyPath& key_path) {
IDB_TRACE("createIDBKeyFromValueAndKeyPath");
return CreateIDBKeyFromValueAndKeyPath(isolate, value, key_path,
exception_state);
}
std::unique_ptr<IDBKey> NativeValueTraits<std::unique_ptr<IDBKey>>::NativeValue(
v8::Isolate* isolate,
v8::Local<v8::Value> value,
ExceptionState& exception_state,
const IDBKeyPath& store_key_path,
const IDBKeyPath& index_key_path) {
IDB_TRACE("createIDBKeyFromValueAndKeyPaths");
return CreateIDBKeyFromValueAndKeyPaths(isolate, value, store_key_path,
index_key_path, exception_state);
}
IDBKeyRange* NativeValueTraits<IDBKeyRange*>::NativeValue(
v8::Isolate* isolate,
v8::Local<v8::Value> value,
ExceptionState& exception_state) {
return V8IDBKeyRange::ToImplWithTypeCheck(isolate, value);
}
#if DCHECK_IS_ON()
// This assertion is used when a value has been retrieved from an object store
// with implicit keys (i.e. a key path). It verifies that either the value
// contains an implicit key matching the primary key (so it was correctly
// extracted when stored) or that the key can be inserted as an own property.
void AssertPrimaryKeyValidOrInjectable(ScriptState* script_state,
const IDBValue* value) {
ScriptState::Scope scope(script_state);
v8::Isolate* isolate = script_state->GetIsolate();
ScriptValue key_value = ScriptValue::From(script_state, value->PrimaryKey());
ScriptValue script_value(isolate, DeserializeIDBValueData(isolate, value));
DummyExceptionStateForTesting exception_state;
std::unique_ptr<IDBKey> expected_key = CreateIDBKeyFromValueAndKeyPath(
isolate, script_value.V8Value(), value->KeyPath(), exception_state);
DCHECK(!exception_state.HadException());
if (expected_key && expected_key->IsEqual(value->PrimaryKey()))
return;
bool injected = InjectV8KeyIntoV8Value(
isolate, key_value.V8Value(), script_value.V8Value(), value->KeyPath());
DCHECK(injected);
}
#endif // DCHECK_IS_ON()
} // namespace blink