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