/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use std::ptr; use dom_struct::dom_struct; use js::conversions::ToJSValConvertible; use js::jsapi::{ ESClass, GetBuiltinClass, IsArrayBufferObject, JS_DeleteUCProperty, JS_GetOwnUCPropertyDescriptor, JS_GetStringLength, JS_IsArrayBufferViewObject, JSObject, ObjectOpResult, ObjectOpResult_SpecialCodes, PropertyDescriptor, }; use js::jsval::UndefinedValue; use js::rust::{HandleValue, MutableHandleValue}; use net_traits::IpcSend; use net_traits::indexeddb_thread::{ AsyncOperation, IndexedDBKeyType, IndexedDBThreadMsg, SyncOperation, }; use profile_traits::ipc; use crate::dom::bindings::cell::DomRefCell; use crate::dom::bindings::codegen::Bindings::IDBDatabaseBinding::IDBObjectStoreParameters; use crate::dom::bindings::codegen::Bindings::IDBObjectStoreBinding::IDBObjectStoreMethods; use crate::dom::bindings::codegen::Bindings::IDBTransactionBinding::IDBTransactionMode; // We need to alias this name, otherwise test-tidy complains at &String reference. use crate::dom::bindings::codegen::UnionTypes::StringOrStringSequence as StrOrStringSequence; use crate::dom::bindings::error::{Error, Fallible}; use crate::dom::bindings::reflector::{DomGlobal, Reflector, reflect_dom_object}; use crate::dom::bindings::root::{DomRoot, MutNullableDom}; use crate::dom::bindings::str::DOMString; use crate::dom::bindings::structuredclone; use crate::dom::domstringlist::DOMStringList; use crate::dom::globalscope::GlobalScope; use crate::dom::idbrequest::IDBRequest; use crate::dom::idbtransaction::IDBTransaction; use crate::script_runtime::{CanGc, JSContext as SafeJSContext}; #[derive(JSTraceable, MallocSizeOf)] pub enum KeyPath { String(DOMString), StringSequence(Vec), } #[dom_struct] pub struct IDBObjectStore { reflector_: Reflector, name: DomRefCell, key_path: Option, index_names: DomRoot, transaction: MutNullableDom, auto_increment: bool, // We store the db name in the object store to be able to find the correct // store in the idb thread when checking if we have a key generator db_name: DOMString, } impl IDBObjectStore { pub fn new_inherited( global: &GlobalScope, db_name: DOMString, name: DOMString, options: Option<&IDBObjectStoreParameters>, can_gc: CanGc, ) -> IDBObjectStore { let key_path: Option = match options { Some(options) => options.keyPath.as_ref().map(|path| match path { StrOrStringSequence::String(inner) => KeyPath::String(inner.clone()), StrOrStringSequence::StringSequence(inner) => { KeyPath::StringSequence(inner.clone()) }, }), None => None, }; IDBObjectStore { reflector_: Reflector::new(), name: DomRefCell::new(name), key_path, index_names: DOMStringList::new(global, Vec::new(), can_gc), transaction: Default::default(), // FIXME:(arihant2math) auto_increment: false, db_name, } } pub fn new( global: &GlobalScope, db_name: DOMString, name: DOMString, options: Option<&IDBObjectStoreParameters>, can_gc: CanGc, ) -> DomRoot { reflect_dom_object( Box::new(IDBObjectStore::new_inherited( global, db_name, name, options, can_gc, )), global, can_gc, ) } pub fn get_name(&self) -> DOMString { self.name.borrow().clone() } pub fn set_transaction(&self, transaction: &IDBTransaction) { self.transaction.set(Some(transaction)); } pub fn transaction(&self) -> Option> { self.transaction.get() } // https://www.w3.org/TR/IndexedDB-2/#valid-key-path pub fn is_valid_key_path(key_path: &StrOrStringSequence) -> bool { fn is_identifier(_s: &str) -> bool { // FIXME: (arihant2math) true } let is_valid = |path: &DOMString| { path.is_empty() || is_identifier(path) || path.split(".").all(is_identifier) }; match key_path { StrOrStringSequence::StringSequence(paths) => { if paths.is_empty() { return false; } paths.iter().all(is_valid) }, StrOrStringSequence::String(path) => is_valid(path), } } #[allow(unsafe_code)] // https://www.w3.org/TR/IndexedDB-2/#convert-value-to-key fn convert_value_to_key( cx: SafeJSContext, input: HandleValue, seen: Option>, ) -> Result { // Step 1: If seen was not given, then let seen be a new empty set. let _seen = seen.unwrap_or_default(); // Step 2: If seen contains input, then return invalid. // FIXME:(rasviitanen) // Check if we have seen this key // Does not currently work with HandleValue, // as it does not implement PartialEq // Step 3 // FIXME:(rasviitanen) Accept buffer, array and date as well if input.is_number() { // FIXME:(rasviitanen) check for NaN let key = structuredclone::write(cx, input, None).expect("Could not serialize key"); return Ok(IndexedDBKeyType::Number(key.serialized)); } if input.is_string() { let key = structuredclone::write(cx, input, None).expect("Could not serialize key"); return Ok(IndexedDBKeyType::String(key.serialized)); } if input.is_object() { rooted!(in(*cx) let object = input.to_object()); unsafe { let mut built_in_class = ESClass::Other; if !GetBuiltinClass(*cx, object.handle().into(), &mut built_in_class) { return Err(Error::Data); } if let ESClass::Date = built_in_class { // FIXME:(arihant2math) implement it the correct way let key = structuredclone::write(cx, input, None).expect("Could not serialize key"); return Ok(IndexedDBKeyType::Date(key.serialized.clone())); } if IsArrayBufferObject(*object) || JS_IsArrayBufferViewObject(*object) { let key = structuredclone::write(cx, input, None).expect("Could not serialize key"); // FIXME:(arihant2math) Return the correct type here // it doesn't really matter at the moment... return Ok(IndexedDBKeyType::Number(key.serialized.clone())); } if let ESClass::Array = built_in_class { // FIXME:(arihant2math) unimplemented!("Arrays as keys is currently unsupported"); } } } Err(Error::Data) } // https://www.w3.org/TR/IndexedDB-2/#evaluate-a-key-path-on-a-value #[allow(unsafe_code)] fn evaluate_key_path_on_value( cx: SafeJSContext, value: HandleValue, mut return_val: MutableHandleValue, key_path: &KeyPath, ) { // The implementation is translated from gecko: // https://github.com/mozilla/gecko-dev/blob/master/dom/indexedDB/KeyPath.cpp return_val.set(*value); rooted!(in(*cx) let mut target_object = ptr::null_mut::()); rooted!(in(*cx) let mut current_val = *value); rooted!(in(*cx) let mut object = ptr::null_mut::()); let mut target_object_prop_name: Option = None; match key_path { KeyPath::String(path) => { // Step 3 let path_as_string = path.to_string(); let mut tokenizer = path_as_string.split('.').peekable(); while let Some(token) = tokenizer.next() { if target_object.get().is_null() { if token == "length" && tokenizer.peek().is_none() && current_val.is_string() { rooted!(in(*cx) let input_val = current_val.to_string()); unsafe { let string_len = JS_GetStringLength(*input_val) as u64; string_len.to_jsval(*cx, return_val); } break; } if !current_val.is_object() { // FIXME:(rasviitanen) Return a proper error return; } object.handle_mut().set(current_val.to_object()); rooted!(in(*cx) let mut desc = PropertyDescriptor::default()); rooted!(in(*cx) let mut intermediate = UndefinedValue()); // So rust says that this value is never read, but it is. #[allow(unused)] let mut has_prop = false; unsafe { let prop_name_as_utf16: Vec = token.encode_utf16().collect(); let mut is_descriptor_none: bool = false; let ok = JS_GetOwnUCPropertyDescriptor( *cx, object.handle().into(), prop_name_as_utf16.as_ptr(), prop_name_as_utf16.len(), desc.handle_mut().into(), &mut is_descriptor_none, ); if !ok { // FIXME:(arihant2math) Handle this return; } if desc.hasWritable_() || desc.hasValue_() { intermediate.handle_mut().set(desc.handle().value_); has_prop = true; } else { // If we get here it means the object doesn't have the property or the // property is available throuch a getter. We don't want to call any // getters to avoid potential re-entrancy. // The blob object is special since its properties are available // only through getters but we still want to support them for key // extraction. So they need to be handled manually. unimplemented!("Blob tokens are not yet supported"); } } if has_prop { // Treat undefined as an error if intermediate.is_undefined() { // FIXME:(rasviitanen) Throw/return error return; } if tokenizer.peek().is_some() { // ...and walk to it if there are more steps... current_val.handle_mut().set(*intermediate); } else { // ...otherwise use it as key return_val.set(*intermediate); } } else { target_object.handle_mut().set(*object); target_object_prop_name = Some(token.to_string()); } } if !target_object.get().is_null() { // We have started inserting new objects or are about to just insert // the first one. // FIXME:(rasviitanen) Implement this piece unimplemented!("keyPath tokens that requires insertion are not supported."); } } // All tokens processed if !target_object.get().is_null() { // If this fails, we lose, and the web page sees a magical property // appear on the object :-( unsafe { let prop_name_as_utf16: Vec = target_object_prop_name.unwrap().encode_utf16().collect(); #[allow(clippy::cast_enum_truncation)] let mut succeeded = ObjectOpResult { code_: ObjectOpResult_SpecialCodes::Uninitialized as usize, }; if !JS_DeleteUCProperty( *cx, target_object.handle().into(), prop_name_as_utf16.as_ptr(), prop_name_as_utf16.len(), &mut succeeded, ) { // FIXME:(rasviitanen) Throw/return error // return; } } } }, KeyPath::StringSequence(_) => { unimplemented!("String sequence keyPath is currently unsupported"); }, } } fn has_key_generator(&self) -> bool { let (sender, receiver) = ipc::channel(self.global().time_profiler_chan().clone()).unwrap(); let operation = SyncOperation::HasKeyGenerator( sender, self.global().origin().immutable().clone(), self.db_name.to_string(), self.name.borrow().to_string(), ); self.global() .resource_threads() .sender() .send(IndexedDBThreadMsg::Sync(operation)) .unwrap(); receiver.recv().unwrap() } // https://www.w3.org/TR/IndexedDB-2/#extract-a-key-from-a-value-using-a-key-path fn extract_key( cx: SafeJSContext, input: HandleValue, key_path: &KeyPath, multi_entry: Option, ) -> Result { // Step 1: Evaluate key path // FIXME:(rasviitanen) Do this propertly rooted!(in(*cx) let mut r = UndefinedValue()); IDBObjectStore::evaluate_key_path_on_value(cx, input, r.handle_mut(), key_path); if let Some(_multi_entry) = multi_entry { // FIXME:(rasviitanen) handle multi_entry cases unimplemented!("multiEntry keys are not yet supported"); } else { IDBObjectStore::convert_value_to_key(cx, r.handle(), None) } } // https://www.w3.org/TR/IndexedDB-2/#object-store-in-line-keys fn uses_inline_keys(&self) -> bool { self.key_path.is_some() } // https://www.w3.org/TR/IndexedDB-2/#dom-idbobjectstore-put fn put( &self, cx: SafeJSContext, value: HandleValue, key: HandleValue, overwrite: bool, can_gc: CanGc, ) -> Fallible> { // Step 1: Let transaction be this object store handle's transaction. let transaction = self .transaction .get() .expect("No transaction in Object Store"); // Step 2: Let store be this object store handle's object store. // This is resolved in the `execute_async` function. // Step 3: If store has been deleted, throw an "InvalidStateError" DOMException. // FIXME:(rasviitanen) // Step 4-5: If transaction is not active, throw a "TransactionInactiveError" DOMException. if !transaction.is_active() { return Err(Error::TransactionInactive); } // Step 5: If transaction is a read-only transaction, throw a "ReadOnlyError" DOMException. if let IDBTransactionMode::Readonly = transaction.get_mode() { return Err(Error::ReadOnly); } // Step 6: If store uses in-line keys and key was given, throw a "DataError" DOMException. if !key.is_undefined() && self.uses_inline_keys() { return Err(Error::Data); } // Step 7: If store uses out-of-line keys and has no key generator // and key was not given, throw a "DataError" DOMException. if !self.uses_inline_keys() && !self.has_key_generator() && key.is_undefined() { return Err(Error::Data); } // Step 8: If key was given, then: convert a value to a key with key let serialized_key: IndexedDBKeyType; if !key.is_undefined() { serialized_key = IDBObjectStore::convert_value_to_key(cx, key, None)?; } else { // Step 11: We should use in-line keys instead if let Ok(kpk) = IDBObjectStore::extract_key( cx, value, self.key_path.as_ref().expect("No key path"), None, ) { serialized_key = kpk; } else { // FIXME:(rasviitanen) // Check if store has a key generator // Check if we can inject a key return Err(Error::Data); } } let serialized_value = structuredclone::write(cx, value, None).expect("Could not serialize value"); IDBRequest::execute_async( self, AsyncOperation::PutItem(serialized_key, serialized_value.serialized, overwrite), None, can_gc, ) } } impl IDBObjectStoreMethods for IDBObjectStore { // https://www.w3.org/TR/IndexedDB-2/#dom-idbobjectstore-put fn Put( &self, cx: SafeJSContext, value: HandleValue, key: HandleValue, ) -> Fallible> { self.put(cx, value, key, true, CanGc::note()) } // https://www.w3.org/TR/IndexedDB-2/#dom-idbobjectstore-add fn Add( &self, cx: SafeJSContext, value: HandleValue, key: HandleValue, ) -> Fallible> { self.put(cx, value, key, false, CanGc::note()) } // https://www.w3.org/TR/IndexedDB-2/#dom-idbobjectstore-delete fn Delete(&self, cx: SafeJSContext, query: HandleValue) -> Fallible> { let serialized_query = IDBObjectStore::convert_value_to_key(cx, query, None); serialized_query.and_then(|q| { IDBRequest::execute_async(self, AsyncOperation::RemoveItem(q), None, CanGc::note()) }) } // https://www.w3.org/TR/IndexedDB-2/#dom-idbobjectstore-clear fn Clear(&self) -> Fallible> { unimplemented!(); } // https://www.w3.org/TR/IndexedDB-2/#dom-idbobjectstore-get fn Get(&self, cx: SafeJSContext, query: HandleValue) -> Fallible> { let serialized_query = IDBObjectStore::convert_value_to_key(cx, query, None); serialized_query.and_then(|q| { IDBRequest::execute_async(self, AsyncOperation::GetItem(q), None, CanGc::note()) }) } // https://www.w3.org/TR/IndexedDB-2/#dom-idbobjectstore-getkey // fn GetKey(&self, _cx: SafeJSContext, _query: HandleValue) -> DomRoot { // unimplemented!(); // } // https://www.w3.org/TR/IndexedDB-2/#dom-idbobjectstore-getall // fn GetAll( // &self, // _cx: SafeJSContext, // _query: HandleValue, // _count: Option, // ) -> DomRoot { // unimplemented!(); // } // https://www.w3.org/TR/IndexedDB-2/#dom-idbobjectstore-getallkeys // fn GetAllKeys( // &self, // _cx: SafeJSContext, // _query: HandleValue, // _count: Option, // ) -> DomRoot { // unimplemented!(); // } // https://www.w3.org/TR/IndexedDB-2/#dom-idbobjectstore-count fn Count(&self, cx: SafeJSContext, query: HandleValue) -> Fallible> { // Step 1 let transaction = self.transaction.get().expect("Could not get transaction"); // Step 2 // FIXME(arihant2math): investigate further // Step 3 // FIXME(arihant2math): Cannot tell if store has been deleted // Step 4 if !transaction.is_active() { return Err(Error::TransactionInactive); } // Step 5 let _serialized_query = IDBObjectStore::convert_value_to_key(cx, query, None); // Step 6 // match serialized_query { // Ok(q) => IDBRequest::execute_async(&*self, AsyncOperation::Count(q), None), // Err(e) => Err(e), // } Err(Error::NotSupported) } // https://www.w3.org/TR/IndexedDB-2/#dom-idbobjectstore-name fn Name(&self) -> DOMString { self.name.borrow().clone() } // https://www.w3.org/TR/IndexedDB-2/#dom-idbobjectstore-setname fn SetName(&self, value: DOMString) { *self.name.borrow_mut() = value; } // https://www.w3.org/TR/IndexedDB-2/#dom-idbobjectstore-keypath // fn KeyPath(&self, _cx: SafeJSContext, _val: MutableHandleValue) { // unimplemented!(); // } // https://www.w3.org/TR/IndexedDB-2/#dom-idbobjectstore-indexnames // fn IndexNames(&self) -> DomRoot { // unimplemented!(); // } // https://www.w3.org/TR/IndexedDB-2/#dom-idbobjectstore-transaction // fn Transaction(&self) -> DomRoot { // unimplemented!(); // } // https://www.w3.org/TR/IndexedDB-2/#dom-idbobjectstore-autoincrement fn AutoIncrement(&self) -> bool { // FIXME(arihant2math): This is wrong self.auto_increment } }