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, IsArrayBufferObject, IsDetachedArrayBufferObject, JS_GetArrayBufferViewBuffer,
13    JS_GetStringLength, JS_IsArrayBufferViewObject, NewArrayObject1, PropertyKey,
14};
15use js::jsval::{DoubleValue, ObjectValue, UndefinedValue};
16use js::rust::wrappers::SameValue;
17use js::rust::wrappers2::{
18    GetArrayLength, IsArrayObject, JS_HasOwnPropertyById, JS_IndexToId, JS_IsIdentifier,
19    JS_NewObject, NewDateObject, ObjectIsDate,
20};
21use js::rust::{HandleValue, MutableHandleValue};
22use js::typedarray::{ArrayBuffer, ArrayBufferView, CreateWith};
23use storage_traits::indexeddb::{BackendError, 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    get_property_jsval, root_from_handlevalue, root_from_object,
30};
31use crate::dom::bindings::error::Error;
32use crate::dom::bindings::str::DOMString;
33use crate::dom::bindings::utils::{
34    define_dictionary_property, get_dictionary_property, has_own_property,
35};
36use crate::dom::blob::Blob;
37use crate::dom::file::File;
38use crate::dom::idbkeyrange::IDBKeyRange;
39use crate::dom::idbobjectstore::KeyPath;
40use crate::script_runtime::CanGc;
41
42// https://www.w3.org/TR/IndexedDB-3/#convert-key-to-value
43#[expect(unsafe_code)]
44pub fn key_type_to_jsval(
45    cx: &mut JSContext,
46    key: &IndexedDBKeyType,
47    mut result: MutableHandleValue,
48) {
49    // Step 1. Let type be key’s type.
50    // Step 2. Let value be key’s value.
51    // Step 3. Switch on type:
52    match key {
53        // Step 3. If type is number, return an ECMAScript Number value equal to value.
54        IndexedDBKeyType::Number(n) => result.set(DoubleValue(*n)),
55
56        // Step 3. If type is string, return an ECMAScript String value equal to value.
57        IndexedDBKeyType::String(s) => s.safe_to_jsval(cx, result),
58
59        IndexedDBKeyType::Date(d) => unsafe {
60            // Step 3.1. Let date be the result of executing the ECMAScript Date
61            // constructor with the single argument value.
62            let date = NewDateObject(cx, ClippedTime { t: *d });
63
64            // Step 3.2. Assert: date is not an abrupt completion.
65            assert!(
66                !date.is_null(),
67                "Failed to convert IndexedDB date key into a Date"
68            );
69
70            // Step 3.3. Return date.
71            date.safe_to_jsval(cx, result);
72        },
73
74        IndexedDBKeyType::Binary(b) => unsafe {
75            // Step 3.1. Let len be value’s length.
76            let len = b.len();
77
78            // Step 3.2. Let buffer be the result of executing the ECMAScript
79            // ArrayBuffer constructor with len.
80            rooted!(&in(cx) let mut buffer = ptr::null_mut::<js::jsapi::JSObject>());
81            assert!(
82                ArrayBuffer::create(cx.raw_cx(), CreateWith::Length(len), buffer.handle_mut())
83                    .is_ok(),
84                "Failed to convert IndexedDB binary key into an ArrayBuffer"
85            );
86
87            // Step 3.3. Assert: buffer is not an abrupt completion.
88
89            // Step 3.4. Set the entries in buffer’s [[ArrayBufferData]] internal slot to the
90            // entries in value.
91            let mut array_buffer = ArrayBuffer::from(buffer.get())
92                .expect("ArrayBuffer::create should create an ArrayBuffer object");
93            array_buffer.as_mut_slice().copy_from_slice(b);
94
95            // Step 3.5. Return buffer.
96            result.set(ObjectValue(buffer.get()));
97        },
98
99        IndexedDBKeyType::Array(a) => unsafe {
100            // Step 3.1. Let array be the result of executing the ECMAScript Array
101            // constructor with no arguments.
102            rooted!(&in(cx) let array = NewArrayObject1(cx.raw_cx(), 0));
103
104            // Step 3.2. Assert: array is not an abrupt completion.
105            assert!(
106                !array.get().is_null(),
107                "Failed to convert IndexedDB array key into an Array"
108            );
109
110            // Step 3.3. Let len be value’s size.
111            let len = a.len();
112
113            // Step 3.4. Let index be 0.
114            let mut index = 0;
115
116            // Step 3.5. While index is less than len:
117            while index < len {
118                // Step 3.5.1. Let entry be the result of converting a key to a value with
119                // value[index].
120                rooted!(&in(cx) let mut entry = UndefinedValue());
121                key_type_to_jsval(cx, &a[index], entry.handle_mut());
122
123                // Step 3.5.2. Let status be CreateDataProperty(array, index, entry).
124                let index_property = CString::new(index.to_string());
125                assert!(
126                    index_property.is_ok(),
127                    "Failed to convert IndexedDB array index to CString"
128                );
129                let index_property = index_property.unwrap();
130                let status = define_dictionary_property(
131                    cx.into(),
132                    array.handle(),
133                    index_property.as_c_str(),
134                    entry.handle(),
135                );
136
137                // Step 3.5.3. Assert: status is true.
138                assert!(
139                    status.is_ok(),
140                    "CreateDataProperty on a fresh JS array should not fail"
141                );
142
143                // Step 3.5.4. Increase index by 1.
144                index += 1;
145            }
146
147            // Step 3.6. Return array.
148            result.set(ObjectValue(array.get()));
149        },
150    }
151}
152
153/// <https://www.w3.org/TR/IndexedDB-3/#valid-key-path>
154pub(crate) fn is_valid_key_path(
155    cx: &mut JSContext,
156    key_path: &StrOrStringSequence,
157) -> Result<bool, Error> {
158    // <https://tc39.es/ecma262/#prod-IdentifierName>
159    #[expect(unsafe_code)]
160    let is_identifier_name = |cx: &mut JSContext, name: &str| -> Result<bool, Error> {
161        rooted!(&in(cx) let mut value = UndefinedValue());
162        name.safe_to_jsval(cx, value.handle_mut());
163        rooted!(&in(cx) let string = value.to_string());
164
165        unsafe {
166            let mut is_identifier = false;
167            if !JS_IsIdentifier(cx, string.handle(), &mut is_identifier) {
168                return Err(Error::JSFailed);
169            }
170            Ok(is_identifier)
171        }
172    };
173
174    // A valid key path is one of:
175    let is_valid = |cx: &mut JSContext, path: &DOMString| -> Result<bool, Error> {
176        // An empty string.
177        let is_empty_string = path.is_empty();
178
179        // An identifier, which is a string matching the IdentifierName production from the
180        // ECMAScript Language Specification [ECMA-262].
181        let is_identifier = is_identifier_name(cx, &path.str())?;
182
183        // A string consisting of two or more identifiers separated by periods (U+002E FULL STOP).
184        let is_identifier_list = path
185            .str()
186            .split('.')
187            .map(|s| is_identifier_name(cx, s))
188            .try_collect::<bool, Vec<bool>, Error>()?
189            .iter()
190            .all(|&value| value);
191
192        Ok(is_empty_string || is_identifier || is_identifier_list)
193    };
194
195    match key_path {
196        StrOrStringSequence::StringSequence(paths) => {
197            // A non-empty list containing only strings conforming to the above requirements.
198            if paths.is_empty() {
199                Ok(false)
200            } else {
201                Ok(paths
202                    .iter()
203                    .map(|s| is_valid(cx, s))
204                    .try_collect::<bool, Vec<bool>, Error>()?
205                    .iter()
206                    .all(|&value| value))
207            }
208        },
209        StrOrStringSequence::String(path) => is_valid(cx, path),
210    }
211}
212
213pub(crate) enum ConversionResult {
214    Valid(IndexedDBKeyType),
215    Invalid,
216}
217
218impl ConversionResult {
219    pub fn into_result(self) -> Result<IndexedDBKeyType, Error> {
220        match self {
221            ConversionResult::Valid(key) => Ok(key),
222            ConversionResult::Invalid => Err(Error::Data(None)),
223        }
224    }
225}
226
227// https://www.w3.org/TR/IndexedDB-3/#convert-value-to-key
228#[expect(unsafe_code)]
229pub fn convert_value_to_key(
230    cx: &mut JSContext,
231    input: HandleValue,
232    seen: Option<Vec<HandleValue>>,
233) -> Result<ConversionResult, Error> {
234    // Step 1: If seen was not given, then let seen be a new empty set.
235    let mut seen = seen.unwrap_or_default();
236
237    // Step 2: If seen contains input, then return "invalid value".
238    for seen_input in &seen {
239        let mut same = false;
240        if unsafe { !SameValue(cx.raw_cx(), *seen_input, input, &mut same) } {
241            return Err(Error::JSFailed);
242        }
243        if same {
244            return Ok(ConversionResult::Invalid);
245        }
246    }
247
248    // Step 3. Jump to the appropriate step below.
249
250    // If Type(input) is Number:
251    if input.is_number() {
252        // 3.1. If input is NaN then return "invalid value".
253        if input.to_number().is_nan() {
254            return Ok(ConversionResult::Invalid);
255        }
256        // 3.2. Otherwise, return a new key with type number and value input.
257        return Ok(ConversionResult::Valid(IndexedDBKeyType::Number(
258            input.to_number(),
259        )));
260    }
261
262    // If Type(input) is String:
263    if input.is_string() {
264        // 3.1. Return a new key with type string and value input.
265        let string_ptr = std::ptr::NonNull::new(input.to_string()).unwrap();
266        let key = unsafe { jsstr_to_string(cx.raw_cx(), string_ptr) };
267        return Ok(ConversionResult::Valid(IndexedDBKeyType::String(key)));
268    }
269
270    if input.is_object() {
271        rooted!(&in(cx) let object = input.to_object());
272        unsafe {
273            let mut is_date = false;
274            if !ObjectIsDate(cx, object.handle(), &mut is_date) {
275                return Err(Error::JSFailed);
276            }
277
278            // If input is a Date (has a [[DateValue]] internal slot):
279            if is_date {
280                // 3.1. Let ms be the value of input's [[DateValue]] internal slot.
281                let mut ms = f64::NAN;
282                if !js::rust::wrappers2::DateGetMsecSinceEpoch(cx, object.handle(), &mut ms) {
283                    return Err(Error::JSFailed);
284                }
285                // 3.2. If ms is NaN then return "invalid value".
286                if ms.is_nan() {
287                    return Ok(ConversionResult::Invalid);
288                }
289                // 3.3. Otherwise, return a new key with type date and value ms.
290                return Ok(ConversionResult::Valid(IndexedDBKeyType::Date(ms)));
291            }
292
293            // If input is a buffer source type:
294            if IsArrayBufferObject(*object) || JS_IsArrayBufferViewObject(*object) {
295                let is_detached = if IsArrayBufferObject(*object) {
296                    IsDetachedArrayBufferObject(*object)
297                } else {
298                    // Shared ArrayBuffers are not supported here, so this stays false.
299                    let mut is_shared = false;
300                    rooted!(
301                        in (cx.raw_cx()) let view_buffer =
302                            JS_GetArrayBufferViewBuffer(
303                                cx.raw_cx(),
304                                object.handle().into(),
305                                &mut is_shared
306                            )
307                    );
308                    !is_shared && IsDetachedArrayBufferObject(*view_buffer.handle())
309                };
310                // 3.1. If input is detached then return "invalid value".
311                if is_detached {
312                    return Ok(ConversionResult::Invalid);
313                }
314                // 3.2. Let bytes be the result of getting a copy of the bytes held
315                // by the buffer source input.
316                let bytes = if IsArrayBufferObject(*object) {
317                    let array_buffer = ArrayBuffer::from(*object).map_err(|()| Error::JSFailed)?;
318                    array_buffer.to_vec()
319                } else {
320                    let array_buffer_view =
321                        ArrayBufferView::from(*object).map_err(|()| Error::JSFailed)?;
322                    array_buffer_view.to_vec()
323                };
324                // 3.3. Return a new key with type binary and value bytes.
325                return Ok(ConversionResult::Valid(IndexedDBKeyType::Binary(bytes)));
326            }
327
328            // If input is an Array exotic object:
329            let mut is_array = false;
330            if !IsArrayObject(cx, input, &mut is_array) {
331                return Err(Error::JSFailed);
332            }
333            if is_array {
334                // 3.1. Let len be ? ToLength( ? Get(input, "length")).
335                let mut len = 0;
336                if !GetArrayLength(cx, object.handle(), &mut len) {
337                    return Err(Error::JSFailed);
338                }
339                // 3.2. Append input to seen.
340                seen.push(input);
341                // 3.3. Let keys be a new empty list.
342                let mut keys = vec![];
343                // 3.4. Let index be 0.
344                let mut index: u32 = 0;
345                // 3.5. While index is less than len:
346                while index < len {
347                    rooted!(&in(cx) let mut id: PropertyKey);
348                    if !JS_IndexToId(cx, index, id.handle_mut()) {
349                        return Err(Error::JSFailed);
350                    }
351                    // 3.5.1. Let hop be ? HasOwnProperty(input, index).
352                    let mut hop = false;
353                    if !JS_HasOwnPropertyById(cx, object.handle(), id.handle(), &mut hop) {
354                        return Err(Error::JSFailed);
355                    }
356                    // 3.5.2. If hop is false, return "invalid value".
357                    if !hop {
358                        return Ok(ConversionResult::Invalid);
359                    }
360                    // 3.5.3. Let entry be ? Get(input, index).
361                    rooted!(&in(cx) let mut entry = UndefinedValue());
362                    if !js::rust::wrappers2::JS_GetPropertyById(
363                        cx,
364                        object.handle(),
365                        id.handle(),
366                        entry.handle_mut(),
367                    ) {
368                        return Err(Error::JSFailed);
369                    }
370
371                    // 3.5.4. Let key be the result of converting a value to a key
372                    //        with arguments entry and seen.
373                    // 3.5.5. ReturnIfAbrupt(key).
374                    let key = match convert_value_to_key(cx, entry.handle(), Some(seen.clone()))? {
375                        ConversionResult::Valid(key) => key,
376                        // 3.5.6. If key is "invalid value" or "invalid type"
377                        //        abort these steps and return "invalid value".
378                        ConversionResult::Invalid => return Ok(ConversionResult::Invalid),
379                    };
380                    // 3.5.7. Append key to keys.
381                    keys.push(key);
382                    // 3.5.8. Increase index by 1.
383                    index += 1;
384                }
385                // 3.6. Return a new array key with value keys.
386                return Ok(ConversionResult::Valid(IndexedDBKeyType::Array(keys)));
387            }
388        }
389    }
390
391    // Otherwise, return "invalid type".
392    Ok(ConversionResult::Invalid)
393}
394
395/// <https://www.w3.org/TR/IndexedDB-3/#convert-a-value-to-a-key-range>
396#[expect(unsafe_code)]
397pub fn convert_value_to_key_range(
398    cx: &mut JSContext,
399    input: HandleValue,
400    null_disallowed: Option<bool>,
401) -> Result<IndexedDBKeyRange, Error> {
402    // Step 1. If value is a key range, return value.
403    if input.is_object() {
404        rooted!(&in(cx) let object = input.to_object());
405        unsafe {
406            if let Ok(obj) = root_from_object::<IDBKeyRange>(object.get(), cx.raw_cx()) {
407                let obj = obj.inner().clone();
408                return Ok(obj);
409            }
410        }
411    }
412
413    // Step 2. If value is undefined or is null, then throw a "DataError" DOMException if null
414    // disallowed flag is set, or return an unbounded key range otherwise.
415    if input.get().is_undefined() || input.get().is_null() {
416        if null_disallowed.is_some_and(|flag| flag) {
417            return Err(Error::Data(None));
418        } else {
419            return Ok(IndexedDBKeyRange {
420                lower: None,
421                upper: None,
422                lower_open: Default::default(),
423                upper_open: Default::default(),
424            });
425        }
426    }
427
428    // Step 3. Let key be the result of running the steps to convert a value to a key with value.
429    // Rethrow any exceptions.
430    let key = convert_value_to_key(cx, input, None)?;
431
432    // Step 4. If key is invalid, throw a "DataError" DOMException.
433    let key = key.into_result()?;
434
435    // Step 5. Return a key range containing only key.
436    Ok(IndexedDBKeyRange::only(key))
437}
438
439pub(crate) fn map_backend_error_to_dom_error(error: BackendError) -> Error {
440    match error {
441        BackendError::QuotaExceeded => Error::QuotaExceeded {
442            quota: None,
443            requested: None,
444        },
445        BackendError::DbErr(details) => {
446            Error::Operation(Some(format!("IndexedDB open failed: {details}")))
447        },
448        other => Error::Operation(Some(format!("IndexedDB open failed: {other:?}"))),
449    }
450}
451
452/// The result of steps in
453/// <https://www.w3.org/TR/IndexedDB-3/#evaluate-a-key-path-on-a-value>
454pub(crate) enum EvaluationResult {
455    Success,
456    Failure,
457}
458
459/// <https://www.w3.org/TR/IndexedDB-3/#evaluate-a-key-path-on-a-value>
460#[expect(unsafe_code)]
461pub(crate) fn evaluate_key_path_on_value(
462    cx: &mut JSContext,
463    value: HandleValue,
464    key_path: &KeyPath,
465    mut return_val: MutableHandleValue,
466) -> Result<EvaluationResult, Error> {
467    match key_path {
468        // Step 1. If keyPath is a list of strings, then:
469        KeyPath::StringSequence(key_path) => {
470            // Step 1.1. Let result be a new Array object created as if by the expression [].
471            rooted!(&in(cx) let mut result = unsafe { JS_NewObject(cx, ptr::null()) });
472
473            // Step 1.2. Let i be 0.
474            // Step 1.3. For each item in keyPath:
475            for (i, item) in key_path.iter().enumerate() {
476                // Step 1.3.1. Let key be the result of recursively running the steps to evaluate a key
477                // path on a value using item as keyPath and value as value.
478                // Step 1.3.2. Assert: key is not an abrupt completion.
479                // Step 1.3.3. If key is failure, abort the overall algorithm and return failure.
480                rooted!(&in(cx) let mut key = UndefinedValue());
481                if let EvaluationResult::Failure = evaluate_key_path_on_value(
482                    cx,
483                    value,
484                    &KeyPath::String(item.clone()),
485                    key.handle_mut(),
486                )? {
487                    return Ok(EvaluationResult::Failure);
488                };
489
490                // Step 1.3.4. Let p be ! ToString(i).
491                // Step 1.3.5. Let status be CreateDataProperty(result, p, key).
492                // Step 1.3.6. Assert: status is true.
493                let i_cstr = std::ffi::CString::new(i.to_string()).unwrap();
494                define_dictionary_property(
495                    cx.into(),
496                    result.handle(),
497                    i_cstr.as_c_str(),
498                    key.handle(),
499                )
500                .map_err(|_| Error::JSFailed)?;
501
502                // Step 1.3.7. Increase i by 1.
503                // Done by for loop with enumerate()
504            }
505
506            // Step 1.4. Return result.
507            result.safe_to_jsval(cx, return_val);
508        },
509        KeyPath::String(key_path) => {
510            // Step 2. If keyPath is the empty string, return value and skip the remaining steps.
511            if key_path.is_empty() {
512                return_val.set(*value);
513                return Ok(EvaluationResult::Success);
514            }
515
516            // NOTE: Use current_value, instead of value described in spec, in the following steps.
517            rooted!(&in(cx) let mut current_value = *value);
518
519            // Step 3. Let identifiers be the result of strictly splitting keyPath on U+002E
520            // FULL STOP characters (.).
521            // Step 4. For each identifier of identifiers, jump to the appropriate step below:
522            for identifier in key_path.str().split('.') {
523                // If Type(value) is String, and identifier is "length"
524                if identifier == "length" && current_value.is_string() {
525                    // Let value be a Number equal to the number of elements in value.
526                    rooted!(&in(cx) let string_value = current_value.to_string());
527                    unsafe {
528                        let string_length = JS_GetStringLength(*string_value) as u64;
529                        string_length.safe_to_jsval(cx, current_value.handle_mut());
530                    }
531                    continue;
532                }
533
534                // If value is an Array and identifier is "length"
535                if identifier == "length" {
536                    unsafe {
537                        let mut is_array = false;
538                        if !IsArrayObject(cx, current_value.handle(), &mut is_array) {
539                            return Err(Error::JSFailed);
540                        }
541                        if is_array {
542                            // Let value be ! ToLength(! Get(value, "length")).
543                            rooted!(&in(cx) let object = current_value.to_object());
544                            get_property_jsval(
545                                cx.into(),
546                                object.handle(),
547                                c"length",
548                                current_value.handle_mut(),
549                            )?;
550
551                            continue;
552                        }
553                    }
554                }
555
556                // If value is a Blob and identifier is "size"
557                if identifier == "size" {
558                    if let Ok(blob) =
559                        root_from_handlevalue::<Blob>(current_value.handle(), cx.into())
560                    {
561                        // Let value be a Number equal to value’s size.
562                        blob.Size().safe_to_jsval(cx, current_value.handle_mut());
563
564                        continue;
565                    }
566                }
567
568                // If value is a Blob and identifier is "type"
569                if identifier == "type" {
570                    if let Ok(blob) =
571                        root_from_handlevalue::<Blob>(current_value.handle(), cx.into())
572                    {
573                        // Let value be a String equal to value’s type.
574                        blob.Type().safe_to_jsval(cx, current_value.handle_mut());
575
576                        continue;
577                    }
578                }
579
580                // If value is a File and identifier is "name"
581                if identifier == "name" {
582                    if let Ok(file) =
583                        root_from_handlevalue::<File>(current_value.handle(), cx.into())
584                    {
585                        // Let value be a String equal to value’s name.
586                        file.name().safe_to_jsval(cx, current_value.handle_mut());
587
588                        continue;
589                    }
590                }
591
592                // If value is a File and identifier is "lastModified"
593                if identifier == "lastModified" {
594                    if let Ok(file) =
595                        root_from_handlevalue::<File>(current_value.handle(), cx.into())
596                    {
597                        // Let value be a Number equal to value’s lastModified.
598                        file.LastModified()
599                            .safe_to_jsval(cx, current_value.handle_mut());
600
601                        continue;
602                    }
603                }
604
605                // If value is a File and identifier is "lastModifiedDate"
606                if identifier == "lastModifiedDate" {
607                    if let Ok(file) =
608                        root_from_handlevalue::<File>(current_value.handle(), cx.into())
609                    {
610                        // Let value be a new Date object with [[DateValue]] internal slot equal to value’s lastModified.
611                        let time = ClippedTime {
612                            t: file.LastModified() as f64,
613                        };
614                        unsafe {
615                            NewDateObject(cx, time).safe_to_jsval(cx, current_value.handle_mut());
616                        }
617
618                        continue;
619                    }
620                }
621
622                // Otherwise
623                unsafe {
624                    // If Type(value) is not Object, return failure.
625                    if !current_value.is_object() {
626                        return Ok(EvaluationResult::Failure);
627                    }
628
629                    rooted!(&in(cx) let object = current_value.to_object());
630                    let identifier_name =
631                        CString::new(identifier).expect("Failed to convert str to CString");
632
633                    // Let hop be ! HasOwnProperty(value, identifier).
634                    let hop =
635                        has_own_property(cx.into(), object.handle(), identifier_name.as_c_str())
636                            .map_err(|_| Error::JSFailed)?;
637
638                    // If hop is false, return failure.
639                    if !hop {
640                        return Ok(EvaluationResult::Failure);
641                    }
642
643                    // Let value be ! Get(value, identifier).
644                    match get_dictionary_property(
645                        cx.raw_cx(),
646                        object.handle(),
647                        identifier_name.as_c_str(),
648                        current_value.handle_mut(),
649                        CanGc::deprecated_note(),
650                    ) {
651                        Ok(true) => {},
652                        Ok(false) => return Ok(EvaluationResult::Failure),
653                        Err(()) => return Err(Error::JSFailed),
654                    }
655
656                    // If value is undefined, return failure.
657                    if current_value.get().is_undefined() {
658                        return Ok(EvaluationResult::Failure);
659                    }
660                }
661            }
662
663            // Step 5. Assert: value is not an abrupt completion.
664            // Done within Step 4.
665
666            // Step 6. Return value.
667            return_val.set(*current_value);
668        },
669    }
670    Ok(EvaluationResult::Success)
671}
672
673/// The result of steps in
674/// <https://www.w3.org/TR/IndexedDB-3/#extract-a-key-from-a-value-using-a-key-path>
675pub(crate) enum ExtractionResult {
676    Key(IndexedDBKeyType),
677    Invalid,
678    Failure,
679}
680
681/// <https://w3c.github.io/IndexedDB/#check-that-a-key-could-be-injected-into-a-value>
682#[expect(unsafe_code)]
683pub(crate) fn can_inject_key_into_value(
684    cx: &mut JSContext,
685    value: HandleValue,
686    key_path: &DOMString,
687) -> Result<bool, Error> {
688    // Step 1. Let identifiers be the result of strictly splitting keyPath on U+002E FULL STOP
689    // characters (.).
690    let key_path_string = key_path.str();
691    let mut identifiers: Vec<&str> = key_path_string.split('.').collect();
692
693    // Step 2. Assert: identifiers is not empty.
694    let Some(_) = identifiers.pop() else {
695        return Ok(false);
696    };
697
698    rooted!(&in(cx) let mut current_value = *value);
699
700    // Step 3. For each remaining identifier of identifiers:
701    for identifier in identifiers {
702        // Step 3.1. If value is not an Object or an Array, return false.
703        if !current_value.is_object() {
704            return Ok(false);
705        }
706
707        rooted!(&in(cx) let current_object = current_value.to_object());
708        let identifier_name =
709            CString::new(identifier).expect("Failed to convert key path identifier to CString");
710
711        // Step 3.2. Let hop be ? HasOwnProperty(value, identifier).
712        let hop = has_own_property(
713            cx.into(),
714            current_object.handle(),
715            identifier_name.as_c_str(),
716        )
717        .map_err(|_| Error::JSFailed)?;
718
719        // Step 3.3. If hop is false, set value to a new Object created as if by the expression
720        // ({}).
721        // We avoid mutating `value` during this check and can return true immediately because the
722        // remaining path can be created from scratch.
723        if !hop {
724            return Ok(true);
725        }
726
727        // Step 3.4. Set value to ? Get(value, identifier).
728        match unsafe {
729            get_dictionary_property(
730                cx.raw_cx(),
731                current_object.handle(),
732                identifier_name.as_c_str(),
733                current_value.handle_mut(),
734                CanGc::deprecated_note(),
735            )
736        } {
737            Ok(true) => {},
738            Ok(false) => return Ok(false),
739            Err(()) => return Err(Error::JSFailed),
740        }
741    }
742
743    // Step 4. Return true if value is an Object or an Array, and false otherwise.
744    Ok(current_value.is_object())
745}
746
747/// <https://w3c.github.io/IndexedDB/#inject-a-key-into-a-value-using-a-key-path>
748#[expect(unsafe_code)]
749pub(crate) fn inject_key_into_value(
750    cx: &mut JSContext,
751    value: HandleValue,
752    key: &IndexedDBKeyType,
753    key_path: &DOMString,
754) -> Result<bool, Error> {
755    // Step 1. Let identifiers be the result of strictly splitting keyPath on U+002E FULL STOP characters (.).
756    let key_path_string = key_path.str();
757    let mut identifiers: Vec<&str> = key_path_string.split('.').collect();
758
759    // Step 2. Assert: identifiers is not empty.
760    let Some(last) = identifiers.pop() else {
761        return Ok(false);
762    };
763
764    // Step 3. Let last be the last item of identifiers and remove it from the list.
765    // Done by `pop()` above.
766
767    rooted!(&in(cx) let mut current_value = *value);
768
769    // Step 4. For each remaining identifier of identifiers:
770    for identifier in identifiers {
771        // Step 4.1 Assert: value is an Object or an Array.
772        if !current_value.is_object() {
773            return Ok(false);
774        }
775
776        rooted!(&in(cx) let current_object = current_value.to_object());
777        let identifier_name =
778            CString::new(identifier).expect("Failed to convert key path identifier to CString");
779
780        // Step 4.2 Let hop be ! HasOwnProperty(value, identifier).
781        let hop = has_own_property(
782            cx.into(),
783            current_object.handle(),
784            identifier_name.as_c_str(),
785        )
786        .map_err(|_| Error::JSFailed)?;
787
788        // Step 4.3 If hop is false, then:
789        if !hop {
790            // Step 4.3.1 Let o be a new Object created as if by the expression ({}).
791            rooted!(&in(cx) let o = unsafe { JS_NewObject(cx, ptr::null()) });
792            rooted!(&in(cx) let mut o_value = UndefinedValue());
793            o.safe_to_jsval(cx, o_value.handle_mut());
794
795            // Step 4.3.2 Let status be CreateDataProperty(value, identifier, o).
796            define_dictionary_property(
797                cx.into(),
798                current_object.handle(),
799                identifier_name.as_c_str(),
800                o_value.handle(),
801            )
802            .map_err(|_| Error::JSFailed)?;
803
804            // Step 4.3.3 Assert: status is true.
805        }
806
807        // Step 4.3 Let value be ! Get(value, identifier).
808        match unsafe {
809            get_dictionary_property(
810                cx.raw_cx(),
811                current_object.handle(),
812                identifier_name.as_c_str(),
813                current_value.handle_mut(),
814                CanGc::deprecated_note(),
815            )
816        } {
817            Ok(true) => {},
818            Ok(false) => return Ok(false),
819            Err(()) => return Err(Error::JSFailed),
820        }
821
822        // Step 5 "Assert: value is an Object or an Array."
823        if !current_value.is_object() {
824            return Ok(false);
825        }
826    }
827
828    // Step 6. Let keyValue be the result of converting a key to a value with key.
829    rooted!(&in(cx) let mut key_value = UndefinedValue());
830    key_type_to_jsval(cx, key, key_value.handle_mut());
831
832    // `current_value` is the parent object where `last` will be defined.
833    if !current_value.is_object() {
834        return Ok(false);
835    }
836    rooted!(&in(cx) let parent_object = current_value.to_object());
837    let last_name = CString::new(last).expect("Failed to convert final key path identifier");
838
839    // Step 7. Let status be CreateDataProperty(value, last, keyValue).
840    define_dictionary_property(
841        cx.into(),
842        parent_object.handle(),
843        last_name.as_c_str(),
844        key_value.handle(),
845    )
846    .map_err(|_| Error::JSFailed)?;
847
848    // Step 8. Assert: status is true.
849    // The JS_DefineProperty success check above enforces this assertion.
850    // "NOTE: Assertions can be made in the above steps because this algorithm is only applied to values that are the output of StructuredDeserialize, and the steps to check that a key could be injected into a value have been run."
851    Ok(true)
852}
853
854/// <https://www.w3.org/TR/IndexedDB-3/#extract-a-key-from-a-value-using-a-key-path>
855pub(crate) fn extract_key(
856    cx: &mut JSContext,
857    value: HandleValue,
858    key_path: &KeyPath,
859    multi_entry: Option<bool>,
860) -> Result<ExtractionResult, Error> {
861    // Step 1. Let r be the result of running the steps to evaluate a key path on a value with
862    // value and keyPath. Rethrow any exceptions.
863    // Step 2. If r is failure, return failure.
864    rooted!(&in(cx) let mut r = UndefinedValue());
865    if let EvaluationResult::Failure =
866        evaluate_key_path_on_value(cx, value, key_path, r.handle_mut())?
867    {
868        return Ok(ExtractionResult::Failure);
869    }
870
871    // Step 3. Let key be the result of running the steps to convert a value to a key with r if the
872    // multiEntry flag is unset, and the result of running the steps to convert a value to a
873    // multiEntry key with r otherwise. Rethrow any exceptions.
874    let key = match multi_entry {
875        Some(true) => {
876            // TODO: implement convert_value_to_multientry_key
877            unimplemented!("multiEntry keys are not yet supported");
878        },
879        _ => match convert_value_to_key(cx, r.handle(), None)? {
880            ConversionResult::Valid(key) => key,
881            // Step 4. If key is invalid, return invalid.
882            ConversionResult::Invalid => return Ok(ExtractionResult::Invalid),
883        },
884    };
885
886    // Step 5. Return key.
887    Ok(ExtractionResult::Key(key))
888}