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