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