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