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