Files
servo/components/script/dom/xpathresult.rs
Simon Wülker 9193b1d310 xpath: Enforce tree order in node sets during evaluation (#40451)
This change adds `NodeSet`, which is more or less a `Vec<Node>` that
knows whether it is sorted, and can sort itself if it isn't. This allows
us to efficiently enforce tree order in intermediary node sets that
occur during evaluation.

Notably, in many cases it is not necessary to sort at all, because all
location step expressions yield node sets that are either already sorted
(`Axis::DescendantOrSelf` for example), or that are in inverse tree
order (`Axis::PrecedingSiblings` for example).

There's still optimization work left to be done. When merging two node
sets that are already sorted then we could use merge sort instead of
appending one to the other and naively sorting. But I suspect that the
sorting algorithm used by the standard library is already well tuned to
handle such cases...

Testing: This change adds a web platform test
Fixes https://github.com/servo/servo/issues/40435

---------

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>
2025-11-06 12:24:07 +00:00

269 lines
10 KiB
Rust

/* This Source Code Form is subject to the terms of the Mozilla Public
* 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::cell::{Cell, RefCell};
use dom_struct::dom_struct;
use js::rust::HandleObject;
use script_bindings::codegen::GenericBindings::WindowBinding::WindowMethods;
use crate::dom::bindings::codegen::Bindings::XPathResultBinding::{
XPathResultConstants, XPathResultMethods,
};
use crate::dom::bindings::error::{Error, Fallible};
use crate::dom::bindings::inheritance::Castable;
use crate::dom::bindings::reflector::{Reflector, reflect_dom_object_with_proto};
use crate::dom::bindings::root::{Dom, DomRoot};
use crate::dom::bindings::str::DOMString;
use crate::dom::node::Node;
use crate::dom::window::Window;
use crate::script_runtime::CanGc;
use crate::xpath::{Value, XPathWrapper};
#[repr(u16)]
#[derive(Clone, Copy, Debug, Eq, JSTraceable, MallocSizeOf, Ord, PartialEq, PartialOrd)]
pub(crate) enum XPathResultType {
Any = XPathResultConstants::ANY_TYPE,
Number = XPathResultConstants::NUMBER_TYPE,
String = XPathResultConstants::STRING_TYPE,
Boolean = XPathResultConstants::BOOLEAN_TYPE,
UnorderedNodeIterator = XPathResultConstants::UNORDERED_NODE_ITERATOR_TYPE,
OrderedNodeIterator = XPathResultConstants::ORDERED_NODE_ITERATOR_TYPE,
UnorderedNodeSnapshot = XPathResultConstants::UNORDERED_NODE_SNAPSHOT_TYPE,
OrderedNodeSnapshot = XPathResultConstants::ORDERED_NODE_SNAPSHOT_TYPE,
AnyUnorderedNode = XPathResultConstants::ANY_UNORDERED_NODE_TYPE,
FirstOrderedNode = XPathResultConstants::FIRST_ORDERED_NODE_TYPE,
}
impl TryFrom<u16> for XPathResultType {
type Error = ();
fn try_from(value: u16) -> Result<Self, Self::Error> {
match value {
XPathResultConstants::ANY_TYPE => Ok(Self::Any),
XPathResultConstants::NUMBER_TYPE => Ok(Self::Number),
XPathResultConstants::STRING_TYPE => Ok(Self::String),
XPathResultConstants::BOOLEAN_TYPE => Ok(Self::Boolean),
XPathResultConstants::UNORDERED_NODE_ITERATOR_TYPE => Ok(Self::UnorderedNodeIterator),
XPathResultConstants::ORDERED_NODE_ITERATOR_TYPE => Ok(Self::OrderedNodeIterator),
XPathResultConstants::UNORDERED_NODE_SNAPSHOT_TYPE => Ok(Self::UnorderedNodeSnapshot),
XPathResultConstants::ORDERED_NODE_SNAPSHOT_TYPE => Ok(Self::OrderedNodeSnapshot),
XPathResultConstants::ANY_UNORDERED_NODE_TYPE => Ok(Self::AnyUnorderedNode),
XPathResultConstants::FIRST_ORDERED_NODE_TYPE => Ok(Self::FirstOrderedNode),
_ => Err(()),
}
}
}
#[derive(Debug, JSTraceable, MallocSizeOf)]
pub(crate) enum XPathResultValue {
Boolean(bool),
/// A IEEE-754 double-precision floating point number
Number(f64),
String(DOMString),
/// A collection of unique nodes
Nodeset(Vec<DomRoot<Node>>),
}
impl From<Value> for XPathResultValue {
fn from(value: Value) -> Self {
match value {
Value::Boolean(b) => XPathResultValue::Boolean(b),
Value::Number(n) => XPathResultValue::Number(n),
Value::String(s) => XPathResultValue::String(s.into()),
Value::NodeSet(nodes) => {
XPathResultValue::Nodeset(nodes.into_iter().map(XPathWrapper::into_inner).collect())
},
}
}
}
#[dom_struct]
pub(crate) struct XPathResult {
reflector_: Reflector,
window: Dom<Window>,
/// The revision of the owner document when this result was created. When iterating over the
/// values in the result, this is used to invalidate the iterator when the document is modified.
version: Cell<u64>,
result_type: Cell<XPathResultType>,
value: RefCell<XPathResultValue>,
iterator_pos: Cell<usize>,
}
impl XPathResult {
fn new_inherited(
window: &Window,
result_type: XPathResultType,
value: XPathResultValue,
) -> XPathResult {
XPathResult {
reflector_: Reflector::new(),
window: Dom::from_ref(window),
version: Cell::new(
window
.Document()
.upcast::<Node>()
.inclusive_descendants_version(),
),
result_type: Cell::new(result_type),
iterator_pos: Cell::new(0),
value: RefCell::new(value),
}
}
fn document_changed_since_creation(&self) -> bool {
let current_document_version = self
.window
.Document()
.upcast::<Node>()
.inclusive_descendants_version();
current_document_version != self.version.get()
}
/// NB: Blindly trusts `result_type` and constructs an object regardless of the contents
/// of `value`. The exception is `XPathResultType::Any`, for which we look at the value
/// to determine the type.
pub(crate) fn new(
window: &Window,
proto: Option<HandleObject>,
can_gc: CanGc,
result_type: XPathResultType,
value: XPathResultValue,
) -> DomRoot<XPathResult> {
reflect_dom_object_with_proto(
Box::new(XPathResult::new_inherited(window, result_type, value)),
window,
proto,
can_gc,
)
}
pub(crate) fn reinitialize_with(&self, result_type: XPathResultType, value: XPathResultValue) {
self.result_type.set(result_type);
*self.value.borrow_mut() = value;
self.version.set(
self.window
.Document()
.upcast::<Node>()
.inclusive_descendants_version(),
);
self.iterator_pos.set(0);
}
}
impl XPathResultMethods<crate::DomTypeHolder> for XPathResult {
/// <https://dom.spec.whatwg.org/#dom-xpathresult-resulttype>
fn ResultType(&self) -> u16 {
self.result_type.get() as u16
}
/// <https://dom.spec.whatwg.org/#dom-xpathresult-numbervalue>
fn GetNumberValue(&self) -> Fallible<f64> {
match (&*self.value.borrow(), self.result_type.get()) {
(XPathResultValue::Number(n), XPathResultType::Number) => Ok(*n),
_ => Err(Error::Type(
"Can't get number value for non-number XPathResult".to_string(),
)),
}
}
/// <https://dom.spec.whatwg.org/#dom-xpathresult-stringvalue>
fn GetStringValue(&self) -> Fallible<DOMString> {
match (&*self.value.borrow(), self.result_type.get()) {
(XPathResultValue::String(s), XPathResultType::String) => Ok(s.clone()),
_ => Err(Error::Type(
"Can't get string value for non-string XPathResult".to_string(),
)),
}
}
/// <https://dom.spec.whatwg.org/#dom-xpathresult-booleanvalue>
fn GetBooleanValue(&self) -> Fallible<bool> {
match (&*self.value.borrow(), self.result_type.get()) {
(XPathResultValue::Boolean(b), XPathResultType::Boolean) => Ok(*b),
_ => Err(Error::Type(
"Can't get boolean value for non-boolean XPathResult".to_string(),
)),
}
}
/// <https://dom.spec.whatwg.org/#dom-xpathresult-iteratenext>
fn IterateNext(&self) -> Fallible<Option<DomRoot<Node>>> {
if !matches!(
self.result_type.get(),
XPathResultType::OrderedNodeIterator | XPathResultType::UnorderedNodeIterator
) {
return Err(Error::Type("Result is not an iterator".into()));
}
if self.document_changed_since_creation() {
return Err(Error::InvalidState(None));
}
let XPathResultValue::Nodeset(nodes) = &*self.value.borrow() else {
return Err(Error::Type(
"Can't iterate on XPathResult that is not a node-set".to_string(),
));
};
let position = self.iterator_pos.get();
if position >= nodes.len() {
Ok(None)
} else {
let node = nodes[position].clone();
self.iterator_pos.set(position + 1);
Ok(Some(node))
}
}
/// <https://dom.spec.whatwg.org/#dom-xpathresult-invaliditeratorstate>
fn InvalidIteratorState(&self) -> bool {
let is_iterable = matches!(
self.result_type.get(),
XPathResultType::OrderedNodeIterator | XPathResultType::UnorderedNodeIterator
);
is_iterable && self.document_changed_since_creation()
}
/// <https://dom.spec.whatwg.org/#dom-xpathresult-snapshotlength>
fn GetSnapshotLength(&self) -> Fallible<u32> {
match (&*self.value.borrow(), self.result_type.get()) {
(
XPathResultValue::Nodeset(nodes),
XPathResultType::OrderedNodeSnapshot | XPathResultType::UnorderedNodeSnapshot,
) => Ok(nodes.len() as u32),
_ => Err(Error::Type(
"Can't get snapshot length of XPathResult that is not a snapshot".to_string(),
)),
}
}
/// <https://dom.spec.whatwg.org/#dom-xpathresult-snapshotitem>
fn SnapshotItem(&self, index: u32) -> Fallible<Option<DomRoot<Node>>> {
match (&*self.value.borrow(), self.result_type.get()) {
(
XPathResultValue::Nodeset(nodes),
XPathResultType::OrderedNodeSnapshot | XPathResultType::UnorderedNodeSnapshot,
) => Ok(nodes.get(index as usize).cloned()),
_ => Err(Error::Type(
"Can't get snapshot item of XPathResult that is not a snapshot".to_string(),
)),
}
}
/// <https://dom.spec.whatwg.org/#dom-xpathresult-singlenodevalue>
fn GetSingleNodeValue(&self) -> Fallible<Option<DomRoot<Node>>> {
match (&*self.value.borrow(), self.result_type.get()) {
(
XPathResultValue::Nodeset(nodes),
XPathResultType::AnyUnorderedNode | XPathResultType::FirstOrderedNode,
) => Ok(nodes.first().cloned()),
_ => Err(Error::Type(
"Getting single value requires result type 'any unordered node' or 'first ordered node'".to_string(),
)),
}
}
}