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