script/
indexed_db.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::ffi::CString;
6use std::ptr;
7
8use ipc_channel::ipc::IpcSender;
9use itertools::Itertools;
10use js::conversions::jsstr_to_string;
11use js::jsapi::{
12    ClippedTime, ESClass, GetArrayLength, GetBuiltinClass, Heap, IsArrayBufferObject,
13    JS_GetStringLength, JS_HasOwnPropertyById, JS_IndexToId, JS_IsArrayBufferViewObject,
14    JS_NewObject, NewDateObject, PropertyKey,
15};
16use js::jsval::{DoubleValue, JSVal, UndefinedValue};
17use js::rust::wrappers::{IsArrayObject, JS_GetProperty, JS_HasOwnProperty, JS_IsIdentifier};
18use js::rust::{HandleValue, IntoHandle, IntoMutableHandle, MutableHandleValue};
19use profile_traits::ipc;
20use profile_traits::ipc::IpcReceiver;
21use script_bindings::script_runtime::CanGc;
22use serde::{Deserialize, Serialize};
23use storage_traits::indexeddb_thread::{BackendResult, IndexedDBKeyRange, IndexedDBKeyType};
24
25use crate::dom::bindings::codegen::Bindings::BlobBinding::BlobMethods;
26use crate::dom::bindings::codegen::Bindings::FileBinding::FileMethods;
27use crate::dom::bindings::codegen::UnionTypes::StringOrStringSequence as StrOrStringSequence;
28use crate::dom::bindings::conversions::{
29    SafeToJSValConvertible, get_property_jsval, root_from_handlevalue, root_from_object,
30};
31use crate::dom::bindings::error::Error;
32use crate::dom::bindings::import::module::SafeJSContext;
33use crate::dom::bindings::root::DomRoot;
34use crate::dom::bindings::str::DOMString;
35use crate::dom::bindings::structuredclone;
36use crate::dom::bindings::utils::set_dictionary_property;
37use crate::dom::blob::Blob;
38use crate::dom::file::File;
39use crate::dom::globalscope::GlobalScope;
40use crate::dom::indexeddb::idbkeyrange::IDBKeyRange;
41use crate::dom::indexeddb::idbobjectstore::KeyPath;
42
43pub fn create_channel<T>(
44    global: DomRoot<GlobalScope>,
45) -> (IpcSender<BackendResult<T>>, IpcReceiver<BackendResult<T>>)
46where
47    T: for<'a> Deserialize<'a> + Serialize,
48{
49    ipc::channel::<BackendResult<T>>(global.time_profiler_chan().clone()).unwrap()
50}
51
52// https://www.w3.org/TR/IndexedDB-2/#convert-key-to-value
53#[allow(unsafe_code)]
54pub fn key_type_to_jsval(
55    cx: SafeJSContext,
56    key: &IndexedDBKeyType,
57    mut result: MutableHandleValue,
58    can_gc: CanGc,
59) {
60    match key {
61        IndexedDBKeyType::Number(n) => result.set(DoubleValue(*n)),
62        IndexedDBKeyType::String(s) => s.safe_to_jsval(cx, result, can_gc),
63        IndexedDBKeyType::Binary(b) => b.safe_to_jsval(cx, result, can_gc),
64        IndexedDBKeyType::Date(d) => {
65            let time = js::jsapi::ClippedTime { t: *d };
66            let date = unsafe { js::jsapi::NewDateObject(*cx, time) };
67            date.safe_to_jsval(cx, result, can_gc);
68        },
69        IndexedDBKeyType::Array(a) => {
70            rooted_vec!(let mut values);
71            for key in a {
72                rooted!(in(*cx) let mut value: JSVal);
73                key_type_to_jsval(cx, key, value.handle_mut(), can_gc);
74                values.push(Heap::boxed(value.get()));
75            }
76            values.safe_to_jsval(cx, result, can_gc);
77        },
78    }
79}
80
81/// <https://www.w3.org/TR/IndexedDB-2/#valid-key-path>
82pub(crate) fn is_valid_key_path(key_path: &StrOrStringSequence) -> Result<bool, Error> {
83    // <https://tc39.es/ecma262/#prod-IdentifierName>
84    #[allow(unsafe_code)]
85    let is_identifier_name = |name: &str| -> Result<bool, Error> {
86        let cx = GlobalScope::get_cx();
87        rooted!(in(*cx) let mut value = UndefinedValue());
88        name.safe_to_jsval(cx, value.handle_mut(), CanGc::note());
89        rooted!(in(*cx) let string = value.to_string());
90
91        unsafe {
92            let mut is_identifier = false;
93            if !JS_IsIdentifier(*cx, string.handle(), &mut is_identifier) {
94                return Err(Error::JSFailed);
95            }
96            Ok(is_identifier)
97        }
98    };
99
100    // A valid key path is one of:
101    let is_valid = |path: &DOMString| -> Result<bool, Error> {
102        // An empty string.
103        let is_empty_string = path.is_empty();
104
105        // An identifier, which is a string matching the IdentifierName production from the
106        // ECMAScript Language Specification [ECMA-262].
107        let is_identifier = is_identifier_name(&path.str())?;
108
109        // A string consisting of two or more identifiers separated by periods (U+002E FULL STOP).
110        let is_identifier_list = path
111            .str()
112            .split('.')
113            .map(is_identifier_name)
114            .try_collect::<bool, Vec<bool>, Error>()?
115            .iter()
116            .all(|&value| value);
117
118        Ok(is_empty_string || is_identifier || is_identifier_list)
119    };
120
121    match key_path {
122        StrOrStringSequence::StringSequence(paths) => {
123            // A non-empty list containing only strings conforming to the above requirements.
124            if paths.is_empty() {
125                Ok(false)
126            } else {
127                Ok(paths
128                    .iter()
129                    .map(is_valid)
130                    .try_collect::<bool, Vec<bool>, Error>()?
131                    .iter()
132                    .all(|&value| value))
133            }
134        },
135        StrOrStringSequence::String(path) => is_valid(path),
136    }
137}
138
139pub(crate) enum ConversionResult {
140    Valid(IndexedDBKeyType),
141    Invalid,
142}
143
144impl ConversionResult {
145    pub fn into_result(self) -> Result<IndexedDBKeyType, Error> {
146        match self {
147            ConversionResult::Valid(key) => Ok(key),
148            ConversionResult::Invalid => Err(Error::Data),
149        }
150    }
151}
152
153// https://www.w3.org/TR/IndexedDB-2/#convert-value-to-key
154#[allow(unsafe_code)]
155pub fn convert_value_to_key(
156    cx: SafeJSContext,
157    input: HandleValue,
158    seen: Option<Vec<HandleValue>>,
159) -> Result<ConversionResult, Error> {
160    // Step 1: If seen was not given, then let seen be a new empty set.
161    let mut seen = seen.unwrap_or_default();
162
163    // Step 2: If seen contains input, then return invalid.
164    // FIXME:(arihant2math) implement this
165    // Check if we have seen this key
166    // Does not currently work with HandleValue,
167    // as it does not implement PartialEq
168
169    // Step 3
170    // FIXME:(arihant2math) Accept array as well
171    if input.is_number() {
172        if input.to_number().is_nan() {
173            return Ok(ConversionResult::Invalid);
174        }
175        return Ok(ConversionResult::Valid(IndexedDBKeyType::Number(
176            input.to_number(),
177        )));
178    }
179
180    if input.is_string() {
181        let string_ptr = std::ptr::NonNull::new(input.to_string()).unwrap();
182        let key = unsafe { jsstr_to_string(*cx, string_ptr) };
183        return Ok(ConversionResult::Valid(IndexedDBKeyType::String(key)));
184    }
185
186    if input.is_object() {
187        rooted!(in(*cx) let object = input.to_object());
188        unsafe {
189            let mut built_in_class = ESClass::Other;
190
191            if !GetBuiltinClass(*cx, object.handle().into(), &mut built_in_class) {
192                return Err(Error::JSFailed);
193            }
194
195            if let ESClass::Date = built_in_class {
196                let mut f = f64::NAN;
197                if !js::jsapi::DateGetMsecSinceEpoch(*cx, object.handle().into(), &mut f) {
198                    return Err(Error::JSFailed);
199                }
200                if f.is_nan() {
201                    return Err(Error::Data);
202                }
203                return Ok(ConversionResult::Valid(IndexedDBKeyType::Date(f)));
204            }
205
206            if IsArrayBufferObject(*object) || JS_IsArrayBufferViewObject(*object) {
207                // FIXME:(arihant2math) implement it the correct way (is this correct?)
208                let key = structuredclone::write(cx, input, None)?;
209                return Ok(ConversionResult::Valid(IndexedDBKeyType::Binary(
210                    key.serialized.clone(),
211                )));
212            }
213
214            if let ESClass::Array = built_in_class {
215                let mut len = 0;
216                if !GetArrayLength(*cx, object.handle().into_handle(), &mut len) {
217                    return Err(Error::JSFailed);
218                }
219                seen.push(input);
220                let mut values = vec![];
221                for i in 0..len {
222                    rooted!(in(*cx) let mut id: PropertyKey);
223                    if !JS_IndexToId(*cx, i, js::jsapi::MutableHandleId::from(id.handle_mut())) {
224                        return Err(Error::JSFailed);
225                    }
226                    let mut has_own = false;
227                    if !JS_HasOwnPropertyById(
228                        *cx,
229                        object.handle().into_handle(),
230                        id.handle().into_handle(),
231                        &mut has_own,
232                    ) {
233                        return Err(Error::JSFailed);
234                    }
235                    if !has_own {
236                        return Ok(ConversionResult::Invalid);
237                    }
238                    rooted!(in(*cx) let mut item = UndefinedValue());
239                    if !js::jsapi::JS_GetPropertyById(
240                        *cx,
241                        object.handle().into_handle(),
242                        id.handle().into_handle(),
243                        item.handle_mut().into_handle_mut(),
244                    ) {
245                        return Err(Error::JSFailed);
246                    }
247                    let key = match convert_value_to_key(cx, item.handle(), Some(seen.clone()))? {
248                        ConversionResult::Valid(key) => key,
249                        ConversionResult::Invalid => return Ok(ConversionResult::Invalid),
250                    };
251                    values.push(key);
252                }
253                return Ok(ConversionResult::Valid(IndexedDBKeyType::Array(values)));
254            }
255        }
256    }
257
258    Ok(ConversionResult::Invalid)
259}
260
261/// <https://www.w3.org/TR/IndexedDB-2/#convert-a-value-to-a-key-range>
262#[allow(unsafe_code)]
263pub fn convert_value_to_key_range(
264    cx: SafeJSContext,
265    input: HandleValue,
266    null_disallowed: Option<bool>,
267) -> Result<IndexedDBKeyRange, Error> {
268    // Step 1. If value is a key range, return value.
269    if input.is_object() {
270        rooted!(in(*cx) let object = input.to_object());
271        unsafe {
272            if let Ok(obj) = root_from_object::<IDBKeyRange>(object.get(), *cx) {
273                let obj = obj.inner().clone();
274                return Ok(obj);
275            }
276        }
277    }
278
279    // Step 2. If value is undefined or is null, then throw a "DataError" DOMException if null
280    // disallowed flag is set, or return an unbounded key range otherwise.
281    if input.get().is_undefined() || input.get().is_null() {
282        if null_disallowed.is_some_and(|flag| flag) {
283            return Err(Error::Data);
284        } else {
285            return Ok(IndexedDBKeyRange {
286                lower: None,
287                upper: None,
288                lower_open: Default::default(),
289                upper_open: Default::default(),
290            });
291        }
292    }
293
294    // Step 3. Let key be the result of running the steps to convert a value to a key with value.
295    // Rethrow any exceptions.
296    let key = convert_value_to_key(cx, input, None)?;
297
298    // Step 4. If key is invalid, throw a "DataError" DOMException.
299    let key = key.into_result()?;
300
301    // Step 5. Return a key range containing only key.
302    Ok(IndexedDBKeyRange::only(key))
303}
304
305/// The result of steps in
306/// <https://www.w3.org/TR/IndexedDB-2/#evaluate-a-key-path-on-a-value>
307pub(crate) enum EvaluationResult {
308    Success,
309    Failure,
310}
311
312/// <https://www.w3.org/TR/IndexedDB-2/#evaluate-a-key-path-on-a-value>
313#[allow(unsafe_code)]
314pub(crate) fn evaluate_key_path_on_value(
315    cx: SafeJSContext,
316    value: HandleValue,
317    key_path: &KeyPath,
318    mut return_val: MutableHandleValue,
319) -> Result<EvaluationResult, Error> {
320    match key_path {
321        // Step 1. If keyPath is a list of strings, then:
322        KeyPath::StringSequence(key_path) => {
323            // Step 1.1. Let result be a new Array object created as if by the expression [].
324            rooted!(in(*cx) let mut result = unsafe { JS_NewObject(*cx, ptr::null()) });
325
326            // Step 1.2. Let i be 0.
327            // Step 1.3. For each item in keyPath:
328            for (i, item) in key_path.iter().enumerate() {
329                // Step 1.3.1. Let key be the result of recursively running the steps to evaluate a key
330                // path on a value using item as keyPath and value as value.
331                // Step 1.3.2. Assert: key is not an abrupt completion.
332                // Step 1.3.3. If key is failure, abort the overall algorithm and return failure.
333                rooted!(in(*cx) let mut key = UndefinedValue());
334                if let EvaluationResult::Failure = evaluate_key_path_on_value(
335                    cx,
336                    value,
337                    &KeyPath::String(item.clone()),
338                    key.handle_mut(),
339                )? {
340                    return Ok(EvaluationResult::Failure);
341                };
342
343                // Step 1.3.4. Let p be ! ToString(i).
344                // Step 1.3.5. Let status be CreateDataProperty(result, p, key).
345                // Step 1.3.6. Assert: status is true.
346                set_dictionary_property(cx, result.handle(), &i.to_string(), key.handle())
347                    .map_err(|_| Error::JSFailed)?;
348
349                // Step 1.3.7. Increase i by 1.
350                // Done by for loop with enumerate()
351            }
352
353            // Step 1.4. Return result.
354            result.safe_to_jsval(cx, return_val, CanGc::note());
355        },
356        KeyPath::String(key_path) => {
357            // Step 2. If keyPath is the empty string, return value and skip the remaining steps.
358            if key_path.is_empty() {
359                return_val.set(*value);
360                return Ok(EvaluationResult::Success);
361            }
362
363            // NOTE: Use current_value, instead of value described in spec, in the following steps.
364            rooted!(in(*cx) let mut current_value = *value);
365
366            // Step 3. Let identifiers be the result of strictly splitting keyPath on U+002E
367            // FULL STOP characters (.).
368            // Step 4. For each identifier of identifiers, jump to the appropriate step below:
369            for identifier in key_path.str().split('.') {
370                // If Type(value) is String, and identifier is "length"
371                if identifier == "length" && current_value.is_string() {
372                    // Let value be a Number equal to the number of elements in value.
373                    rooted!(in(*cx) let string_value = current_value.to_string());
374                    unsafe {
375                        let string_length = JS_GetStringLength(*string_value) as u64;
376                        string_length.safe_to_jsval(cx, current_value.handle_mut(), CanGc::note());
377                    }
378                    continue;
379                }
380
381                // If value is an Array and identifier is "length"
382                if identifier == "length" {
383                    unsafe {
384                        let mut is_array = false;
385                        if !IsArrayObject(*cx, current_value.handle(), &mut is_array) {
386                            return Err(Error::JSFailed);
387                        }
388                        if is_array {
389                            // Let value be ! ToLength(! Get(value, "length")).
390                            rooted!(in(*cx) let object = current_value.to_object());
391                            get_property_jsval(
392                                cx,
393                                object.handle(),
394                                "length",
395                                current_value.handle_mut(),
396                            )?;
397
398                            continue;
399                        }
400                    }
401                }
402
403                // If value is a Blob and identifier is "size"
404                if identifier == "size" {
405                    if let Ok(blob) = root_from_handlevalue::<Blob>(current_value.handle(), cx) {
406                        // Let value be a Number equal to value’s size.
407                        blob.Size()
408                            .safe_to_jsval(cx, current_value.handle_mut(), CanGc::note());
409
410                        continue;
411                    }
412                }
413
414                // If value is a Blob and identifier is "type"
415                if identifier == "type" {
416                    if let Ok(blob) = root_from_handlevalue::<Blob>(current_value.handle(), cx) {
417                        // Let value be a String equal to value’s type.
418                        blob.Type()
419                            .safe_to_jsval(cx, current_value.handle_mut(), CanGc::note());
420
421                        continue;
422                    }
423                }
424
425                // If value is a File and identifier is "name"
426                if identifier == "name" {
427                    if let Ok(file) = root_from_handlevalue::<File>(current_value.handle(), cx) {
428                        // Let value be a String equal to value’s name.
429                        file.name()
430                            .safe_to_jsval(cx, current_value.handle_mut(), CanGc::note());
431
432                        continue;
433                    }
434                }
435
436                // If value is a File and identifier is "lastModified"
437                if identifier == "lastModified" {
438                    if let Ok(file) = root_from_handlevalue::<File>(current_value.handle(), cx) {
439                        // Let value be a Number equal to value’s lastModified.
440                        file.LastModified().safe_to_jsval(
441                            cx,
442                            current_value.handle_mut(),
443                            CanGc::note(),
444                        );
445
446                        continue;
447                    }
448                }
449
450                // If value is a File and identifier is "lastModifiedDate"
451                if identifier == "lastModifiedDate" {
452                    if let Ok(file) = root_from_handlevalue::<File>(current_value.handle(), cx) {
453                        // Let value be a new Date object with [[DateValue]] internal slot equal to value’s lastModified.
454                        let time = ClippedTime {
455                            t: file.LastModified() as f64,
456                        };
457                        unsafe {
458                            NewDateObject(*cx, time).safe_to_jsval(
459                                cx,
460                                current_value.handle_mut(),
461                                CanGc::note(),
462                            );
463                        }
464
465                        continue;
466                    }
467                }
468
469                // Otherwise
470                unsafe {
471                    // If Type(value) is not Object, return failure.
472                    if !current_value.is_object() {
473                        return Ok(EvaluationResult::Failure);
474                    }
475
476                    rooted!(in(*cx) let object = current_value.to_object());
477                    let identifier_name =
478                        CString::new(identifier).expect("Failed to convert str to CString");
479
480                    // Let hop be ! HasOwnProperty(value, identifier).
481                    let mut hop = false;
482                    if !JS_HasOwnProperty(*cx, object.handle(), identifier_name.as_ptr(), &mut hop)
483                    {
484                        return Err(Error::JSFailed);
485                    }
486
487                    // If hop is false, return failure.
488                    if !hop {
489                        return Ok(EvaluationResult::Failure);
490                    }
491
492                    // Let value be ! Get(value, identifier).
493                    if !JS_GetProperty(
494                        *cx,
495                        object.handle(),
496                        identifier_name.as_ptr(),
497                        current_value.handle_mut(),
498                    ) {
499                        return Err(Error::JSFailed);
500                    }
501
502                    // If value is undefined, return failure.
503                    if current_value.get().is_undefined() {
504                        return Ok(EvaluationResult::Failure);
505                    }
506                }
507            }
508
509            // Step 5. Assert: value is not an abrupt completion.
510            // Done within Step 4.
511
512            // Step 6. Return value.
513            return_val.set(*current_value);
514        },
515    }
516    Ok(EvaluationResult::Success)
517}
518
519/// The result of steps in
520/// <https://www.w3.org/TR/IndexedDB-2/#extract-a-key-from-a-value-using-a-key-path>
521pub(crate) enum ExtractionResult {
522    Key(IndexedDBKeyType),
523    Invalid,
524    Failure,
525}
526
527/// <https://www.w3.org/TR/IndexedDB-2/#extract-a-key-from-a-value-using-a-key-path>
528pub(crate) fn extract_key(
529    cx: SafeJSContext,
530    value: HandleValue,
531    key_path: &KeyPath,
532    multi_entry: Option<bool>,
533) -> Result<ExtractionResult, Error> {
534    // Step 1. Let r be the result of running the steps to evaluate a key path on a value with
535    // value and keyPath. Rethrow any exceptions.
536    // Step 2. If r is failure, return failure.
537    rooted!(in(*cx) let mut r = UndefinedValue());
538    if let EvaluationResult::Failure =
539        evaluate_key_path_on_value(cx, value, key_path, r.handle_mut())?
540    {
541        return Ok(ExtractionResult::Failure);
542    }
543
544    // Step 3. Let key be the result of running the steps to convert a value to a key with r if the
545    // multiEntry flag is unset, and the result of running the steps to convert a value to a
546    // multiEntry key with r otherwise. Rethrow any exceptions.
547    let key = match multi_entry {
548        Some(true) => {
549            // TODO: implement convert_value_to_multientry_key
550            unimplemented!("multiEntry keys are not yet supported");
551        },
552        _ => match convert_value_to_key(cx, r.handle(), None)? {
553            ConversionResult::Valid(key) => key,
554            // Step 4. If key is invalid, return invalid.
555            ConversionResult::Invalid => return Ok(ExtractionResult::Invalid),
556        },
557    };
558
559    // Step 5. Return key.
560    Ok(ExtractionResult::Key(key))
561}