Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 33 additions & 21 deletions moli-renderer-v8/src/native_bridge/document/queries/xpath/result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ struct XPathResultBaseDeclaration<'scope> {
}

#[derive(WebApiFunctionTemplate)]
#[webapi(interface = web_api_interfaces::XPathResult, enumerable)]
#[webapi(interface = web_api_interfaces::XPathResult, enumerable, receiver)]
struct XPathResultPrototypeDeclaration {
#[webapi(accessor_property, getter = xpath_result_result_type_getter_callback)]
result_type: (),
Expand All @@ -67,7 +67,7 @@ struct XPathResultPrototypeDeclaration {
snapshot_length: (),
#[webapi(method, length = 0, callback = xpath_result_iterate_next_callback)]
iterate_next: (),
#[webapi(method, length = 0, callback = xpath_result_snapshot_item_callback)]
#[webapi(method, length = 1, callback = xpath_result_snapshot_item_callback)]
snapshot_item: (),
}

Expand Down Expand Up @@ -133,12 +133,24 @@ pub(super) fn install_xpath_result_template_bindings<'s>(
XPathResultPrototypeDeclaration::initialize_prototype_template(scope, prototype);
}

fn set_xpath_result_slot(
scope: &mut v8::PinScope<'_, '_>,
object: v8::Local<'_, v8::Object>,
fn xpath_result_slot<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
name: &'static str,
value: v8::Local<'_, v8::Value>,
) -> Option<v8::Local<'s, v8::Value>> {
let object = moli_webapi_declare::web_api_object_target(scope, object)?;
get_private_value(scope, object, name)
}

fn set_xpath_result_slot<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
name: &'static str,
value: v8::Local<'s, v8::Value>,
) {
let Some(object) = moli_webapi_declare::web_api_object_target(scope, object) else {
return;
};
set_private_value(scope, object, name, value);
}

Expand Down Expand Up @@ -234,7 +246,7 @@ fn xpath_result_type<'s>(
scope: &mut v8::PinScope<'s, '_>,
result: v8::Local<'s, v8::Object>,
) -> u32 {
get_private_value(scope, result, XPATH_RESULT_TYPE_SLOT)
xpath_result_slot(scope, result, XPATH_RESULT_TYPE_SLOT)
.and_then(|value| value.uint32_value(scope))
.unwrap_or(XPATH_ANY_TYPE)
}
Expand Down Expand Up @@ -268,7 +280,7 @@ fn xpath_result_baseline_query_version<'s>(
scope: &mut v8::PinScope<'s, '_>,
result: v8::Local<'s, v8::Object>,
) -> Option<u64> {
let value = get_private_value(scope, result, XPATH_RESULT_QUERY_VERSION_SLOT)?;
let value = xpath_result_slot(scope, result, XPATH_RESULT_QUERY_VERSION_SLOT)?;
let big = v8::Local::<v8::BigInt>::try_from(value).ok()?;
let (version, _lossless) = big.u64_value();
Some(version)
Expand All @@ -278,7 +290,7 @@ fn xpath_result_runtime_ptr<'s>(
scope: &mut v8::PinScope<'s, '_>,
result: v8::Local<'s, v8::Object>,
) -> Option<*mut JsContextHost> {
let value = get_private_value(scope, result, XPATH_RESULT_RUNTIME_SLOT)?;
let value = xpath_result_slot(scope, result, XPATH_RESULT_RUNTIME_SLOT)?;
let external = v8::Local::<v8::External>::try_from(value).ok()?;
let runtime_ptr = external.value() as *mut JsContextHost;
(!runtime_ptr.is_null()).then_some(runtime_ptr)
Expand All @@ -288,7 +300,7 @@ fn xpath_result_object_tree_root<'s>(
scope: &mut v8::PinScope<'s, '_>,
result: v8::Local<'s, v8::Object>,
) -> Option<v8::Local<'s, v8::Object>> {
get_private_value(scope, result, XPATH_RESULT_OBJECT_TREE_ROOT_SLOT)
xpath_result_slot(scope, result, XPATH_RESULT_OBJECT_TREE_ROOT_SLOT)
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
}

