1use 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::{
29 get_property_jsval, root_from_handlevalue, root_from_object,
30};
31use crate::dom::bindings::error::Error;
32use crate::dom::bindings::str::DOMString;
33use crate::dom::bindings::utils::{define_dictionary_property, has_own_property};
34use crate::dom::blob::Blob;
35use crate::dom::file::File;
36use crate::dom::idbkeyrange::IDBKeyRange;
37use crate::dom::idbobjectstore::KeyPath;
38
39#[expect(unsafe_code)]
41pub fn key_type_to_jsval(
42 cx: &mut JSContext,
43 key: &IndexedDBKeyType,
44 mut result: MutableHandleValue,
45) {
46 match key {
50 IndexedDBKeyType::Number(n) => result.set(DoubleValue(*n)),
52
53 IndexedDBKeyType::String(s) => s.to_jsval(cx, result),
55
56 IndexedDBKeyType::Date(d) => unsafe {
57 let date = NewDateObject(cx, ClippedTime { t: *d });
60
61 assert!(
63 !date.is_null(),
64 "Failed to convert IndexedDB date key into a Date"
65 );
66
67 date.to_jsval(cx, result);
69 },
70
71 IndexedDBKeyType::Binary(b) => {
72 rooted!(&in(cx) let mut buffer = ptr::null_mut::<js::jsapi::JSObject>());
77
78 assert!(
83 create_buffer_source::<ArrayBufferU8>(cx, b, buffer.handle_mut()).is_ok(),
84 "Failed to convert IndexedDB binary key into an ArrayBuffer"
85 );
86
87 result.set(ObjectValue(buffer.get()));
89 },
90
91 IndexedDBKeyType::Array(a) => unsafe {
92 rooted!(&in(cx) let array = NewArrayObject1(cx.raw_cx(), 0));
95
96 assert!(
98 !array.get().is_null(),
99 "Failed to convert IndexedDB array key into an Array"
100 );
101
102 let len = a.len();
104
105 let mut index = 0;
107
108 while index < len {
110 rooted!(&in(cx) let mut entry = UndefinedValue());
113 key_type_to_jsval(cx, &a[index], entry.handle_mut());
114
115 let index_property = CString::new(index.to_string());
117 assert!(
118 index_property.is_ok(),
119 "Failed to convert IndexedDB array index to CString"
120 );
121 let index_property = index_property.unwrap();
122 let status = define_dictionary_property(
123 cx,
124 array.handle(),
125 index_property.as_c_str(),
126 entry.handle(),
127 );
128
129 assert!(
131 status.is_ok(),
132 "CreateDataProperty on a fresh JS array should not fail"
133 );
134
135 index += 1;
137 }
138
139 result.set(ObjectValue(array.get()));
141 },
142 }
143}
144
145pub(crate) fn is_valid_key_path(
147 cx: &mut JSContext,
148 key_path: &StrOrStringSequence,
149) -> Result<bool, Error> {
150 #[expect(unsafe_code)]
152 let is_identifier_name = |cx: &mut JSContext, name: &str| -> Result<bool, Error> {
153 rooted!(&in(cx) let mut value = UndefinedValue());
154 name.to_jsval(cx, value.handle_mut());
155 rooted!(&in(cx) let string = value.to_string());
156
157 unsafe {
158 let mut is_identifier = false;
159 if !JS_IsIdentifier(cx, string.handle(), &mut is_identifier) {
160 return Err(Error::JSFailed);
161 }
162 Ok(is_identifier)
163 }
164 };
165
166 let is_valid = |cx: &mut JSContext, path: &DOMString| -> Result<bool, Error> {
168 let is_empty_string = path.is_empty();
170
171 let is_identifier = is_identifier_name(cx, &path.str())?;
174
175 let is_identifier_list = path
177 .str()
178 .split('.')
179 .map(|s| is_identifier_name(cx, s))
180 .try_collect::<bool, Vec<bool>, Error>()?
181 .iter()
182 .all(|&value| value);
183
184 Ok(is_empty_string || is_identifier || is_identifier_list)
185 };
186
187 match key_path {
188 StrOrStringSequence::StringSequence(paths) => {
189 if paths.is_empty() {
191 Ok(false)
192 } else {
193 Ok(paths
194 .iter()
195 .map(|s| is_valid(cx, s))
196 .try_collect::<bool, Vec<bool>, Error>()?
197 .iter()
198 .all(|&value| value))
199 }
200 },
201 StrOrStringSequence::String(path) => is_valid(cx, path),
202 }
203}
204
205pub(crate) enum ConversionResult {
206 Valid(IndexedDBKeyType),
207 Invalid,
208}
209
210impl ConversionResult {
211 pub fn into_result(self) -> Result<IndexedDBKeyType, Error> {
212 match self {
213 ConversionResult::Valid(key) => Ok(key),
214 ConversionResult::Invalid => Err(Error::Data(None)),
215 }
216 }
217}
218
219#[expect(unsafe_code)]
221pub fn convert_value_to_key(
222 cx: &mut JSContext,
223 input: HandleValue,
224 seen: Option<Vec<HandleValue>>,
225) -> Result<ConversionResult, Error> {
226 let mut seen = seen.unwrap_or_default();
228
229 for seen_input in &seen {
231 let mut same = false;
232 if unsafe { !SameValue(cx, *seen_input, input, &mut same) } {
233 return Err(Error::JSFailed);
234 }
235 if same {
236 return Ok(ConversionResult::Invalid);
237 }
238 }
239
240 if input.is_number() {
244 if input.to_number().is_nan() {
246 return Ok(ConversionResult::Invalid);
247 }
248 return Ok(ConversionResult::Valid(IndexedDBKeyType::Number(
250 input.to_number(),
251 )));
252 }
253
254 if input.is_string() {
256 let string_ptr = std::ptr::NonNull::new(input.to_string()).unwrap();
258 let key = unsafe { jsstr_to_string(cx, string_ptr) };
259 return Ok(ConversionResult::Valid(IndexedDBKeyType::String(key)));
260 }
261
262 if input.is_object() {
263 rooted!(&in(cx) let object = input.to_object());
264 unsafe {
265 let mut is_date = false;
266 if !ObjectIsDate(cx, object.handle(), &mut is_date) {
267 return Err(Error::JSFailed);
268 }
269
270 if is_date {
272 let mut ms = f64::NAN;
274 if !js::rust::wrappers2::DateGetMsecSinceEpoch(cx, object.handle(), &mut ms) {
275 return Err(Error::JSFailed);
276 }
277 if ms.is_nan() {
279 return Ok(ConversionResult::Invalid);
280 }
281 return Ok(ConversionResult::Valid(IndexedDBKeyType::Date(ms)));
283 }
284
285 if IsArrayBufferObject(*object) || JS_IsArrayBufferViewObject(*object) {
287 let is_detached = if IsArrayBufferObject(*object) {
288 IsDetachedArrayBufferObject(*object)
289 } else {
290 let mut is_shared = false;
292 rooted!(
293 in (cx.raw_cx()) let view_buffer =
294 JS_GetArrayBufferViewBuffer(
295 cx.raw_cx(),
296 object.handle().into(),
297 &mut is_shared
298 )
299 );
300 !is_shared && IsDetachedArrayBufferObject(*view_buffer.handle())
301 };
302 if is_detached {
304 return Ok(ConversionResult::Invalid);
305 }
306 let bytes = if IsArrayBufferObject(*object) {
309 let array_buffer = ArrayBuffer::from(*object).map_err(|()| Error::JSFailed)?;
310 array_buffer.to_vec()
311 } else {
312 let array_buffer_view =
313 ArrayBufferView::from(*object).map_err(|()| Error::JSFailed)?;
314 array_buffer_view.to_vec()
315 }
316 .expect("Already checked for detached buffers");
317 return Ok(ConversionResult::Valid(IndexedDBKeyType::Binary(bytes)));
319 }
320
321 let mut is_array = false;
323 if !IsArrayObject(cx, input, &mut is_array) {
324 return Err(Error::JSFailed);
325 }
326 if is_array {
327 let mut len = 0;
329 if !GetArrayLength(cx, object.handle(), &mut len) {
330 return Err(Error::JSFailed);
331 }
332 seen.push(input);
334 let mut keys = vec![];
336 let mut index: u32 = 0;
338 while index < len {
340 rooted!(&in(cx) let mut id: PropertyKey);
341 if !JS_IndexToId(cx, index, id.handle_mut()) {
342 return Err(Error::JSFailed);
343 }
344 let mut hop = false;
346 if !JS_HasOwnPropertyById(cx, object.handle(), id.handle(), &mut hop) {
347 return Err(Error::JSFailed);
348 }
349 if !hop {
351 return Ok(ConversionResult::Invalid);
352 }
353 rooted!(&in(cx) let mut entry = UndefinedValue());
355 if !js::rust::wrappers2::JS_GetPropertyById(
356 cx,
357 object.handle(),
358 id.handle(),
359 entry.handle_mut(),
360 ) {
361 return Err(Error::JSFailed);
362 }
363
364 let key = match convert_value_to_key(cx, entry.handle(), Some(seen.clone()))? {
368 ConversionResult::Valid(key) => key,
369 ConversionResult::Invalid => return Ok(ConversionResult::Invalid),
372 };
373 keys.push(key);
375 index += 1;
377 }
378 return Ok(ConversionResult::Valid(IndexedDBKeyType::Array(keys)));
380 }
381 }
382 }
383
384 Ok(ConversionResult::Invalid)
386}
387
388#[expect(unsafe_code)]
390pub fn convert_value_to_key_range(
391 cx: &mut JSContext,
392 input: HandleValue,
393 null_disallowed: Option<bool>,
394) -> Result<IndexedDBKeyRange, Error> {
395 if input.is_object() {
397 rooted!(&in(cx) let object = input.to_object());
398 unsafe {
399 if let Ok(obj) = root_from_object::<IDBKeyRange>(cx, object.get()) {
400 let obj = obj.inner().clone();
401 return Ok(obj);
402 }
403 }
404 }
405
406 if input.get().is_undefined() || input.get().is_null() {
409 if null_disallowed.is_some_and(|flag| flag) {
410 return Err(Error::Data(None));
411 } else {
412 return Ok(IndexedDBKeyRange {
413 lower: None,
414 upper: None,
415 lower_open: Default::default(),
416 upper_open: Default::default(),
417 });
418 }
419 }
420
421 let key = convert_value_to_key(cx, input, None)?;
424
425 let key = key.into_result()?;
427
428 Ok(IndexedDBKeyRange::only(key))
430}
431
432pub(crate) fn map_backend_error_to_dom_error(error: BackendError) -> Error {
433 match error {
434 BackendError::QuotaExceeded => Error::QuotaExceeded {
435 quota: None,
436 requested: None,
437 },
438 BackendError::DbErr(details) => {
439 Error::Operation(Some(format!("IndexedDB open failed: {details}")))
440 },
441 other => Error::Operation(Some(format!("IndexedDB open failed: {other:?}"))),
442 }
443}
444
445pub(crate) enum EvaluationResult {
448 Success,
449 Failure,
450}
451
452#[expect(unsafe_code)]
454pub(crate) fn evaluate_key_path_on_value(
455 cx: &mut JSContext,
456 value: HandleValue,
457 key_path: &KeyPath,
458 mut return_val: MutableHandleValue,
459) -> Result<EvaluationResult, Error> {
460 match key_path {
461 KeyPath::StringSequence(key_path) => {
463 rooted!(&in(cx) let mut result = unsafe { JS_NewObject(cx, ptr::null()) });
465
466 for (i, item) in key_path.iter().enumerate() {
469 rooted!(&in(cx) let mut key = UndefinedValue());
474 if let EvaluationResult::Failure = evaluate_key_path_on_value(
475 cx,
476 value,
477 &KeyPath::String(item.clone()),
478 key.handle_mut(),
479 )? {
480 return Ok(EvaluationResult::Failure);
481 };
482
483 let i_cstr = std::ffi::CString::new(i.to_string()).unwrap();
487 define_dictionary_property(cx, result.handle(), i_cstr.as_c_str(), key.handle())
488 .map_err(|_| Error::JSFailed)?;
489
490 }
493
494 result.to_jsval(cx, return_val);
496 },
497 KeyPath::String(key_path) => {
498 if key_path.is_empty() {
500 return_val.set(*value);
501 return Ok(EvaluationResult::Success);
502 }
503
504 rooted!(&in(cx) let mut current_value = *value);
506
507 for identifier in key_path.str().split('.') {
511 if identifier == "length" && current_value.is_string() {
513 rooted!(&in(cx) let string_value = current_value.to_string());
515 unsafe {
516 let string_length = JS_GetStringLength(*string_value) as u64;
517 string_length.to_jsval(cx, current_value.handle_mut());
518 }
519 continue;
520 }
521
522 if identifier == "length" {
524 let mut is_array = false;
525 if unsafe { !IsArrayObject(cx, current_value.handle(), &mut is_array) } {
526 return Err(Error::JSFailed);
527 }
528 if is_array {
529 rooted!(&in(cx) let object = current_value.to_object());
531 get_property_jsval(
532 cx,
533 object.handle(),
534 c"length",
535 current_value.handle_mut(),
536 )?;
537
538 continue;
539 }
540 }
541
542 if identifier == "size" &&
544 let Ok(blob) = root_from_handlevalue::<Blob>(cx, current_value.handle())
545 {
546 blob.Size().to_jsval(cx, current_value.handle_mut());
548
549 continue;
550 }
551
552 if identifier == "type" &&
554 let Ok(blob) = root_from_handlevalue::<Blob>(cx, current_value.handle())
555 {
556 blob.Type().to_jsval(cx, current_value.handle_mut());
558
559 continue;
560 }
561
562 if identifier == "name" &&
564 let Ok(file) = root_from_handlevalue::<File>(cx, current_value.handle())
565 {
566 file.name().to_jsval(cx, current_value.handle_mut());
568
569 continue;
570 }
571
572 if identifier == "lastModified" &&
574 let Ok(file) = root_from_handlevalue::<File>(cx, current_value.handle())
575 {
576 file.LastModified().to_jsval(cx, current_value.handle_mut());
578
579 continue;
580 }
581
582 if identifier == "lastModifiedDate" &&
584 let Ok(file) = root_from_handlevalue::<File>(cx, current_value.handle())
585 {
586 let time = ClippedTime {
588 t: file.LastModified() as f64,
589 };
590 unsafe {
591 NewDateObject(cx, time).to_jsval(cx, current_value.handle_mut());
592 }
593
594 continue;
595 }
596
597 if !current_value.is_object() {
600 return Ok(EvaluationResult::Failure);
601 }
602
603 rooted!(&in(cx) let object = current_value.to_object());
604 let identifier_name =
605 CString::new(identifier).expect("Failed to convert str to CString");
606
607 let hop = has_own_property(cx, object.handle(), identifier_name.as_c_str())
609 .map_err(|_| Error::JSFailed)?;
610
611 if !hop {
613 return Ok(EvaluationResult::Failure);
614 }
615
616 get_property_jsval(
618 cx,
619 object.handle(),
620 identifier_name.as_c_str(),
621 current_value.handle_mut(),
622 )?;
623
624 if current_value.get().is_undefined() {
626 return Ok(EvaluationResult::Failure);
627 }
628 }
629
630 return_val.set(*current_value);
635 },
636 }
637 Ok(EvaluationResult::Success)
638}
639
640pub(crate) enum ExtractionResult {
643 Key(IndexedDBKeyType),
644 Invalid,
645 Failure,
646}
647
648pub(crate) fn can_inject_key_into_value(
650 cx: &mut JSContext,
651 value: HandleValue,
652 key_path: &DOMString,
653) -> Result<bool, Error> {
654 let key_path_string = key_path.str();
657 let mut identifiers: Vec<&str> = key_path_string.split('.').collect();
658
659 let Some(_) = identifiers.pop() else {
661 return Ok(false);
662 };
663
664 rooted!(&in(cx) let mut current_value = *value);
665
666 for identifier in identifiers {
668 if !current_value.is_object() {
670 return Ok(false);
671 }
672
673 rooted!(&in(cx) let current_object = current_value.to_object());
674 let identifier_name =
675 CString::new(identifier).expect("Failed to convert key path identifier to CString");
676
677 let hop = has_own_property(cx, current_object.handle(), identifier_name.as_c_str())
679 .map_err(|_| Error::JSFailed)?;
680
681 if !hop {
686 return Ok(true);
687 }
688
689 get_property_jsval(
691 cx,
692 current_object.handle(),
693 identifier_name.as_c_str(),
694 current_value.handle_mut(),
695 )?;
696 }
697
698 Ok(current_value.is_object())
700}
701
702#[expect(unsafe_code)]
704pub(crate) fn inject_key_into_value(
705 cx: &mut JSContext,
706 value: HandleValue,
707 key: &IndexedDBKeyType,
708 key_path: &DOMString,
709) -> Result<bool, Error> {
710 let key_path_string = key_path.str();
712 let mut identifiers: Vec<&str> = key_path_string.split('.').collect();
713
714 let Some(last) = identifiers.pop() else {
716 return Ok(false);
717 };
718
719 rooted!(&in(cx) let mut current_value = *value);
723
724 for identifier in identifiers {
726 if !current_value.is_object() {
728 return Ok(false);
729 }
730
731 rooted!(&in(cx) let current_object = current_value.to_object());
732 let identifier_name =
733 CString::new(identifier).expect("Failed to convert key path identifier to CString");
734
735 let hop = has_own_property(cx, current_object.handle(), identifier_name.as_c_str())
737 .map_err(|_| Error::JSFailed)?;
738
739 if !hop {
741 rooted!(&in(cx) let o = unsafe { JS_NewObject(cx, ptr::null()) });
743 rooted!(&in(cx) let mut o_value = UndefinedValue());
744 o.to_jsval(cx, o_value.handle_mut());
745
746 define_dictionary_property(
748 cx,
749 current_object.handle(),
750 identifier_name.as_c_str(),
751 o_value.handle(),
752 )
753 .map_err(|_| Error::JSFailed)?;
754
755 }
757
758 get_property_jsval(
760 cx,
761 current_object.handle(),
762 identifier_name.as_c_str(),
763 current_value.handle_mut(),
764 )?;
765
766 if !current_value.is_object() {
768 return Ok(false);
769 }
770 }
771
772 rooted!(&in(cx) let mut key_value = UndefinedValue());
774 key_type_to_jsval(cx, key, key_value.handle_mut());
775
776 if !current_value.is_object() {
778 return Ok(false);
779 }
780 rooted!(&in(cx) let parent_object = current_value.to_object());
781 let last_name = CString::new(last).expect("Failed to convert final key path identifier");
782
783 define_dictionary_property(
785 cx,
786 parent_object.handle(),
787 last_name.as_c_str(),
788 key_value.handle(),
789 )
790 .map_err(|_| Error::JSFailed)?;
791
792 Ok(true)
796}
797
798pub(crate) fn extract_key(
800 cx: &mut JSContext,
801 value: HandleValue,
802 key_path: &KeyPath,
803 multi_entry: Option<bool>,
804) -> Result<ExtractionResult, Error> {
805 rooted!(&in(cx) let mut r = UndefinedValue());
809 if let EvaluationResult::Failure =
810 evaluate_key_path_on_value(cx, value, key_path, r.handle_mut())?
811 {
812 return Ok(ExtractionResult::Failure);
813 }
814
815 let key = match multi_entry {
819 Some(true) => {
820 unimplemented!("multiEntry keys are not yet supported");
822 },
823 _ => match convert_value_to_key(cx, r.handle(), None)? {
824 ConversionResult::Valid(key) => key,
825 ConversionResult::Invalid => return Ok(ExtractionResult::Invalid),
827 },
828 };
829
830 Ok(ExtractionResult::Key(key))
832}