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