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