Skip to main content

script/
indexeddb.rs

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