Skip to main content

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