script: Re-implement evaluate_key_path_on_value in IndexedDB (#38847)

The current implementation of evaluate_key_path_on_value was translated
from gecko, and it is incomplete. The unimplemented part occurs many
crashes in WPT tests.

This PR re-implements it according to the spec. It should eliminate many
crashed WPT tests, and increase the code readability.

Testing: Update WPT test expectation
Fixes: #38817 partially, and #25325

---------

Signed-off-by: Kingsley Yung <kingsley@kkoyung.dev>
This commit is contained in:
Kingsley Yung 2025-09-05 16:00:07 +08:00 committed by GitHub
parent ebc1282c7a
commit b29eab0ffe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 694 additions and 178 deletions

View file

@ -29,8 +29,9 @@ use crate::dom::domstringlist::DOMStringList;
use crate::dom::globalscope::GlobalScope;
use crate::dom::idbrequest::IDBRequest;
use crate::dom::idbtransaction::IDBTransaction;
use crate::indexed_db;
use crate::indexed_db::{convert_value_to_key, convert_value_to_key_range, extract_key};
use crate::indexed_db::{
self, ExtractionResult, convert_value_to_key, convert_value_to_key_range, extract_key,
};
use crate::script_runtime::{CanGc, JSContext as SafeJSContext};
#[derive(JSTraceable, MallocSizeOf)]
@ -242,7 +243,7 @@ impl IDBObjectStore {
serialized_key = Some(convert_value_to_key(cx, key, None)?);
} else {
// Step 11: We should use in-line keys instead
if let Some(Ok(kpk)) = self
if let Some(Ok(ExtractionResult::Key(kpk))) = self
.key_path
.as_ref()
.map(|p| extract_key(cx, value, p, None))

View file

@ -2,6 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::ffi::CString;
use std::iter::repeat_n;
use std::ptr;
@ -9,24 +10,31 @@ use ipc_channel::ipc::IpcSender;
use js::conversions::jsstr_to_string;
use js::gc::MutableHandle;
use js::jsapi::{
ESClass, GetBuiltinClass, IsArrayBufferObject, JS_DeleteUCProperty,
JS_GetOwnUCPropertyDescriptor, JS_GetStringLength, JS_IsArrayBufferViewObject, JSObject,
ObjectOpResult, ObjectOpResult_SpecialCodes, PropertyDescriptor,
ClippedTime, ESClass, GetBuiltinClass, IsArrayBufferObject, JS_GetStringLength,
JS_IsArrayBufferViewObject, JS_NewObject, NewDateObject,
};
use js::jsval::{DoubleValue, UndefinedValue};
use js::rust::wrappers::{IsArrayObject, JS_GetProperty, JS_HasOwnProperty};
use js::rust::{HandleValue, MutableHandleValue};
use net_traits::indexeddb_thread::{BackendResult, IndexedDBKeyRange, IndexedDBKeyType};
use profile_traits::ipc;
use profile_traits::ipc::IpcReceiver;
use script_bindings::conversions::{SafeToJSValConvertible, root_from_object};
use script_bindings::root::DomRoot;
use script_bindings::str::DOMString;
use serde::{Deserialize, Serialize};
use crate::dom::bindings::codegen::Bindings::BlobBinding::BlobMethods;
use crate::dom::bindings::codegen::Bindings::FileBinding::FileMethods;
use crate::dom::bindings::codegen::UnionTypes::StringOrStringSequence as StrOrStringSequence;
use crate::dom::bindings::conversions::{
SafeToJSValConvertible, get_property_jsval, root_from_handlevalue, root_from_object,
};
use crate::dom::bindings::error::Error;
use crate::dom::bindings::import::module::SafeJSContext;
use crate::dom::bindings::root::DomRoot;
use crate::dom::bindings::str::DOMString;
use crate::dom::bindings::structuredclone;
use crate::dom::bindings::utils::set_dictionary_property;
use crate::dom::blob::Blob;
use crate::dom::file::File;
use crate::dom::globalscope::GlobalScope;
use crate::dom::idbkeyrange::IDBKeyRange;
use crate::dom::idbobjectstore::KeyPath;
@ -187,158 +195,265 @@ pub fn convert_value_to_key_range(
Ok(IndexedDBKeyRange::only(key))
}
// https://www.w3.org/TR/IndexedDB-2/#evaluate-a-key-path-on-a-value
/// The result of steps in
/// <https://www.w3.org/TR/IndexedDB-2/#evaluate-a-key-path-on-a-value>
pub(crate) enum EvaluationResult {
Success,
Failure,
}
/// <https://www.w3.org/TR/IndexedDB-2/#evaluate-a-key-path-on-a-value>
#[allow(unsafe_code)]
pub fn evaluate_key_path_on_value(
pub(crate) fn evaluate_key_path_on_value(
cx: SafeJSContext,
value: HandleValue,
mut return_val: MutableHandleValue,
key_path: &KeyPath,
) {
// The implementation is translated from gecko:
// https://github.com/mozilla/gecko-dev/blob/master/dom/indexedDB/KeyPath.cpp
return_val.set(*value);
rooted!(in(*cx) let mut target_object = ptr::null_mut::<JSObject>());
rooted!(in(*cx) let mut current_val = *value);
rooted!(in(*cx) let mut object = ptr::null_mut::<JSObject>());
let mut target_object_prop_name: Option<String> = None;
mut return_val: MutableHandleValue,
) -> Result<EvaluationResult, Error> {
match key_path {
KeyPath::String(path) => {
// Step 3
let path_as_string = path.to_string();
let mut tokenizer = path_as_string.split('.').peekable();
// Step 1. If keyPath is a list of strings, then:
KeyPath::StringSequence(key_path) => {
// Step 1.1. Let result be a new Array object created as if by the expression [].
rooted!(in(*cx) let mut result = unsafe { JS_NewObject(*cx, ptr::null()) });
while let Some(token) = tokenizer.next() {
if target_object.get().is_null() {
if token == "length" && tokenizer.peek().is_none() && current_val.is_string() {
rooted!(in(*cx) let input_val = current_val.to_string());
unsafe {
let string_len = JS_GetStringLength(*input_val) as u64;
string_len.safe_to_jsval(cx, return_val);
}
break;
}
// Step 1.2. Let i be 0.
// Step 1.3. For each item in keyPath:
for (i, item) in key_path.iter().enumerate() {
// Step 1.3.1. Let key be the result of recursively running the steps to evaluate a key
// path on a value using item as keyPath and value as value.
// Step 1.3.2. Assert: key is not an abrupt completion.
// Step 1.3.3. If key is failure, abort the overall algorithm and return failure.
rooted!(in(*cx) let mut key = UndefinedValue());
if let EvaluationResult::Failure = evaluate_key_path_on_value(
cx,
value,
&KeyPath::String(item.clone()),
key.handle_mut(),
)? {
return Ok(EvaluationResult::Failure);
};
if !current_val.is_object() {
// FIXME:(rasviitanen) Return a proper error
return;
}
object.handle_mut().set(current_val.to_object());
rooted!(in(*cx) let mut desc = PropertyDescriptor::default());
rooted!(in(*cx) let mut intermediate = UndefinedValue());
// So rust says that this value is never read, but it is.
#[allow(unused)]
let mut has_prop = false;
unsafe {
let prop_name_as_utf16: Vec<u16> = token.encode_utf16().collect();
let mut is_descriptor_none: bool = false;
let ok = JS_GetOwnUCPropertyDescriptor(
*cx,
object.handle().into(),
prop_name_as_utf16.as_ptr(),
prop_name_as_utf16.len(),
desc.handle_mut().into(),
&mut is_descriptor_none,
);
if !ok {
// FIXME:(arihant2math) Handle this
return;
}
if desc.hasWritable_() || desc.hasValue_() {
intermediate.handle_mut().set(desc.handle().value_);
has_prop = true;
} else {
// If we get here it means the object doesn't have the property or the
// property is available through a getter. We don't want to call any
// getters to avoid potential re-entrancy.
// The blob object is special since its properties are available
// only through getters but we still want to support them for key
// extraction. So they need to be handled manually.
unimplemented!("Blob tokens are not yet supported");
}
}
if has_prop {
// Treat undefined as an error
if intermediate.is_undefined() {
// FIXME:(rasviitanen) Throw/return error
return;
}
if tokenizer.peek().is_some() {
// ...and walk to it if there are more steps...
current_val.handle_mut().set(*intermediate);
} else {
// ...otherwise use it as key
return_val.set(*intermediate);
}
} else {
target_object.handle_mut().set(*object);
target_object_prop_name = Some(token.to_string());
}
}
if !target_object.get().is_null() {
// We have started inserting new objects or are about to just insert
// the first one.
// FIXME:(rasviitanen) Implement this piece
unimplemented!("keyPath tokens that requires insertion are not supported.");
}
} // All tokens processed
if !target_object.get().is_null() {
// If this fails, we lose, and the web page sees a magical property
// appear on the object :-(
// Step 1.3.4. Let p be ! ToString(i).
// Step 1.3.5. Let status be CreateDataProperty(result, p, key).
// Step 1.3.6. Assert: status is true.
unsafe {
let prop_name_as_utf16: Vec<u16> =
target_object_prop_name.unwrap().encode_utf16().collect();
#[allow(clippy::cast_enum_truncation)]
let mut succeeded = ObjectOpResult {
code_: ObjectOpResult_SpecialCodes::Uninitialized as usize,
};
if !JS_DeleteUCProperty(
set_dictionary_property(*cx, result.handle(), &i.to_string(), key.handle())
.map_err(|_| Error::JSFailed)?;
}
// Step 1.3.7. Increase i by 1.
// Done by for loop with enumerate()
}
// Step 1.4. Return result.
result.safe_to_jsval(cx, return_val);
},
KeyPath::String(key_path) => {
// Step 2. If keyPath is the empty string, return value and skip the remaining steps.
if key_path.is_empty() {
return_val.set(*value);
return Ok(EvaluationResult::Success);
}
// NOTE: Use current_value, instead of value described in spec, in the following steps.
rooted!(in(*cx) let mut current_value = *value);
// Step 3. Let identifiers be the result of strictly splitting keyPath on U+002E
// FULL STOP characters (.).
// Step 4. For each identifier of identifiers, jump to the appropriate step below:
for identifier in key_path.split('.') {
// If Type(value) is String, and identifier is "length"
if identifier == "length" && current_value.is_string() {
// Let value be a Number equal to the number of elements in value.
rooted!(in(*cx) let string_value = current_value.to_string());
unsafe {
let string_length = JS_GetStringLength(*string_value) as u64;
string_length.safe_to_jsval(cx, current_value.handle_mut());
}
continue;
}
// If value is an Array and identifier is "length"
if identifier == "length" {
unsafe {
let mut is_array = false;
if !IsArrayObject(*cx, current_value.handle(), &mut is_array) {
return Err(Error::JSFailed);
}
if is_array {
// Let value be ! ToLength(! Get(value, "length")).
rooted!(in(*cx) let object = current_value.to_object());
get_property_jsval(
cx,
object.handle(),
"length",
current_value.handle_mut(),
)?;
continue;
}
}
}
// If value is a Blob and identifier is "size"
if identifier == "size" {
unsafe {
if let Ok(blob) = root_from_handlevalue::<Blob>(current_value.handle(), *cx)
{
// Let value be a Number equal to values size.
blob.Size().safe_to_jsval(cx, current_value.handle_mut());
continue;
}
}
}
// If value is a Blob and identifier is "type"
if identifier == "type" {
unsafe {
if let Ok(blob) = root_from_handlevalue::<Blob>(current_value.handle(), *cx)
{
// Let value be a String equal to values type.
blob.Type().safe_to_jsval(cx, current_value.handle_mut());
continue;
}
}
}
// If value is a File and identifier is "name"
if identifier == "name" {
unsafe {
if let Ok(file) = root_from_handlevalue::<File>(current_value.handle(), *cx)
{
// Let value be a String equal to values name.
file.name().safe_to_jsval(cx, current_value.handle_mut());
continue;
}
}
}
// If value is a File and identifier is "lastModified"
if identifier == "lastModified" {
unsafe {
if let Ok(file) = root_from_handlevalue::<File>(current_value.handle(), *cx)
{
// Let value be a Number equal to values lastModified.
file.LastModified()
.safe_to_jsval(cx, current_value.handle_mut());
continue;
}
}
}
// If value is a File and identifier is "lastModifiedDate"
if identifier == "lastModifiedDate" {
unsafe {
if let Ok(file) = root_from_handlevalue::<File>(current_value.handle(), *cx)
{
// Let value be a new Date object with [[DateValue]] internal slot equal to values lastModified.
let time = ClippedTime {
t: file.LastModified() as f64,
};
NewDateObject(*cx, time).safe_to_jsval(cx, current_value.handle_mut());
continue;
}
}
}
// Otherwise
unsafe {
// If Type(value) is not Object, return failure.
if !current_value.is_object() {
return Ok(EvaluationResult::Failure);
}
rooted!(in(*cx) let object = current_value.to_object());
let identifier_name =
CString::new(identifier).expect("Failed to convert str to CString");
// Let hop be ! HasOwnProperty(value, identifier).
let mut hop = false;
if !JS_HasOwnProperty(*cx, object.handle(), identifier_name.as_ptr(), &mut hop)
{
return Err(Error::JSFailed);
}
// If hop is false, return failure.
if !hop {
return Ok(EvaluationResult::Failure);
}
// Let value be ! Get(value, identifier).
if !JS_GetProperty(
*cx,
target_object.handle().into(),
prop_name_as_utf16.as_ptr(),
prop_name_as_utf16.len(),
&mut succeeded,
object.handle(),
identifier_name.as_ptr(),
current_value.handle_mut(),
) {
// FIXME:(rasviitanen) Throw/return error
// return;
return Err(Error::JSFailed);
}
// If value is undefined, return failure.
if current_value.get().is_undefined() {
return Ok(EvaluationResult::Failure);
}
}
}
},
KeyPath::StringSequence(_) => {
unimplemented!("String sequence keyPath is currently unsupported");
// Step 5. Assert: value is not an abrupt completion.
// Done within Step 4.
// Step 6. Return value.
return_val.set(*current_value);
},
}
Ok(EvaluationResult::Success)
}
// https://www.w3.org/TR/IndexedDB-2/#extract-a-key-from-a-value-using-a-key-path
pub fn extract_key(
/// The result of steps in
/// <https://www.w3.org/TR/IndexedDB-2/#extract-a-key-from-a-value-using-a-key-path>
pub(crate) enum ExtractionResult {
Key(IndexedDBKeyType),
// NOTE: Invalid is not used for now. Remove the unused annotation when it is used.
#[expect(unused)]
Invalid,
Failure,
}
/// <https://www.w3.org/TR/IndexedDB-2/#extract-a-key-from-a-value-using-a-key-path>
pub(crate) fn extract_key(
cx: SafeJSContext,
input: HandleValue,
value: HandleValue,
key_path: &KeyPath,
multi_entry: Option<bool>,
) -> Result<IndexedDBKeyType, Error> {
// Step 1: Evaluate key path
// FIXME:(rasviitanen) Do this propertly
) -> Result<ExtractionResult, Error> {
// Step 1. Let r be the result of running the steps to evaluate a key path on a value with
// value and keyPath. Rethrow any exceptions.
// Step 2. If r is failure, return failure.
rooted!(in(*cx) let mut r = UndefinedValue());
evaluate_key_path_on_value(cx, input, r.handle_mut(), key_path);
if let Some(_multi_entry) = multi_entry {
// FIXME:(rasviitanen) handle multi_entry cases
unimplemented!("multiEntry keys are not yet supported");
} else {
convert_value_to_key(cx, r.handle(), None)
if let EvaluationResult::Failure =
evaluate_key_path_on_value(cx, value, key_path, r.handle_mut())?
{
return Ok(ExtractionResult::Failure);
}
// Step 3. Let key be the result of running the steps to convert a value to a key with r if the
// multiEntry flag is unset, and the result of running the steps to convert a value to a
// multiEntry key with r otherwise. Rethrow any exceptions.
let key = match multi_entry {
Some(true) => {
// TODO: implement convert_value_to_multientry_key
unimplemented!("multiEntry keys are not yet supported");
},
_ => convert_value_to_key(cx, r.handle(), None)?,
};
// TODO: Step 4. If key is invalid, return invalid.
// Step 5. Return key.
Ok(ExtractionResult::Key(key))
}