#[cfg(feature = "embedded")]
use crate::sxd_document as sxd_document_no_unsafe;
use snafu::{OptionExt, ResultExt, Snafu};
use std::fmt;
use sxd_document_no_unsafe::QName;
use crate::Value::{Boolean, Number};
use crate::axis::{Axis, AxisLike};
use crate::context;
use crate::function;
use crate::node_test::NodeTest;
use crate::nodeset::{Nodeset, OrderedNodes};
use crate::{LiteralValue, OwnedPrefixedName, Value};
#[derive(Debug, Snafu, Clone, PartialEq, Hash)]
#[snafu(context(suffix(false)))]
#[cfg_attr(test, snafu(visibility(pub(crate))))]
pub enum Error {
NotANodeset,
#[snafu(display("unknown function {:?}", name))]
UnknownFunction { name: OwnedPrefixedName },
#[snafu(display("unknown variable {:?}", name))]
UnknownVariable { name: OwnedPrefixedName },
#[snafu(display("unknown namespace prefix {}", prefix))]
UnknownNamespace { prefix: String },
#[snafu(display("error while evaluating function: {}", source))]
FunctionEvaluation { source: function::Error },
}
impl From<function::Error> for Error {
fn from(source: function::Error) -> Self {
Self::FunctionEvaluation { source }
}
}
fn value_into_nodeset(v: Value<'_>) -> Result<Nodeset<'_>, Error> {
match v {
Value::Nodeset(ns) => Ok(ns),
_ => Err(Error::NotANodeset),
}
}
fn value_into_ordered_nodes<'c, 'd>(
context: &context::Evaluation<'c, 'd>,
v: Value<'d>,
) -> Result<OrderedNodes<'d>, Error> {
match v {
Value::Nodeset(ns) => ns
.document_order_with_context(context)
.map(Into::into)
.map_err(|source| Error::FunctionEvaluation { source }),
_ => Err(Error::NotANodeset),
}
}
pub trait Expression: fmt::Debug {
fn evaluate<'c, 'd>(&self, context: &context::Evaluation<'c, 'd>) -> Result<Value<'d>, Error>;
fn ast_depth(&self) -> usize {
1
}
}
impl<T: ?Sized> Expression for Box<T>
where
T: Expression,
{
fn evaluate<'c, 'd>(&self, context: &context::Evaluation<'c, 'd>) -> Result<Value<'d>, Error> {
(**self).evaluate(context)
}
fn ast_depth(&self) -> usize {
(**self).ast_depth()
}
}
pub type SubExpression = Box<dyn Expression + 'static>;
macro_rules! binary_constructor(
($t:ident) => (
impl $t {
#[allow(clippy::new_ret_no_self)]
pub fn new(left: SubExpression, right: SubExpression) -> SubExpression {
Box::new($t{ left, right })
}
}
);
);
fn binary_ast_depth(left: &SubExpression, right: &SubExpression) -> usize {
1usize.saturating_add(left.ast_depth().max(right.ast_depth()))
}
#[derive(Debug)]
pub struct And {
pub left: SubExpression,
pub right: SubExpression,
}
binary_constructor!(And);
impl Expression for And {
fn evaluate<'c, 'd>(&self, context: &context::Evaluation<'c, 'd>) -> Result<Value<'d>, Error> {
let left = self.left.evaluate(context)?.boolean();
let v = left && self.right.evaluate(context)?.boolean();
Ok(Boolean(v))
}
fn ast_depth(&self) -> usize {
binary_ast_depth(&self.left, &self.right)
}
}
#[allow(missing_copy_implementations)]
#[derive(Debug)]
pub struct ContextNode;
impl Expression for ContextNode {
fn evaluate<'c, 'd>(&self, context: &context::Evaluation<'c, 'd>) -> Result<Value<'d>, Error> {
Ok(Value::Nodeset(nodeset![context.node.clone()]))
}
}
#[derive(Debug)]
pub struct Equal {
pub left: SubExpression,
pub right: SubExpression,
}
binary_constructor!(Equal);
impl Equal {
fn boolean_evaluate<'c, 'd>(
&self,
context: &context::Evaluation<'c, 'd>,
) -> Result<bool, Error> {
let left_val = self.left.evaluate(context)?;
let right_val = self.right.evaluate(context)?;
compare_equality_values(context, &left_val, &right_val, Equality::Equal)
}
}
impl Expression for Equal {
fn evaluate<'c, 'd>(&self, context: &context::Evaluation<'c, 'd>) -> Result<Value<'d>, Error> {
self.boolean_evaluate(context).map(Boolean)
}
fn ast_depth(&self) -> usize {
binary_ast_depth(&self.left, &self.right)
}
}
#[derive(Debug)]
pub struct NotEqual {
left: SubExpression,
right: SubExpression,
}
impl NotEqual {
#[allow(clippy::new_ret_no_self)]
pub fn new(left: SubExpression, right: SubExpression) -> SubExpression {
Box::new(NotEqual { left, right })
}
}
impl Expression for NotEqual {
fn evaluate<'c, 'd>(&self, context: &context::Evaluation<'c, 'd>) -> Result<Value<'d>, Error> {
let left = self.left.evaluate(context)?;
let right = self.right.evaluate(context)?;
compare_equality_values(context, &left, &right, Equality::NotEqual).map(Boolean)
}
fn ast_depth(&self) -> usize {
binary_ast_depth(&self.left, &self.right)
}
}
#[derive(Clone, Copy)]
enum Equality {
Equal,
NotEqual,
}
impl Equality {
fn strings(
self,
context: &context::Evaluation<'_, '_>,
left: &str,
right: &str,
) -> Result<bool, Error> {
if left.len() != right.len() {
return Ok(self.result(false));
}
context
.charge_work(left.len())
.context(FunctionEvaluation)?;
Ok(match self {
Self::Equal => left == right,
Self::NotEqual => left != right,
})
}
fn numbers(self, left: f64, right: f64) -> bool {
match self {
Self::Equal => left == right,
Self::NotEqual => left != right,
}
}
fn booleans(self, left: bool, right: bool) -> bool {
match self {
Self::Equal => left == right,
Self::NotEqual => left != right,
}
}
fn result(self, equal: bool) -> bool {
match self {
Self::Equal => equal,
Self::NotEqual => !equal,
}
}
}
fn materialize_node_string<'c, 'd>(
context: &context::Evaluation<'c, 'd>,
node: &crate::nodeset::Node<'d>,
) -> Result<String, Error> {
node.string_value_with_context(context)
.context(FunctionEvaluation)
}
fn compare_nodesets<'c, 'd>(
context: &context::Evaluation<'c, 'd>,
left: &Nodeset<'d>,
right: &Nodeset<'d>,
comparison: Equality,
) -> Result<bool, Error> {
let (materialized, compared) = if left.size() <= right.size() {
(left, right)
} else {
(right, left)
};
for node in materialized.iter() {
let value = materialize_node_string(context, &node)?;
for candidate in compared.iter() {
context.charge_work(1).context(FunctionEvaluation)?;
if comparison.result(
candidate
.string_value_eq_with_context(context, &value)
.context(FunctionEvaluation)?,
) {
return Ok(true);
}
}
}
Ok(false)
}
fn compare_nodes_with_string(
context: &context::Evaluation<'_, '_>,
nodes: &Nodeset<'_>,
value: &str,
comparison: Equality,
) -> Result<bool, Error> {
for node in nodes.iter() {
context.charge_work(1).context(FunctionEvaluation)?;
if comparison.result(
node.string_value_eq_with_context(context, value)
.context(FunctionEvaluation)?,
) {
return Ok(true);
}
}
Ok(false)
}
fn compare_equality_values<'c, 'd>(
context: &context::Evaluation<'c, 'd>,
left: &Value<'d>,
right: &Value<'d>,
comparison: Equality,
) -> Result<bool, Error> {
let result = match (left, right) {
(Value::Nodeset(left), Value::Nodeset(right)) => {
return compare_nodesets(context, left, right, comparison);
}
(Value::Nodeset(nodes), Boolean(value)) | (Boolean(value), Value::Nodeset(nodes)) => {
comparison.booleans(nodes.size() != 0, *value)
}
(Value::Nodeset(nodes), Number(value)) | (Number(value), Value::Nodeset(nodes)) => {
for node in nodes.iter() {
context.charge_work(1).context(FunctionEvaluation)?;
let string = materialize_node_string(context, &node)?;
if comparison.numbers(crate::str_to_num(&string), *value) {
return Ok(true);
}
}
false
}
(Value::Nodeset(nodes), Value::String(value))
| (Value::String(value), Value::Nodeset(nodes)) => {
compare_nodes_with_string(context, nodes, value, comparison)?
}
(Value::Nodeset(nodes), Value::ResultTreeFragment(_, value))
| (Value::ResultTreeFragment(_, value), Value::Nodeset(nodes)) => {
compare_nodes_with_string(context, nodes, value, comparison)?
}
(Boolean(_), _) | (_, Boolean(_)) => comparison.booleans(left.boolean(), right.boolean()),
(Number(_), _) | (_, Number(_)) => comparison.numbers(
left.number(context).context(FunctionEvaluation)?,
right.number(context).context(FunctionEvaluation)?,
),
(Value::String(left), Value::String(right)) => comparison.strings(context, left, right)?,
(Value::String(left), Value::ResultTreeFragment(_, right)) => {
comparison.strings(context, left, right)?
}
(Value::ResultTreeFragment(_, left), Value::String(right)) => {
comparison.strings(context, left, right)?
}
(Value::ResultTreeFragment(_, left), Value::ResultTreeFragment(_, right)) => {
comparison.strings(context, left, right)?
}
};
Ok(result)
}
#[derive(Debug)]
pub struct Function {
pub name: OwnedPrefixedName,
pub arguments: Vec<SubExpression>,
}
impl Expression for Function {
fn evaluate<'c, 'd>(&self, context: &context::Evaluation<'c, 'd>) -> Result<Value<'d>, Error> {
let name = resolve_prefixed_name(context, &self.name)?;
context
.function_for_name(name)
.context(UnknownFunction { name: &self.name })
.and_then(|fun| {
let bytes = self
.arguments
.len()
.checked_mul(std::mem::size_of::<Value<'_>>())
.filter(|bytes| *bytes <= isize::MAX as usize)
.ok_or_else(|| crate::function::Error::Other {
what: "XPath argument storage exceeds addressable memory".into(),
})
.context(FunctionEvaluation)?;
context
.reserve_temporary_allocation(bytes)
.context(FunctionEvaluation)?;
let mut args = Vec::with_capacity(self.arguments.len());
for argument in &self.arguments {
args.push(argument.evaluate(context)?);
}
fun.evaluate(context, args).context(FunctionEvaluation)
})
}
fn ast_depth(&self) -> usize {
1usize.saturating_add(
self.arguments
.iter()
.map(|argument| argument.ast_depth())
.max()
.unwrap_or(0),
)
}
}
#[derive(Debug)]
pub struct Literal {
value: LiteralValue,
}
impl From<LiteralValue> for Literal {
fn from(other: LiteralValue) -> Literal {
Literal { value: other }
}
}
impl Expression for Literal {
fn evaluate<'c, 'd>(&self, context: &context::Evaluation<'c, 'd>) -> Result<Value<'d>, Error> {
self.value
.clone_with_context(context)
.context(FunctionEvaluation)
}
}
pub struct Math {
left: SubExpression,
right: SubExpression,
operation: fn(f64, f64) -> f64,
}
fn add(a: f64, b: f64) -> f64 {
a + b
}
fn subtract(a: f64, b: f64) -> f64 {
a - b
}
fn multiply(a: f64, b: f64) -> f64 {
a * b
}
fn divide(a: f64, b: f64) -> f64 {
a / b
}
fn modulus(a: f64, b: f64) -> f64 {
a % b
}
impl Math {
pub fn addition(left: SubExpression, right: SubExpression) -> SubExpression {
Box::new(Math {
left,
right,
operation: add,
})
}
pub fn subtraction(left: SubExpression, right: SubExpression) -> SubExpression {
Box::new(Math {
left,
right,
operation: subtract,
})
}
pub fn multiplication(left: SubExpression, right: SubExpression) -> SubExpression {
Box::new(Math {
left,
right,
operation: multiply,
})
}
pub fn division(left: SubExpression, right: SubExpression) -> SubExpression {
Box::new(Math {
left,
right,
operation: divide,
})
}
pub fn remainder(left: SubExpression, right: SubExpression) -> SubExpression {
Box::new(Math {
left,
right,
operation: modulus,
})
}
}
impl Expression for Math {
fn evaluate<'c, 'd>(&self, context: &context::Evaluation<'c, 'd>) -> Result<Value<'d>, Error> {
let left = self.left.evaluate(context)?;
let right = self.right.evaluate(context)?;
let op = self.operation;
let left = left.number(context).context(FunctionEvaluation)?;
let right = right.number(context).context(FunctionEvaluation)?;
Ok(Number(op(left, right)))
}
fn ast_depth(&self) -> usize {
binary_ast_depth(&self.left, &self.right)
}
}
impl fmt::Debug for Math {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Math {{ left: {:?}, right: {:?} }}",
self.left, self.right
)
}
}
#[derive(Debug)]
pub struct Negation {
pub expression: SubExpression,
}
impl Expression for Negation {
fn evaluate<'c, 'd>(&self, context: &context::Evaluation<'c, 'd>) -> Result<Value<'d>, Error> {
let value = self.expression.evaluate(context)?;
value
.number(context)
.map(|number| Number(-number))
.context(FunctionEvaluation)
}
fn ast_depth(&self) -> usize {
1usize.saturating_add(self.expression.ast_depth())
}
}
#[derive(Debug)]
pub struct Or {
left: SubExpression,
right: SubExpression,
}
binary_constructor!(Or);
impl Expression for Or {
fn evaluate<'c, 'd>(&self, context: &context::Evaluation<'c, 'd>) -> Result<Value<'d>, Error> {
let left = self.left.evaluate(context)?.boolean();
let v = left || self.right.evaluate(context)?.boolean();
Ok(Boolean(v))
}
fn ast_depth(&self) -> usize {
binary_ast_depth(&self.left, &self.right)
}
}
#[derive(Debug)]
pub struct Path {
start_point: SubExpression,
steps: Vec<Step>,
}
impl Path {
#[allow(clippy::new_ret_no_self)]
pub fn new(start_point: SubExpression, steps: Vec<Step>) -> SubExpression {
Box::new(Path { start_point, steps })
}
}
impl Expression for Path {
fn evaluate<'c, 'd>(&self, context: &context::Evaluation<'c, 'd>) -> Result<Value<'d>, Error> {
let result = self.start_point.evaluate(context)?;
let mut result = value_into_nodeset(result)?;
for step in &self.steps {
result = step.evaluate(context, result)?;
}
Ok(Value::Nodeset(result))
}
fn ast_depth(&self) -> usize {
1usize.saturating_add(
self.start_point.ast_depth().max(
self.steps
.iter()
.map(ParameterizedStep::ast_depth)
.max()
.unwrap_or(0),
),
)
}
}
#[derive(Debug)]
pub struct Filter {
node_selector: SubExpression,
predicate: Predicate,
}
impl Filter {
#[allow(clippy::new_ret_no_self)]
pub fn new(node_selector: SubExpression, predicate: SubExpression) -> SubExpression {
let predicate = Predicate {
expression: predicate,
};
Box::new(Filter {
node_selector,
predicate,
})
}
}
impl Expression for Filter {
fn evaluate<'c, 'd>(&self, context: &context::Evaluation<'c, 'd>) -> Result<Value<'d>, Error> {
self.node_selector
.evaluate(context)
.and_then(|value| value_into_ordered_nodes(context, value))
.and_then(|nodes| self.predicate.select(context, nodes))
.map(|nodes| Value::Nodeset(nodes.into()))
}
fn ast_depth(&self) -> usize {
1usize.saturating_add(
self.node_selector
.ast_depth()
.max(self.predicate.expression.ast_depth()),
)
}
}
pub struct Relational {
pub left: SubExpression,
pub right: SubExpression,
pub operation: fn(f64, f64) -> bool,
}
fn less_than(left: f64, right: f64) -> bool {
left < right
}
fn less_than_or_equal(left: f64, right: f64) -> bool {
left <= right
}
fn greater_than(left: f64, right: f64) -> bool {
left > right
}
fn greater_than_or_equal(left: f64, right: f64) -> bool {
left >= right
}
impl Relational {
pub fn less_than(left: SubExpression, right: SubExpression) -> SubExpression {
Box::new(Relational {
left,
right,
operation: less_than,
})
}
pub fn less_than_or_equal(left: SubExpression, right: SubExpression) -> SubExpression {
Box::new(Relational {
left,
right,
operation: less_than_or_equal,
})
}
pub fn greater_than(left: SubExpression, right: SubExpression) -> SubExpression {
Box::new(Relational {
left,
right,
operation: greater_than,
})
}
pub fn greater_than_or_equal(left: SubExpression, right: SubExpression) -> SubExpression {
Box::new(Relational {
left,
right,
operation: greater_than_or_equal,
})
}
}
impl Expression for Relational {
fn evaluate<'c, 'd>(&self, context: &context::Evaluation<'c, 'd>) -> Result<Value<'d>, Error> {
let left_val = self.left.evaluate(context)?;
let right_val = self.right.evaluate(context)?;
let op = self.operation;
let result = compare_relational_values(context, &left_val, &right_val, op)?;
Ok(Boolean(result))
}
fn ast_depth(&self) -> usize {
binary_ast_depth(&self.left, &self.right)
}
}
fn compare_relational_values(
context: &context::Evaluation<'_, '_>,
left: &Value<'_>,
right: &Value<'_>,
operation: fn(f64, f64) -> bool,
) -> Result<bool, Error> {
match (left, right) {
(Value::Nodeset(left), Value::Nodeset(right)) => {
if left.size() <= right.size() {
let left = collect_node_numbers(context, left)?;
for right in right.iter() {
context.charge_work(1).context(FunctionEvaluation)?;
let right = crate::node_to_num_with_context(context, &right)
.context(FunctionEvaluation)?;
for left in &left {
context.charge_work(1).context(FunctionEvaluation)?;
if operation(*left, right) {
return Ok(true);
}
}
}
} else {
let right = collect_node_numbers(context, right)?;
for left in left.iter() {
context.charge_work(1).context(FunctionEvaluation)?;
let left = crate::node_to_num_with_context(context, &left)
.context(FunctionEvaluation)?;
for right in &right {
context.charge_work(1).context(FunctionEvaluation)?;
if operation(left, *right) {
return Ok(true);
}
}
}
}
Ok(false)
}
(Value::Nodeset(left), right) => {
let right = right.number(context).context(FunctionEvaluation)?;
for left in left.iter() {
context.charge_work(1).context(FunctionEvaluation)?;
let left =
crate::node_to_num_with_context(context, &left).context(FunctionEvaluation)?;
if operation(left, right) {
return Ok(true);
}
}
Ok(false)
}
(left, Value::Nodeset(right)) => {
let left = left.number(context).context(FunctionEvaluation)?;
for right in right.iter() {
context.charge_work(1).context(FunctionEvaluation)?;
let right =
crate::node_to_num_with_context(context, &right).context(FunctionEvaluation)?;
if operation(left, right) {
return Ok(true);
}
}
Ok(false)
}
(left, right) => Ok(operation(
left.number(context).context(FunctionEvaluation)?,
right.number(context).context(FunctionEvaluation)?,
)),
}
}
fn collect_node_numbers(
context: &context::Evaluation<'_, '_>,
nodes: &Nodeset<'_>,
) -> Result<Vec<f64>, Error> {
if nodes.size() > usize::MAX / std::mem::size_of::<f64>() {
return Err(Error::FunctionEvaluation {
source: function::Error::Other {
what: "XPath numeric workspace size overflow".into(),
},
});
}
context
.reserve_string_allocation(nodes.size() * std::mem::size_of::<f64>())
.context(FunctionEvaluation)?;
let mut values = Vec::with_capacity(nodes.size());
for node in nodes.iter() {
context.charge_work(1).context(FunctionEvaluation)?;
values.push(crate::node_to_num_with_context(context, &node).context(FunctionEvaluation)?);
}
Ok(values)
}
impl fmt::Debug for Relational {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Relational {{ left: {:?}, right: {:?} }}",
self.left, self.right
)
}
}
#[allow(missing_copy_implementations)]
#[derive(Debug)]
pub struct RootNode;
impl Expression for RootNode {
fn evaluate<'c, 'd>(&self, context: &context::Evaluation<'c, 'd>) -> Result<Value<'d>, Error> {
let node = context
.node
.clone_with_context(context)
.context(FunctionEvaluation)?;
let mut nodes = Nodeset::new();
nodes
.add_metered(context, context.document_root_for(node))
.context(FunctionEvaluation)?;
Ok(Value::Nodeset(nodes))
}
}
#[derive(Debug)]
pub(crate) struct Predicate {
pub expression: SubExpression,
}
impl Predicate {
fn select<'c, 'd>(
&self,
context: &context::Evaluation<'c, 'd>,
nodes: OrderedNodes<'d>,
) -> Result<OrderedNodes<'d>, Error> {
context
.clone()
.new_contexts_for(nodes)
.filter_map(|ctx| match self.matches(&ctx) {
Ok(true) => Some(Ok(ctx.node)),
Ok(false) => None,
Err(e) => Some(Err(e)),
})
.collect()
}
fn matches(&self, context: &context::Evaluation<'_, '_>) -> Result<bool, Error> {
context.charge_work(1).context(FunctionEvaluation)?;
let value = self.expression.evaluate(context)?;
let v = match value {
Number(v) => context.position as f64 == v,
_ => value.boolean(),
};
Ok(v)
}
}
pub type Step = ParameterizedStep<Axis>;
pub type StepTest = Box<dyn NodeTest + 'static>;
#[derive(Debug)]
pub struct ParameterizedStep<A> {
axis: A,
node_test: StepTest,
predicates: Vec<Predicate>,
}
impl<A> ParameterizedStep<A>
where
A: AxisLike,
{
pub(crate) fn from_predicates(
axis: A,
node_test: StepTest,
predicates: Vec<Predicate>,
) -> Self {
ParameterizedStep {
axis,
node_test,
predicates,
}
}
fn evaluate<'c, 'd>(
&self,
context: &context::Evaluation<'c, 'd>,
starting_nodes: Nodeset<'d>,
) -> Result<Nodeset<'d>, Error> {
let mut unique = Nodeset::new();
for node in starting_nodes.iter() {
let child_context = context.new_context_for(node);
let mut nodes = self.axis.select_nodes(&child_context, &self.node_test)?;
for predicate in &self.predicates {
nodes = predicate.select(context, nodes)?;
}
for node in Vec::from(nodes) {
unique
.add_metered(context, node)
.context(FunctionEvaluation)?;
}
}
Ok(unique)
}
fn ast_depth(&self) -> usize {
self.predicates
.iter()
.map(|predicate| predicate.expression.ast_depth())
.max()
.unwrap_or(0)
}
}
#[derive(Debug)]
pub struct Union {
pub left: SubExpression,
pub right: SubExpression,
}
binary_constructor!(Union);
impl Expression for Union {
fn evaluate<'c, 'd>(&self, context: &context::Evaluation<'c, 'd>) -> Result<Value<'d>, Error> {
let as_nodes = |e: &SubExpression| e.evaluate(context).and_then(value_into_nodeset);
let mut left_nodes = as_nodes(&self.left)?;
let right_nodes = as_nodes(&self.right)?;
for node in right_nodes {
left_nodes
.add_metered(context, node)
.context(FunctionEvaluation)?;
}
Ok(Value::Nodeset(left_nodes))
}
fn ast_depth(&self) -> usize {
binary_ast_depth(&self.left, &self.right)
}
}
fn resolve_prefixed_name<'a>(
context: &'a context::Evaluation<'_, '_>,
name: &'a OwnedPrefixedName,
) -> Result<QName<'a>, Error> {
let ns_uri = match name.prefix.as_ref().map(|p| &**p) {
None => None,
Some(prefix) => match context.namespace_for(prefix) {
None => return UnknownNamespace { prefix }.fail(),
Some(uri) => Some(uri),
},
};
Ok(QName::with_namespace_uri(ns_uri, name.local_part.as_str()))
}
#[derive(Debug)]
pub struct Variable {
pub name: OwnedPrefixedName,
}
impl Expression for Variable {
fn evaluate<'c, 'd>(&self, context: &context::Evaluation<'c, 'd>) -> Result<Value<'d>, Error> {
let name = resolve_prefixed_name(context, &self.name)?;
context
.value_of(name)
.context(UnknownVariable { name: &self.name })
.and_then(|value| {
value
.clone_with_context(context)
.context(FunctionEvaluation)
})
}
}
#[cfg(test)]
mod test {
use std::borrow::ToOwned;
use std::cell::RefCell;
use std::rc::Rc;
use sxd_document_no_unsafe::Package;
use sxd_document_no_unsafe::dom::Document;
use crate::Value::{self, Boolean, Number, String};
use crate::axis::AxisLike;
use crate::context::{self, Context};
use crate::function;
use crate::node_test::NodeTest;
use crate::nodeset::OrderedNodes;
use super::*;
#[test]
fn evaluator_containers_reserve_before_materialization() {
for source in ["/", "count(0,0,0,0,0,0,0,0)"] {
let package = Package::new();
let mut context = Context::new();
context.set_string_allocation_limit(0);
let expression = crate::Factory::new().build(source).expect("valid syntax");
let error = expression
.evaluate(&context, package.as_document().root())
.expect_err("container requires storage");
assert!(
error.to_string().contains("allocation budget"),
"{source}: {error}"
);
}
}
#[test]
fn fragment_equality_reserves_lexical_work() {
let package = Package::new();
let mut context = Context::new();
context.set_variable(
"left",
Value::ResultTreeFragment(1, std::sync::Arc::new("x".repeat(4096))),
);
context.set_variable(
"right",
Value::ResultTreeFragment(2, std::sync::Arc::new("x".repeat(4096))),
);
context.set_evaluation_work_limit(0);
let expression = crate::Factory::new()
.build("$left = $right")
.expect("valid equality");
let error = expression
.evaluate(&context, package.as_document().root())
.expect_err("comparison needs work");
assert!(error.to_string().contains("budget"));
let mut context = Context::new();
context.set_evaluation_work_limit(0);
let evaluation = context::Evaluation::new(&context, package.as_document().root().into());
assert!(
!Equality::Equal
.strings(&evaluation, "x", "longer")
.expect("length gate")
);
assert!(
Equality::NotEqual
.strings(&evaluation, "x", "longer")
.expect("length gate")
);
}
#[test]
fn union_reserves_growth_before_combining_variables() {
let package = Package::new();
let mut setup = Setup::new(&package);
let left = setup.doc.create_element("left");
let right = setup.doc.create_element("right");
setup.context.set_variable("left", crate::nodeset![left]);
setup.context.set_variable("right", crate::nodeset![right]);
setup.context.set_string_allocation_limit(0);
let expression = Union::new(
Box::new(Variable {
name: "left".into(),
}),
Box::new(Variable {
name: "right".into(),
}),
);
assert!(matches!(
expression.evaluate(&setup.context()),
Err(Error::FunctionEvaluation { .. })
));
}
#[test]
fn nodeset_equality_charges_each_candidate_comparison() {
let package = Package::new();
let mut setup = Setup::new(&package);
let left = setup.doc.create_text("");
let right = setup.doc.create_text("different");
setup.context.set_variable("left", crate::nodeset![left]);
setup.context.set_variable("right", crate::nodeset![right]);
setup.context.set_evaluation_work_limit(0);
let expression = Equal::new(
Box::new(Variable {
name: "left".into(),
}),
Box::new(Variable {
name: "right".into(),
}),
);
assert!(matches!(
expression.evaluate(&setup.context()),
Err(Error::FunctionEvaluation { .. })
));
}
#[test]
fn numeric_nodeset_conversions_consume_evaluation_work() {
let package = Package::new();
let mut setup = Setup::new(&package);
let value = setup.doc.create_text("");
setup.context.set_variable("value", crate::nodeset![value]);
setup.context.set_variable("empty", crate::nodeset![]);
setup.context.set_evaluation_work_limit(0);
let equality = Equal::new(
Box::new(Variable {
name: "value".into(),
}),
Box::new(Literal {
value: Value::Number(1.0),
}),
);
let relational = Relational::less_than(
Box::new(Variable {
name: "value".into(),
}),
Box::new(Variable {
name: "empty".into(),
}),
);
for expression in [equality, relational] {
assert!(matches!(
expression.evaluate(&setup.context()),
Err(Error::FunctionEvaluation { .. })
));
}
}
#[derive(Debug)]
struct FailExpression;
impl Expression for FailExpression {
fn evaluate<'c, 'd>(&self, _: &context::Evaluation<'c, 'd>) -> Result<Value<'d>, Error> {
panic!("Should never be called");
}
}
struct Setup<'d> {
doc: Document<'d>,
context: Context<'d>,
}
impl<'d> Setup<'d> {
fn new(package: &'d Package) -> Setup<'d> {
Setup {
doc: package.as_document(),
context: Context::without_core_functions(),
}
}
fn context(&'d self) -> context::Evaluation<'d, 'd> {
let node = self.doc.create_element("test");
context::Evaluation::new(&self.context, node.into())
}
}
#[test]
fn expression_and_returns_logical_and() {
let package = Package::new();
let setup = Setup::new(&package);
let left = Box::new(Literal {
value: Value::Boolean(true),
});
let right = Box::new(Literal {
value: Value::Boolean(true),
});
let expr = And { left, right };
let context = setup.context();
let res = expr.evaluate(&context);
assert_eq!(res, Ok(Boolean(true)));
}
#[test]
fn expression_and_short_circuits_when_left_argument_is_false() {
let package = Package::new();
let setup = Setup::new(&package);
let left = Box::new(Literal {
value: Value::Boolean(false),
});
let right = Box::new(FailExpression);
let expr = And { left, right };
let context = setup.context();
let res = expr.evaluate(&context);
assert_eq!(res, Ok(Boolean(false)));
}
#[test]
fn expression_equal_intersects_string_values_of_two_nodesets() {
let package = Package::new();
let mut setup = Setup::new(&package);
let string_value_1 = setup.doc.create_text("same");
let string_value_2 = setup.doc.create_text("same");
setup.context.set_variable("left", nodeset![string_value_1]);
setup
.context
.set_variable("right", nodeset![string_value_2]);
let left = Box::new(Variable {
name: "left".into(),
});
let right = Box::new(Variable {
name: "right".into(),
});
let expr = Equal { left, right };
let context = setup.context();
let res = expr.evaluate(&context);
assert_eq!(res, Ok(Boolean(true)));
}
#[test]
fn expression_equal_compares_number_value_of_nodeset_to_number() {
let package = Package::new();
let mut setup = Setup::new(&package);
let string_value = setup.doc.create_text("3.14");
setup.context.set_variable("left", nodeset![string_value]);
let left = Box::new(Variable {
name: "left".into(),
});
let right = Box::new(Literal {
value: Value::Number(6.3),
});
let expr = Equal { left, right };
let context = setup.context();
let res = expr.evaluate(&context);
assert_eq!(res, Ok(Boolean(false)));
}
#[test]
fn expression_equal_compares_string_value_of_nodeset_to_string() {
let package = Package::new();
let mut setup = Setup::new(&package);
let string_value_1 = setup.doc.create_text("gravy");
let string_value_2 = setup.doc.create_text("boat");
setup
.context
.set_variable("left", nodeset![string_value_1, string_value_2]);
let left = Box::new(Variable {
name: "left".into(),
});
let right = Box::new(Literal {
value: Value::String("boat".to_owned()),
});
let expr = Equal { left, right };
let context = setup.context();
let res = expr.evaluate(&context);
assert_eq!(res, Ok(Boolean(true)));
}
#[test]
fn expression_equality_reserves_node_string_values_before_allocation() {
let package = Package::new();
let document = package.as_document();
let left_value = document.create_text("four");
let right_value = document.create_text("four");
let expression = Equal {
left: Box::new(Variable {
name: "left".into(),
}),
right: Box::new(Variable {
name: "right".into(),
}),
};
let mut insufficient = Context::without_core_functions();
insufficient.set_variable("left", nodeset![left_value.clone()]);
insufficient.set_variable("right", nodeset![right_value.clone()]);
let variable_storage = 2
* crate::nodeset::hashset_growth_bytes::<crate::nodeset::Node<'_>>(0)
.expect("one-node set storage is representable");
insufficient.set_string_allocation_limit(variable_storage + 3);
let context_node = document.create_element("test");
let evaluation = context::Evaluation::new(&insufficient, context_node.clone().into());
assert!(matches!(
expression.evaluate(&evaluation),
Err(Error::FunctionEvaluation { source: function::Error::Other { what } })
if what.contains("allocation budget")
));
let mut exact = Context::without_core_functions();
exact.set_variable("left", nodeset![left_value]);
exact.set_variable("right", nodeset![right_value]);
exact.set_string_allocation_limit(variable_storage + 4);
let evaluation = context::Evaluation::new(&exact, context_node.into());
assert_eq!(expression.evaluate(&evaluation), Ok(Boolean(true)));
}
#[test]
fn arithmetic_and_relational_expressions_bound_node_numeric_conversion() {
let package = Package::new();
let mut setup = Setup::new(&package);
let value = setup.doc.create_text("1234");
setup.context.set_variable("value", nodeset![value]);
setup.context.set_string_allocation_limit(3);
let variable = || {
Box::new(Variable {
name: "value".into(),
}) as SubExpression
};
let literal = || {
Box::new(Literal {
value: Value::Number(0.0),
}) as SubExpression
};
let expressions: [SubExpression; 2] = [
Math::addition(variable(), literal()),
Relational::greater_than(variable(), literal()),
];
for expression in expressions {
let error = expression
.evaluate(&setup.context())
.expect_err("numeric conversion must cross the string allocation gate");
assert!(matches!(
error,
Error::FunctionEvaluation {
source: function::Error::Other { what }
} if what.contains("allocation budget")
));
}
}
#[test]
fn result_tree_fragment_equality_checks_every_nodeset_value_symmetrically() {
let package = Package::new();
let setup = Setup::new(&package);
let first = setup.doc.create_text("first");
let matching = setup.doc.create_text("matching");
let nodes = Value::Nodeset(nodeset![first, matching]);
let fragment = Value::ResultTreeFragment(1, std::sync::Arc::new("matching".into()));
let context = setup.context();
assert_eq!(
compare_equality_values(&context, &nodes, &fragment, Equality::Equal),
Ok(true)
);
assert_eq!(
compare_equality_values(&context, &fragment, &nodes, Equality::Equal),
Ok(true)
);
}
#[test]
fn expression_equal_compares_as_boolean_if_one_argument_is_a_boolean() {
let package = Package::new();
let setup = Setup::new(&package);
let actual_bool = Box::new(Literal {
value: Value::Boolean(false),
});
let truthy_str = Box::new(Literal {
value: Value::String("hello".to_owned()),
});
let expr = Equal {
left: actual_bool,
right: truthy_str,
};
let context = setup.context();
let res = expr.evaluate(&context);
assert_eq!(res, Ok(Boolean(false)));
}
#[test]
fn expression_equal_compares_as_number_if_one_argument_is_a_number() {
let package = Package::new();
let setup = Setup::new(&package);
let actual_number = Box::new(Literal {
value: Value::Number(-42.0),
});
let number_str = Box::new(Literal {
value: Value::String("-42.0".to_owned()),
});
let expr = Equal {
left: number_str,
right: actual_number,
};
let context = setup.context();
let res = expr.evaluate(&context);
assert_eq!(res, Ok(Boolean(true)));
}
#[test]
fn expression_equal_compares_as_string_otherwise() {
let package = Package::new();
let setup = Setup::new(&package);
let a_str = Box::new(Literal {
value: Value::String("hello".to_owned()),
});
let b_str = Box::new(Literal {
value: Value::String("World".to_owned()),
});
let expr = Equal {
left: a_str,
right: b_str,
};
let context = setup.context();
let res = expr.evaluate(&context);
assert_eq!(res, Ok(Boolean(false)));
}
#[test]
fn expression_not_equal_negates_equality() {
let package = Package::new();
let setup = Setup::new(&package);
let a_str = Box::new(Literal {
value: Value::Boolean(true),
});
let b_str = Box::new(Literal {
value: Value::Boolean(false),
});
let expr = NotEqual::new(a_str, b_str);
let context = setup.context();
let res = expr.evaluate(&context);
assert_eq!(res, Ok(Boolean(true)));
}
#[test]
fn expression_not_equal_requires_a_node_for_nodeset_comparisons() {
let package = Package::new();
let mut setup = Setup::new(&package);
setup.context.set_variable("empty", nodeset![]);
let expr = NotEqual::new(
Box::new(Variable {
name: "empty".into(),
}),
Box::new(Literal {
value: Value::String("".to_owned()),
}),
);
assert_eq!(expr.evaluate(&setup.context()), Ok(Boolean(false)));
}
#[test]
fn expression_not_equal_uses_existential_nodeset_semantics() {
let package = Package::new();
let mut setup = Setup::new(&package);
let same = setup.doc.create_text("same");
let different = setup.doc.create_text("different");
setup
.context
.set_variable("values", nodeset![same, different]);
let expr = NotEqual::new(
Box::new(Variable {
name: "values".into(),
}),
Box::new(Literal {
value: Value::String("same".to_owned()),
}),
);
assert_eq!(expr.evaluate(&setup.context()), Ok(Boolean(true)));
}
struct StubFunction {
value: &'static str,
}
impl function::Function for StubFunction {
fn evaluate<'c, 'd>(
&self,
_: &context::Evaluation<'c, 'd>,
_: Vec<Value<'d>>,
) -> Result<Value<'d>, function::Error> {
Ok(String(self.value.to_owned()))
}
}
#[test]
fn expression_function_evaluates_input_arguments() {
let package = Package::new();
let mut setup = Setup::new(&package);
let arg_expr: Box<dyn Expression> = Box::new(Literal {
value: Value::Boolean(true),
});
setup.context.set_function(
"test-fn",
StubFunction {
value: "the function ran",
},
);
let expr = Function {
name: "test-fn".into(),
arguments: vec![arg_expr],
};
let context = setup.context();
let res = expr.evaluate(&context);
assert_eq!(res, Ok(String("the function ran".to_owned())));
}
#[test]
fn expression_function_unknown_function_is_reported_as_an_error() {
let package = Package::new();
let setup = Setup::new(&package);
let expr = Function {
name: "unknown-fn".into(),
arguments: vec![],
};
let context = setup.context();
let res = expr.evaluate(&context);
assert_eq!(res, UnknownFunction { name: "unknown-fn" }.fail());
}
#[test]
fn expression_math_does_basic_math() {
let package = Package::new();
let setup = Setup::new(&package);
let left = Box::new(Literal {
value: Value::Number(10.0),
});
let right = Box::new(Literal {
value: Value::Number(5.0),
});
let expr = Math::multiplication(left, right);
let context = setup.context();
let res = expr.evaluate(&context);
assert_eq!(res, Ok(Number(50.0)));
}
#[test]
fn filter_with_numeric_predicate_selects_that_node() {
let package = Package::new();
let mut setup = Setup::new(&package);
let input_node_1 = setup.doc.create_element("one");
let input_node_2 = setup.doc.create_element("two");
let input_nodeset = nodeset![input_node_1, input_node_2];
setup.context.set_variable("nodes", input_nodeset);
let parent = setup.doc.create_element("parent");
setup.doc.root().append_child(parent);
parent.append_child(input_node_1);
parent.append_child(input_node_2);
let selected_nodes = Box::new(Variable {
name: "nodes".into(),
});
let predicate = Box::new(Literal {
value: Value::Number(1.0),
});
let expr = Filter::new(selected_nodes, predicate);
let context = setup.context();
let res = expr.evaluate(&context);
assert_eq!(res, Ok(Value::Nodeset(nodeset![input_node_1])));
}
#[test]
fn filter_with_false_predicate_selects_no_nodes() {
let package = Package::new();
let mut setup = Setup::new(&package);
let input_node_1 = setup.doc.create_element("one");
let input_node_2 = setup.doc.create_element("two");
let input_nodeset = nodeset![input_node_1, input_node_2];
setup.context.set_variable("nodes", input_nodeset);
let selected_nodes = Box::new(Variable {
name: "nodes".into(),
});
let predicate = Box::new(Literal {
value: Value::Boolean(false),
});
let expr = Filter::new(selected_nodes, predicate);
let context = setup.context();
let res = expr.evaluate(&context);
assert_eq!(res, Ok(Value::Nodeset(nodeset![])));
}
#[test]
fn expression_relational_does_basic_comparisons() {
let package = Package::new();
let setup = Setup::new(&package);
let left = Box::new(Literal {
value: Value::Number(10.0),
});
let right = Box::new(Literal {
value: Value::Number(5.0),
});
let expr = Relational::less_than(left, right);
let context = setup.context();
let res = expr.evaluate(&context);
assert_eq!(res, Ok(Boolean(false)));
}
#[test]
fn expression_root_node_finds_the_root() {
let package = Package::new();
let setup = Setup::new(&package);
let expr = RootNode;
let context = setup.context();
let res = expr.evaluate(&context);
assert_eq!(res, Ok(Value::Nodeset(nodeset![setup.doc.root()])));
}
#[derive(Debug, Clone)]
struct MockAxis {
calls: Rc<RefCell<usize>>,
}
impl MockAxis {
fn new() -> MockAxis {
MockAxis {
calls: Rc::new(RefCell::new(0)),
}
}
fn calls(&self) -> usize {
*self.calls.borrow()
}
}
impl AxisLike for MockAxis {
fn select_nodes<'c, 'd>(
&self,
_context: &context::Evaluation<'c, 'd>,
_node_test: &dyn NodeTest,
) -> Result<OrderedNodes<'d>, Error> {
*self.calls.borrow_mut() += 1;
Ok(OrderedNodes::new())
}
}
#[derive(Debug)]
struct DummyNodeTest;
impl NodeTest for DummyNodeTest {
fn test(
&self,
_context: &context::Evaluation<'_, '_>,
_result: &mut OrderedNodes<'_>,
) -> Result<(), Error> {
Ok(())
}
}
#[test]
fn step_delegates_to_the_axis() {
let package = Package::new();
let setup = Setup::new(&package);
let axis = MockAxis::new();
let node_test = DummyNodeTest;
let expr = ParameterizedStep::from_predicates(axis.clone(), Box::new(node_test), vec![]);
let context = setup.context();
expr.evaluate(&context, nodeset![context.node.clone()])
.unwrap();
assert_eq!(1, axis.calls());
}
#[test]
fn expression_union_combines_nodesets() {
let package = Package::new();
let mut setup = Setup::new(&package);
let left_node = setup.doc.create_element("left");
let nodes = nodeset![left_node];
setup.context.set_variable("left", nodes);
let left = Box::new(Variable {
name: "left".into(),
});
let right_node = setup.doc.create_element("right");
let nodes = nodeset![right_node];
setup.context.set_variable("right", nodes);
let right = Box::new(Variable {
name: "right".into(),
});
let expr = Union { left, right };
let context = setup.context();
let res = expr.evaluate(&context);
assert_eq!(res, Ok(Value::Nodeset(nodeset![left_node, right_node])));
}
#[test]
fn expression_variable_looks_up_the_variable() {
let package = Package::new();
let mut setup = Setup::new(&package);
setup.context.set_variable("foo", true);
let expr = Variable { name: "foo".into() };
let context = setup.context();
let res = expr.evaluate(&context);
assert_eq!(res, Ok(Boolean(true)));
}
}