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