Expand Down Expand Up @@ -340,7 +352,7 @@ fn xpath_result_number_value_getter_callback<'s>(
xpath_result_throw_type_error(scope, "The result type is not a number.");
return;
}
if let Some(value) = get_private_value(scope, result, XPATH_RESULT_NUMBER_VALUE_SLOT) {
if let Some(value) = xpath_result_slot(scope, result, XPATH_RESULT_NUMBER_VALUE_SLOT) {
rv.set(value);
}
}
Expand All @@ -355,7 +367,7 @@ fn xpath_result_string_value_getter_callback<'s>(
xpath_result_throw_type_error(scope, "The result type is not a string.");
return;
}
if let Some(value) = get_private_value(scope, result, XPATH_RESULT_STRING_VALUE_SLOT) {
if let Some(value) = xpath_result_slot(scope, result, XPATH_RESULT_STRING_VALUE_SLOT) {
rv.set(value);
}
}
Expand All @@ -370,7 +382,7 @@ fn xpath_result_boolean_value_getter_callback<'s>(
xpath_result_throw_type_error(scope, "The result type is not a boolean.");
return;
}
if let Some(value) = get_private_value(scope, result, XPATH_RESULT_BOOLEAN_VALUE_SLOT) {
if let Some(value) = xpath_result_slot(scope, result, XPATH_RESULT_BOOLEAN_VALUE_SLOT) {
rv.set(value);
}
}
Expand All @@ -385,7 +397,7 @@ fn xpath_result_single_node_value_getter_callback<'s>(
xpath_result_throw_type_error(scope, "The result type is not a single node.");
return;
}
if let Some(value) = get_private_value(scope, result, XPATH_RESULT_SINGLE_NODE_VALUE_SLOT) {
if let Some(value) = xpath_result_slot(scope, result, XPATH_RESULT_SINGLE_NODE_VALUE_SLOT) {
rv.set(value);
}
}
Expand All @@ -400,7 +412,7 @@ fn xpath_result_snapshot_length_getter_callback<'s>(
xpath_result_throw_type_error(scope, "The result type is not a snapshot.");
return;
}
if let Some(value) = get_private_value(scope, result, XPATH_RESULT_SNAPSHOT_LENGTH_SLOT) {
if let Some(value) = xpath_result_slot(scope, result, XPATH_RESULT_SNAPSHOT_LENGTH_SLOT) {
rv.set(value);
}
}
Expand Down Expand Up @@ -516,13 +528,13 @@ fn xpath_result_iterate_next_callback<'s>(
);
return;
}
let Some(nodes) = get_private_value(scope, result, XPATH_RESULT_NODES_SLOT)
let Some(nodes) = xpath_result_slot(scope, result, XPATH_RESULT_NODES_SLOT)
.and_then(|value| v8::Local::<v8::Array>::try_from(value).ok())
else {
rv.set_null();
return;
};
let index = get_private_value(scope, result, XPATH_RESULT_INDEX_SLOT)
let index = xpath_result_slot(scope, result, XPATH_RESULT_INDEX_SLOT)
.and_then(|value| value.uint32_value(scope))
.unwrap_or(0);
if index >= nodes.length() {
Expand All @@ -547,19 +559,19 @@ fn xpath_result_snapshot_item_callback<'s>(
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
let result = args.this();
let Some(parsed) = webidl::parse_args::<XPathResultSnapshotItemArgs>(scope, &args) else {
return;
};
if !xpath_result_type_is_snapshot(xpath_result_type(scope, result)) {
xpath_result_throw_type_error(scope, "The result type is not a snapshot.");
return;
}
let Some(nodes) = get_private_value(scope, result, XPATH_RESULT_NODES_SLOT)
let Some(nodes) = xpath_result_slot(scope, result, XPATH_RESULT_NODES_SLOT)
.and_then(|value| v8::Local::<v8::Array>::try_from(value).ok())
else {
rv.set_null();
return;
};
let Some(parsed) = webidl::parse_args::<XPathResultSnapshotItemArgs>(scope, &args) else {
return;
};
if parsed.index >= nodes.length() {
rv.set_null();
return;
Expand Down
1 change: 1 addition & 0 deletions moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,4 @@ mod video_codec_support;
mod event_prototype_attributes;

mod named_node_map_receivers;
mod xpath_result_bindings;
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
use super::*;

#[test]
fn xpath_result_members_validate_receivers_and_convert_indices_before_algorithms() {
let mut vm = new_parsed_test_vm(
"https://xpath-result-bindings.test/",
"<!doctype html><iframe id=child></iframe>",
);
let result = vm
.eval(include_str!(
"../../../../tests/fixtures/xpath-result-native-bindings.js"
))
.expect("XPathResult binding matrix should complete");
assert_eq!(vm.eval("__uiEventResults.total").unwrap(), "3240");
assert_eq!(
result,
"true",
"{}",
vm.eval("JSON.stringify(__uiEventResults.rows.filter(row => !row.passed))")
.unwrap()
);
}

#[test]
fn xpath_result_registered_native_aliases_share_values_cursor_and_mutation_state() {
let mut vm = new_parsed_test_vm(
"https://xpath-result-native-aliases.test/",
"<!doctype html><iframe id=child></iframe>",
);
vm.eval(r#"
globalThis.xpathProxyRecords = [];
globalThis.xpathProxyTraps = 0;
const other = document.querySelector('#child').contentWindow;
for (const owner of [globalThis, other]) for (const flavor of ['live', 'html', 'xml']) {
const doc = flavor === 'live' ? owner.document : flavor === 'html' ?
owner.document.implementation.createHTMLDocument('xpath') : owner.document.implementation.createDocument(null, 'r');
for (let type=1;type<=9;type++) {
const root = doc.createElement('root');
const a = root.appendChild(doc.createElement('a'));
const b = root.appendChild(doc.createElement('b'));
(doc.body || doc.documentElement).appendChild(root);
xpathProxyRecords.push({owner, doc, type, root, a, b,
callee: owner === globalThis ? other : globalThis,
handler: {get() {xpathProxyTraps++; throw Error('get');},
getPrototypeOf() {xpathProxyTraps++; throw Error('getPrototypeOf');}}});
}
}
// Materialize the trees before taking any iterator mutation baseline.
for (const record of xpathProxyRecords) {
const {doc,root,type} = record;
record.target = doc.evaluate(type===1?'7':type===2?"'value'":type===3?'true()':'child::*',root,null,type,null);
}
true;
"#).unwrap();
vm.with_default_context_scope_and_checkpoint_for_test(|scope, _host_ptr| {
let global = scope.get_current_context().global(scope);
let key = v8::String::new(scope, "xpathProxyRecords").unwrap();
let value = global.get(scope, key.into()).unwrap();
let records = v8::Local::<v8::Array>::try_from(value).unwrap();
assert_eq!(records.length(), 54);
for index in 0..records.length() {
let value = records.get_index(scope, index).unwrap();
let record = v8::Local::<v8::Object>::try_from(value).unwrap();
let key = v8::String::new(scope, "target").unwrap();
let value = record.get(scope, key.into()).unwrap();
let target = v8::Local::<v8::Object>::try_from(value).unwrap();
let target = moli_webapi_declare::web_api_object_target(scope, target).unwrap();
let context = target.get_creation_context(scope).unwrap();
let key = v8::String::new(scope, "handler").unwrap();
let value = record.get(scope, key.into()).unwrap();
let handler = v8::Local::<v8::Object>::try_from(value).unwrap();
for name in ["firstAlias", "secondAlias"] {
let proxy = {
let scope = &mut v8::ContextScope::new(scope, context);
let private_handler = crate::util::new_null_prototype_object(scope);
for trap in ["get", "getPrototypeOf"] {
let key = v8::String::new(scope, trap).unwrap();
let value = handler.get(scope, key.into()).unwrap();
assert!(
private_handler
.create_data_property(scope, key.into(), value)
.unwrap()
);
}
let proxy = v8::Proxy::new(scope, target, private_handler).unwrap();
moli_webapi_declare::register_web_api_proxy(scope, proxy).unwrap();
proxy
};
assert!(crate::web_api_interfaces::XPathResult::is_instance(
scope,
proxy.into()
));
let key = v8::String::new(scope, name).unwrap();
assert!(record.set(scope, key.into(), proxy.into()).unwrap());
}
}
Ok(())
})
.unwrap();
assert_eq!(vm.eval(r#"(() => {
for (const [index, record] of xpathProxyRecords.entries()) {
const {callee,target,firstAlias,secondAlias,type,a,b} = record;
const methods = callee.XPathResult.prototype;
const get = (name,receiver) => Object.getOwnPropertyDescriptor(methods,name).get.call(receiver);
const check = (value,name) => {if (!value) throw Error(`${index}/${type}: ${name}`);};
const typeError = body => {try {body();return false;} catch(error) {return Object.getPrototypeOf(error) === callee.TypeError.prototype;}};
for (const receiver of [target,firstAlias,secondAlias]) {
check(get('resultType',receiver) === type && !get('invalidIteratorState',receiver), 'initial state');
if (type===1) check(get('numberValue',receiver) === 7, 'number');
if (type===2) check(get('stringValue',receiver) === 'value', 'string');
if (type===3) check(get('booleanValue',receiver) === true, 'boolean');
if (type===8 || type===9) check(get('singleNodeValue',receiver) === a, 'single node');
if (type===6 || type===7) check(get('snapshotLength',receiver) === 2 && methods.snapshotItem.call(receiver,0) === a && methods.snapshotItem.call(receiver,1) === b, 'snapshot');
}
if (type===4 || type===5) check(methods.iterateNext.call(firstAlias) === a &&
methods.iterateNext.call(target) === b && methods.iterateNext.call(secondAlias) === null &&
methods.iterateNext.call(firstAlias) === null, 'shared iterator cursor');
let converted = 0;const sentinel = {};
try {methods.snapshotItem.call(firstAlias,{valueOf(){converted++;throw sentinel;}});throw Error('missing exception');}
catch(error) {check(error === sentinel && converted === 1,'index conversion before result kind');}
const author = new Proxy(firstAlias,{});
for (const member of ['resultType','invalidIteratorState','numberValue','stringValue','booleanValue','singleNodeValue','snapshotLength'])
check(typeError(() => get(member,author)), 'author alias ' + member);
converted = 0;
check(typeError(() => methods.snapshotItem.call(author,{valueOf(){converted++;return 0;}})) && converted === 0, 'author alias before conversion');
check(xpathProxyTraps === 0, 'no author traps');
}
// Mutations invalidate a whole Document. Test all cursors before mutating
// any tree so another record's legitimate invalidation cannot mask a bug.
for (const [index, record] of xpathProxyRecords.entries()) {
const {callee,target,firstAlias,secondAlias,type,root,a,b} = record;
if (type<4 || type>7) continue;
root.setAttribute('mutation','changed');
const methods = callee.XPathResult.prototype;
const get = (name,receiver) => Object.getOwnPropertyDescriptor(methods,name).get.call(receiver);
for (const receiver of [target,firstAlias,secondAlias]) {
if (type===4 || type===5) {
if (!get('invalidIteratorState',receiver)) throw Error(`${index}: missing invalidation`);
let error;try {methods.iterateNext.call(receiver);} catch(caught) {error=caught;}
if (error?.name !== 'InvalidStateError' || error.code !== 11 || Object.getPrototypeOf(error) !== callee.DOMException.prototype)
throw Error(`${index}: invalid iterator error`);
} else if (get('invalidIteratorState',receiver) || get('snapshotLength',receiver) !== 2 ||
methods.snapshotItem.call(receiver,0) !== a || methods.snapshotItem.call(receiver,1) !== b)
throw Error(`${index}: mutated snapshot changed`);
}
}
for (const record of xpathProxyRecords) record.root.remove();
return true;
})()"#).unwrap(), "true");
}
Original file line number Diff line number Diff line change
Expand Up @@ -637,7 +637,7 @@ fn document_xpath_result_uses_browser_like_object_shape() {

assert_eq!(
result,
r#"{"constructorType":"function","constructorName":"XPathResult","constructorLength":0,"illegalConstructor":"throw:TypeError","tag":"[object XPathResult]","instanceofXPathResult":true,"prototypeMatch":true,"constructorOnPrototype":true,"ownKeys":[],"ownIterateNext":false,"ownResultType":false,"resultTypeValue":5,"invalidIteratorStateValue":false,"prototypeIterateNext":["function","iterateNext",0,true,true,true],"prototypeSnapshotItem":["function","snapshotItem",0,true,true,true],"prototypeToStringTag":["XPathResult",false,true,false],"prototypeResultTypePresent":true,"constructorConstantsEnumerable":true,"prototypeConstantsEnumerable":true,"prototypeMethodsEnumerable":[true,true],"constructorConstant":5,"prototypeConstant":5,"windowEnumerable":false}"#
r#"{"constructorType":"function","constructorName":"XPathResult","constructorLength":0,"illegalConstructor":"throw:TypeError","tag":"[object XPathResult]","instanceofXPathResult":true,"prototypeMatch":true,"constructorOnPrototype":true,"ownKeys":[],"ownIterateNext":false,"ownResultType":false,"resultTypeValue":5,"invalidIteratorStateValue":false,"prototypeIterateNext":["function","iterateNext",0,true,true,true],"prototypeSnapshotItem":["function","snapshotItem",1,true,true,true],"prototypeToStringTag":["XPathResult",false,true,false],"prototypeResultTypePresent":true,"constructorConstantsEnumerable":true,"prototypeConstantsEnumerable":true,"prototypeMethodsEnumerable":[true,true],"constructorConstant":5,"prototypeConstant":5,"windowEnumerable":false}"#
);
}

Expand Down Expand Up @@ -717,7 +717,7 @@ fn document_xpath_result_slots_ignore_reflection_and_spoofing() {
snapshotLength: snapshot.snapshotLength,
snapshotFirst: snapshot.snapshotItem(0).id,
numberValue: number.numberValue,
fakeResultType: Object.getOwnPropertyDescriptor(proto, "resultType").get.call(fake),
fakeResultType: probe(() => Object.getOwnPropertyDescriptor(proto, "resultType").get.call(fake)),
fakeStringValue: probe(() => Object.getOwnPropertyDescriptor(proto, "stringValue").get.call(fake)),
fakeSnapshotItem: probe(() => proto.snapshotItem.call(fake, 0))
});
Expand All @@ -728,7 +728,7 @@ fn document_xpath_result_slots_ignore_reflection_and_spoofing() {

assert_eq!(
result,
r#"{"internalNamesBefore":{"iterator":[],"snapshot":[],"number":[]},"iteratorType":5,"iteratorInvalid":false,"iteratorNext":"first","snapshotLength":2,"snapshotFirst":"first","numberValue":2,"fakeResultType":0,"fakeStringValue":"throw:TypeError","fakeSnapshotItem":"throw:TypeError"}"#
r#"{"internalNamesBefore":{"iterator":[],"snapshot":[],"number":[]},"iteratorType":5,"iteratorInvalid":false,"iteratorNext":"first","snapshotLength":2,"snapshotFirst":"first","numberValue":2,"fakeResultType":"throw:TypeError","fakeStringValue":"throw:TypeError","fakeSnapshotItem":"throw:TypeError"}"#
);
}

Expand Down
Loading
Loading