use std::cell::{Cell, RefCell};
use std::collections::HashSet;
use std::fmt::Write as _;
use sup_xml_tree::dom::Node;
use super::ast::*;
use super::index::{DocIndexLike, NodeId, XPathNodeKind};
use crate::error::{ErrorDomain, ErrorLevel, XmlError};
type Result<T> = std::result::Result<T, XmlError>;
pub fn xpath_err(msg: impl Into<String>) -> XmlError {
XmlError::new(ErrorDomain::XPath, ErrorLevel::Error, msg)
}
pub const DEFAULT_MAX_EVAL_STEPS: u64 = 20_000_000;
thread_local! {
static EVAL_STEPS_BUDGET: Cell<u64> = const { Cell::new(DEFAULT_MAX_EVAL_STEPS) };
static EVAL_STEPS_REMAINING: Cell<u64> = const { Cell::new(DEFAULT_MAX_EVAL_STEPS) };
}
pub fn set_eval_budget(max_steps: u64) {
EVAL_STEPS_BUDGET.with(|c| c.set(max_steps));
}
pub fn reset_eval_budget() {
let budget = EVAL_STEPS_BUDGET.with(|c| c.get());
EVAL_STEPS_REMAINING.with(|c| c.set(budget));
}
thread_local! {
static STABLE_NOW: Cell<Option<i64>> = const { Cell::new(None) };
}
pub fn refresh_stable_now() {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
STABLE_NOW.with(|c| c.set(Some(now)));
}
fn stable_now() -> i64 {
STABLE_NOW.with(|c| {
if let Some(n) = c.get() {
return n;
}
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
c.set(Some(now));
now
})
}
thread_local! {
pub static DEFAULT_COLLATION: RefCell<Option<String>> = const { RefCell::new(None) };
}
pub fn with_default_collation<R>(uri: Option<String>, f: impl FnOnce() -> R) -> R {
let prev = DEFAULT_COLLATION.with(|c| c.borrow().clone());
DEFAULT_COLLATION.with(|c| *c.borrow_mut() = uri);
struct Guard(Option<String>);
impl Drop for Guard {
fn drop(&mut self) {
let p = self.0.take();
DEFAULT_COLLATION.with(|c| *c.borrow_mut() = p);
}
}
let _g = Guard(prev);
f()
}
fn effective_collation(explicit: Option<String>) -> Option<String> {
explicit.or_else(|| DEFAULT_COLLATION.with(|c| c.borrow().clone()))
}
thread_local! {
static XPATH_1_0_COMPAT: Cell<bool> = const { Cell::new(false) };
}
pub fn with_xpath_1_0_compat<R>(f: impl FnOnce() -> R) -> R {
let prev = XPATH_1_0_COMPAT.with(|c| c.replace(true));
struct Guard(bool);
impl Drop for Guard {
fn drop(&mut self) { XPATH_1_0_COMPAT.with(|c| c.set(self.0)); }
}
let _g = Guard(prev);
f()
}
pub fn in_xpath_1_0_compat() -> bool {
XPATH_1_0_COMPAT.with(|c| c.get())
}
thread_local! {
static CONTEXT_ITEM: RefCell<Option<Value>> = const { RefCell::new(None) };
static FOCUS_UNDEFINED: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
pub fn with_focus_undefined<R>(undefined: bool, f: impl FnOnce() -> R) -> R {
let prev = FOCUS_UNDEFINED.with(|c| c.replace(undefined));
let r = f();
FOCUS_UNDEFINED.with(|c| c.set(prev));
r
}
pub fn focus_is_undefined() -> bool {
FOCUS_UNDEFINED.with(|c| c.get())
}
pub fn with_atomic_context_item<R>(item: Option<Value>, f: impl FnOnce() -> R) -> R {
with_context_item(item, f)
}
fn with_context_item<R>(item: Option<Value>, f: impl FnOnce() -> R) -> R {
let prev = CONTEXT_ITEM.with(|c| c.replace(item));
let r = f();
CONTEXT_ITEM.with(|c| { c.replace(prev); });
r
}
pub fn current_context_item() -> Option<Value> {
CONTEXT_ITEM.with(|c| c.borrow().clone())
}
fn is_bare_dot_path(path: &crate::xpath::ast::LocationPath) -> bool {
use crate::xpath::ast::{Axis, LocationPath, NodeTest};
let steps = match path {
LocationPath::Relative(s) => s,
LocationPath::Absolute(_) => return false,
};
if steps.len() != 1 { return false; }
let s = &steps[0];
s.axis == Axis::Self_
&& matches!(s.node_test, NodeTest::AnyNode)
&& s.predicates.is_empty()
&& s.filter.is_none()
}
#[inline]
fn charge_eval_step() -> Result<()> {
EVAL_STEPS_REMAINING.with(|c| {
let r = c.get();
if r == 0 {
let budget = EVAL_STEPS_BUDGET.with(|b| b.get());
return Err(xpath_err(format!(
"XPath evaluation step budget exceeded ({budget}); \
expression too expensive for the configured limit"
)));
}
c.set(r - 1);
Ok(())
})
}
pub type ForeignNodePtr = *const Node<'static>;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Numeric {
Integer(i64),
Decimal(rust_decimal::Decimal),
Double(f64),
Float(f64),
}
impl Numeric {
#[inline]
pub fn as_f64(self) -> f64 {
use rust_decimal::prelude::ToPrimitive;
match self {
Numeric::Integer(i) => i as f64,
Numeric::Decimal(d) => d.to_f64().unwrap_or(f64::NAN),
Numeric::Double(n) | Numeric::Float(n) => n,
}
}
#[inline]
pub fn integer_from_f64(n: f64) -> Numeric {
const I64_LIMIT: f64 = 9_223_372_036_854_775_808.0; if n.is_finite() && n >= -I64_LIMIT && n < I64_LIMIT {
Numeric::Integer(n as i64)
} else if let Some(d) = rust_decimal::Decimal::from_f64_retain(n) {
Numeric::Decimal(d)
} else {
Numeric::Double(n)
}
}
#[inline]
pub fn of_kind(kind: &str, n: f64) -> Numeric {
match kind {
"integer" => Numeric::integer_from_f64(n),
"decimal" => match rust_decimal::Decimal::from_f64_retain(n) {
Some(d) => Numeric::Decimal(d),
None => Numeric::Double(n),
},
"float" => Numeric::Float(n as f32 as f64),
_ => Numeric::Double(n),
}
}
#[inline]
fn rank(self) -> u8 {
match self {
Numeric::Integer(_) => 0,
Numeric::Decimal(_) => 1,
Numeric::Float(_) => 2,
Numeric::Double(_) => 3,
}
}
#[inline]
fn from_rank(rank: u8, n: f64) -> Numeric {
match rank {
0 => Numeric::integer_from_f64(n),
1 => match rust_decimal::Decimal::from_f64_retain(n) {
Some(d) => Numeric::Decimal(d),
None => Numeric::Double(n),
},
2 => Numeric::Float(n as f32 as f64),
_ => Numeric::Double(n),
}
}
#[inline]
pub fn kind(self) -> &'static str {
match self {
Numeric::Integer(_) => "integer",
Numeric::Decimal(_) => "decimal",
Numeric::Double(_) => "double",
Numeric::Float(_) => "float",
}
}
}
impl From<f64> for Numeric {
#[inline]
fn from(n: f64) -> Numeric { Numeric::Double(n) }
}
#[derive(Debug, Clone)]
pub enum Value {
NodeSet(Vec<NodeId>),
ForeignNodeSet(Vec<ForeignNodePtr>),
String(String),
Number(Numeric),
Boolean(bool),
Typed(Box<TypedAtomic>),
Sequence(Vec<Value>),
IntRange { lo: i64, hi: i64 },
Map(Box<Vec<(Value, Value)>>),
Array(Box<Vec<Value>>),
Function(Box<FunctionItem>),
}
#[derive(Debug, Clone)]
pub enum FunctionItem {
Inline {
params: Vec<String>,
sig: Box<crate::xpath::ast::FunctionSig>,
body: crate::xpath::ast::Expr,
closure: Vec<(String, Value)>,
},
Named {
name: String, ns: String, arity: usize,
sig: Option<Box<crate::xpath::ast::FunctionSig>>,
},
Partial { base: Box<FunctionItem>, bound: Vec<Option<Value>> },
}
impl FunctionItem {
pub fn arity(&self) -> usize {
match self {
FunctionItem::Inline { params, .. } => params.len(),
FunctionItem::Named { arity, .. } => *arity,
FunctionItem::Partial { bound, .. } =>
bound.iter().filter(|b| b.is_none()).count(),
}
}
pub fn declared_sig(&self) -> Option<&crate::xpath::ast::FunctionSig> {
match self {
FunctionItem::Named { sig, .. } => sig.as_deref(),
FunctionItem::Inline { sig, .. } => Some(sig),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct TypedAtomic {
pub kind: &'static str,
pub lexical: String,
pub numeric: Option<f64>,
pub boolean: Option<bool>,
pub user_type: Option<Box<(String, String)>>,
}
impl TypedAtomic {
pub fn builtin(kind: &'static str, lexical: String, numeric: Option<f64>, boolean: Option<bool>) -> Self {
TypedAtomic { kind, lexical, numeric, boolean, user_type: None }
}
}
pub fn typed_str(kind: &'static str, lexical: String) -> Value {
Value::Typed(Box::new(TypedAtomic::builtin(kind, lexical, None, None)))
}
pub fn parent_atomic_type(t: &str) -> Option<&'static str> {
Some(match t {
"long" => "integer",
"int" => "long",
"short" => "int",
"byte" => "short",
"unsignedLong" => "nonNegativeInteger",
"unsignedInt" => "unsignedLong",
"unsignedShort" => "unsignedInt",
"unsignedByte" => "unsignedShort",
"nonNegativeInteger" => "integer",
"nonPositiveInteger" => "integer",
"positiveInteger" => "nonNegativeInteger",
"negativeInteger" => "nonPositiveInteger",
"integer" => "decimal",
"normalizedString" => "string",
"token" => "normalizedString",
"language" => "token",
"Name" => "token",
"NCName" => "Name",
"ID" => "NCName",
"IDREF" => "NCName",
"ENTITY" => "NCName",
"NMTOKEN" => "token",
"dayTimeDuration" => "duration",
"yearMonthDuration" => "duration",
"decimal" | "double" | "float" | "boolean" | "string"
| "date" | "dateTime" | "time" | "duration"
| "gYear" | "gYearMonth" | "gMonth" | "gMonthDay" | "gDay"
| "hexBinary" | "base64Binary" | "anyURI" | "QName"
| "NOTATION" | "untypedAtomic"
| "IDREFS" | "ENTITIES" | "NMTOKENS"
=> "anyAtomicType",
"anyAtomicType" => "anySimpleType",
"anySimpleType" => "anyType",
_ => return None,
})
}
pub fn xsd_is_subtype_of(t: &str, target: &str) -> bool {
if t == target { return true; }
let mut cur = t;
while let Some(p) = parent_atomic_type(cur) {
if p == target { return true; }
cur = p;
}
false
}
impl Value {
pub fn untyped(self) -> Self {
match self {
Value::Typed(t) => {
if let Some(n) = t.numeric { Value::Number(Numeric::Double(n)) }
else if let Some(b) = t.boolean { Value::Boolean(b) }
else {
Value::String(t.lexical)
}
}
Value::Sequence(mut items) => {
if items.len() == 1 {
return items.remove(0).untyped();
}
Value::Sequence(items)
}
other => other,
}
}
}
pub trait XPathBindings {
fn resolve_prefix(&self, _prefix: &str) -> Option<String> { None }
fn call_function(
&self, _ns_uri: &str, _name: &str, _args: Vec<Value>,
) -> Option<Result<Value>> { None }
fn call_function_in(
&self, ns_uri: &str, name: &str, args: Vec<Value>,
_xpath_context_node: NodeId,
) -> Option<Result<Value>> {
self.call_function(ns_uri, name, args)
}
fn function_available_in(&self, _ns_uri: &str, _name: &str, _arity: usize) -> bool {
false
}
fn function_signature_in(
&self, _ns_uri: &str, _name: &str, _arity: usize,
) -> Option<crate::xpath::ast::FunctionSig> {
None
}
fn castable_as_user_type(
&self, _ns_uri: &str, _local: &str, _value: &str, _source_kind: Option<&str>,
) -> Option<bool> {
None
}
fn cast_to_user_type(&self, _ns_uri: &str, _local: &str, _value: &str)
-> Option<Result<Value>> {
None
}
fn instance_of_user_type(
&self, _target_ns: &str, _target_local: &str, _value_type: Option<(&str, &str)>,
) -> Option<bool> {
None
}
fn schema_type_exists(&self, _ns: &str, _local: &str) -> bool {
false
}
fn node_schema_type(&self, _node_id: NodeId) -> Option<(String, String)> {
None
}
fn node_typed_value(&self, _node_id: NodeId, _lexical: &str) -> Option<Value> {
None
}
fn variable(&self, _name: &str) -> Option<Value> { None }
fn xpath_version_2_or_later(&self) -> bool { false }
fn load_document(
&self, _uri: &str, _base_uri: Option<&str>,
) -> Option<Result<Vec<ForeignNodePtr>>> { None }
fn apply_foreign_path(
&self,
_nodes: &[ForeignNodePtr],
_predicates: &[Expr],
_steps: &[Step],
) -> Option<Result<Vec<ForeignNodePtr>>> { None }
fn static_base_uri(&self) -> Option<String> { None }
fn node_base_uri(&self, _id: NodeId) -> Option<String> { None }
fn foreign_string_value(&self, _p: ForeignNodePtr) -> String { String::new() }
fn load_dynamic_document(
&self, _uri: &str,
) -> Option<std::result::Result<NodeId, crate::error::XmlError>> {
None
}
fn regex_dialect(&self) -> crate::regex::Dialect {
crate::regex::Dialect::Xpath
}
}
pub struct NoBindings;
impl XPathBindings for NoBindings {}
fn implicit_prefix(prefix: &str) -> Option<&'static str> {
match prefix {
"xml" => Some("http://www.w3.org/XML/1998/namespace"),
"xmlns" => Some("http://www.w3.org/2000/xmlns/"),
"xs" => Some("http://www.w3.org/2001/XMLSchema"),
"xsi" => Some("http://www.w3.org/2001/XMLSchema-instance"),
_ => None,
}
}
fn resolve_prefix_or_implicit(bindings: &dyn XPathBindings, prefix: &str) -> Option<String> {
bindings
.resolve_prefix(prefix)
.or_else(|| implicit_prefix(prefix).map(str::to_string))
}
fn named_function_namespace(name: &str, bindings: &dyn XPathBindings) -> String {
match name.split_once(':') {
Some((prefix, _)) => resolve_prefix_or_implicit(bindings, prefix).unwrap_or_default(),
None => FN_NAMESPACE.to_string(),
}
}
static NO_BINDINGS: NoBindings = NoBindings;
#[derive(Debug, Clone, Copy)]
pub struct StaticContext {
pub xpath_2_0: bool,
pub xpath_3_0: bool,
pub libxml2_compatible: bool,
pub current_node: Option<NodeId>,
}
impl Default for StaticContext {
fn default() -> Self {
StaticContext { xpath_2_0: false, xpath_3_0: false, libxml2_compatible: false, current_node: None }
}
}
impl StaticContext {
pub fn num_style(&self) -> NumStyle {
NumStyle::from_context(self.libxml2_compatible, self.xpath_2_0)
}
}
pub static DEFAULT_STATIC_CTX: StaticContext = StaticContext {
xpath_2_0: false,
xpath_3_0: false,
libxml2_compatible: false,
current_node: None,
};
pub struct EvalCtx<'b> {
pub context_node: NodeId,
pub pos: usize,
pub size: usize,
pub bindings: &'b dyn XPathBindings,
pub static_ctx: &'b StaticContext,
}
impl EvalCtx<'static> {
pub fn root() -> Self {
Self {
context_node: 0,
pos: 1,
size: 1,
bindings: &NO_BINDINGS,
static_ctx: &DEFAULT_STATIC_CTX,
}
}
}
pub fn validate_prefixes(expr: &Expr, bindings: &dyn XPathBindings) -> Result<()> {
match expr {
Expr::Or(l, r) | Expr::And(l, r) | Expr::Eq(l, r) | Expr::Ne(l, r)
| Expr::Lt(l, r) | Expr::Gt(l, r) | Expr::Le(l, r) | Expr::Ge(l, r)
| Expr::ValueEq(l, r) | Expr::ValueNe(l, r)
| Expr::ValueLt(l, r) | Expr::ValueGt(l, r)
| Expr::ValueLe(l, r) | Expr::ValueGe(l, r)
| Expr::Add(l, r) | Expr::Sub(l, r) | Expr::Mul(l, r) | Expr::Div(l, r)
| Expr::Mod(l, r) | Expr::Union(l, r)
| Expr::SimpleMap(l, r)
| Expr::NodeBefore(l, r) | Expr::NodeAfter(l, r) | Expr::NodeIs(l, r) => {
validate_prefixes(l, bindings)?;
validate_prefixes(r, bindings)
}
Expr::Neg(e) => validate_prefixes(e, bindings),
Expr::Path(p) => match p {
LocationPath::Absolute(steps) | LocationPath::Relative(steps) => {
validate_steps(steps, bindings)
}
},
Expr::FilterPath { primary, predicates, steps } => {
validate_prefixes(primary, bindings)?;
for p in predicates { validate_prefixes(p, bindings)?; }
validate_steps(steps, bindings)
}
Expr::FunctionCall(_, args) => {
for a in args { validate_prefixes(a, bindings)?; }
Ok(())
}
Expr::Variable(_) | Expr::Literal(_)
| Expr::Integer(_) | Expr::Decimal(_) | Expr::Double(_) => Ok(()),
Expr::IfThenElse { cond, then_branch, else_branch } => {
validate_prefixes(cond, bindings)?;
validate_prefixes(then_branch, bindings)?;
validate_prefixes(else_branch, bindings)
}
Expr::For { bindings: binds, body }
| Expr::Let { bindings: binds, body } => {
for (_, e) in binds { validate_prefixes(e, bindings)?; }
validate_prefixes(body, bindings)
}
Expr::Range(a, b) => {
validate_prefixes(a, bindings)?;
validate_prefixes(b, bindings)
}
Expr::Sequence(items) => {
for e in items { validate_prefixes(e, bindings)?; }
Ok(())
}
Expr::Quantified { bindings: binds, test, .. } => {
for (_, e) in binds { validate_prefixes(e, bindings)?; }
validate_prefixes(test, bindings)
}
Expr::IDiv(a, b) | Expr::Intersect(a, b) | Expr::Except(a, b) => {
validate_prefixes(a, bindings)?;
validate_prefixes(b, bindings)
}
Expr::InstanceOf(a, _) | Expr::CastAs(a, _)
| Expr::CastableAs(a, _) | Expr::TreatAs(a, _) => validate_prefixes(a, bindings),
Expr::TryCatch { body, catches } => {
validate_prefixes(body, bindings)?;
for c in catches { validate_prefixes(&c.body, bindings)?; }
Ok(())
}
Expr::WithDefaultCollation(_, inner) => validate_prefixes(inner, bindings),
Expr::BackwardsCompat(inner) => validate_prefixes(inner, bindings),
Expr::MapConstructor(entries) => {
for (k, v) in entries {
validate_prefixes(k, bindings)?;
validate_prefixes(v, bindings)?;
}
Ok(())
}
Expr::ArrayConstructor { members, .. } => {
for m in members { validate_prefixes(m, bindings)?; }
Ok(())
}
Expr::Lookup(base, key) => {
validate_prefixes(base, bindings)?;
validate_lookup_key_prefixes(key, bindings)
}
Expr::UnaryLookup(key) => validate_lookup_key_prefixes(key, bindings),
Expr::InlineFunction { body, .. } => validate_prefixes(body, bindings),
Expr::DynamicCall { func, args } => {
validate_prefixes(func, bindings)?;
for a in args { validate_prefixes(a, bindings)?; }
Ok(())
}
Expr::NamedFunctionRef { .. } | Expr::Placeholder | Expr::ContextItem => Ok(()),
}
}
fn validate_lookup_key_prefixes(
key: &crate::xpath::ast::LookupKey, bindings: &dyn XPathBindings,
) -> Result<()> {
if let crate::xpath::ast::LookupKey::Expr(e) = key {
validate_prefixes(e, bindings)?;
}
Ok(())
}
fn validate_steps(steps: &[Step], bindings: &dyn XPathBindings) -> Result<()> {
for step in steps {
if let NodeTest::QName(prefix, _) | NodeTest::PrefixWildcard(prefix) = &step.node_test
&& resolve_prefix_or_implicit(bindings, prefix).is_none()
{
return Err(xpath_err(format!(
"Undefined namespace prefix: {prefix}"
)));
}
for pred in &step.predicates {
validate_prefixes(pred, bindings)?;
}
}
Ok(())
}
pub fn eval_to_bool<I: DocIndexLike>(
expr: &Expr,
idx: &I,
context_node: NodeId,
bindings: &dyn XPathBindings,
) -> Result<bool> {
let static_ctx = StaticContext {
xpath_2_0: bindings.xpath_version_2_or_later(),
xpath_3_0: false,
libxml2_compatible: false,
current_node: Some(context_node),
};
let ctx = EvalCtx {
context_node,
pos: 1,
size: 1,
bindings,
static_ctx: &static_ctx,
};
let v = eval_expr(expr, &ctx, idx)?;
Ok(value_to_bool(&v, idx))
}
pub fn eval_expr<I: DocIndexLike>(expr: &Expr, ctx: &EvalCtx<'_>, idx: &I) -> Result<Value> {
charge_eval_step()?;
match expr {
Expr::ContextItem => Ok(current_context_item()
.unwrap_or_else(|| Value::NodeSet(vec![ctx.context_node]))),
Expr::Or(l, r) => {
if value_to_bool(&eval_expr(l, ctx, idx)?, idx) {
return Ok(Value::Boolean(true));
}
Ok(Value::Boolean(value_to_bool(&eval_expr(r, ctx, idx)?, idx)))
}
Expr::And(l, r) => {
if !value_to_bool(&eval_expr(l, ctx, idx)?, idx) {
return Ok(Value::Boolean(false));
}
Ok(Value::Boolean(value_to_bool(&eval_expr(r, ctx, idx)?, idx)))
}
Expr::Eq(l, r) => {
let lv = eval_expr(l, ctx, idx)?;
let rv = eval_expr(r, ctx, idx)?;
reject_string_vs_numeric_cmp_2_0(&lv, &rv, ctx, "=")?;
Ok(Value::Boolean(values_eq(&lv, &rv, idx, ctx.bindings)))
}
Expr::Ne(l, r) => {
let lv = eval_expr(l, ctx, idx)?;
let rv = eval_expr(r, ctx, idx)?;
reject_string_vs_numeric_cmp_2_0(&lv, &rv, ctx, "!=")?;
Ok(Value::Boolean(values_ne(&lv, &rv, idx, ctx.bindings)))
}
Expr::Lt(l, r) => cmp_op(l, r, ctx, idx, |a, b| a < b),
Expr::Gt(l, r) => cmp_op(l, r, ctx, idx, |a, b| a > b),
Expr::Le(l, r) => cmp_op(l, r, ctx, idx, |a, b| a <= b),
Expr::Ge(l, r) => cmp_op(l, r, ctx, idx, |a, b| a >= b),
Expr::ValueEq(l, r) => value_compare(l, r, ctx, idx, ValueCmp::Eq),
Expr::ValueNe(l, r) => value_compare(l, r, ctx, idx, ValueCmp::Ne),
Expr::ValueLt(l, r) => value_compare(l, r, ctx, idx, ValueCmp::Lt),
Expr::ValueGt(l, r) => value_compare(l, r, ctx, idx, ValueCmp::Gt),
Expr::ValueLe(l, r) => value_compare(l, r, ctx, idx, ValueCmp::Le),
Expr::ValueGe(l, r) => value_compare(l, r, ctx, idx, ValueCmp::Ge),
Expr::Add(l, r) => {
let lv = eval_expr(l, ctx, idx)?;
let rv = eval_expr(r, ctx, idx)?;
if let Some(v) = date_arith_add(&lv, &rv) { return Ok(v); }
if arith_empty_2_0(&lv, &rv, ctx) { return Ok(Value::NodeSet(Vec::new())); }
reject_string_arith_2_0(&lv, &rv, ctx, "+")?;
Ok(compat_numeric_op(&lv, &rv, idx, ctx.bindings, NumericOp::Add))
}
Expr::Sub(l, r) => {
let lv = eval_expr(l, ctx, idx)?;
let rv = eval_expr(r, ctx, idx)?;
if let Some(v) = date_arith_sub(&lv, &rv) { return Ok(v); }
if arith_empty_2_0(&lv, &rv, ctx) { return Ok(Value::NodeSet(Vec::new())); }
reject_string_arith_2_0(&lv, &rv, ctx, "-")?;
Ok(compat_numeric_op(&lv, &rv, idx, ctx.bindings, NumericOp::Sub))
}
Expr::Mul(l, r) => {
let lv = eval_expr(l, ctx, idx)?;
let rv = eval_expr(r, ctx, idx)?;
if let Some(v) = duration_mul(&lv, &rv, idx, ctx.bindings) { return Ok(v); }
if arith_empty_2_0(&lv, &rv, ctx) { return Ok(Value::NodeSet(Vec::new())); }
reject_string_arith_2_0(&lv, &rv, ctx, "*")?;
Ok(compat_numeric_op(&lv, &rv, idx, ctx.bindings, NumericOp::Mul))
}
Expr::Div(l, r) => {
let lv = eval_expr(l, ctx, idx)?;
let rv = eval_expr(r, ctx, idx)?;
if let Some(v) = duration_div(&lv, &rv, idx, ctx.bindings) { return Ok(v); }
if arith_empty_2_0(&lv, &rv, ctx) { return Ok(Value::NodeSet(Vec::new())); }
if integer_decimal_zero_divisor(&lv, &rv, idx, ctx.bindings) {
return Err(xpath_err("division by zero").with_xpath_code("FOAR0001"));
}
reject_string_arith_2_0(&lv, &rv, ctx, "div")?;
Ok(compat_numeric_op(&lv, &rv, idx, ctx.bindings, NumericOp::Div))
}
Expr::Mod(l, r) => {
let lv = eval_expr(l, ctx, idx)?;
let rv = eval_expr(r, ctx, idx)?;
if arith_empty_2_0(&lv, &rv, ctx) { return Ok(Value::NodeSet(Vec::new())); }
if integer_decimal_zero_divisor(&lv, &rv, idx, ctx.bindings) {
return Err(xpath_err("modulo by zero").with_xpath_code("FOAR0001"));
}
Ok(compat_numeric_op(&lv, &rv, idx, ctx.bindings, NumericOp::Mod))
}
Expr::Neg(inner) => {
let v = eval_expr(inner, ctx, idx)?;
if in_xpath_1_0_compat() {
let n = -value_to_number_with(&v, idx, ctx.bindings);
return Ok(Value::Number(Numeric::Double(n)));
}
if let Value::Number(Numeric::Integer(i)) = v {
return Ok(match i.checked_neg() {
Some(n) => Value::Number(Numeric::Integer(n)),
None => Value::Number(Numeric::Decimal(-rust_decimal::Decimal::from(i))),
});
}
if matches!(numeric_kind_of(&v), Some("integer")) {
let f = -value_to_number_with(&v, idx, ctx.bindings);
return Ok(preserve_numeric_kind(&v, f));
}
if let Some(d) = exact_decimal(&v) {
return Ok(Value::Number(Numeric::Decimal(-d)));
}
let n = -value_to_number_with(&v, idx, ctx.bindings);
Ok(preserve_numeric_kind(&v, n))
}
Expr::Union(l, r) => {
let lv = eval_expr(l, ctx, idx)?;
let rv = eval_expr(r, ctx, idx)?;
match (lv, rv) {
(Value::NodeSet(mut a), Value::NodeSet(b)) => {
a.extend(b);
dedup_sort(&mut a);
Ok(Value::NodeSet(a))
}
(Value::ForeignNodeSet(mut a), Value::ForeignNodeSet(b)) => {
a.extend(b);
dedup_foreign(&mut a);
Ok(Value::ForeignNodeSet(a))
}
(Value::NodeSet(_), Value::ForeignNodeSet(_))
| (Value::ForeignNodeSet(_), Value::NodeSet(_)) => Err(xpath_err(
"mixed primary/foreign node-set union is not supported",
)),
_ => Err(xpath_err("union operator requires node-sets on both sides")),
}
}
Expr::Path(path) => {
if is_bare_dot_path(path) {
if let Some(v) = current_context_item() {
return Ok(v);
}
}
let nodes = eval_path(path, ctx.context_node, idx, ctx.bindings, ctx.static_ctx.libxml2_compatible, ctx.static_ctx.current_node)?;
Ok(Value::NodeSet(nodes))
}
Expr::FilterPath { primary, predicates, steps } => {
let pv = eval_expr(primary, ctx, idx)?;
match pv {
Value::NodeSet(ns) => {
let mut nodes =
apply_predicates_cur(ns, predicates, idx, ctx.bindings, ctx.static_ctx.libxml2_compatible, ctx.static_ctx.current_node)?;
for step in steps {
nodes = eval_step_on_nodes_cur(nodes, step, idx, ctx.bindings, ctx.static_ctx.libxml2_compatible, ctx.static_ctx.current_node)?;
}
Ok(Value::NodeSet(nodes))
}
Value::ForeignNodeSet(ns) => {
match ctx.bindings.apply_foreign_path(&ns, predicates, steps) {
Some(r) => r.map(Value::ForeignNodeSet),
None => Err(xpath_err(
"foreign-doc path traversal not supported by bindings",
)),
}
}
Value::Sequence(items) if steps.is_empty() => {
let kept = filter_sequence_by_predicates(
items, predicates, ctx, idx,
)?;
if kept.len() == 1 {
Ok(kept.into_iter().next().unwrap())
} else {
Ok(Value::Sequence(kept))
}
}
other if steps.is_empty() => {
let kept = filter_sequence_by_predicates(
vec![other], predicates, ctx, idx,
)?;
match kept.len() {
0 => Ok(Value::NodeSet(Vec::new())),
1 => Ok(kept.into_iter().next().unwrap()),
_ => Ok(Value::Sequence(kept)),
}
}
Value::Sequence(items) => {
let kept = filter_sequence_by_predicates(
items, predicates, ctx, idx,
)?;
let mut nodes: Vec<NodeId> = Vec::new();
for it in kept {
if let Value::NodeSet(ns) = it {
nodes.extend(ns);
}
}
nodes.sort_unstable();
nodes.dedup();
for step in steps {
nodes = eval_step_on_nodes_cur(nodes, step, idx, ctx.bindings, ctx.static_ctx.libxml2_compatible, ctx.static_ctx.current_node)?;
}
Ok(Value::NodeSet(nodes))
}
_ => Err(xpath_err("predicate applied to non-node-set")),
}
}
Expr::FunctionCall(name, args) => eval_function(name, args, ctx, idx),
Expr::Variable(name) => {
match ctx.bindings.variable(name) {
Some(v) => Ok(v),
None => Err(xpath_err(format!("undefined XPath variable: ${name}"))),
}
}
Expr::Literal(s) => Ok(Value::String(s.clone())),
Expr::Integer(i) => Ok(Value::Number(if ctx.static_ctx.xpath_2_0 {
Numeric::Integer(*i)
} else {
Numeric::Double(*i as f64)
})),
Expr::Decimal(n) => Ok(Value::Number(if ctx.static_ctx.xpath_2_0 {
Numeric::Decimal(*n)
} else {
use rust_decimal::prelude::ToPrimitive;
Numeric::Double(n.to_f64().unwrap_or(f64::NAN))
})),
Expr::Double(n) => Ok(Value::Number(Numeric::Double(*n))),
Expr::IfThenElse { cond, then_branch, else_branch } => {
let truth = value_to_bool(&eval_expr(cond, ctx, idx)?, idx);
let branch = if truth { then_branch } else { else_branch };
eval_expr(branch, ctx, idx)
}
Expr::IDiv(l, r) => {
let a = value_to_number(&eval_expr(l, ctx, idx)?, idx);
let b = value_to_number(&eval_expr(r, ctx, idx)?, idx);
if b == 0.0 || b.is_nan() || a.is_nan() {
return Err(xpath_err("idiv: division by zero or NaN")
.with_xpath_code("FOAR0001"));
}
Ok(integer_result((a / b).trunc() as i64, ctx.bindings))
}
Expr::Intersect(l, r) => {
let lns = node_set_of(eval_expr(l, ctx, idx)?);
let rns = node_set_of(eval_expr(r, ctx, idx)?);
let rset: std::collections::BTreeSet<NodeId> = rns.into_iter().collect();
let mut out: Vec<NodeId> = lns.into_iter().filter(|n| rset.contains(n)).collect();
out.sort_unstable();
out.dedup();
Ok(Value::NodeSet(out))
}
Expr::Except(l, r) => {
let lns = node_set_of(eval_expr(l, ctx, idx)?);
let rns = node_set_of(eval_expr(r, ctx, idx)?);
let rset: std::collections::BTreeSet<NodeId> = rns.into_iter().collect();
let mut out: Vec<NodeId> = lns.into_iter().filter(|n| !rset.contains(n)).collect();
out.sort_unstable();
out.dedup();
Ok(Value::NodeSet(out))
}
Expr::InstanceOf(inner, st) => {
let v = eval_expr(inner, ctx, idx)?;
let st = resolve_kind_test_namespaces(st, ctx.bindings);
if let crate::xpath::ast::ItemType::Atomic(name) = &st.item {
let target = match name.split_once(':') {
Some((prefix, local)) => resolve_prefix_or_implicit(ctx.bindings, prefix)
.map(|uri| (uri, local)),
None if atomic_kind_static(name).is_none() =>
Some((String::new(), name.as_str())),
None => None,
};
if let Some((uri, local)) = target {
let value_type = match &v {
Value::Typed(t) => t.user_type.as_deref()
.map(|(ns, l)| (ns.as_str(), l.as_str())),
_ => None,
};
if let Some(matches_type) =
ctx.bindings.instance_of_user_type(&uri, local, value_type)
{
let ok = match sequence_len(&v) {
0 => matches!(st.occurrence,
crate::xpath::ast::Occurrence::Optional
| crate::xpath::ast::Occurrence::ZeroOrMore),
1 => matches_type,
_ => false,
};
return Ok(Value::Boolean(ok));
}
const XSD_NS: &str = "http://www.w3.org/2001/XMLSchema";
if uri != XSD_NS && !ctx.bindings.schema_type_exists(&uri, local) {
return Err(xpath_err(format!(
"instance of: type {name} is not a known schema type \
(XPST0051)")).with_xpath_code("XPST0051"));
}
}
}
Ok(Value::Boolean(value_matches_sequence_type(&v, &st, idx)))
}
Expr::CastAs(inner, st) => {
if let crate::xpath::ast::ItemType::Atomic(name) = &st.item {
match name.as_str() {
"anyType" | "anySimpleType" | "untyped" =>
return Err(xpath_err(format!(
"cast as: target type xs:{name} is not an atomic \
type (XPST0051)"))),
"anyAtomicType" | "NOTATION" =>
return Err(xpath_err(format!(
"cast as: xs:{name} is not a permitted target \
type (XPST0080)"))),
_ => {}
}
}
let v = eval_expr(inner, ctx, idx)?;
cast_value_to_atomic(&v, st, idx)
}
Expr::TreatAs(inner, st) => {
let v = eval_expr(inner, ctx, idx)?;
let st = resolve_kind_test_namespaces(st, ctx.bindings);
if !value_matches_sequence_type(&v, &st, idx) {
return Err(xpath_err(format!(
"treat as failed: value doesn't match {st:?}"
)));
}
Ok(v)
}
Expr::WithDefaultCollation(uri, inner) => {
with_default_collation(Some(uri.clone()), ||
eval_expr(inner, ctx, idx))
}
Expr::BackwardsCompat(inner) => {
with_xpath_1_0_compat(|| eval_expr(inner, ctx, idx))
}
Expr::MapConstructor(entries) => {
let mut out: Vec<(Value, Value)> = Vec::with_capacity(entries.len());
for (ke, ve) in entries {
let key = eval_expr(ke, ctx, idx)?;
let key = first_atomic_key(&key, idx);
let val = eval_expr(ve, ctx, idx)?;
if out.iter().any(|(k, _)| map_key_eq(k, &key, idx)) {
return Err(xpath_err(
"duplicate key in map constructor (XQDY0137)"));
}
out.push((key, val));
}
Ok(Value::Map(Box::new(out)))
}
Expr::ArrayConstructor { members, square } => {
if *square {
let mut out = Vec::with_capacity(members.len());
for m in members { out.push(eval_expr(m, ctx, idx)?); }
Ok(Value::Array(Box::new(out)))
} else {
let v = match members.first() {
Some(e) => eval_expr(e, ctx, idx)?,
None => return Ok(Value::Array(Box::new(Vec::new()))),
};
Ok(Value::Array(Box::new(items_of(&v))))
}
}
Expr::Lookup(base, key) => {
let b = eval_expr(base, ctx, idx)?;
eval_lookup(&b, key, ctx, idx)
}
Expr::UnaryLookup(key) => {
let ctx_item = current_context_item()
.unwrap_or(Value::NodeSet(vec![ctx.context_node]));
eval_lookup(&ctx_item, key, ctx, idx)
}
Expr::InlineFunction { params, sig, body } => {
let mut refs = Vec::new();
collect_var_refs(body, &mut refs);
let mut closure = Vec::new();
for name in refs {
if params.iter().any(|p| p == &name) { continue; }
if let Some(v) = ctx.bindings.variable(&name) {
closure.push((name, v));
}
}
Ok(Value::Function(Box::new(FunctionItem::Inline {
params: params.clone(),
sig: sig.clone(),
body: (**body).clone(),
closure,
})))
}
Expr::NamedFunctionRef { name, arity } => {
let ns = named_function_namespace(name, ctx.bindings);
let local = name.rsplit(':').next().unwrap_or(name);
let sig = ctx.bindings
.function_signature_in(&ns, local, *arity)
.map(Box::new);
Ok(Value::Function(Box::new(FunctionItem::Named {
name: name.clone(), ns, arity: *arity, sig,
})))
}
Expr::DynamicCall { func, args } => {
let f = eval_expr(func, ctx, idx)?;
if args.len() == 1 && !matches!(args[0], Expr::Placeholder) {
match &f {
Value::Map(m) => {
let key = first_atomic_key(&eval_expr(&args[0], ctx, idx)?, idx);
return Ok(m.iter().find(|(k, _)| map_key_eq(k, &key, idx))
.map(|(_, v)| v.clone())
.unwrap_or(Value::NodeSet(Vec::new())));
}
Value::Array(a) => {
let n = value_to_number(&eval_expr(&args[0], ctx, idx)?, idx) as i64;
if n < 1 || n as usize > a.len() {
return Err(xpath_err(format!(
"array index {n} is out of bounds (FOAY0001)"))
.with_xpath_code("FOAY0001"));
}
return Ok(a[(n - 1) as usize].clone());
}
_ => {}
}
}
let fi = match &f {
Value::Function(fi) => (**fi).clone(),
_ => return Err(xpath_err(
"dynamic call target is not a function item (XPTY0004)")),
};
if args.iter().any(|a| matches!(a, Expr::Placeholder)) {
let mut bound = Vec::with_capacity(args.len());
for a in args {
bound.push(match a {
Expr::Placeholder => None,
e => Some(eval_expr(e, ctx, idx)?),
});
}
return Ok(Value::Function(Box::new(
FunctionItem::Partial { base: Box::new(fi), bound })));
}
let argv: Vec<Value> = args.iter()
.map(|a| eval_expr(a, ctx, idx)).collect::<Result<_>>()?;
call_function_item(&fi, argv, ctx, idx)
}
Expr::Placeholder => Err(xpath_err(
"'?' placeholder is only valid as a function-call argument")),
Expr::CastableAs(inner, st) => {
let v = eval_expr(inner, ctx, idx)?;
if let crate::xpath::ast::ItemType::Atomic(name) = &st.item {
if let Some((prefix, local)) = name.split_once(':') {
if let Some(uri) = resolve_prefix_or_implicit(ctx.bindings, prefix) {
match sequence_len(&v) {
0 => return Ok(Value::Boolean(matches!(st.occurrence,
crate::xpath::ast::Occurrence::Optional
| crate::xpath::ast::Occurrence::ZeroOrMore))),
1 => {}
_ => return Ok(Value::Boolean(false)),
}
let source_kind = match &v {
Value::Typed(t) => Some(t.kind),
Value::Number(n) => Some(n.kind()),
Value::Boolean(_) => Some("boolean"),
_ => None,
};
let s = value_to_string(&v, idx);
if let Some(ok) =
ctx.bindings.castable_as_user_type(&uri, local, &s, source_kind)
{
return Ok(Value::Boolean(ok));
}
}
}
}
Ok(Value::Boolean(cast_value_to_atomic(&v, st, idx).is_ok()))
}
Expr::TryCatch { body, catches } => {
match eval_expr(body, ctx, idx) {
Ok(v) => Ok(v),
Err(e) => {
let code_local = e.xpath_code.clone()
.unwrap_or_else(|| "FOER0000".to_string());
let err_uri = "http://www.w3.org/2005/xqt-errors";
for c in catches {
if !xpath_catch_matches(&c.matchers, err_uri, &code_local, ctx.bindings) {
continue;
}
let scoped = build_err_scope(ctx.bindings, err_uri, &code_local, &e.message);
let inner_ctx = EvalCtx {
context_node: ctx.context_node,
pos: ctx.pos, size: ctx.size,
bindings: &scoped,
static_ctx: ctx.static_ctx,
};
return eval_expr(&c.body, &inner_ctx, idx);
}
Err(e)
}
}
}
Expr::NodeBefore(l, r) | Expr::NodeAfter(l, r) => {
let after = matches!(expr, Expr::NodeAfter(_, _));
let lv = eval_expr(l, ctx, idx)?;
let rv = eval_expr(r, ctx, idx)?;
let pick = |v: &Value| -> Option<NodeId> {
match v {
Value::NodeSet(ns) if ns.len() == 1 => Some(ns[0]),
_ => None,
}
};
let (Some(a), Some(b)) = (pick(&lv), pick(&rv)) else {
return Ok(Value::NodeSet(Vec::new()));
};
Ok(Value::Boolean(if after { a > b } else { a < b }))
}
Expr::NodeIs(l, r) => {
let lv = eval_expr(l, ctx, idx)?;
let rv = eval_expr(r, ctx, idx)?;
let pick = |v: &Value| -> Option<NodeId> {
match v {
Value::NodeSet(ns) if ns.len() == 1 => Some(ns[0]),
_ => None,
}
};
let pick_foreign = |v: &Value| -> Option<ForeignNodePtr> {
match v {
Value::ForeignNodeSet(fs) if fs.len() == 1 => Some(fs[0]),
_ => None,
}
};
if let (Some(a), Some(b)) = (pick(&lv), pick(&rv)) {
return Ok(Value::Boolean(a == b));
}
if let (Some(a), Some(b)) = (pick_foreign(&lv), pick_foreign(&rv)) {
return Ok(Value::Boolean(a == b));
}
Ok(Value::NodeSet(Vec::new()))
}
Expr::SimpleMap(lhs, rhs) => {
let lv = eval_expr(lhs, ctx, idx)?;
let total = sequence_len(&lv);
let mut out_nodes: Vec<NodeId> = Vec::new();
let mut out_seq: Vec<Value> = Vec::new();
let mut any_atomic = false;
for (i, item) in iter_items(&lv).enumerate() {
let (cx, cit) = match &item {
Value::NodeSet(ns) if ns.len() == 1 => (ns[0], None),
_ => (ctx.context_node, Some(item.clone())),
};
let inner = EvalCtx {
context_node: cx, pos: i + 1, size: total,
bindings: ctx.bindings, static_ctx: ctx.static_ctx,
};
let v = with_context_item(cit, || eval_expr(rhs, &inner, idx))?;
match v {
Value::NodeSet(ns) => out_nodes.extend(ns),
Value::ForeignNodeSet(_) => {}
Value::Sequence(s) => { any_atomic = true; out_seq.extend(s); }
other => { any_atomic = true; out_seq.push(other); }
}
}
if !any_atomic {
return Ok(Value::NodeSet(out_nodes));
}
for id in out_nodes {
out_seq.push(Value::NodeSet(vec![id]));
}
if out_seq.len() == 1 {
Ok(out_seq.into_iter().next().unwrap())
} else {
Ok(Value::Sequence(out_seq))
}
}
Expr::Range(lo, hi) => {
let m_v = eval_expr(lo, ctx, idx)?;
let n_v = eval_expr(hi, ctx, idx)?;
let m = value_to_number(&m_v, idx).round() as i64;
let n = value_to_number(&n_v, idx).round() as i64;
if m > n {
return Ok(Value::NodeSet(Vec::new()));
}
Ok(Value::IntRange { lo: m, hi: n })
}
Expr::Sequence(items) => {
let mut evaluated: Vec<Value> = Vec::with_capacity(items.len());
for item in items {
let v = eval_expr(item, ctx, idx)?;
match v {
Value::Sequence(inner) => evaluated.extend(inner),
other => evaluated.push(other),
}
}
if evaluated.is_empty() {
return Ok(Value::NodeSet(Vec::new()));
}
if evaluated.len() == 1 {
return Ok(evaluated.into_iter().next().unwrap());
}
let any_node = evaluated.iter().any(|v|
matches!(v, Value::NodeSet(_) | Value::ForeignNodeSet(_)));
let any_lazy = evaluated.iter().any(|v|
matches!(v, Value::Typed(_) | Value::IntRange { .. }));
if !any_node || any_lazy {
return Ok(Value::Sequence(evaluated));
}
let mut nodes: Vec<NodeId> = Vec::new();
let mut atoms: Vec<String> = Vec::new();
for v in evaluated {
match v {
Value::NodeSet(ns) => nodes.extend(ns),
Value::ForeignNodeSet(_) => {}
Value::String(s) => atoms.push(s),
Value::Number(n) => atoms.push(value_to_string(&Value::Number(n), idx)),
Value::Boolean(b) => atoms.push((if b { "true" } else { "false" }).to_string()),
Value::Typed(t) => atoms.push(t.lexical),
Value::Sequence(_) => unreachable!(), Value::IntRange { .. } => unreachable!(), Value::Map(_) | Value::Array(_) | Value::Function(_) => {}
}
}
if atoms.is_empty() {
Ok(Value::NodeSet(nodes))
} else {
for n in &nodes { atoms.push(idx.string_value(*n)); }
match idx.allocate_rtf_text_nodes(atoms.clone()) {
Some(ids) => Ok(Value::NodeSet(ids)),
None => Ok(Value::String(atoms.join(""))),
}
}
}
Expr::Quantified { kind, bindings, test } => {
use crate::xpath::ast::QuantifierKind::*;
let mut found_match = false;
let mut all_match = true;
let mut seen_any = false;
eval_quantified_recursive(
bindings, test, 0, ctx, idx,
&mut |result| {
seen_any = true;
if result { found_match = true; }
else { all_match = false; }
},
)?;
Ok(Value::Boolean(match kind {
Some => found_match,
Every => if !seen_any { true } else { all_match },
}))
}
Expr::For { bindings, body } => {
let mut out_items: Vec<Value> = Vec::new();
eval_for_recursive_typed(bindings, body, 0, ctx, idx, &mut out_items)?;
if out_items.is_empty() {
return Ok(Value::NodeSet(Vec::new()));
}
let all_nodes = out_items.iter().all(|v| matches!(v, Value::NodeSet(_)));
if all_nodes {
let mut nodes: Vec<NodeId> = Vec::new();
for v in out_items {
if let Value::NodeSet(ns) = v { nodes.extend(ns); }
}
return Ok(Value::NodeSet(nodes));
}
Ok(Value::Sequence(out_items))
}
Expr::Let { bindings, body } => eval_let(bindings, body, 0, ctx, idx),
}
}
fn eval_let<I: DocIndexLike>(
bindings: &[(String, Expr)],
body: &Expr,
depth: usize,
ctx: &EvalCtx<'_>,
idx: &I,
) -> Result<Value> {
if depth == bindings.len() {
return eval_expr(body, ctx, idx);
}
let (name, bound_expr) = &bindings[depth];
let value = eval_expr(bound_expr, ctx, idx)?;
let scoped = ScopedBindings { parent: ctx.bindings, name, value };
let inner = EvalCtx {
context_node: ctx.context_node, pos: ctx.pos, size: ctx.size,
bindings: &scoped, static_ctx: ctx.static_ctx,
};
eval_let(bindings, body, depth + 1, &inner, idx)
}
fn eval_for_recursive_typed<I: DocIndexLike>(
bindings: &[(String, Expr)],
body: &Expr,
depth: usize,
ctx: &EvalCtx<'_>,
idx: &I,
out_items: &mut Vec<Value>,
) -> Result<()> {
if depth == bindings.len() {
match eval_expr(body, ctx, idx)? {
Value::Sequence(items) => out_items.extend(items),
other => out_items.push(other),
}
return Ok(());
}
let (name, in_expr) = &bindings[depth];
let seq = eval_expr(in_expr, ctx, idx)?;
match seq {
Value::NodeSet(ns) => {
for id in ns {
let scoped = ScopedBindings {
parent: ctx.bindings, name, value: Value::NodeSet(vec![id]),
};
let inner_ctx = EvalCtx {
context_node: ctx.context_node, pos: ctx.pos, size: ctx.size,
bindings: &scoped, static_ctx: ctx.static_ctx,
};
eval_for_recursive_typed(bindings, body, depth + 1, &inner_ctx, idx, out_items)?;
}
}
Value::ForeignNodeSet(ns) => {
for p in ns {
let scoped = ScopedBindings {
parent: ctx.bindings, name, value: Value::ForeignNodeSet(vec![p]),
};
let inner_ctx = EvalCtx {
context_node: ctx.context_node, pos: ctx.pos, size: ctx.size,
bindings: &scoped, static_ctx: ctx.static_ctx,
};
eval_for_recursive_typed(bindings, body, depth + 1, &inner_ctx, idx, out_items)?;
}
}
Value::Sequence(items) => {
for v in items {
let scoped = ScopedBindings {
parent: ctx.bindings, name, value: v,
};
let inner_ctx = EvalCtx {
context_node: ctx.context_node, pos: ctx.pos, size: ctx.size,
bindings: &scoped, static_ctx: ctx.static_ctx,
};
eval_for_recursive_typed(bindings, body, depth + 1, &inner_ctx, idx, out_items)?;
}
}
Value::IntRange { lo, hi } => {
for i in lo..=hi {
let scoped = ScopedBindings {
parent: ctx.bindings, name, value: Value::Number(Numeric::Double(i as f64)),
};
let inner_ctx = EvalCtx {
context_node: ctx.context_node, pos: ctx.pos, size: ctx.size,
bindings: &scoped, static_ctx: ctx.static_ctx,
};
eval_for_recursive_typed(bindings, body, depth + 1, &inner_ctx, idx, out_items)?;
}
}
atomic => {
let scoped = ScopedBindings {
parent: ctx.bindings, name, value: atomic,
};
let inner_ctx = EvalCtx {
context_node: ctx.context_node, pos: ctx.pos, size: ctx.size,
bindings: &scoped, static_ctx: ctx.static_ctx,
};
eval_for_recursive_typed(bindings, body, depth + 1, &inner_ctx, idx, out_items)?;
}
}
Ok(())
}
fn eval_quantified_recursive<I: DocIndexLike>(
bindings: &[(String, Expr)],
test: &Expr,
depth: usize,
ctx: &EvalCtx<'_>,
idx: &I,
report: &mut dyn FnMut(bool),
) -> Result<()> {
if depth == bindings.len() {
let t = eval_expr(test, ctx, idx)?;
report(value_to_bool(&t, idx));
return Ok(());
}
let (name, in_expr) = &bindings[depth];
let seq = eval_expr(in_expr, ctx, idx)?;
match seq {
Value::NodeSet(ns) => {
for id in ns {
let scoped = ScopedBindings { parent: ctx.bindings, name, value: Value::NodeSet(vec![id]) };
let inner = EvalCtx {
context_node: ctx.context_node, pos: ctx.pos, size: ctx.size,
bindings: &scoped, static_ctx: ctx.static_ctx,
};
eval_quantified_recursive(bindings, test, depth + 1, &inner, idx, report)?;
}
}
Value::ForeignNodeSet(ns) => {
for p in ns {
let scoped = ScopedBindings { parent: ctx.bindings, name, value: Value::ForeignNodeSet(vec![p]) };
let inner = EvalCtx {
context_node: ctx.context_node, pos: ctx.pos, size: ctx.size,
bindings: &scoped, static_ctx: ctx.static_ctx,
};
eval_quantified_recursive(bindings, test, depth + 1, &inner, idx, report)?;
}
}
Value::IntRange { lo, hi } => {
for i in lo..=hi {
let scoped = ScopedBindings { parent: ctx.bindings, name, value: Value::Number(Numeric::Double(i as f64)) };
let inner = EvalCtx {
context_node: ctx.context_node, pos: ctx.pos, size: ctx.size,
bindings: &scoped, static_ctx: ctx.static_ctx,
};
eval_quantified_recursive(bindings, test, depth + 1, &inner, idx, report)?;
}
}
Value::Sequence(items) => {
for item in items.iter().flat_map(iter_items) {
let scoped = ScopedBindings { parent: ctx.bindings, name, value: item };
let inner = EvalCtx {
context_node: ctx.context_node, pos: ctx.pos, size: ctx.size,
bindings: &scoped, static_ctx: ctx.static_ctx,
};
eval_quantified_recursive(bindings, test, depth + 1, &inner, idx, report)?;
}
}
atomic => {
let scoped = ScopedBindings { parent: ctx.bindings, name, value: atomic };
let inner = EvalCtx {
context_node: ctx.context_node, pos: ctx.pos, size: ctx.size,
bindings: &scoped, static_ctx: ctx.static_ctx,
};
eval_quantified_recursive(bindings, test, depth + 1, &inner, idx, report)?;
}
}
Ok(())
}
struct ClosureBindings<'p> {
vars: std::collections::HashMap<String, Value>,
base: &'p dyn XPathBindings,
}
impl<'p> XPathBindings for ClosureBindings<'p> {
fn variable(&self, name: &str) -> Option<Value> {
self.vars.get(name).cloned().or_else(|| self.base.variable(name))
}
fn resolve_prefix(&self, p: &str) -> Option<String> { self.base.resolve_prefix(p) }
fn call_function(&self, ns: &str, n: &str, a: Vec<Value>) -> Option<Result<Value>> {
self.base.call_function(ns, n, a)
}
fn call_function_in(&self, ns: &str, n: &str, a: Vec<Value>, cn: NodeId) -> Option<Result<Value>> {
self.base.call_function_in(ns, n, a, cn)
}
fn function_available_in(&self, ns: &str, n: &str, a: usize) -> bool {
self.base.function_available_in(ns, n, a)
}
fn function_signature_in(&self, ns: &str, n: &str, a: usize)
-> Option<crate::xpath::ast::FunctionSig> {
self.base.function_signature_in(ns, n, a)
}
fn castable_as_user_type(&self, ns: &str, l: &str, v: &str, sk: Option<&str>) -> Option<bool> {
self.base.castable_as_user_type(ns, l, v, sk)
}
fn instance_of_user_type(&self, ns: &str, l: &str, vt: Option<(&str, &str)>) -> Option<bool> {
self.base.instance_of_user_type(ns, l, vt)
}
fn schema_type_exists(&self, ns: &str, l: &str) -> bool {
self.base.schema_type_exists(ns, l)
}
fn cast_to_user_type(&self, ns: &str, l: &str, v: &str) -> Option<Result<Value>> {
self.base.cast_to_user_type(ns, l, v)
}
fn node_schema_type(&self, id: NodeId) -> Option<(String, String)> {
self.base.node_schema_type(id)
}
fn node_typed_value(&self, id: NodeId, lexical: &str) -> Option<Value> {
self.base.node_typed_value(id, lexical)
}
fn xpath_version_2_or_later(&self) -> bool { self.base.xpath_version_2_or_later() }
fn load_document(&self, u: &str, b: Option<&str>) -> Option<Result<Vec<ForeignNodePtr>>> {
self.base.load_document(u, b)
}
fn apply_foreign_path(&self, n: &[ForeignNodePtr], p: &[Expr], s: &[Step])
-> Option<Result<Vec<ForeignNodePtr>>> { self.base.apply_foreign_path(n, p, s) }
fn static_base_uri(&self) -> Option<String> { self.base.static_base_uri() }
fn node_base_uri(&self, id: NodeId) -> Option<String> { self.base.node_base_uri(id) }
fn foreign_string_value(&self, p: ForeignNodePtr) -> String { self.base.foreign_string_value(p) }
fn load_dynamic_document(&self, u: &str)
-> Option<std::result::Result<NodeId, crate::error::XmlError>> {
self.base.load_dynamic_document(u)
}
fn regex_dialect(&self) -> crate::regex::Dialect { self.base.regex_dialect() }
}
fn collect_var_refs(e: &Expr, out: &mut Vec<String>) {
use crate::xpath::ast::Expr as E;
let push = |n: &str, out: &mut Vec<String>| {
if !out.iter().any(|x| x == n) { out.push(n.to_string()); }
};
match e {
E::Variable(n) => push(n, out),
E::Or(a, b) | E::And(a, b) | E::Eq(a, b) | E::Ne(a, b)
| E::Lt(a, b) | E::Gt(a, b) | E::Le(a, b) | E::Ge(a, b)
| E::ValueEq(a, b) | E::ValueNe(a, b) | E::ValueLt(a, b)
| E::ValueGt(a, b) | E::ValueLe(a, b) | E::ValueGe(a, b)
| E::Add(a, b) | E::Sub(a, b) | E::Mul(a, b) | E::Div(a, b)
| E::Mod(a, b) | E::IDiv(a, b) | E::Union(a, b)
| E::Intersect(a, b) | E::Except(a, b) | E::Range(a, b)
| E::SimpleMap(a, b) | E::NodeBefore(a, b) | E::NodeAfter(a, b)
| E::NodeIs(a, b) => {
collect_var_refs(a, out); collect_var_refs(b, out);
}
E::Neg(x) | E::InstanceOf(x, _) | E::CastAs(x, _)
| E::CastableAs(x, _) | E::TreatAs(x, _)
| E::WithDefaultCollation(_, x) | E::BackwardsCompat(x) => collect_var_refs(x, out),
E::IfThenElse { cond, then_branch, else_branch } => {
collect_var_refs(cond, out);
collect_var_refs(then_branch, out);
collect_var_refs(else_branch, out);
}
E::For { bindings, body } | E::Let { bindings, body }
| E::Quantified { bindings, test: body, .. } => {
for (_, ex) in bindings { collect_var_refs(ex, out); }
collect_var_refs(body, out);
}
E::Sequence(items) => for x in items { collect_var_refs(x, out); },
E::FunctionCall(_, args) => for a in args { collect_var_refs(a, out); },
E::DynamicCall { func, args } => {
collect_var_refs(func, out);
for a in args { collect_var_refs(a, out); }
}
E::FilterPath { primary, predicates, steps } => {
collect_var_refs(primary, out);
for pr in predicates { collect_var_refs(pr, out); }
for s in steps { for pr in &s.predicates { collect_var_refs(pr, out); } }
}
E::Path(p) => collect_path_var_refs(p, out),
E::TryCatch { body, catches } => {
collect_var_refs(body, out);
for c in catches { collect_var_refs(&c.body, out); }
}
E::MapConstructor(es) => for (k, v) in es { collect_var_refs(k, out); collect_var_refs(v, out); },
E::ArrayConstructor { members, .. } => for m in members { collect_var_refs(m, out); },
E::Lookup(b, key) => {
collect_var_refs(b, out);
if let crate::xpath::ast::LookupKey::Expr(x) = key { collect_var_refs(x, out); }
}
E::UnaryLookup(key) =>
if let crate::xpath::ast::LookupKey::Expr(x) = key { collect_var_refs(x, out); },
E::InlineFunction { body, .. } => collect_var_refs(body, out),
E::Literal(_) | E::Integer(_) | E::Decimal(_) | E::Double(_)
| E::NamedFunctionRef { .. } | E::Placeholder | E::ContextItem => {}
}
}
fn collect_path_var_refs(p: &crate::xpath::ast::LocationPath, out: &mut Vec<String>) {
use crate::xpath::ast::LocationPath;
let steps = match p { LocationPath::Absolute(s) | LocationPath::Relative(s) => s };
for s in steps {
for pr in &s.predicates { collect_var_refs(pr, out); }
}
}
fn call_function_item<I: DocIndexLike>(
fi: &FunctionItem, args: Vec<Value>, ctx: &EvalCtx<'_>, idx: &I,
) -> Result<Value> {
match fi {
FunctionItem::Inline { params, body, closure, .. } => {
if args.len() != params.len() {
return Err(xpath_err(format!(
"inline function expects {} argument(s), got {}",
params.len(), args.len())));
}
let mut vars: std::collections::HashMap<String, Value> =
closure.iter().cloned().collect();
for (p, a) in params.iter().zip(args) { vars.insert(p.clone(), a); }
let cb = ClosureBindings { vars, base: ctx.bindings };
let inner = EvalCtx {
context_node: ctx.context_node, pos: ctx.pos, size: ctx.size,
bindings: &cb, static_ctx: ctx.static_ctx,
};
eval_expr(body, &inner, idx)
}
FunctionItem::Named { name, ns, arity, .. } => {
if args.len() != *arity {
return Err(xpath_err(format!(
"function {name}#{arity} called with {} argument(s)", args.len())));
}
if !ns.is_empty() && ns.as_str() != FN_NAMESPACE {
let local = name.rsplit(':').next().unwrap_or(name);
if let Some(r) =
ctx.bindings.call_function_in(ns, local, args.clone(), ctx.context_node)
{
return r;
}
if let Some(r) = super::exslt::dispatch(ns, local, args.clone(), idx) {
return r;
}
}
let mut vars = std::collections::HashMap::new();
let mut arg_exprs = Vec::with_capacity(args.len());
for (i, a) in args.into_iter().enumerate() {
let vn = format!("\u{1}fnarg{i}");
vars.insert(vn.clone(), a);
arg_exprs.push(Expr::Variable(vn));
}
let cb = ClosureBindings { vars, base: ctx.bindings };
let inner = EvalCtx {
context_node: ctx.context_node, pos: ctx.pos, size: ctx.size,
bindings: &cb, static_ctx: ctx.static_ctx,
};
eval_function(name, &arg_exprs, &inner, idx)
}
FunctionItem::Partial { base, bound } => {
let mut supplied = args.into_iter();
let mut full = Vec::with_capacity(bound.len());
for b in bound {
match b {
Some(v) => full.push(v.clone()),
None => full.push(supplied.next().ok_or_else(||
xpath_err("too few arguments for partial application"))?),
}
}
call_function_item(base, full, ctx, idx)
}
}
}
struct ScopedBindings<'p> {
parent: &'p dyn XPathBindings,
name: &'p str,
value: Value,
}
impl<'p> XPathBindings for ScopedBindings<'p> {
fn resolve_prefix(&self, prefix: &str) -> Option<String> {
self.parent.resolve_prefix(prefix)
}
fn variable(&self, name: &str) -> Option<Value> {
if name == self.name { Some(self.value.clone()) } else { self.parent.variable(name) }
}
fn call_function(
&self, ns_uri: &str, name: &str, args: Vec<Value>,
) -> Option<std::result::Result<Value, crate::error::XmlError>> {
self.parent.call_function(ns_uri, name, args)
}
fn call_function_in(
&self, ns_uri: &str, name: &str, args: Vec<Value>,
xpath_context_node: NodeId,
) -> Option<std::result::Result<Value, crate::error::XmlError>> {
self.parent.call_function_in(ns_uri, name, args, xpath_context_node)
}
fn function_available_in(&self, ns: &str, n: &str, a: usize) -> bool {
self.parent.function_available_in(ns, n, a)
}
fn function_signature_in(&self, ns: &str, n: &str, a: usize)
-> Option<crate::xpath::ast::FunctionSig> {
self.parent.function_signature_in(ns, n, a)
}
fn castable_as_user_type(&self, ns: &str, l: &str, v: &str, sk: Option<&str>) -> Option<bool> {
self.parent.castable_as_user_type(ns, l, v, sk)
}
fn instance_of_user_type(&self, ns: &str, l: &str, vt: Option<(&str, &str)>) -> Option<bool> {
self.parent.instance_of_user_type(ns, l, vt)
}
fn schema_type_exists(&self, ns: &str, l: &str) -> bool {
self.parent.schema_type_exists(ns, l)
}
fn cast_to_user_type(&self, ns: &str, l: &str, v: &str) -> Option<Result<Value>> {
self.parent.cast_to_user_type(ns, l, v)
}
fn node_schema_type(&self, id: NodeId) -> Option<(String, String)> {
self.parent.node_schema_type(id)
}
fn node_typed_value(&self, id: NodeId, lexical: &str) -> Option<Value> {
self.parent.node_typed_value(id, lexical)
}
fn foreign_string_value(
&self, p: crate::xpath::eval::ForeignNodePtr,
) -> String {
self.parent.foreign_string_value(p)
}
}
struct ErrBindings<'p> {
parent: &'p dyn XPathBindings,
err_uri: String,
code_local: String,
description: String,
}
impl<'p> XPathBindings for ErrBindings<'p> {
fn resolve_prefix(&self, prefix: &str) -> Option<String> {
self.parent.resolve_prefix(prefix)
.or_else(|| (prefix == "err").then(|| self.err_uri.clone()))
}
fn variable(&self, name: &str) -> Option<Value> {
let key_lex = format!("err:{}", "code");
let key_clark = format!("{{{}}}{}", self.err_uri, "code");
let local = if name == "err:code" || name == key_clark {
Some("code")
} else if name == "err:description"
|| name == format!("{{{}}}description", self.err_uri).as_str() {
Some("description")
} else if name == "err:value"
|| name == format!("{{{}}}value", self.err_uri).as_str() {
Some("value")
} else if name == "err:module"
|| name == format!("{{{}}}module", self.err_uri).as_str() {
Some("module")
} else if name == "err:line-number"
|| name == format!("{{{}}}line-number", self.err_uri).as_str() {
Some("line-number")
} else if name == "err:column-number"
|| name == format!("{{{}}}column-number", self.err_uri).as_str() {
Some("column-number")
} else {
None
};
let _ = key_lex;
match local {
Some("code") => Some(Value::String(format!("err:{}", self.code_local))),
Some("description") => Some(Value::String(self.description.clone())),
Some("value") => Some(Value::NodeSet(Vec::new())),
Some("module") => Some(Value::NodeSet(Vec::new())),
Some("line-number") => Some(Value::NodeSet(Vec::new())),
Some("column-number") => Some(Value::NodeSet(Vec::new())),
_ => self.parent.variable(name),
}
}
fn call_function(
&self, ns_uri: &str, name: &str, args: Vec<Value>,
) -> Option<std::result::Result<Value, crate::error::XmlError>> {
self.parent.call_function(ns_uri, name, args)
}
fn call_function_in(
&self, ns_uri: &str, name: &str, args: Vec<Value>,
xpath_context_node: NodeId,
) -> Option<std::result::Result<Value, crate::error::XmlError>> {
self.parent.call_function_in(ns_uri, name, args, xpath_context_node)
}
fn function_available_in(&self, ns: &str, n: &str, a: usize) -> bool {
self.parent.function_available_in(ns, n, a)
}
fn function_signature_in(&self, ns: &str, n: &str, a: usize)
-> Option<crate::xpath::ast::FunctionSig> {
self.parent.function_signature_in(ns, n, a)
}
fn castable_as_user_type(&self, ns: &str, l: &str, v: &str, sk: Option<&str>) -> Option<bool> {
self.parent.castable_as_user_type(ns, l, v, sk)
}
fn instance_of_user_type(&self, ns: &str, l: &str, vt: Option<(&str, &str)>) -> Option<bool> {
self.parent.instance_of_user_type(ns, l, vt)
}
fn schema_type_exists(&self, ns: &str, l: &str) -> bool {
self.parent.schema_type_exists(ns, l)
}
fn cast_to_user_type(&self, ns: &str, l: &str, v: &str) -> Option<Result<Value>> {
self.parent.cast_to_user_type(ns, l, v)
}
fn node_schema_type(&self, id: NodeId) -> Option<(String, String)> {
self.parent.node_schema_type(id)
}
fn node_typed_value(&self, id: NodeId, lexical: &str) -> Option<Value> {
self.parent.node_typed_value(id, lexical)
}
fn foreign_string_value(
&self, p: crate::xpath::eval::ForeignNodePtr,
) -> String {
self.parent.foreign_string_value(p)
}
}
fn build_err_scope<'p>(
parent: &'p dyn XPathBindings,
err_uri: &str, code_local: &str, description: &str,
) -> ErrBindings<'p> {
ErrBindings {
parent,
err_uri: err_uri.to_string(),
code_local: code_local.to_string(),
description: description.to_string(),
}
}
fn xpath_catch_matches(
matchers: &[crate::xpath::ast::CatchNameTest],
err_uri: &str, err_local: &str,
bindings: &dyn XPathBindings,
) -> bool {
use crate::xpath::ast::CatchNameTest::*;
matchers.iter().any(|m| match m {
Any => true,
LocalNameOnly(local) => err_local == local,
PrefixWildcard(prefix) => bindings.resolve_prefix(prefix)
.as_deref() == Some(err_uri),
QName { prefix, local } => {
let want_uri = match prefix {
Some(p) => bindings.resolve_prefix(p).unwrap_or_default(),
None => String::new(),
};
want_uri == err_uri && local == err_local
}
})
}
fn eval_path<I: DocIndexLike>(
path: &LocationPath, ctx_node: NodeId, idx: &I,
bindings: &dyn XPathBindings, compat: bool, current_node: Option<NodeId>,
) -> Result<Vec<NodeId>> {
let (initial, steps) = match path {
LocationPath::Absolute(steps) => {
if focus_is_undefined() {
return Err(xpath_err(
"absolute path requires a context item (XPDY0002)"
).with_xpath_code("XPDY0002"));
}
let mut root = ctx_node;
while let Some(p) = idx.parent(root) {
root = p;
}
(vec![root], steps.as_slice())
},
LocationPath::Relative(steps) => (vec![ctx_node], steps.as_slice()),
};
let mut current = initial;
for step in steps {
current = eval_step_on_nodes_cur(current, step, idx, bindings, compat, current_node)?;
}
Ok(current)
}
pub fn eval_step_on_nodes<I: DocIndexLike>(
nodes: Vec<NodeId>, step: &Step, idx: &I,
bindings: &dyn XPathBindings, compat: bool,
) -> Result<Vec<NodeId>> {
eval_step_on_nodes_cur(nodes, step, idx, bindings, compat, None)
}
pub fn eval_step_on_nodes_cur<I: DocIndexLike>(
nodes: Vec<NodeId>, step: &Step, idx: &I,
bindings: &dyn XPathBindings, compat: bool, current_node: Option<NodeId>,
) -> Result<Vec<NodeId>> {
let sc = StaticContext {
xpath_2_0: bindings.xpath_version_2_or_later(),
xpath_3_0: false,
libxml2_compatible: compat,
current_node,
};
if let Some(filter) = &step.filter {
let mut out: Vec<NodeId> = Vec::new();
let mut all_native_nodes = true;
for node in nodes {
charge_eval_step()?;
let ctx = EvalCtx {
context_node: node, pos: 1, size: 1, bindings,
static_ctx: &sc,
};
let v = with_focus_undefined(false, || eval_expr(filter, &ctx, idx))?;
match v {
Value::NodeSet(ns) => out.extend(ns),
Value::ForeignNodeSet(_) => { }
Value::Sequence(items) => {
let mut atoms: Vec<String> = Vec::new();
for item in items {
match item {
Value::NodeSet(ns) => out.extend(ns),
Value::ForeignNodeSet(_) => {}
atomic => atoms.push(value_to_string_with(&atomic, idx, bindings)),
}
}
if !atoms.is_empty() {
all_native_nodes = false;
if let Some(ids) = idx.allocate_rtf_text_nodes(atoms) {
out.extend(ids);
}
}
}
atomic => {
all_native_nodes = false;
let s = value_to_string_with(&atomic, idx, bindings);
if let Some(ids) = idx.allocate_rtf_text_nodes(vec![s]) {
out.extend(ids);
}
}
}
}
if all_native_nodes && bindings.xpath_version_2_or_later() {
dedup_sort(&mut out);
}
return apply_predicates_cur(out, &step.predicates, idx, bindings, compat, current_node);
}
let mut candidates: Vec<NodeId> = Vec::new();
for node in nodes {
charge_eval_step()?;
let mut cands = axis_nodes(&step.axis, node, idx);
cands.retain(|&n| node_matches(n, &step.node_test, &step.axis, idx, bindings, compat));
let filtered = apply_predicates_cur(cands, &step.predicates, idx, bindings, compat, current_node)?;
candidates.extend(filtered);
}
dedup_sort(&mut candidates);
Ok(candidates)
}
pub fn apply_predicates<I: DocIndexLike>(
nodes: Vec<NodeId>, predicates: &[Expr], idx: &I,
bindings: &dyn XPathBindings, compat: bool,
) -> Result<Vec<NodeId>> {
apply_predicates_cur(nodes, predicates, idx, bindings, compat, None)
}
pub fn apply_predicates_cur<I: DocIndexLike>(
nodes: Vec<NodeId>, predicates: &[Expr], idx: &I,
bindings: &dyn XPathBindings, compat: bool, current_node: Option<NodeId>,
) -> Result<Vec<NodeId>> {
let sc = StaticContext {
xpath_2_0: bindings.xpath_version_2_or_later(),
xpath_3_0: false,
libxml2_compatible: compat,
current_node,
};
let mut result = nodes;
for pred in predicates {
if let Some(n) = positional_index(pred) {
charge_eval_step()?;
result = match n.checked_sub(1).and_then(|i| result.get(i).copied()) {
Some(node) => vec![node],
None => Vec::new(),
};
continue;
}
let size = result.len();
let mut next = Vec::new();
for (i, node) in result.into_iter().enumerate() {
charge_eval_step()?;
let ctx = EvalCtx {
context_node: node, pos: i + 1, size, bindings,
static_ctx: &sc,
};
let v = eval_expr(pred, &ctx, idx)?;
let keep = match &v {
Value::Number(n) => (i + 1) as f64 == n.as_f64(),
Value::Typed(t) => match t.numeric {
Some(n) => (i + 1) as f64 == n,
None => value_to_bool(&v, idx),
},
Value::NodeSet(ns) if ns.len() == 1
&& matches!(idx.kind(ns[0]),
crate::xpath::XPathNodeKind::Text)
&& idx.parent(ns[0]).is_none()
=> {
let s = idx.string_value(ns[0]);
match s.trim().parse::<f64>() {
Ok(n) if n.fract() == 0.0 => (i + 1) as f64 == n,
_ => value_to_bool(&v, idx),
}
}
other => value_to_bool(other, idx),
};
if keep {
next.push(node);
}
}
result = next;
}
Ok(result)
}
fn positional_index(pred: &Expr) -> Option<usize> {
fn lit_pos_int(e: &Expr) -> Option<usize> {
match e {
Expr::Integer(i) if *i >= 1 => Some(*i as usize),
Expr::Decimal(n) => {
use rust_decimal::prelude::ToPrimitive;
let whole = n.trunc() == *n;
let positive = *n >= rust_decimal::Decimal::ONE;
let fits = n.to_usize();
match (whole, positive, fits) {
(true, true, Some(u)) => Some(u),
_ => None,
}
}
_ => None,
}
}
fn is_position_call(e: &Expr) -> bool {
matches!(e, Expr::FunctionCall(name, args) if name == "position" && args.is_empty())
}
if let Some(n) = lit_pos_int(pred) { return Some(n); }
if let Expr::Eq(l, r) = pred {
if is_position_call(l) { return lit_pos_int(r); }
if is_position_call(r) { return lit_pos_int(l); }
}
None
}
fn axis_nodes<I: DocIndexLike>(axis: &Axis, node: NodeId, idx: &I) -> Vec<NodeId> {
match axis {
Axis::Self_ => vec![node],
Axis::Child => idx.children(node).to_vec(),
Axis::Parent => idx.parent(node).into_iter().collect(),
Axis::Attribute => idx.attr_range(node).collect(),
Axis::Ancestor => ancestors(node, idx),
Axis::AncestorOrSelf => {
let mut a = vec![node];
a.extend(ancestors(node, idx));
a
}
Axis::Descendant => descendants(node, idx, false),
Axis::DescendantOrSelf => descendants(node, idx, true),
Axis::FollowingSibling => following_siblings(node, idx),
Axis::PrecedingSibling => preceding_siblings(node, idx),
Axis::Following => following(node, idx),
Axis::Preceding => preceding(node, idx),
Axis::Namespace => idx.ns_range(node).collect(),
}
}
fn ancestors<I: DocIndexLike>(node: NodeId, idx: &I) -> Vec<NodeId> {
let mut result = Vec::new();
let mut cur = idx.parent(node);
while let Some(p) = cur {
result.push(p);
cur = idx.parent(p);
}
result
}
fn is_valid_lexical_qname(s: &str) -> bool {
fn is_ncname(s: &str) -> bool {
let mut chars = s.chars();
let first = match chars.next() { Some(c) => c, None => return false };
let start_ok = first == '_' || first.is_ascii_alphabetic()
|| (first as u32 >= 0x80 && crate::charsets::is_name_start_char(first));
if !start_ok || first == ':' { return false; }
for c in chars {
if c == ':' { return false; }
let ok = c == '_' || c == '-' || c == '.'
|| c.is_ascii_alphanumeric()
|| (c as u32 >= 0x80 && crate::charsets::is_name_char_unicode(c));
if !ok { return false; }
}
true
}
match s.split_once(':') {
None => is_ncname(s),
Some((p, l)) => is_ncname(p) && is_ncname(l),
}
}
fn doc_root_of<I: DocIndexLike>(node: NodeId, idx: &I) -> NodeId {
let mut cur = node;
while let Some(p) = idx.parent(cur) { cur = p; }
cur
}
fn descendants<I: DocIndexLike>(node: NodeId, idx: &I, include_self: bool) -> Vec<NodeId> {
let mut result = Vec::new();
if include_self {
result.push(node);
}
collect_desc(node, idx, &mut result);
result
}
fn collect_desc<I: DocIndexLike>(node: NodeId, idx: &I, out: &mut Vec<NodeId>) {
for &child in idx.children(node) {
out.push(child);
collect_desc(child, idx, out);
}
}
fn following_siblings<I: DocIndexLike>(node: NodeId, idx: &I) -> Vec<NodeId> {
let parent = match idx.parent(node) {
Some(p) => p,
None => return Vec::new(),
};
let siblings = idx.children(parent);
siblings.iter().position(|&n| n == node)
.map(|p| siblings[p + 1..].to_vec())
.unwrap_or_default()
}
fn preceding_siblings<I: DocIndexLike>(node: NodeId, idx: &I) -> Vec<NodeId> {
let parent = match idx.parent(node) {
Some(p) => p,
None => return Vec::new(),
};
let siblings = idx.children(parent);
let pos = siblings.iter().position(|&n| n == node).unwrap_or(0);
siblings[..pos].iter().rev().copied().collect()
}
fn following<I: DocIndexLike>(node: NodeId, idx: &I) -> Vec<NodeId> {
let desc_set: HashSet<NodeId> = descendants(node, idx, true).into_iter().collect();
let mut result = Vec::new();
let mut cur = node;
while let Some(parent) = idx.parent(cur) {
let siblings = idx.children(parent);
let start = siblings.iter().position(|&n| n == cur).map_or(0, |p| p + 1);
for &sib in &siblings[start..] {
if !desc_set.contains(&sib) {
result.push(sib);
collect_desc(sib, idx, &mut result);
}
}
cur = parent;
}
dedup_sort(&mut result);
result
}
fn preceding<I: DocIndexLike>(node: NodeId, idx: &I) -> Vec<NodeId> {
let ancestor_set: HashSet<NodeId> = ancestors(node, idx).into_iter().collect();
let mut result = Vec::new();
let mut cur = node;
while let Some(parent) = idx.parent(cur) {
let siblings = idx.children(parent);
let pos = siblings.iter().position(|&n| n == cur).unwrap_or(0);
for &sib in siblings[..pos].iter().rev() {
if !ancestor_set.contains(&sib) {
let mut d = Vec::new();
collect_desc(sib, idx, &mut d);
for id in d.into_iter().rev() {
result.push(id);
}
result.push(sib);
}
}
cur = parent;
}
result
}
fn principal_kind(axis: &Axis) -> XPathNodeKind {
match axis {
Axis::Attribute => XPathNodeKind::Attribute,
Axis::Namespace => XPathNodeKind::Namespace,
_ => XPathNodeKind::Element,
}
}
pub fn node_matches_child<I: DocIndexLike>(
node: NodeId, test: &NodeTest, idx: &I, bindings: &dyn XPathBindings,
) -> bool {
node_matches(node, test, &Axis::Child, idx, bindings, false)
}
fn node_matches<I: DocIndexLike>(
node: NodeId, test: &NodeTest, axis: &Axis, idx: &I,
bindings: &dyn XPathBindings, libxml2_compatible: bool,
) -> bool {
match test {
NodeTest::AnyNode => true,
NodeTest::Text => matches!(idx.kind(node), XPathNodeKind::Text | XPathNodeKind::CData),
NodeTest::Comment => matches!(idx.kind(node), XPathNodeKind::Comment),
NodeTest::PI(None) => matches!(idx.kind(node), XPathNodeKind::PI),
NodeTest::PI(Some(target)) => {
(matches!(idx.kind(node), XPathNodeKind::PI) && idx.pi_target(node) == *target)
}
NodeTest::Document(inner) => {
if !matches!(idx.kind(node), XPathNodeKind::Document) {
return false;
}
match inner {
None => true,
Some(t) => idx.children(node).iter().any(|c|
idx.kind(*c) == XPathNodeKind::Element
&& node_matches(*c, t, &Axis::Child, idx, bindings, libxml2_compatible)),
}
}
NodeTest::Wildcard => idx.kind(node) == principal_kind(axis),
NodeTest::PrefixWildcard(prefix) => {
if idx.kind(node) != principal_kind(axis) {
return false;
}
if let Some(uri) = resolve_prefix_or_implicit(bindings, prefix) {
idx.namespace_uri(node) == uri
} else {
let name = idx.node_name(node);
name.starts_with(&format!("{prefix}:"))
}
}
NodeTest::LocalNameOnly(local) => {
if idx.kind(node) != principal_kind(axis) {
return false;
}
idx.local_name(node) == local
}
NodeTest::LocalName(local) => {
if idx.kind(node) != principal_kind(axis) {
return false;
}
if idx.local_name(node) != local {
return false;
}
if libxml2_compatible || matches!(axis, Axis::Namespace) {
return true;
}
idx.namespace_uri(node).is_empty()
}
NodeTest::DefaultNamespaceName { uri, local } => {
if idx.kind(node) != principal_kind(axis) {
return false;
}
if !matches!(axis,
Axis::Child | Axis::Descendant | Axis::DescendantOrSelf
| Axis::Self_ | Axis::Parent | Axis::Ancestor
| Axis::AncestorOrSelf | Axis::FollowingSibling
| Axis::PrecedingSibling | Axis::Following | Axis::Preceding)
{
return false;
}
idx.local_name(node) == local.as_str()
&& idx.namespace_uri(node) == uri.as_str()
}
NodeTest::QName(prefix, local) => {
if idx.kind(node) != principal_kind(axis) {
return false;
}
if let Some(uri) = resolve_prefix_or_implicit(bindings, prefix) {
return idx.local_name(node) == local.as_str()
&& idx.namespace_uri(node) == uri;
}
let expected_qname = format!("{prefix}:{local}");
if idx.node_name(node) == expected_qname {
return true;
}
idx.local_name(node) == local.as_str()
&& idx.namespace_prefix(node) == Some(prefix.as_str())
}
}
}
pub fn value_to_bool<I: DocIndexLike>(v: &Value, idx: &I) -> bool {
match v {
Value::Boolean(b) => *b,
Value::Number(n) => { let n = n.as_f64(); n != 0.0 && !n.is_nan() }
Value::String(s) => !s.is_empty(),
Value::NodeSet(ns) => !ns.is_empty(),
Value::ForeignNodeSet(ns) => !ns.is_empty(),
Value::Typed(t) => {
if let Some(b) = t.boolean { return b; }
if let Some(n) = t.numeric { return n != 0.0 && !n.is_nan(); }
!t.lexical.is_empty()
}
Value::Sequence(items) => match items.first() {
None => false,
Some(v) => value_to_bool(v, idx),
}
Value::IntRange { lo, hi } if lo == hi => *lo != 0,
Value::IntRange { .. } => true,
Value::Map(_) | Value::Array(_) | Value::Function(_) => true,
}
}
pub fn value_to_number<I: DocIndexLike>(v: &Value, idx: &I) -> f64 {
value_to_number_with(v, idx, &NO_BINDINGS)
}
pub fn value_to_number_with<I: DocIndexLike>(
v: &Value, idx: &I, bindings: &dyn XPathBindings,
) -> f64 {
match v {
Value::Number(n) => n.as_f64(),
Value::Boolean(b) => if *b { 1.0 } else { 0.0 },
Value::String(s) => s.trim().parse().unwrap_or(f64::NAN),
Value::NodeSet(ns) => {
if ns.is_empty() {
return f64::NAN;
}
let s = idx.string_value(ns[0]);
s.trim().parse().unwrap_or(f64::NAN)
}
Value::ForeignNodeSet(ns) => {
if ns.is_empty() {
return f64::NAN;
}
bindings.foreign_string_value(ns[0]).trim().parse().unwrap_or(f64::NAN)
}
Value::Typed(t) => {
if let Some(n) = t.numeric { return n; }
if let Some(b) = t.boolean { return if b { 1.0 } else { 0.0 }; }
t.lexical.trim().parse().unwrap_or(f64::NAN)
}
Value::Sequence(items) => match items.first() {
Some(v) => value_to_number_with(v, idx, bindings),
None => f64::NAN,
}
Value::IntRange { lo, .. } => *lo as f64,
Value::Map(_) | Value::Array(_) | Value::Function(_) => f64::NAN,
}
}
fn value_is_function(v: &Value) -> bool {
matches!(v, Value::Function(_))
}
fn round_decimal_half_to_pos_inf(d: rust_decimal::Decimal, precision: i32) -> rust_decimal::Decimal {
use rust_decimal::Decimal;
let half = Decimal::new(5, 1); if precision >= 0 {
let shift = Decimal::from(10i64.pow((precision as u32).min(18)));
(d * shift + half).floor() / shift
} else {
let shift = Decimal::from(10i64.pow(((-precision) as u32).min(18)));
(d / shift + half).floor() * shift
}
}
fn round_decimal_half_to_even(d: rust_decimal::Decimal, precision: i32) -> rust_decimal::Decimal {
use rust_decimal::{Decimal, RoundingStrategy};
if precision >= 0 {
d.round_dp_with_strategy((precision as u32).min(28), RoundingStrategy::MidpointNearestEven)
} else {
let shift = Decimal::from(10i64.pow(((-precision) as u32).min(18)));
(d / shift).round_dp_with_strategy(0, RoundingStrategy::MidpointNearestEven) * shift
}
}
fn value_seq_has_function(v: &Value) -> bool {
match v {
Value::Function(_) => true,
Value::Sequence(items) => items.iter().any(value_seq_has_function),
_ => false,
}
}
pub fn value_to_string<I: DocIndexLike>(v: &Value, idx: &I) -> String {
value_to_string_with(v, idx, &NO_BINDINGS)
}
pub fn value_to_string_with<I: DocIndexLike>(
v: &Value, idx: &I, bindings: &dyn XPathBindings,
) -> String {
value_to_string_with_compat(v, idx, bindings, false)
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum NumStyle { Xpath10, Libxml2, Xpath20 }
impl NumStyle {
pub fn from_context(compat: bool, xpath_2_or_later: bool) -> NumStyle {
if compat { NumStyle::Libxml2 }
else if xpath_2_or_later { NumStyle::Xpath20 }
else { NumStyle::Xpath10 }
}
}
pub fn format_numeric_styled(n: Numeric, style: NumStyle) -> String {
if let Numeric::Decimal(d) = n {
return format_decimal_canonical(d);
}
let f = n.as_f64();
if matches!(n, Numeric::Float(_)) && matches!(style, NumStyle::Xpath20) {
return canonical_float_lex(f, &Value::Number(n));
}
match style {
NumStyle::Libxml2 => format_number_libxml2(f),
NumStyle::Xpath20 if matches!(n, Numeric::Double(_)) =>
format_number_xpath20(f),
_ => format_number(f),
}
}
fn format_decimal_canonical(d: rust_decimal::Decimal) -> String {
if d.is_zero() { return "0".into(); }
let s = d.to_string();
let trimmed = match s.split_once('.') {
Some((w, f)) => {
let f = f.trim_end_matches('0');
if f.is_empty() { w.to_string() } else { format!("{w}.{f}") }
}
None => s,
};
if trimmed == "-0" { "0".into() } else { trimmed }
}
pub fn value_to_string_with_compat<I: DocIndexLike>(
v: &Value, idx: &I, bindings: &dyn XPathBindings, compat: bool,
) -> String {
let style = NumStyle::from_context(compat, bindings.xpath_version_2_or_later());
value_to_string_styled_with(v, idx, bindings, style)
}
pub fn value_to_string_styled<I: DocIndexLike>(
v: &Value, idx: &I, style: NumStyle,
) -> String {
value_to_string_styled_with(v, idx, &NO_BINDINGS, style)
}
pub fn value_to_string_styled_with<I: DocIndexLike>(
v: &Value, idx: &I, bindings: &dyn XPathBindings, style: NumStyle,
) -> String {
match v {
Value::String(s) => s.clone(),
Value::Boolean(b) => (if *b { "true" } else { "false" }).to_string(),
Value::Number(n) => format_numeric_styled(*n, style),
Value::NodeSet(ns) => {
if ns.is_empty() {
String::new()
} else {
idx.string_value(ns[0])
}
}
Value::ForeignNodeSet(ns) => {
if ns.is_empty() {
String::new()
} else {
bindings.foreign_string_value(ns[0])
}
}
Value::Typed(t) => t.lexical.clone(),
Value::Sequence(items) => match items.first() {
Some(v) => value_to_string_styled_with(v, idx, bindings, style),
None => String::new(),
}
Value::IntRange { lo, .. } => lo.to_string(),
Value::Map(_) | Value::Array(_) | Value::Function(_) => String::new(),
}
}
fn preserve_numeric_kind(arg: &Value, result: f64) -> Value {
match numeric_kind_of(arg) {
Some(kind) => Value::Number(Numeric::of_kind(kind, result)),
None => Value::Number(Numeric::Double(result)),
}
}
fn format_number_xpath20(n: f64) -> String {
if n.is_nan() { return "NaN".to_string(); }
if n.is_infinite() { return if n > 0.0 { "INF" } else { "-INF" }.to_string(); }
if n == 0.0 {
return if n.is_sign_negative() { "-0".to_string() } else { "0".to_string() };
}
let abs = n.abs();
if (1e-6..1e6).contains(&abs) {
return format_number(n); }
let s = format!("{n:e}");
let (mantissa, exp) = s.split_once('e').unwrap_or((s.as_str(), "0"));
let mantissa = if mantissa.contains('.') { mantissa.to_string() }
else { format!("{mantissa}.0") };
format!("{mantissa}E{exp}")
}
fn format_number(n: f64) -> String {
if n.is_nan() {
"NaN".to_string()
} else if n.is_infinite() {
if n > 0.0 { "Infinity".to_string() } else { "-Infinity".to_string() }
} else if n == 0.0 && n.is_sign_negative() {
"-0".to_string()
} else if n.fract() == 0.0 && n.abs() < 1e15 {
format!("{}", n as i64)
} else {
format!("{n}")
}
}
fn format_number_libxml2(n: f64) -> String {
if n.is_nan() {
return "NaN".to_string();
}
if n.is_infinite() {
return if n > 0.0 { "Infinity".into() } else { "-Infinity".into() };
}
if n.fract() == 0.0 && n.abs() < 1e15 {
return format!("{}", n as i64);
}
let abs = n.abs();
if abs >= 1e15 || (abs > 0.0 && abs < 1e-5) {
let s = format!("{:.14e}", n);
let Some(idx) = s.find('e') else { return s; };
let (mantissa, exp_with_e) = s.split_at(idx);
let exp = &exp_with_e[1..];
let mantissa = trim_mantissa(mantissa);
if exp.starts_with('-') {
format!("{}e{}", mantissa, exp)
} else {
format!("{}e+{}", mantissa, exp)
}
} else {
format!("{n}")
}
}
fn trim_mantissa(s: &str) -> String {
let Some(dot_idx) = s.find('.') else { return s.to_string(); };
let trimmed = s.trim_end_matches('0');
let trimmed = trimmed.trim_end_matches('.');
if trimmed.len() <= dot_idx { format!("{}.0", &s[..dot_idx]) } else { trimmed.to_string() }
}
fn intrange_to_sequence(v: &Value) -> Option<Value> {
if let Value::IntRange { lo, hi } = v {
let items: Vec<Value> = (*lo..=*hi).map(|i| Value::Number(Numeric::Double(i as f64))).collect();
return Some(Value::Sequence(items));
}
None
}
fn values_ne<I: DocIndexLike>(
l: &Value, r: &Value, idx: &I, bindings: &dyn XPathBindings,
) -> bool {
if let Some(lv) = intrange_to_sequence(l) {
return values_ne(&lv, r, idx, bindings);
}
if let Some(rv) = intrange_to_sequence(r) {
return values_ne(l, &rv, idx, bindings);
}
match (l, r) {
(Value::Map(_) | Value::Array(_) | Value::Function(_), _) | (_, Value::Map(_) | Value::Array(_) | Value::Function(_)) => false,
(Value::NodeSet(ls), Value::NodeSet(rs)) => {
let l_vals: HashSet<String> = ls.iter().map(|&id| idx.string_value(id)).collect();
let r_vals: HashSet<String> = rs.iter().map(|&id| idx.string_value(id)).collect();
if l_vals.is_empty() || r_vals.is_empty() { return false; }
l_vals.union(&r_vals).count() > 1
}
(Value::ForeignNodeSet(ls), Value::ForeignNodeSet(rs)) => {
let l_vals: HashSet<String> = ls.iter()
.map(|&p| bindings.foreign_string_value(p)).collect();
let r_vals: HashSet<String> = rs.iter()
.map(|&p| bindings.foreign_string_value(p)).collect();
if l_vals.is_empty() || r_vals.is_empty() { return false; }
l_vals.union(&r_vals).count() > 1
}
(Value::NodeSet(ns), Value::ForeignNodeSet(fs))
| (Value::ForeignNodeSet(fs), Value::NodeSet(ns)) => {
let l_vals: HashSet<String> = ns.iter().map(|&id| idx.string_value(id)).collect();
let r_vals: HashSet<String> = fs.iter()
.map(|&p| bindings.foreign_string_value(p)).collect();
if l_vals.is_empty() || r_vals.is_empty() { return false; }
l_vals.union(&r_vals).count() > 1
}
(Value::NodeSet(ns), other) | (other, Value::NodeSet(ns)) => {
match other {
Value::Boolean(b) => value_to_bool(&Value::NodeSet(ns.clone()), idx) != *b,
Value::Number(n) => ns.iter().any(|&id| {
idx.string_value(id).trim().parse::<f64>().ok() != Some(n.as_f64())
}),
Value::String(s) => ns.iter().any(|&id| idx.string_value(id) != *s),
Value::Typed(t) => {
if let Some(n) = t.numeric {
ns.iter().any(|&id| idx.string_value(id).trim().parse::<f64>().ok() != Some(n))
} else if let Some(b) = t.boolean {
!ns.is_empty() != b
} else {
ns.iter().any(|&id| idx.string_value(id) != t.lexical)
}
}
Value::Sequence(items) => items.iter().any(|v| {
values_ne(&Value::NodeSet(ns.clone()), v, idx, bindings)
}),
Value::NodeSet(_) | Value::ForeignNodeSet(_) | Value::IntRange { .. }
| Value::Map(_) | Value::Array(_) | Value::Function(_) => unreachable!(),
}
}
(Value::ForeignNodeSet(fs), other) | (other, Value::ForeignNodeSet(fs)) => {
match other {
Value::Boolean(b) => !fs.is_empty() != *b,
Value::Number(n) => fs.iter().any(|&p| {
bindings.foreign_string_value(p).trim().parse::<f64>().ok() != Some(n.as_f64())
}),
Value::String(s) => fs.iter().any(|&p| bindings.foreign_string_value(p) != *s),
Value::Typed(t) => {
if let Some(n) = t.numeric {
fs.iter().any(|&p| bindings.foreign_string_value(p)
.trim().parse::<f64>().ok() != Some(n))
} else if let Some(b) = t.boolean {
!fs.is_empty() != b
} else {
fs.iter().any(|&p| bindings.foreign_string_value(p) != t.lexical)
}
}
Value::Sequence(items) => items.iter().any(|v| {
values_ne(&Value::ForeignNodeSet(fs.clone()), v, idx, bindings)
}),
Value::NodeSet(_) | Value::ForeignNodeSet(_) | Value::IntRange { .. }
| Value::Map(_) | Value::Array(_) | Value::Function(_) => unreachable!(),
}
}
(Value::Sequence(items), other) | (other, Value::Sequence(items)) => {
items.iter().any(|v| values_ne(v, other, idx, bindings))
}
_ => !values_eq(l, r, idx, bindings),
}
}
fn values_eq<I: DocIndexLike>(
l: &Value, r: &Value, idx: &I, bindings: &dyn XPathBindings,
) -> bool {
if let Some(lv) = intrange_to_sequence(l) {
return values_eq(&lv, r, idx, bindings);
}
if let Some(rv) = intrange_to_sequence(r) {
return values_eq(l, &rv, idx, bindings);
}
let ci = is_ascii_ci_collation(
DEFAULT_COLLATION.with(|c| c.borrow().clone()).as_deref());
let str_eq = |a: &str, b: &str| if ci {
ascii_ci_fold(a) == ascii_ci_fold(b)
} else { a == b };
match (l, r) {
(Value::Map(_) | Value::Array(_) | Value::Function(_), _) | (_, Value::Map(_) | Value::Array(_) | Value::Function(_)) => false,
(Value::NodeSet(ls), Value::NodeSet(rs)) => {
let (small, large) = if ls.len() <= rs.len() { (ls, rs) } else { (rs, ls) };
let mut set: HashSet<String> = HashSet::with_capacity(small.len());
for &a in small {
set.insert(idx.string_value(a));
}
for &b in large {
if set.contains(&idx.string_value(b)) {
return true;
}
}
false
}
(Value::ForeignNodeSet(ls), Value::ForeignNodeSet(rs)) => {
let (small, large) = if ls.len() <= rs.len() { (ls, rs) } else { (rs, ls) };
let mut set: HashSet<String> = HashSet::with_capacity(small.len());
for &a in small {
set.insert(bindings.foreign_string_value(a));
}
for &b in large {
if set.contains(&bindings.foreign_string_value(b)) {
return true;
}
}
false
}
(Value::NodeSet(ns), Value::ForeignNodeSet(fs))
| (Value::ForeignNodeSet(fs), Value::NodeSet(ns)) => {
let mut set: HashSet<String> = HashSet::with_capacity(ns.len());
for &id in ns {
set.insert(idx.string_value(id));
}
for &p in fs {
if set.contains(&bindings.foreign_string_value(p)) {
return true;
}
}
false
}
(Value::NodeSet(ns), other) | (other, Value::NodeSet(ns)) => {
match other {
Value::Boolean(b) => value_to_bool(&Value::NodeSet(ns.clone()), idx) == *b,
Value::Number(n) => ns.iter().any(|&id| {
idx.string_value(id).trim().parse::<f64>().ok() == Some(n.as_f64())
}),
Value::String(s) => ns.iter().any(|&id| idx.string_value(id) == *s),
Value::Typed(t) => {
if let Some(n) = t.numeric {
ns.iter().any(|&id| idx.string_value(id).trim().parse::<f64>().ok() == Some(n))
} else if let Some(b) = t.boolean {
!ns.is_empty() == b
} else {
ns.iter().any(|&id| idx.string_value(id) == t.lexical)
}
}
Value::Sequence(items) => items.iter().any(|v| {
values_eq(&Value::NodeSet(ns.clone()), v, idx, bindings)
}),
Value::NodeSet(_) | Value::ForeignNodeSet(_) | Value::IntRange { .. }
| Value::Map(_) | Value::Array(_) | Value::Function(_) => unreachable!(),
}
}
(Value::ForeignNodeSet(fs), other) | (other, Value::ForeignNodeSet(fs)) => {
match other {
Value::Boolean(b) => !fs.is_empty() == *b,
Value::Number(n) => fs.iter().any(|&p| {
bindings.foreign_string_value(p).trim().parse::<f64>().ok() == Some(n.as_f64())
}),
Value::String(s) => fs.iter().any(|&p| bindings.foreign_string_value(p) == *s),
Value::Typed(t) => {
if let Some(n) = t.numeric {
fs.iter().any(|&p| bindings.foreign_string_value(p)
.trim().parse::<f64>().ok() == Some(n))
} else if let Some(b) = t.boolean {
!fs.is_empty() == b
} else {
fs.iter().any(|&p| bindings.foreign_string_value(p) == t.lexical)
}
}
Value::Sequence(items) => items.iter().any(|v| {
values_eq(&Value::ForeignNodeSet(fs.clone()), v, idx, bindings)
}),
Value::NodeSet(_) | Value::ForeignNodeSet(_) | Value::IntRange { .. }
| Value::Map(_) | Value::Array(_) | Value::Function(_) => unreachable!(),
}
}
(Value::Sequence(items), other) | (other, Value::Sequence(items)) => {
items.iter().any(|v| values_eq(v, other, idx, bindings))
}
(Value::Boolean(a), b) => *a == value_to_bool(b, idx),
(a, Value::Boolean(b)) => value_to_bool(a, idx) == *b,
(Value::Number(a), Value::Number(b)) => a.as_f64() == b.as_f64(),
(Value::Number(a), Value::String(b)) => a.as_f64() == b.trim().parse::<f64>().unwrap_or(f64::NAN),
(Value::String(a), Value::Number(b)) => a.trim().parse::<f64>().unwrap_or(f64::NAN) == b.as_f64(),
(Value::String(a), Value::String(b)) => str_eq(a, b),
(Value::Typed(t), Value::Typed(u)) => {
match (t.numeric, u.numeric) {
(Some(a), Some(b)) => a == b,
_ => {
let date_eq = matches!(t.kind, "date" | "dateTime" | "time")
&& matches!(u.kind, "date" | "dateTime" | "time")
&& t.kind == u.kind;
if date_eq {
if let (Some(a), Some(b)) = (
dt_to_utc_seconds(&t.lexical, t.kind),
dt_to_utc_seconds(&u.lexical, u.kind),
) {
return a == b;
}
}
if t.kind == "dayTimeDuration" && u.kind == "dayTimeDuration" {
if let (Some(a), Some(b)) = (
parse_day_time_duration_secs(&t.lexical),
parse_day_time_duration_secs(&u.lexical),
) {
return a == b;
}
}
if matches!((t.kind, u.kind),
("duration", "duration")
| ("duration", "dayTimeDuration") | ("dayTimeDuration", "duration")
| ("duration", "yearMonthDuration") | ("yearMonthDuration", "duration"))
{
if let (Some(a), Some(b)) = (
parse_duration_split(&t.lexical),
parse_duration_split(&u.lexical),
) {
return a == b;
}
}
str_eq(&t.lexical, &u.lexical)
}
}
}
(Value::Typed(t), Value::Number(n)) | (Value::Number(n), Value::Typed(t)) => {
t.numeric.map(|a| a == n.as_f64())
.unwrap_or_else(|| t.lexical.trim().parse::<f64>().ok() == Some(n.as_f64()))
}
(Value::Typed(t), Value::String(s)) | (Value::String(s), Value::Typed(t)) => {
str_eq(&t.lexical, s)
}
(Value::IntRange { .. }, _) | (_, Value::IntRange { .. }) =>
unreachable!("IntRange normalised at values_eq entry"),
}
}
pub fn value_equality_key(v: &Value) -> Option<String> {
let t = match v { Value::Typed(t) => t, _ => return None };
match t.kind {
"date" | "dateTime" | "time" =>
dt_to_utc_seconds(&t.lexical, t.kind).map(|s| format!("{}#{s}", t.kind)),
"dayTimeDuration" =>
parse_day_time_duration_secs(&t.lexical).map(|s| format!("dtd#{s}")),
"yearMonthDuration" =>
parse_year_month_duration_months(&t.lexical).map(|m| format!("ymd#{m}")),
_ => None,
}
}
fn dt_to_utc_seconds(s: &str, kind: &str) -> Option<i64> {
let dk = match kind {
"date" => DateKind::Date,
"dateTime" => DateKind::DateTime,
"time" => DateKind::Time,
_ => return None,
};
let (y, mo, d, h, mi, sec, _frac, tz) = parse_xsd_date_time(s, dk)?;
let (yy, mm, dd) = if matches!(dk, DateKind::Time) {
(1970, 1, 1)
} else { (y, mo, d) };
let days = ymd_to_days(yy, mm as u32, dd as u32);
let secs_in_day = (h as i64) * 3600 + (mi as i64) * 60 + sec as i64;
let local_total = days * 86_400 + secs_in_day;
let tz_offset_secs = tz.map(|m| m as i64 * 60).unwrap_or(0);
Some(local_total - tz_offset_secs)
}
fn is_date_like_kind(k: &str) -> bool {
matches!(k, "date" | "dateTime" | "time"
| "gYear" | "gYearMonth" | "gMonth" | "gMonthDay" | "gDay")
}
fn is_duration_kind(k: &str) -> bool {
matches!(k, "duration" | "dayTimeDuration" | "yearMonthDuration")
}
fn parse_day_time_duration_secs(s: &str) -> Option<i64> {
let s = s.trim();
let (sign, body) = if let Some(rest) = s.strip_prefix('-') {
(-1i64, rest)
} else { (1i64, s) };
let body = body.strip_prefix('P')?;
let (day_part, time_part) = match body.find('T') {
Some(i) => (&body[..i], &body[i + 1..]),
None => (body, ""),
};
let parse_comp = |part: &str, marker: char| -> Option<i64> {
let i = match part.find(marker) { Some(i) => i, None => return Some(0) };
let start = part[..i].rfind(|c: char| !c.is_ascii_digit() && c != '.')
.map(|n| n + 1).unwrap_or(0);
Some(part[start..i].parse::<i64>().unwrap_or(0))
};
let days = parse_comp(day_part, 'D')?;
let hours = parse_comp(time_part, 'H')?;
let mins = parse_comp(time_part, 'M')?;
let secs = parse_comp(time_part, 'S')?;
Some(sign * (days * 86_400 + hours * 3600 + mins * 60 + secs))
}
fn duration_seq_total(items: &[Value]) -> Option<(&'static str, i64, i64)> {
let kind = match items.first()? {
Value::Typed(t) if matches!(t.kind, "dayTimeDuration" | "yearMonthDuration") => t.kind,
_ => return None,
};
let mut total: i64 = 0;
for v in items {
let t = match v {
Value::Typed(t) if t.kind == kind => t,
_ => return None,
};
total += if kind == "dayTimeDuration" {
parse_day_time_duration_secs(&t.lexical)?
} else {
parse_year_month_duration_months(&t.lexical)?
};
}
Some((kind, total, items.len() as i64))
}
fn duration_value(kind: &'static str, units: i64) -> Value {
let lexical = if kind == "dayTimeDuration" {
format_day_time_duration_secs(units)
} else {
format_year_month_duration_months(units)
};
Value::Typed(Box::new(TypedAtomic { kind, lexical, numeric: None, boolean: None, user_type: None }))
}
fn format_day_time_duration_secs(mut total: i64) -> String {
let mut out = String::with_capacity(16);
if total < 0 { out.push('-'); total = -total; }
out.push('P');
let days = total / 86_400;
let rem = total % 86_400;
if days > 0 { out.push_str(&days.to_string()); out.push('D'); }
if rem > 0 || days == 0 {
out.push('T');
let h = rem / 3600;
let m = (rem % 3600) / 60;
let s = rem % 60;
if h > 0 { out.push_str(&h.to_string()); out.push('H'); }
if m > 0 { out.push_str(&m.to_string()); out.push('M'); }
if s > 0 || (h == 0 && m == 0) { out.push_str(&s.to_string()); out.push('S'); }
}
out
}
fn parse_xsd_date_only(s: &str) -> Option<(i32, u32, u32, Option<i16>)> {
let s = s.trim();
let (sign, body) = if let Some(rest) = s.strip_prefix('-') {
(-1i32, rest)
} else { (1i32, s) };
let parts: Vec<&str> = body.splitn(3, '-').collect();
if parts.len() != 3 { return None; }
let y: i32 = parts[0].parse().ok()?;
let m: u32 = parts[1].parse().ok()?;
let day_tail = parts[2];
let (d_str, tz_tail) = if day_tail.len() >= 2 {
(&day_tail[..2], &day_tail[2..])
} else { return None; };
let d: u32 = d_str.parse().ok()?;
let signed_year = sign * y;
if !(1..=12).contains(&m) { return None; }
let max_day = days_in_month(signed_year, m);
if !(1..=max_day).contains(&d) { return None; }
let tz = parse_tz_suffix(tz_tail);
Some((signed_year, m, d, tz))
}
fn ymd_to_days(y: i32, m: u32, d: u32) -> i64 {
let y = if m <= 2 { y - 1 } else { y } as i64;
let era = if y >= 0 { y } else { y - 399 } / 400;
let yoe = (y - era * 400) as u64;
let m = m as u64;
let d = d as u64;
let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + d - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
era * 146097 + doe as i64 - 719468
}
fn coerce_to_double(v: &Value) -> Option<f64> {
match v {
Value::Number(n) => Some(n.as_f64()),
Value::Boolean(b) => Some(if *b { 1.0 } else { 0.0 }),
Value::Typed(t) => t.numeric.or_else(|| t.lexical.trim().parse().ok()),
Value::String(s) => s.trim().parse().ok(),
Value::NodeSet(ns) if ns.len() == 1 => {
let _ = ns;
None
}
_ => None,
}
}
fn parse_duration_split(s: &str) -> Option<(i64, i64)> {
let s = s.trim();
let (sign, body) = if let Some(rest) = s.strip_prefix('-') {
(-1i64, rest)
} else { (1i64, s) };
let body = body.strip_prefix('P')?;
let (date_part, time_part) = match body.find('T') {
Some(i) => (&body[..i], &body[i + 1..]),
None => (body, ""),
};
let mut years: i64 = 0;
let mut months: i64 = 0;
let mut days: i64 = 0;
let mut cur = String::new();
for c in date_part.chars() {
if c.is_ascii_digit() { cur.push(c); }
else {
let n: i64 = cur.parse().ok()?;
cur.clear();
match c {
'Y' => years = n,
'M' => months = n,
'D' => days = n,
_ => return None,
}
}
}
if !cur.is_empty() { return None; }
let mut hours: i64 = 0;
let mut mins: i64 = 0;
let mut secs: i64 = 0;
cur.clear();
let mut frac: f64 = 0.0;
let mut in_frac = false;
let mut frac_str = String::new();
for c in time_part.chars() {
if c.is_ascii_digit() {
if in_frac { frac_str.push(c); }
else { cur.push(c); }
} else if c == '.' {
in_frac = true;
} else {
let n: i64 = cur.parse().ok()?;
cur.clear();
if !frac_str.is_empty() {
let denom = 10f64.powi(frac_str.len() as i32);
frac = frac_str.parse::<f64>().unwrap_or(0.0) / denom;
frac_str.clear();
}
in_frac = false;
match c {
'H' => hours = n,
'M' => mins = n,
'S' => secs = n + frac.round() as i64,
_ => return None,
}
}
}
if !cur.is_empty() { return None; }
Some((sign * (years * 12 + months),
sign * (days * 86_400 + hours * 3600 + mins * 60 + secs)))
}
fn parse_year_month_duration_months(s: &str) -> Option<i64> {
let (neg, rest) = match s.strip_prefix('-') {
Some(r) => (true, r),
None => (false, s),
};
let rest = rest.strip_prefix('P')?;
let mut years: i64 = 0;
let mut months: i64 = 0;
let mut cur = String::new();
let mut any_field = false;
for c in rest.chars() {
if c.is_ascii_digit() {
cur.push(c);
} else if c == 'Y' {
years = cur.parse().ok()?;
cur.clear();
any_field = true;
} else if c == 'M' {
months = cur.parse().ok()?;
cur.clear();
any_field = true;
} else {
return None;
}
}
if !any_field || !cur.is_empty() { return None; }
let total = years * 12 + months;
Some(if neg { -total } else { total })
}
fn format_year_month_duration_months(months: i64) -> String {
if months == 0 { return "P0M".into(); }
let neg = months < 0;
let abs = months.unsigned_abs() as u64;
let years = abs / 12;
let months = abs % 12;
let mut out = String::new();
if neg { out.push('-'); }
out.push('P');
if years > 0 { out.push_str(&format!("{years}Y")); }
if months > 0 { out.push_str(&format!("{months}M")); }
if out.ends_with('P') {
out.push_str("0M");
}
out
}
fn round_months_half_up(x: f64) -> i64 {
(x + 0.5).floor() as i64
}
fn duration_mul<I: DocIndexLike>(
l: &Value, r: &Value, idx: &I, bindings: &dyn XPathBindings,
) -> Option<Value> {
let (dur, num) = match (l, r) {
(Value::Typed(d), other) if is_duration_kind(d.kind) => (d, other),
(other, Value::Typed(d)) if is_duration_kind(d.kind) => (d, other),
_ => return None,
};
let factor = coerce_to_double(num)
.unwrap_or_else(|| value_to_number_with(num, idx, bindings));
if !factor.is_finite() { return None; }
if dur.kind == "yearMonthDuration" {
let months = parse_year_month_duration_months(&dur.lexical)?;
let scaled = round_months_half_up(months as f64 * factor);
return Some(Value::Typed(Box::new(TypedAtomic {
kind: "yearMonthDuration",
lexical: format_year_month_duration_months(scaled),
numeric: None, boolean: None, user_type: None,
})));
}
let us = parse_day_time_duration_micros(&dur.lexical)?;
let scaled = (us as f64 * factor).round() as i64;
Some(Value::Typed(Box::new(TypedAtomic {
kind: "dayTimeDuration",
lexical: canonical_day_time_duration_lex(&format_day_time_duration_micros(scaled)),
numeric: None, boolean: None, user_type: None,
})))
}
fn duration_div<I: DocIndexLike>(
l: &Value, r: &Value, idx: &I, bindings: &dyn XPathBindings,
) -> Option<Value> {
match (l, r) {
(Value::Typed(a), Value::Typed(b))
if is_duration_kind(a.kind) && is_duration_kind(b.kind) =>
{
let (av, bv): (f64, f64) = if a.kind == "yearMonthDuration" {
let am = parse_year_month_duration_months(&a.lexical)?;
let bm = parse_year_month_duration_months(&b.lexical)?;
(am as f64, bm as f64)
} else {
let av = parse_day_time_duration_micros(&a.lexical)?;
let bv = parse_day_time_duration_micros(&b.lexical)?;
(av as f64, bv as f64)
};
if bv == 0.0 { return None; }
Some(Value::Number(Numeric::Double(av / bv)))
}
(Value::Typed(d), other) if is_duration_kind(d.kind) => {
let factor = coerce_to_double(other)
.unwrap_or_else(|| value_to_number_with(other, idx, bindings));
if !factor.is_finite() || factor == 0.0 { return None; }
if d.kind == "yearMonthDuration" {
let months = parse_year_month_duration_months(&d.lexical)?;
let scaled = round_months_half_up(months as f64 / factor);
return Some(Value::Typed(Box::new(TypedAtomic {
kind: "yearMonthDuration",
lexical: format_year_month_duration_months(scaled),
numeric: None, boolean: None, user_type: None,
})));
}
let us = parse_day_time_duration_micros(&d.lexical)?;
let scaled = (us as f64 / factor).round() as i64;
Some(Value::Typed(Box::new(TypedAtomic {
kind: "dayTimeDuration",
lexical: canonical_day_time_duration_lex(&format_day_time_duration_micros(scaled)),
numeric: None, boolean: None, user_type: None,
})))
}
_ => None,
}
}
fn add_months_to_ymd(y: i32, m: u32, d: u32, months: i64) -> (i32, u32, u32) {
let total_months = (y as i64) * 12 + (m as i64) - 1 + months;
let ny = total_months.div_euclid(12) as i32;
let nm = total_months.rem_euclid(12) as u32 + 1;
let last = days_in_month(ny, nm);
let nd = d.min(last);
(ny, nm, nd)
}
fn days_in_month(y: i32, m: u32) -> u32 {
match m {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 => if is_leap_year(y) { 29 } else { 28 },
_ => 0,
}
}
fn is_leap_year(y: i32) -> bool {
(y % 4 == 0 && y % 100 != 0) || y % 400 == 0
}
fn add_one_day(year: i32, month: u8, day: u8) -> (i32, u8, u8) {
if (day as u32) < days_in_month(year, month as u32) {
(year, month, day + 1)
} else if month < 12 {
(year, month + 1, 1)
} else {
(year + 1, 1, 1)
}
}
fn duration_combine(a: &TypedAtomic, b: &TypedAtomic, subtract: bool) -> Option<Value> {
let mk = |kind: &'static str, lexical: String| {
Value::Typed(Box::new(TypedAtomic { kind, lexical, numeric: None, boolean: None, user_type: None }))
};
match (a.kind, b.kind) {
("yearMonthDuration", "yearMonthDuration") => {
let lm = parse_year_month_duration_months(&a.lexical)?;
let rm = parse_year_month_duration_months(&b.lexical)?;
let m = if subtract { lm.checked_sub(rm) } else { lm.checked_add(rm) }?;
Some(mk("yearMonthDuration", format_year_month_duration_months(m)))
}
("dayTimeDuration", "dayTimeDuration") => {
let lu = parse_day_time_duration_micros(&a.lexical)?;
let ru = parse_day_time_duration_micros(&b.lexical)?;
let u = if subtract { lu.checked_sub(ru) } else { lu.checked_add(ru) }?;
let lex = canonical_day_time_duration_lex(
&format_day_time_duration_micros(i64::try_from(u).ok()?));
Some(mk("dayTimeDuration", lex))
}
_ => None,
}
}
fn date_arith_add(l: &Value, r: &Value) -> Option<Value> {
let (date, dur, date_kind) = match (l, r) {
(Value::Typed(a), Value::Typed(b))
if is_date_like_kind(a.kind) && is_duration_kind(b.kind)
=> (a, b, a.kind),
(Value::Typed(a), Value::Typed(b))
if is_duration_kind(a.kind) && is_date_like_kind(b.kind)
=> (b, a, b.kind),
(Value::Typed(a), Value::Typed(b))
if is_duration_kind(a.kind) && is_duration_kind(b.kind) =>
{
return duration_combine(a, b, false);
}
_ => return None,
};
match date_kind {
"date" => {
let (y, m, d, _tz) = parse_xsd_date_only(&date.lexical)?;
if dur.kind == "yearMonthDuration" {
let months = parse_year_month_duration_months(&dur.lexical)?;
let (ny, nm, nd) = add_months_to_ymd(y, m, d, months);
let lex = format!("{:04}-{:02}-{:02}", ny, nm, nd);
return Some(Value::Typed(Box::new(TypedAtomic {
kind: "date", lexical: lex, numeric: None, boolean: None, user_type: None,
})));
}
let sec = parse_day_time_duration_secs(&dur.lexical)?;
let day_delta = sec / 86_400;
let new_days = ymd_to_days(y, m, d) + day_delta;
let (ny, nm, nd) = days_to_ymd(new_days);
let lex = format!("{:04}-{:02}-{:02}", ny, nm, nd);
Some(Value::Typed(Box::new(TypedAtomic {
kind: "date", lexical: lex, numeric: None, boolean: None, user_type: None,
})))
}
"time" => {
if dur.kind == "yearMonthDuration" { return None; }
let dur_us = parse_day_time_duration_micros(&dur.lexical)?;
let (h, m, s, time_frac, tz) = parse_xsd_time(&date.lexical)?;
let day_us = ((h as i128) * 3600 + (m as i128) * 60 + s as i128)
* 1_000_000 + time_frac as i128;
let total_us = (day_us + dur_us).rem_euclid(86_400i128 * 1_000_000);
let total = (total_us / 1_000_000) as i64;
let frac = (total_us % 1_000_000) as u32;
let nh = (total / 3600) as u8;
let nm = ((total / 60) % 60) as u8;
let ns = (total % 60) as u8;
let lex = if frac == 0 {
let mut l = format!("{:02}:{:02}:{:02}", nh, nm, ns);
if let Some(tz_m) = tz { l.push_str(&format_tz_suffix(tz_m)); }
l
} else {
let mut l = format!("{:02}:{:02}:{:02}.{:06}", nh, nm, ns, frac);
while l.ends_with('0') { l.pop(); }
if let Some(tz_m) = tz { l.push_str(&format_tz_suffix(tz_m)); }
l
};
Some(Value::Typed(Box::new(TypedAtomic {
kind: "time", lexical: lex, numeric: None, boolean: None, user_type: None,
})))
}
"dateTime" => {
if dur.kind == "yearMonthDuration" {
let months = parse_year_month_duration_months(&dur.lexical)?;
let (y, mo, d, h, mi, s, frac, tz) =
parse_xsd_date_time(&date.lexical, DateKind::DateTime)?;
let (ny, nm, nd) = add_months_to_ymd(y, mo as u32, d as u32, months);
let lex = format_datetime_lexical(ny, nm as u8, nd as u8, h, mi, s, frac, tz);
return Some(Value::Typed(Box::new(TypedAtomic {
kind: "dateTime", lexical: lex, numeric: None, boolean: None, user_type: None,
})));
}
let dur_us = parse_day_time_duration_micros(&dur.lexical)?;
let (y, mo, d, h, mi, s, frac, tz) =
parse_xsd_date_time(&date.lexical, DateKind::DateTime)?;
let day_us = ((h as i128) * 3600 + (mi as i128) * 60 + s as i128)
* 1_000_000 + (frac as i128);
let total = day_us + dur_us;
let us_per_day = 86_400i128 * 1_000_000;
let day_delta = total.div_euclid(us_per_day);
let remain = total.rem_euclid(us_per_day);
let new_days = ymd_to_days(y, mo as u32, d as u32) + day_delta as i64;
let (ny, nm, nd) = days_to_ymd(new_days);
let remain_secs = (remain / 1_000_000) as i64;
let new_frac = (remain % 1_000_000) as u32;
let nh = (remain_secs / 3600) as u8;
let nmi = ((remain_secs / 60) % 60) as u8;
let ns = (remain_secs % 60) as u8;
let lex = format_datetime_lexical(ny, nm as u8, nd as u8, nh, nmi, ns, new_frac, tz);
Some(Value::Typed(Box::new(TypedAtomic {
kind: "dateTime", lexical: lex, numeric: None, boolean: None, user_type: None,
})))
}
_ => None,
}
}
fn format_datetime_lexical(
y: i32, mo: u8, d: u8, h: u8, mi: u8, s: u8, frac_us: u32, tz: Option<i16>,
) -> String {
let mut out = format!("{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
y, mo, d, h, mi, s);
if frac_us != 0 {
let mut frac = format!(".{:06}", frac_us);
while frac.ends_with('0') { frac.pop(); }
out.push_str(&frac);
}
if let Some(tz) = tz {
out.push_str(&format_tz_suffix(tz));
}
out
}
fn format_tz_suffix(minutes: i16) -> String {
if minutes == 0 { return "Z".into(); }
let sign = if minutes < 0 { '-' } else { '+' };
let abs = minutes.unsigned_abs() as i32;
format!("{sign}{:02}:{:02}", abs / 60, abs % 60)
}
fn date_arith_sub(l: &Value, r: &Value) -> Option<Value> {
match (l, r) {
(Value::Typed(a), Value::Typed(b))
if a.kind == "date" && b.kind == "date" =>
{
let (ay, am, ad, _) = parse_xsd_date_only(&a.lexical)?;
let (by, bm, bd, _) = parse_xsd_date_only(&b.lexical)?;
let diff_days = ymd_to_days(ay, am, ad) - ymd_to_days(by, bm, bd);
let lex = format_day_time_duration_secs(diff_days * 86_400);
Some(Value::Typed(Box::new(TypedAtomic {
kind: "dayTimeDuration", lexical: lex,
numeric: None, boolean: None, user_type: None,
})))
}
(Value::Typed(a), Value::Typed(b))
if a.kind == "date" && is_duration_kind(b.kind) =>
{
let (y, m, d, _) = parse_xsd_date_only(&a.lexical)?;
let sec = parse_day_time_duration_secs(&b.lexical)?;
let day_delta = sec / 86_400;
let new_days = ymd_to_days(y, m, d) - day_delta;
let (ny, nm, nd) = days_to_ymd(new_days);
let lex = format!("{:04}-{:02}-{:02}", ny, nm, nd);
Some(Value::Typed(Box::new(TypedAtomic {
kind: "date", lexical: lex, numeric: None, boolean: None, user_type: None,
})))
}
(Value::Typed(a), Value::Typed(b))
if matches!(a.kind, "dateTime" | "time") && is_duration_kind(b.kind) =>
{
let negated = TypedAtomic {
kind: b.kind,
lexical: negate_duration_lex(&b.lexical),
numeric: None,
boolean: None, user_type: None,
};
return date_arith_add(l, &Value::Typed(Box::new(negated)));
}
(Value::Typed(a), Value::Typed(b))
if matches!(a.kind, "dateTime" | "time")
&& matches!(b.kind, "dateTime" | "time") && a.kind == b.kind =>
{
let dk = if a.kind == "dateTime" { DateKind::DateTime } else { DateKind::Time };
let a_us = date_value_to_utc_micros(&a.lexical, dk)?;
let b_us = date_value_to_utc_micros(&b.lexical, dk)?;
let diff_us = (a_us - b_us) as i64;
let lex = canonical_day_time_duration_lex(
&format_day_time_duration_micros(diff_us)
);
Some(Value::Typed(Box::new(TypedAtomic {
kind: "dayTimeDuration", lexical: lex,
numeric: None, boolean: None, user_type: None,
})))
}
(Value::Typed(a), Value::Typed(b))
if is_duration_kind(a.kind) && is_duration_kind(b.kind) =>
{
duration_combine(a, b, true)
}
_ => None,
}
}
#[derive(Clone, Copy)]
#[allow(dead_code)]
enum NumericOp { Add, Sub, Mul, Div, Mod, IDiv }
fn numeric_promote_kind(a: Option<&str>, b: Option<&str>) -> Option<&'static str> {
fn rank(k: &str) -> Option<u8> {
Some(match k {
"integer" | "long" | "int" | "short" | "byte"
| "unsignedLong" | "unsignedInt" | "unsignedShort" | "unsignedByte"
| "nonNegativeInteger" | "nonPositiveInteger"
| "positiveInteger" | "negativeInteger" => 0, "decimal" => 1,
"float" => 2,
"double" => 3,
_ => return None,
})
}
let ra = a.and_then(rank);
let rb = b.and_then(rank);
let r = ra.max(rb)?;
Some(match r {
0 => "integer",
1 => "decimal",
2 => "float",
_ => "double",
})
}
fn integer_result(n: i64, bindings: &dyn XPathBindings) -> Value {
if bindings.xpath_version_2_or_later() {
Value::Number(Numeric::Integer(n))
} else {
Value::Number(Numeric::Double(n as f64))
}
}
fn numeric_kind_of(v: &Value) -> Option<&'static str> {
match v {
Value::Number(n) => Some(n.kind()),
Value::Typed(t) if t.kind == "untypedAtomic" => Some("double"),
Value::Typed(t) if t.numeric.is_some() => Some(t.kind),
Value::IntRange { .. } => Some("integer"),
_ => None,
}
}
fn integer_decimal_zero_divisor<I: DocIndexLike>(
l: &Value, r: &Value, idx: &I, bindings: &dyn XPathBindings,
) -> bool {
if in_xpath_1_0_compat() { return false; }
if value_to_number_with(r, idx, bindings) != 0.0 { return false; }
let is_int_dec = |k| matches!(k, Some("integer") | Some("decimal"));
is_int_dec(numeric_kind_of(l)) && is_int_dec(numeric_kind_of(r))
}
fn value_atomizes_empty(v: &Value) -> bool {
match v {
Value::NodeSet(n) => n.is_empty(),
Value::ForeignNodeSet(n) => n.is_empty(),
Value::Sequence(s) => s.is_empty(),
_ => false,
}
}
fn arith_empty_2_0(l: &Value, r: &Value, ctx: &EvalCtx) -> bool {
ctx.static_ctx.xpath_2_0
&& !in_xpath_1_0_compat()
&& (value_atomizes_empty(l) || value_atomizes_empty(r))
}
fn compat_numeric_op<I: DocIndexLike>(
l: &Value, r: &Value, idx: &I, bindings: &dyn XPathBindings, op: NumericOp,
) -> Value {
if in_xpath_1_0_compat() {
let a = value_to_number_with(l, idx, bindings);
let b = value_to_number_with(r, idx, bindings);
let result = match op {
NumericOp::Add => a + b,
NumericOp::Sub => a - b,
NumericOp::Mul => a * b,
NumericOp::Div => a / b,
NumericOp::Mod => a % b,
NumericOp::IDiv => (a / b).trunc(),
};
return Value::Number(Numeric::Double(result));
}
typed_numeric_op(l, r, idx, bindings, op)
}
fn reject_string_vs_numeric_cmp_2_0(
l: &Value, r: &Value, ctx: &EvalCtx, op: &str,
) -> Result<()> {
if !ctx.static_ctx.xpath_2_0 || in_xpath_1_0_compat() {
return Ok(());
}
let is_str = |v: &Value| matches!(v, Value::String(_));
let is_num = |v: &Value| matches!(v, Value::Number(_))
|| matches!(v, Value::Typed(t) if t.numeric.is_some()
&& !matches!(t.kind, "untypedAtomic"));
if (is_str(l) && is_num(r)) || (is_num(l) && is_str(r)) {
return Err(xpath_err(format!(
"general comparison '{op}' between an xs:string and a \
numeric value (XPTY0004)"
)).with_xpath_code("XPTY0004"));
}
Ok(())
}
fn reject_string_arith_2_0(
l: &Value, r: &Value, ctx: &EvalCtx, op: &str,
) -> Result<()> {
if !ctx.static_ctx.xpath_2_0 || in_xpath_1_0_compat() {
return Ok(());
}
for v in [l, r] {
if let Value::String(s) = v {
return Err(xpath_err(format!(
"arithmetic '{op}' on an xs:string operand '{s}' (XPTY0004)"
)).with_xpath_code("XPTY0004"));
}
}
Ok(())
}
fn exact_decimal(v: &Value) -> Option<rust_decimal::Decimal> {
match v {
Value::Number(Numeric::Integer(i)) => Some(rust_decimal::Decimal::from(*i)),
Value::Number(Numeric::Decimal(d)) => Some(*d),
Value::Typed(t) if matches!(t.kind,
"integer" | "decimal"
| "int" | "long" | "short" | "byte"
| "unsignedInt" | "unsignedLong" | "unsignedShort" | "unsignedByte"
| "nonNegativeInteger" | "nonPositiveInteger"
| "positiveInteger" | "negativeInteger"
) => t.lexical.parse().ok(),
_ => None,
}
}
fn typed_numeric_op<I: DocIndexLike>(
l: &Value, r: &Value, idx: &I, bindings: &dyn XPathBindings, op: NumericOp,
) -> Value {
if let (Some(da), Some(db)) = (exact_decimal(l), exact_decimal(r)) {
use rust_decimal::prelude::ToPrimitive;
let zero = db.is_zero();
let exact = match op {
NumericOp::Add => da.checked_add(db),
NumericOp::Sub => da.checked_sub(db),
NumericOp::Mul => da.checked_mul(db),
NumericOp::Div if zero => None,
NumericOp::Div => da.checked_div(db),
NumericOp::Mod if zero => None,
NumericOp::Mod => da.checked_rem(db),
NumericOp::IDiv if zero => None,
NumericOp::IDiv => da.checked_div(db).map(|q| q.trunc()),
};
if let Some(d) = exact {
let both_integer = matches!(
(l, r),
(Value::Number(Numeric::Integer(_)), Value::Number(Numeric::Integer(_)))
);
return match op {
NumericOp::IDiv => match d.to_i64() {
Some(i) => Value::Number(Numeric::Integer(i)),
None => Value::Number(Numeric::Decimal(d)),
},
NumericOp::Div => Value::Number(Numeric::Decimal(d)),
_ if both_integer => match d.to_i64() {
Some(i) => Value::Number(Numeric::Integer(i)),
None => Value::Number(Numeric::Decimal(d)),
},
_ => Value::Number(Numeric::Decimal(d)),
};
}
}
let a = value_to_number_with(l, idx, bindings);
let b = value_to_number_with(r, idx, bindings);
let result = match op {
NumericOp::Add => a + b,
NumericOp::Sub => a - b,
NumericOp::Mul => a * b,
NumericOp::Div => a / b,
NumericOp::Mod => a % b,
NumericOp::IDiv => (a / b).trunc(),
};
if let (Value::Number(la), Value::Number(rb)) = (l, r) {
let rank = match op {
NumericOp::IDiv => 0,
NumericOp::Div => la.rank().max(rb.rank()).max(1),
_ => la.rank().max(rb.rank()),
};
return Value::Number(Numeric::from_rank(rank, result));
}
match numeric_promote_kind(numeric_kind_of(l), numeric_kind_of(r)) {
Some(mut kind) => {
if matches!(op, NumericOp::IDiv) {
kind = "integer";
} else if matches!(op, NumericOp::Div) && kind == "integer" {
kind = "decimal";
}
Value::Number(Numeric::of_kind(kind, result))
}
None => Value::Number(Numeric::Double(result)),
}
}
#[allow(dead_code)]
fn arith<I: DocIndexLike>(
l: &Expr,
r: &Expr,
ctx: &EvalCtx,
idx: &I,
op: impl Fn(f64, f64) -> f64,
) -> Result<Value> {
let lv = eval_expr(l, ctx, idx)?;
let rv = eval_expr(r, ctx, idx)?;
Ok(Value::Number(Numeric::Double(op(
value_to_number_with(&lv, idx, ctx.bindings),
value_to_number_with(&rv, idx, ctx.bindings),
))))
}
fn cmp_op<I: DocIndexLike>(
l: &Expr,
r: &Expr,
ctx: &EvalCtx,
idx: &I,
op: impl Fn(f64, f64) -> bool,
) -> Result<Value> {
let lv = eval_expr(l, ctx, idx)?;
let rv = eval_expr(r, ctx, idx)?;
reject_string_vs_numeric_cmp_2_0(&lv, &rv, ctx, "<=>")?;
let node_to_number = |id: NodeId| -> f64 {
idx.string_value(id).trim().parse::<f64>().unwrap_or(f64::NAN)
};
let foreign_to_number = |p: ForeignNodePtr| -> f64 {
ctx.bindings.foreign_string_value(p)
.trim().parse::<f64>().unwrap_or(f64::NAN)
};
match (&lv, &rv) {
(Value::NodeSet(ns), other) | (other, Value::NodeSet(ns))
if !matches!(other, Value::NodeSet(_) | Value::ForeignNodeSet(_)) =>
{
let n_other = value_to_number_with(other, idx, ctx.bindings);
let any = ns.iter().any(|&id| {
let node_n = node_to_number(id);
if matches!(lv, Value::NodeSet(_)) { op(node_n, n_other) }
else { op(n_other, node_n) }
});
return Ok(Value::Boolean(any));
}
(Value::ForeignNodeSet(fs), other) | (other, Value::ForeignNodeSet(fs))
if !matches!(other, Value::NodeSet(_) | Value::ForeignNodeSet(_)) =>
{
let n_other = value_to_number_with(other, idx, ctx.bindings);
let any = fs.iter().any(|&p| {
let node_n = foreign_to_number(p);
if matches!(lv, Value::ForeignNodeSet(_)) { op(node_n, n_other) }
else { op(n_other, node_n) }
});
return Ok(Value::Boolean(any));
}
(Value::NodeSet(ls), Value::NodeSet(rs)) => {
let l_nums: Vec<f64> = ls.iter().map(|&id| node_to_number(id)).collect();
let r_nums: Vec<f64> = rs.iter().map(|&id| node_to_number(id)).collect();
let any = l_nums.iter().any(|&a| r_nums.iter().any(|&b| op(a, b)));
return Ok(Value::Boolean(any));
}
_ => {}
}
if let (Value::Typed(t), Value::Typed(u)) = (&lv, &rv) {
if t.kind == u.kind {
if matches!(t.kind, "date" | "dateTime" | "time") {
if let (Some(a), Some(b)) = (
dt_to_utc_seconds(&t.lexical, t.kind),
dt_to_utc_seconds(&u.lexical, u.kind),
) {
return Ok(Value::Boolean(op(a as f64, b as f64)));
}
}
if t.kind == "dayTimeDuration" {
if let (Some(a), Some(b)) = (
parse_day_time_duration_secs(&t.lexical),
parse_day_time_duration_secs(&u.lexical),
) {
return Ok(Value::Boolean(op(a as f64, b as f64)));
}
}
if t.kind == "yearMonthDuration" {
if let (Some(a), Some(b)) = (
parse_year_month_duration_months(&t.lexical),
parse_year_month_duration_months(&u.lexical),
) {
return Ok(Value::Boolean(op(a as f64, b as f64)));
}
}
}
}
let ln = value_to_number_with(&lv, idx, ctx.bindings);
let rn = value_to_number_with(&rv, idx, ctx.bindings);
Ok(Value::Boolean(op(ln, rn)))
}
fn dyn_evaluate<I: DocIndexLike>(
args: &[Value], ctx: &EvalCtx<'_>, idx: &I,
) -> Result<Value> {
if args.len() != 1 {
return Err(xpath_err("dyn:evaluate takes 1 argument"));
}
let src = value_to_string(&args[0], idx);
let opts = super::XPathOptions {
libxml2_compatible: ctx.static_ctx.libxml2_compatible,
..super::XPathOptions::default()
};
let expr = match super::parse_xpath_with(&src, &opts) {
Ok(e) => e,
Err(_) => return Ok(Value::NodeSet(Vec::new())),
};
match eval_expr(&expr, ctx, idx) {
Ok(v) => Ok(v),
Err(_) => Ok(Value::NodeSet(Vec::new())),
}
}
fn eval_map_function<I: DocIndexLike>(
local: &str, args: &[Value], ctx: &EvalCtx<'_>, idx: &I,
) -> Result<Value> {
let as_map = |v: &Value| -> Result<Vec<(Value, Value)>> {
match v {
Value::Map(m) => Ok((**m).clone()),
_ => Err(xpath_err(format!("map:{local}: expected a map argument"))),
}
};
let empty = || Value::NodeSet(Vec::new());
match local {
"size" => Ok(Value::Number(Numeric::Integer(as_map(&args[0])?.len() as i64))),
"contains" => {
let m = as_map(&args[0])?;
let k = first_atomic_key(&args[1], idx);
Ok(Value::Boolean(m.iter().any(|(mk, _)| map_key_eq(mk, &k, idx))))
}
"get" => {
let m = as_map(&args[0])?;
let k = first_atomic_key(&args[1], idx);
Ok(m.into_iter().find(|(mk, _)| map_key_eq(mk, &k, idx))
.map(|(_, v)| v).unwrap_or_else(empty))
}
"keys" => Ok(seq_from_items(
as_map(&args[0])?.into_iter().map(|(k, _)| k).collect())),
"put" => {
let mut m = as_map(&args[0])?;
let k = first_atomic_key(&args[1], idx);
m.retain(|(mk, _)| !map_key_eq(mk, &k, idx));
m.push((k, args[2].clone()));
Ok(Value::Map(Box::new(m)))
}
"remove" => {
let mut m = as_map(&args[0])?;
let ks = items_of(&args[1]);
m.retain(|(mk, _)| !ks.iter().any(|k| map_key_eq(mk, k, idx)));
Ok(Value::Map(Box::new(m)))
}
"entry" => Ok(Value::Map(Box::new(vec![
(first_atomic_key(&args[0], idx), args[1].clone())]))),
"merge" => {
let use_last = match args.get(1) {
Some(Value::Map(opts)) => opts.iter()
.find(|(k, _)| value_to_string(k, idx) == "duplicates")
.map(|(_, v)| value_to_string(v, idx) == "use-last")
.unwrap_or(false),
_ => false,
};
let mut out: Vec<(Value, Value)> = Vec::new();
for item in items_of(&args[0]) {
if let Value::Map(m) = item {
for (k, v) in m.iter() {
match out.iter().position(|(ok, _)| map_key_eq(ok, k, idx)) {
Some(p) if use_last => out[p].1 = v.clone(),
Some(_) => {} None => out.push((k.clone(), v.clone())),
}
}
}
}
Ok(Value::Map(Box::new(out)))
}
"for-each" => {
let m = as_map(&args[0])?;
let f = value_as_function(&args[1])?;
let mut out = Vec::new();
for (k, v) in m {
out.push(call_function_item(f, vec![k, v], ctx, idx)?);
}
Ok(seq_from_items(out))
}
"find" => {
let key = first_atomic_key(&args[1], idx);
let mut found = Vec::new();
fn search<I: DocIndexLike>(
v: &Value, key: &Value, idx: &I, out: &mut Vec<Value>,
) {
for item in items_of(v) {
match item {
Value::Map(m) => {
for (k, val) in m.iter() {
if map_key_eq(k, key, idx) { out.push(val.clone()); }
search(val, key, idx, out);
}
}
Value::Array(a) => for member in a.iter() {
search(member, key, idx, out);
},
_ => {}
}
}
}
search(&args[0], &key, idx, &mut found);
Ok(Value::Array(Box::new(found)))
}
_ => Err(xpath_err(format!(
"map:{local} is not supported in this build"))),
}
}
fn eval_array_function<I: DocIndexLike>(
local: &str, args: &[Value], ctx: &EvalCtx<'_>, idx: &I,
) -> Result<Value> {
let as_array = |v: &Value| -> Result<Vec<Value>> {
match v {
Value::Array(a) => Ok((**a).clone()),
_ => Err(xpath_err(format!("array:{local}: expected an array argument"))),
}
};
match local {
"size" => Ok(Value::Number(Numeric::Integer(as_array(&args[0])?.len() as i64))),
"get" => {
let a = as_array(&args[0])?;
let pos = value_to_number(&args[1], idx);
if pos.fract() == 0.0 && pos >= 1.0 && (pos as usize) <= a.len() {
Ok(a[pos as usize - 1].clone())
} else {
Err(xpath_err("array:get: index out of bounds (FOAY0001)"))
}
}
"append" => {
let mut a = as_array(&args[0])?;
a.push(args[1].clone());
Ok(Value::Array(Box::new(a)))
}
"head" => {
let a = as_array(&args[0])?;
a.into_iter().next().ok_or_else(|| xpath_err("array:head: empty array (FOAY0001)"))
}
"tail" => {
let mut a = as_array(&args[0])?;
if a.is_empty() { return Err(xpath_err("array:tail: empty array (FOAY0001)")); }
a.remove(0);
Ok(Value::Array(Box::new(a)))
}
"reverse" => {
let mut a = as_array(&args[0])?;
a.reverse();
Ok(Value::Array(Box::new(a)))
}
"subarray" => {
let a = as_array(&args[0])?;
let start = value_to_number(&args[1], idx).round() as i64;
let len = match args.get(2) {
Some(v) => value_to_number(v, idx).round() as i64,
None => a.len() as i64 - start + 1,
};
let s = (start - 1).max(0) as usize;
let e = ((start - 1 + len).max(0) as usize).min(a.len());
Ok(Value::Array(Box::new(a.get(s..e).map(<[_]>::to_vec).unwrap_or_default())))
}
"join" => {
let mut out = Vec::new();
for item in items_of(&args[0]) {
if let Value::Array(a) = item { out.extend((*a).clone()); }
}
Ok(Value::Array(Box::new(out)))
}
"flatten" => {
fn flat(v: &Value, out: &mut Vec<Value>) {
for item in items_of(v) {
match item {
Value::Array(a) => for m in a.iter() { flat(m, out); },
other => out.push(other),
}
}
}
let mut out = Vec::new();
flat(&args[0], &mut out);
Ok(seq_from_items(out))
}
"for-each" => {
let a = as_array(&args[0])?;
let f = value_as_function(&args[1])?;
let mut out = Vec::with_capacity(a.len());
for m in a {
out.push(call_function_item(f, vec![m], ctx, idx)?);
}
Ok(Value::Array(Box::new(out)))
}
"filter" => {
let a = as_array(&args[0])?;
let f = value_as_function(&args[1])?;
let mut out = Vec::new();
for m in a {
let keep = call_function_item(f, vec![m.clone()], ctx, idx)?;
if value_to_bool(&keep, idx) { out.push(m); }
}
Ok(Value::Array(Box::new(out)))
}
"fold-left" => {
let a = as_array(&args[0])?;
let f = value_as_function(&args[2])?;
let mut acc = args[1].clone();
for m in a {
acc = call_function_item(f, vec![acc, m], ctx, idx)?;
}
Ok(acc)
}
"fold-right" => {
let a = as_array(&args[0])?;
let f = value_as_function(&args[2])?;
let mut acc = args[1].clone();
for m in a.into_iter().rev() {
acc = call_function_item(f, vec![m, acc], ctx, idx)?;
}
Ok(acc)
}
"for-each-pair" => {
let a = as_array(&args[0])?;
let b = as_array(&args[1])?;
let f = value_as_function(&args[2])?;
let mut out = Vec::new();
for (x, y) in a.into_iter().zip(b) {
out.push(call_function_item(f, vec![x, y], ctx, idx)?);
}
Ok(Value::Array(Box::new(out)))
}
"sort" => {
let a = as_array(&args[0])?;
let keyfn = match args.get(2) {
Some(v) => Some(value_as_function(v)?),
None => None,
};
let mut keyed: Vec<(Value, Value)> = Vec::with_capacity(a.len());
for m in a {
let k = match keyfn {
Some(f) => call_function_item(f, vec![m.clone()], ctx, idx)?,
None => m.clone(),
};
keyed.push((k, m));
}
keyed.sort_by(|(ka, _), (kb, _)| {
compare_atomic_for_sort(ka, kb, idx, ctx.bindings)
});
Ok(Value::Array(Box::new(keyed.into_iter().map(|(_, m)| m).collect())))
}
"put" => {
let mut a = as_array(&args[0])?;
let pos = value_to_number(&args[1], idx);
if pos.fract() == 0.0 && pos >= 1.0 && (pos as usize) <= a.len() {
a[pos as usize - 1] = args[2].clone();
Ok(Value::Array(Box::new(a)))
} else {
Err(xpath_err("array:put: position out of bounds (FOAY0001)"))
}
}
"insert-before" => {
let mut a = as_array(&args[0])?;
let pos = value_to_number(&args[1], idx);
if pos.fract() == 0.0 && pos >= 1.0 && (pos as usize) <= a.len() + 1 {
a.insert(pos as usize - 1, args[2].clone());
Ok(Value::Array(Box::new(a)))
} else {
Err(xpath_err("array:insert-before: position out of bounds (FOAY0001)"))
}
}
"remove" => {
let a = as_array(&args[0])?;
let mut drop: std::collections::HashSet<usize> = std::collections::HashSet::new();
for p in items_of(&args[1]) {
let n = value_to_number(&p, idx);
if n.fract() != 0.0 || n < 1.0 || (n as usize) > a.len() {
return Err(xpath_err("array:remove: position out of bounds (FOAY0001)"));
}
drop.insert(n as usize);
}
let out: Vec<Value> = a.into_iter().enumerate()
.filter(|(i, _)| !drop.contains(&(i + 1)))
.map(|(_, m)| m)
.collect();
Ok(Value::Array(Box::new(out)))
}
_ => Err(xpath_err(format!(
"array:{local} is not supported in this build"))),
}
}
fn value_as_function(v: &Value) -> Result<&FunctionItem> {
match v {
Value::Function(fi) => Ok(fi),
_ => Err(xpath_err("expected a function item (XPTY0004)")),
}
}
fn compare_atomic_for_sort<I: DocIndexLike>(
a: &Value, b: &Value, idx: &I, bindings: &dyn XPathBindings,
) -> std::cmp::Ordering {
use std::cmp::Ordering;
let numeric = |v: &Value| -> Option<f64> {
match v {
Value::Number(_) => Some(value_to_number(v, idx)),
Value::Typed(t) if matches!(t.kind,
"integer" | "decimal" | "double" | "float" | "long" | "int"
| "short" | "byte" | "numeric") => Some(value_to_number(v, idx)),
_ => None,
}
};
match (numeric(a), numeric(b)) {
(Some(x), Some(y)) => x.partial_cmp(&y).unwrap_or(Ordering::Equal),
_ => value_to_string_with(a, idx, bindings)
.cmp(&value_to_string_with(b, idx, bindings)),
}
}
fn eval_hof_function<I: DocIndexLike>(
local: &str, args: &[Value], ctx: &EvalCtx<'_>, idx: &I,
) -> Option<Result<Value>> {
let f = |i: usize| value_as_function(&args[i]);
let res = match local {
"for-each" if args.len() == 2 => (|| {
let func = f(1)?;
let mut out = Vec::new();
for item in iter_items(&args[0]) {
out.push(call_function_item(func, vec![item], ctx, idx)?);
}
Ok(seq_from_items(out))
})(),
"filter" if args.len() == 2 => (|| {
let func = f(1)?;
let mut out = Vec::new();
for item in iter_items(&args[0]) {
let keep = call_function_item(func, vec![item.clone()], ctx, idx)?;
if value_to_bool(&keep, idx) { out.push(item); }
}
Ok(seq_from_items(out))
})(),
"fold-left" if args.len() == 3 => (|| {
let func = f(2)?;
let mut acc = args[1].clone();
for item in iter_items(&args[0]) {
acc = call_function_item(func, vec![acc, item], ctx, idx)?;
}
Ok(acc)
})(),
"fold-right" if args.len() == 3 => (|| {
let func = f(2)?;
let items: Vec<Value> = iter_items(&args[0]).collect();
let mut acc = args[1].clone();
for item in items.into_iter().rev() {
acc = call_function_item(func, vec![item, acc], ctx, idx)?;
}
Ok(acc)
})(),
"for-each-pair" if args.len() == 3 => (|| {
let func = f(2)?;
let mut out = Vec::new();
for (x, y) in iter_items(&args[0]).zip(iter_items(&args[1])) {
out.push(call_function_item(func, vec![x, y], ctx, idx)?);
}
Ok(seq_from_items(out))
})(),
"sort" if (1..=3).contains(&args.len()) => (|| {
let keyfn = match args.get(2) {
Some(v) => Some(value_as_function(v)?),
None => None,
};
let mut keyed: Vec<(Value, Value)> = Vec::new();
for item in iter_items(&args[0]) {
let k = match keyfn {
Some(func) => call_function_item(func, vec![item.clone()], ctx, idx)?,
None => item.clone(),
};
keyed.push((k, item));
}
keyed.sort_by(|(ka, _), (kb, _)|
compare_atomic_for_sort(ka, kb, idx, ctx.bindings));
Ok(seq_from_items(keyed.into_iter().map(|(_, v)| v).collect()))
})(),
"apply" if args.len() == 2 => (|| {
let func = f(0)?;
let call_args = match &args[1] {
Value::Array(a) => (**a).clone(),
_ => return Err(xpath_err("fn:apply: second argument must be an array")),
};
call_function_item(func, call_args, ctx, idx)
})(),
"function-arity" if args.len() == 1 =>
f(0).map(|func| Value::Number(Numeric::Integer(func.arity() as i64))),
"function-name" if args.len() == 1 => f(0).map(|func| match func {
FunctionItem::Named { name, ns, .. } => {
let local = name.rsplit(':').next().unwrap_or(name);
let lexical = if ns.is_empty() {
local.to_string()
} else {
format!("{{{ns}}}{local}")
};
Value::Typed(Box::new(TypedAtomic {
kind: "QName", lexical, numeric: None, boolean: None, user_type: None,
}))
}
_ => Value::NodeSet(Vec::new()),
}),
"function-lookup" if args.len() == 2 => {
let qname = value_to_string(&args[0], idx);
let arity = value_to_number(&args[1], idx) as usize;
let (ns, local) = if let Some(rest) = qname.strip_prefix('{') {
rest.split_once('}')
.map(|(u, l)| (u.to_string(), l.to_string()))
.unwrap_or_else(|| (String::new(), qname.clone()))
} else if let Some((prefix, l)) = qname.split_once(':') {
(resolve_prefix_or_implicit(ctx.bindings, prefix).unwrap_or_default(),
l.to_string())
} else {
(FN_NAMESPACE.to_string(), qname.clone())
};
let available = if ns.is_empty() || ns.as_str() == FN_NAMESPACE {
xpath_function_available(&local, ctx)
} else {
ctx.bindings.function_available_in(&ns, &local, arity)
};
if available {
let sig = ctx.bindings.function_signature_in(&ns, &local, arity).map(Box::new);
Ok(Value::Function(Box::new(FunctionItem::Named { name: local, ns, arity, sig })))
} else {
Ok(Value::NodeSet(Vec::new()))
}
}
_ => return None,
};
Some(res)
}
fn eval_json_function<I: DocIndexLike>(
local: &str, args: &[Value], _ctx: &EvalCtx<'_>, idx: &I,
) -> Option<Result<Value>> {
let opt_str = |opts: Option<&Value>, key: &str| -> Option<String> {
match opts {
Some(Value::Map(m)) => m.iter()
.find(|(k, _)| value_to_string(k, idx) == key)
.map(|(_, v)| value_to_string(v, idx)),
_ => None,
}
};
let opt_bool = |opts: Option<&Value>, key: &str| -> Option<bool> {
match opts {
Some(Value::Map(m)) => m.iter()
.find(|(k, _)| value_to_string(k, idx) == key)
.map(|(_, v)| value_to_bool(v, idx)),
_ => None,
}
};
let res = match local {
"parse-json" if (1..=2).contains(&args.len()) => (|| {
if sequence_len(&args[0]) == 0 {
return Ok(Value::Sequence(Vec::new()));
}
let text = value_to_string(&args[0], idx);
let opts = args.get(1);
let dup = opt_str(opts, "duplicates")
.unwrap_or_else(|| "use-first".to_string());
let escape = opt_bool(opts, "escape").unwrap_or(false);
let liberal = opt_bool(opts, "liberal").unwrap_or(false);
parse_json_value(&text, &dup, escape, liberal)
})(),
"xml-to-json" if (1..=2).contains(&args.len()) => (|| {
let root = match &args[0] {
Value::NodeSet(ns) if ns.len() == 1 => ns[0],
Value::NodeSet(ns) if ns.is_empty() =>
return Ok(Value::Sequence(Vec::new())),
_ => return Err(xpath_err(
"xml-to-json: argument must be a single document or element node")),
};
let elem = match idx.kind(root) {
XPathNodeKind::Document => {
let mut elems = idx.children(root).iter().copied()
.filter(|&c| matches!(idx.kind(c), XPathNodeKind::Element));
let first = elems.next().ok_or_else(||
xpath_err("xml-to-json: empty document (FOJS0006)"))?;
if elems.next().is_some() {
return Err(xpath_err(
"xml-to-json: more than one top-level element (FOJS0006)"));
}
first
}
XPathNodeKind::Element => root,
_ => return Err(xpath_err(
"xml-to-json: argument must be a document or element node")),
};
let mut out = String::new();
xml_node_to_json(elem, idx, false, &mut out)?;
Ok(Value::String(out))
})(),
"serialize" if (1..=2).contains(&args.len()) => (|| {
let method = opt_str(args.get(1), "method").unwrap_or_default();
if method == "json" {
let mut out = String::new();
value_to_json(&args[0], idx, &mut out)?;
Ok(Value::String(out))
} else {
Ok(Value::String(value_to_string(&args[0], idx)))
}
})(),
_ => return None,
};
Some(res)
}
fn value_to_json<I: DocIndexLike>(v: &Value, idx: &I, out: &mut String) -> Result<()> {
match v {
Value::Map(m) => {
out.push('{');
for (i, (k, val)) in m.iter().enumerate() {
if i > 0 { out.push(','); }
out.push('"');
json_escape_into(&value_to_string(k, idx), out);
out.push_str("\":");
value_to_json(val, idx, out)?;
}
out.push('}');
}
Value::Array(a) => {
out.push('[');
for (i, val) in a.iter().enumerate() {
if i > 0 { out.push(','); }
value_to_json(val, idx, out)?;
}
out.push(']');
}
Value::Boolean(b) => out.push_str(if *b { "true" } else { "false" }),
Value::Number(_) => out.push_str(&value_to_string(v, idx)),
Value::Typed(t) if t.numeric.is_some() => out.push_str(&value_to_string(v, idx)),
Value::NodeSet(ns) if ns.is_empty() => out.push_str("null"),
Value::Sequence(items) if items.is_empty() => out.push_str("null"),
other => {
out.push('"');
json_escape_into(&value_to_string(other, idx), out);
out.push('"');
}
}
Ok(())
}
pub enum JsonEvent {
StartObject,
EndObject,
StartArray,
EndArray,
Key(String),
Str(String),
Number(String),
Bool(bool),
Null,
}
pub fn parse_json_events(
json: &str, escape: bool, liberal: bool, sink: &mut dyn FnMut(JsonEvent),
) -> Result<()> {
let chars: Vec<char> = json.chars().collect();
let mut p = JsonParser { chars: &chars, pos: 0, escape, liberal };
p.skip_ws();
p.parse_value(sink)?;
p.skip_ws();
if p.pos != p.chars.len() {
return Err(xpath_err("parse-json: trailing content after JSON value (FOJS0001)"));
}
Ok(())
}
pub fn parse_json_value(text: &str, duplicates: &str, escape: bool, liberal: bool)
-> Result<Value>
{
enum Frame { Obj(Vec<(Value, Value)>, Option<String>), Arr(Vec<Value>) }
let mut stack: Vec<Frame> = Vec::new();
let mut result: Option<Value> = None;
let mut dup_err: Option<crate::error::XmlError> = None;
let mut attach = |stack: &mut Vec<Frame>, result: &mut Option<Value>, v: Value| {
match stack.last_mut() {
Some(Frame::Arr(a)) => a.push(v),
Some(Frame::Obj(entries, pending)) => {
let key = pending.take().unwrap_or_default();
match entries.iter().position(|(k, _)| value_as_str(k) == key) {
Some(i) => match duplicates {
"reject" => {
dup_err.get_or_insert_with(|| xpath_err(format!(
"parse-json: duplicate key {key:?} (FOJS0003)")));
}
"use-last" => entries[i].1 = v,
_ => {}
},
None => entries.push((Value::String(key), v)),
}
}
None => *result = Some(v),
}
};
parse_json_events(text, escape, liberal, &mut |ev| match ev {
JsonEvent::StartObject => stack.push(Frame::Obj(Vec::new(), None)),
JsonEvent::StartArray => stack.push(Frame::Arr(Vec::new())),
JsonEvent::Key(k) => {
if let Some(Frame::Obj(_, pending)) = stack.last_mut() { *pending = Some(k); }
}
JsonEvent::EndObject => {
if let Some(Frame::Obj(entries, _)) = stack.pop() {
attach(&mut stack, &mut result, Value::Map(Box::new(entries)));
}
}
JsonEvent::EndArray => {
if let Some(Frame::Arr(members)) = stack.pop() {
attach(&mut stack, &mut result, Value::Array(Box::new(members)));
}
}
JsonEvent::Str(s) => attach(&mut stack, &mut result, Value::String(s)),
JsonEvent::Number(n) => attach(&mut stack, &mut result,
Value::Number(Numeric::Double(n.parse::<f64>().unwrap_or(f64::NAN)))),
JsonEvent::Bool(b) => attach(&mut stack, &mut result, Value::Boolean(b)),
JsonEvent::Null => attach(&mut stack, &mut result, Value::Sequence(Vec::new())),
})?;
if let Some(e) = dup_err { return Err(e); }
Ok(result.unwrap_or_else(|| Value::Sequence(Vec::new())))
}
struct JsonParser<'a> {
chars: &'a [char],
pos: usize,
escape: bool,
liberal: bool,
}
impl<'a> JsonParser<'a> {
fn peek(&self) -> Option<char> { self.chars.get(self.pos).copied() }
fn skip_ws(&mut self) {
while let Some(c) = self.peek() {
if c == ' ' || c == '\t' || c == '\n' || c == '\r' { self.pos += 1; }
else { break; }
}
}
fn parse_value(&mut self, sink: &mut dyn FnMut(JsonEvent)) -> Result<()> {
match self.peek() {
Some('{') => self.parse_object(sink),
Some('[') => self.parse_array(sink),
Some('"') => { let s = self.parse_string()?; sink(JsonEvent::Str(s)); Ok(()) }
Some('t') | Some('f') => self.parse_bool(sink),
Some('n') => self.parse_null(sink),
Some(c) if c == '-' || c.is_ascii_digit() => self.parse_number(sink),
_ => Err(xpath_err("parse-json: unexpected character (FOJS0001)")),
}
}
fn parse_object(&mut self, sink: &mut dyn FnMut(JsonEvent)) -> Result<()> {
self.pos += 1; sink(JsonEvent::StartObject);
self.skip_ws();
if self.peek() == Some('}') { self.pos += 1; sink(JsonEvent::EndObject); return Ok(()); }
loop {
self.skip_ws();
if self.peek() != Some('"') {
return Err(xpath_err("parse-json: expected object key (FOJS0001)"));
}
let key = self.parse_string()?;
sink(JsonEvent::Key(key));
self.skip_ws();
if self.peek() != Some(':') {
return Err(xpath_err("parse-json: expected ':' in object (FOJS0001)"));
}
self.pos += 1;
self.skip_ws();
self.parse_value(sink)?;
self.skip_ws();
match self.peek() {
Some(',') => { self.pos += 1; continue; }
Some('}') => { self.pos += 1; break; }
_ => return Err(xpath_err("parse-json: expected ',' or '}' (FOJS0001)")),
}
}
sink(JsonEvent::EndObject);
Ok(())
}
fn parse_array(&mut self, sink: &mut dyn FnMut(JsonEvent)) -> Result<()> {
self.pos += 1; sink(JsonEvent::StartArray);
self.skip_ws();
if self.peek() == Some(']') { self.pos += 1; sink(JsonEvent::EndArray); return Ok(()); }
loop {
self.skip_ws();
self.parse_value(sink)?;
self.skip_ws();
match self.peek() {
Some(',') => { self.pos += 1; continue; }
Some(']') => { self.pos += 1; break; }
_ => return Err(xpath_err("parse-json: expected ',' or ']' (FOJS0001)")),
}
}
sink(JsonEvent::EndArray);
Ok(())
}
fn parse_string(&mut self) -> Result<String> {
self.pos += 1; let mut s = String::new();
loop {
match self.peek() {
None => return Err(xpath_err("parse-json: unterminated string (FOJS0001)")),
Some('"') => { self.pos += 1; break; }
Some('\\') => {
self.pos += 1;
let esc = self.peek().ok_or_else(||
xpath_err("parse-json: dangling escape (FOJS0001)"))?;
self.pos += 1;
if self.escape && esc != 'u' {
s.push('\\'); s.push(esc); continue;
}
match esc {
'"' => s.push('"'),
'\\' => s.push('\\'),
'/' => s.push('/'),
'b' => s.push('\u{0008}'),
'f' => s.push('\u{000C}'),
'n' => s.push('\n'),
'r' => s.push('\r'),
't' => s.push('\t'),
'u' => {
let cp = self.parse_hex4()?;
if (0xD800..=0xDBFF).contains(&cp) {
if self.peek() == Some('\\') {
self.pos += 1;
if self.peek() == Some('u') {
self.pos += 1;
let lo = self.parse_hex4()?;
let c = 0x10000
+ ((cp - 0xD800) << 10)
+ (lo - 0xDC00);
if self.escape {
s.push_str(&format!("\\u{cp:04X}\\u{lo:04X}"));
} else if let Some(ch) = char::from_u32(c) {
s.push(ch);
}
continue;
}
}
return Err(xpath_err(
"parse-json: invalid surrogate (FOJS0001)"));
}
if self.escape {
s.push_str(&format!("\\u{cp:04X}"));
} else if let Some(ch) = char::from_u32(cp) {
s.push(ch);
}
}
_ => return Err(xpath_err(format!(
"parse-json: invalid escape \\{esc} (FOJS0001)"))),
}
}
Some(c) => {
if (c as u32) < 0x20 && !self.liberal {
return Err(xpath_err(
"parse-json: unescaped control character (FOJS0001)"));
}
s.push(c);
self.pos += 1;
}
}
}
Ok(s)
}
fn parse_hex4(&mut self) -> Result<u32> {
let mut v = 0u32;
for _ in 0..4 {
let c = self.peek().ok_or_else(||
xpath_err("parse-json: short \\u escape (FOJS0001)"))?;
let d = c.to_digit(16).ok_or_else(||
xpath_err("parse-json: invalid hex in \\u escape (FOJS0001)"))?;
v = v * 16 + d;
self.pos += 1;
}
Ok(v)
}
fn parse_bool(&mut self, sink: &mut dyn FnMut(JsonEvent)) -> Result<()> {
if self.matches_literal("true") { sink(JsonEvent::Bool(true)); Ok(()) }
else if self.matches_literal("false") { sink(JsonEvent::Bool(false)); Ok(()) }
else { Err(xpath_err("parse-json: invalid literal (FOJS0001)")) }
}
fn parse_null(&mut self, sink: &mut dyn FnMut(JsonEvent)) -> Result<()> {
if self.matches_literal("null") { sink(JsonEvent::Null); Ok(()) }
else { Err(xpath_err("parse-json: invalid literal (FOJS0001)")) }
}
fn matches_literal(&mut self, lit: &str) -> bool {
let end = self.pos + lit.len();
if end <= self.chars.len()
&& self.chars[self.pos..end].iter().copied().eq(lit.chars())
{
self.pos = end;
true
} else {
false
}
}
fn parse_number(&mut self, sink: &mut dyn FnMut(JsonEvent)) -> Result<()> {
let start = self.pos;
let digits = |p: &mut Self| {
let s = p.pos;
while p.peek().is_some_and(|c| c.is_ascii_digit()) { p.pos += 1; }
p.pos > s
};
let bad = || xpath_err("parse-json: invalid number (FOJS0001)");
if self.liberal {
if matches!(self.peek(), Some('-') | Some('+')) { self.pos += 1; }
while self.peek().is_some_and(|c|
c.is_ascii_digit() || matches!(c, '.' | 'e' | 'E' | '+' | '-')) {
self.pos += 1;
}
} else {
if self.peek() == Some('-') { self.pos += 1; }
match self.peek() {
Some('0') => self.pos += 1, Some(c) if c.is_ascii_digit() => { digits(self); }
_ => return Err(bad()),
}
if self.peek() == Some('.') {
self.pos += 1;
if !digits(self) { return Err(bad()); }
}
if matches!(self.peek(), Some('e') | Some('E')) {
self.pos += 1;
if matches!(self.peek(), Some('+') | Some('-')) { self.pos += 1; }
if !digits(self) { return Err(bad()); }
}
}
let lex: String = self.chars[start..self.pos].iter().collect();
match lex.parse::<f64>() {
Ok(n) if n.is_finite() => { sink(JsonEvent::Number(lex)); Ok(()) }
_ => Err(xpath_err(format!("parse-json: invalid number {lex:?} (FOJS0001)"))),
}
}
}
pub fn json_option_bool<I: DocIndexLike>(
opts: Option<&Value>, key: &str, idx: &I,
) -> Result<Option<bool>> {
fn single_bool(v: &Value) -> Option<bool> {
match v {
Value::Boolean(b) => Some(*b),
Value::Typed(t) if t.kind == "boolean" => t.boolean,
Value::Sequence(items) if items.len() == 1 => single_bool(&items[0]),
_ => None,
}
}
let Some(Value::Map(m)) = opts else { return Ok(None) };
match m.iter().find(|(k, _)| value_to_string(k, idx) == key) {
None => Ok(None),
Some((_, v)) => single_bool(v).map(Some).ok_or_else(||
xpath_err(format!("JSON option {key:?} must be a single boolean (FOJS0005)"))),
}
}
fn value_as_str(v: &Value) -> String {
match v {
Value::String(s) => s.clone(),
Value::Typed(t) => t.lexical.clone(),
_ => String::new(),
}
}
const FN_NAMESPACE: &str = "http://www.w3.org/2005/xpath-functions";
fn xml_node_to_json<I: DocIndexLike>(
elem: NodeId, idx: &I, in_map: bool, out: &mut String,
) -> Result<()> {
let err = |m: &str| xpath_err(format!("xml-to-json: {m} (FOJS0006)"));
if idx.namespace_uri(elem) != FN_NAMESPACE {
return Err(err("element is not in the JSON namespace"));
}
let local = idx.local_name(elem).to_string();
for a in idx.attr_range(elem) {
if !idx.namespace_uri(a).is_empty() { continue; }
match idx.local_name(a) {
"key" | "escaped" | "escaped-key" => {}
other => return Err(err(&format!("unexpected attribute {other:?}"))),
}
}
let has_key = attr_value(elem, "key", idx).is_some();
if in_map && !has_key { return Err(err("map entry is missing its key")); }
if !in_map && has_key { return Err(err("key attribute outside a map")); }
let elem_children: Vec<NodeId> = idx.children(elem).iter().copied()
.filter(|&c| matches!(idx.kind(c), XPathNodeKind::Element)).collect();
let text = direct_text(elem, idx);
let text_is_blank = text.trim().is_empty();
if in_map {
let key = attr_value(elem, "key", idx).unwrap_or_default();
out.push('"');
if json_bool_attr(elem, "escaped-key", idx)?.unwrap_or(false) {
validate_json_escapes(&key)?;
out.push_str(&key);
} else {
json_escape_into(&key, out);
}
out.push_str("\":");
}
match local.as_str() {
"null" => {
if !elem_children.is_empty() || !text_is_blank {
return Err(err("null must be empty"));
}
out.push_str("null");
}
"boolean" => {
if !elem_children.is_empty() { return Err(err("boolean has element content")); }
match text.trim() {
"true" | "1" => out.push_str("true"),
"false" | "0" => out.push_str("false"),
_ => return Err(err("boolean value is not true/false")),
}
}
"number" => {
if !elem_children.is_empty() { return Err(err("number has element content")); }
let n = text.trim();
match n.parse::<f64>() {
Ok(f) if f.is_finite() => out.push_str(n),
_ => return Err(err("number is not a valid JSON number")),
}
}
"string" => {
if !elem_children.is_empty() { return Err(err("string has element content")); }
out.push('"');
if json_bool_attr(elem, "escaped", idx)?.unwrap_or(false) {
validate_json_escapes(&text)?;
out.push_str(&text);
} else {
json_escape_into(&text, out);
}
out.push('"');
}
"array" => {
if !text_is_blank { return Err(err("array has text content")); }
out.push('[');
for (i, &c) in elem_children.iter().enumerate() {
if i > 0 { out.push(','); }
xml_node_to_json(c, idx, false, out)?;
}
out.push(']');
}
"map" => {
if !text_is_blank { return Err(err("map has text content")); }
out.push('{');
let mut seen: Vec<String> = Vec::new();
for (i, &c) in elem_children.iter().enumerate() {
if idx.namespace_uri(c) == FN_NAMESPACE {
if let Some(k) = attr_value(c, "key", idx) {
if seen.contains(&k) {
return Err(err("duplicate key in map"));
}
seen.push(k);
}
}
if i > 0 { out.push(','); }
xml_node_to_json(c, idx, true, out)?;
}
out.push('}');
}
other => return Err(err(&format!("unexpected element {other:?}"))),
}
Ok(())
}
fn direct_text<I: DocIndexLike>(elem: NodeId, idx: &I) -> String {
let mut s = String::new();
for &c in idx.children(elem) {
if matches!(idx.kind(c), XPathNodeKind::Text | XPathNodeKind::CData) {
s.push_str(&idx.string_value(c));
}
}
s
}
fn json_bool_attr<I: DocIndexLike>(elem: NodeId, name: &str, idx: &I) -> Result<Option<bool>> {
match attr_value(elem, name, idx) {
None => Ok(None),
Some(v) => match v.trim() {
"true" | "1" => Ok(Some(true)),
"false" | "0" => Ok(Some(false)),
_ => Err(xpath_err(format!(
"xml-to-json: {name} is not a boolean (FOJS0006)"))),
},
}
}
fn validate_json_escapes(s: &str) -> Result<()> {
let chars: Vec<char> = s.chars().collect();
let mut i = 0;
while i < chars.len() {
if chars[i] == '\\' {
let Some(&next) = chars.get(i + 1) else {
return Err(xpath_err("xml-to-json: dangling backslash (FOJS0007)"));
};
match next {
'"' | '\\' | '/' | 'b' | 'f' | 'n' | 'r' | 't' => i += 2,
'u' => {
let hex = chars.get(i + 2..i + 6);
if !hex.is_some_and(|h| h.iter().all(|c| c.is_ascii_hexdigit())) {
return Err(xpath_err(
"xml-to-json: invalid \\u escape (FOJS0007)"));
}
i += 6;
}
_ => return Err(xpath_err(format!(
"xml-to-json: invalid escape \\{next} (FOJS0007)"))),
}
} else {
i += 1;
}
}
Ok(())
}
fn attr_value<I: DocIndexLike>(elem: NodeId, name: &str, idx: &I) -> Option<String> {
idx.attr_range(elem)
.find(|&a| idx.namespace_uri(a).is_empty() && idx.local_name(a) == name)
.map(|a| idx.string_value(a))
}
fn json_escape_into(s: &str, out: &mut String) {
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\u{0008}' => out.push_str("\\b"),
'\u{000C}' => out.push_str("\\f"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04X}", c as u32)),
c => out.push(c),
}
}
}
fn eval_function<I: DocIndexLike>(name: &str, args: &[Expr], ctx: &EvalCtx<'_>, idx: &I) -> Result<Value> {
let (call_uri, call_local) = match name.split_once(':') {
Some((prefix, local)) => match resolve_prefix_or_implicit(ctx.bindings, prefix) {
Some(uri) => (uri, local.to_string()),
None => return Err(xpath_err(format!(
"Undefined namespace prefix in function name: {prefix}"
))),
},
None => (String::new(), name.to_string()),
};
let mut arg_vals: Option<Vec<Value>> = None;
let take_args = |vals: &mut Option<Vec<Value>>, args: &[Expr]| -> Result<Vec<Value>> {
if let Some(v) = vals.take() { return Ok(v); }
args.iter().map(|e| eval_expr(e, ctx, idx)).collect()
};
{
let vs = take_args(&mut arg_vals, args)?;
let res = ctx.bindings.call_function_in(&call_uri, &call_local, vs.clone(), ctx.context_node);
if let Some(r) = res { return r; }
if let Some(r) = super::exslt::dispatch(&call_uri, &call_local, vs.clone(), idx) {
return r;
}
if call_uri == super::exslt::DYN_NS && call_local == "evaluate" {
return dyn_evaluate(&vs, ctx, idx);
}
if call_uri == "http://www.w3.org/2001/XMLSchema" {
return Ok(xs_constructor(&call_local, &vs, idx, ctx.bindings)?);
}
if call_uri == "http://www.w3.org/2005/xpath-functions/map" {
return eval_map_function(&call_local, &vs, ctx, idx);
}
if call_uri == "http://www.w3.org/2005/xpath-functions/array" {
return eval_array_function(&call_local, &vs, ctx, idx);
}
if call_uri.is_empty()
|| call_uri == "http://www.w3.org/2005/xpath-functions"
{
if let Some(r) = eval_hof_function(&call_local, &vs, ctx, idx) {
return r;
}
if let Some(r) = eval_json_function(&call_local, &vs, ctx, idx) {
return r;
}
}
arg_vals = Some(vs);
}
if !call_uri.is_empty()
&& call_uri != "http://www.w3.org/2005/xpath-functions"
{
return Err(xpath_err(format!(
"Unregistered XPath function: {{{call_uri}}}{call_local}"
)));
}
let _ = arg_vals; macro_rules! arg {
($n:expr) => {
eval_expr(&args[$n], ctx, idx)?
};
}
macro_rules! arg_str {
($n:expr) => {
value_to_string_with(&arg!($n), idx, ctx.bindings)
};
}
macro_rules! arg_num {
($n:expr) => {
value_to_number_with(&arg!($n), idx, ctx.bindings)
};
}
macro_rules! check_args {
($n:expr) => {
if args.len() != $n {
return Err(xpath_err(format!("{}() requires {} argument(s)", name, $n))
.with_xpath_code("XPTY0004"));
}
};
}
let name = call_local.as_str();
match name {
"true" => { check_args!(0); Ok(Value::Boolean(true)) }
"false" => { check_args!(0); Ok(Value::Boolean(false)) }
"not" => {
check_args!(1);
Ok(Value::Boolean(!value_to_bool(&arg!(0), idx)))
}
"boolean" => {
check_args!(1);
Ok(Value::Boolean(value_to_bool(&arg!(0), idx)))
}
"lang" => {
if args.is_empty() || args.len() > 2 {
return Err(xpath_err("lang() requires 1 or 2 arguments"));
}
let want = value_to_string(&arg!(0), idx);
let want_lc = want.to_ascii_lowercase();
let start = if args.len() == 2 {
match arg!(1) {
Value::NodeSet(ns) => ns.first().copied(),
_ => return Err(xpath_err(
"lang() second argument must be a node")),
}
} else {
Some(ctx.context_node)
};
let mut current = start;
while let Some(node) = current {
for attr in idx.attr_range(node) {
if !(idx.local_name(attr) == "lang" && idx.namespace_uri(attr) == "http://www.w3.org/XML/1998/namespace") { continue; }
let val_lc = idx.string_value(attr).to_ascii_lowercase();
let ok = val_lc == want_lc
|| (val_lc.len() > want_lc.len()
&& val_lc.starts_with(&want_lc)
&& val_lc.as_bytes()[want_lc.len()] == b'-');
return Ok(Value::Boolean(ok));
}
current = idx.parent(node);
}
Ok(Value::Boolean(false))
}
"last" => { check_args!(0); Ok(integer_result(ctx.size as i64, ctx.bindings)) }
"position" => { check_args!(0); Ok(integer_result(ctx.pos as i64, ctx.bindings)) }
"count" => {
check_args!(1);
Ok(integer_result(sequence_len(&arg!(0)) as i64, ctx.bindings))
}
"id" => {
if args.is_empty() || args.len() > 2 {
return Err(xpath_err("id() requires 1 or 2 arguments"));
}
let v = arg!(0);
let search_root = if args.len() == 2 {
match arg!(1) {
Value::NodeSet(ns) => match ns.first().copied() {
Some(n) => doc_root_of(n, idx),
None => return Ok(Value::NodeSet(Vec::new())),
},
_ => return Err(xpath_err(
"id() second argument must be a node")),
}
} else {
doc_root_of(ctx.context_node, idx)
};
let tokens: Vec<String> = match &v {
Value::NodeSet(ns) => {
let mut out: Vec<String> = Vec::new();
for &n in ns {
out.extend(
idx.string_value(n)
.split_whitespace()
.map(str::to_string),
);
}
out
}
_ => value_to_string(&v, idx)
.split_whitespace()
.map(str::to_string)
.collect(),
};
let mut hits: Vec<NodeId> = Vec::new();
for node in descendants(search_root, idx, true) {
if !matches!(idx.kind(node), XPathNodeKind::Element) { continue; }
for attr in idx.attr_range(node) {
if !idx.is_id_attribute(attr) { continue; }
let av = idx.string_value(attr);
if tokens.iter().any(|t| *t == av) {
hits.push(node);
break;
}
}
}
dedup_sort(&mut hits);
Ok(Value::NodeSet(hits))
}
"idref" => {
if args.is_empty() || args.len() > 2 {
return Err(xpath_err("idref() requires 1 or 2 arguments"));
}
let v = arg!(0);
let search_root = if args.len() == 2 {
match arg!(1) {
Value::NodeSet(ns) => match ns.first().copied() {
Some(n) => doc_root_of(n, idx),
None => return Ok(Value::NodeSet(Vec::new())),
},
_ => return Err(xpath_err(
"idref() second argument must be a node")),
}
} else {
doc_root_of(ctx.context_node, idx)
};
let tokens: Vec<String> = match &v {
Value::NodeSet(ns) => ns.iter()
.flat_map(|&n| idx.string_value(n)
.split_whitespace().map(str::to_string).collect::<Vec<_>>())
.collect(),
_ => value_to_string(&v, idx)
.split_whitespace().map(str::to_string).collect(),
};
let mut hits: Vec<NodeId> = Vec::new();
for node in descendants(search_root, idx, true) {
if !matches!(idx.kind(node), XPathNodeKind::Element) { continue; }
for attr in idx.attr_range(node) {
if !idx.is_idref_attribute(attr) { continue; }
let av = idx.string_value(attr);
if av.split_whitespace().any(|w| tokens.iter().any(|t| t == w)) {
hits.push(attr);
}
}
}
dedup_sort(&mut hits);
Ok(Value::NodeSet(hits))
}
"local-name" => {
let id = if args.is_empty() {
if ctx.static_ctx.xpath_2_0 && matches!(current_context_item(),
Some(v) if !matches!(v,
Value::NodeSet(_) | Value::ForeignNodeSet(_)))
{
return Err(xpath_err(
"local-name(): context item is not a node (XPTY0004)"
).with_xpath_code("XPTY0004"));
}
Some(ctx.context_node)
} else {
match arg!(0) {
Value::NodeSet(ns) => ns.first().copied(),
_ => return Err(xpath_err("local-name() requires a node-set or no argument")),
}
};
Ok(Value::String(id.map(|i| idx.local_name(i).to_string()).unwrap_or_default()))
}
"name" => {
let id = if args.is_empty() {
if ctx.static_ctx.xpath_2_0 && matches!(current_context_item(),
Some(v) if !matches!(v,
Value::NodeSet(_) | Value::ForeignNodeSet(_)))
{
return Err(xpath_err(
"name(): context item is not a node (XPTY0004)"
).with_xpath_code("XPTY0004"));
}
Some(ctx.context_node)
} else {
match arg!(0) {
Value::NodeSet(ns) => ns.first().copied(),
_ => return Err(xpath_err("name() requires a node-set or no argument")),
}
};
Ok(Value::String(id.map(|i| idx.node_name(i).to_string()).unwrap_or_default()))
}
"namespace-uri" => {
let id = if args.is_empty() {
if ctx.static_ctx.xpath_2_0 && matches!(current_context_item(),
Some(v) if !matches!(v,
Value::NodeSet(_) | Value::ForeignNodeSet(_)))
{
return Err(xpath_err(
"namespace-uri(): context item is not a node (XPTY0004)"
).with_xpath_code("XPTY0004"));
}
Some(ctx.context_node)
} else {
match arg!(0) {
Value::NodeSet(ns) => ns.first().copied(),
_ => return Err(xpath_err("namespace-uri() requires a node-set or no argument")),
}
};
Ok(Value::String(id.map(|i| idx.namespace_uri(i).to_string()).unwrap_or_default()))
}
"string" => {
if args.is_empty() {
Ok(Value::String(idx.string_value(ctx.context_node)))
} else {
check_args!(1);
let a = arg!(0);
if value_is_function(&a) {
return Err(xpath_err(
"string(): a function item has no string value (FOTY0014)")
.with_xpath_code("FOTY0014"));
}
Ok(Value::String(value_to_string_with_compat(
&a, idx, ctx.bindings, ctx.static_ctx.libxml2_compatible)))
}
}
"concat" => {
if args.len() < 2 {
return Err(xpath_err("concat() requires at least 2 arguments"));
}
let mut s = String::new();
for a in args {
let v = eval_expr(a, ctx, idx)?;
s.push_str(&value_to_string_with_compat(
&v, idx, ctx.bindings, ctx.static_ctx.libxml2_compatible));
}
Ok(Value::String(s))
}
"starts-with" => {
if args.len() < 2 || args.len() > 3 {
return Err(xpath_err("starts-with() requires 2 or 3 arguments"));
}
let s = arg_str!(0);
let pre = arg_str!(1);
let coll = effective_collation(if args.len() == 3 { Some(arg_str!(2)) } else { None });
Ok(Value::Boolean(collation_starts_with(&s, &pre, coll.as_deref())))
}
"contains" => {
if args.len() < 2 || args.len() > 3 {
return Err(xpath_err("contains() requires 2 or 3 arguments"));
}
let s = arg_str!(0);
let sub = arg_str!(1);
let coll = effective_collation(if args.len() == 3 { Some(arg_str!(2)) } else { None });
Ok(Value::Boolean(collation_contains(&s, &sub, coll.as_deref())))
}
"substring-before" => {
if args.len() < 2 || args.len() > 3 {
return Err(xpath_err("substring-before() requires 2 or 3 arguments"));
}
let s = arg_str!(0);
let sep = arg_str!(1);
let coll = effective_collation(if args.len() == 3 { Some(arg_str!(2)) } else { None });
let find_pos = if is_ascii_ci_collation(coll.as_deref()) {
ascii_ci_fold(&s).find(&ascii_ci_fold(&sep))
} else {
s.find(sep.as_str())
};
Ok(Value::String(match find_pos {
Some(pos) => s[..pos].to_string(),
None => String::new(),
}))
}
"substring-after" => {
if args.len() < 2 || args.len() > 3 {
return Err(xpath_err("substring-after() requires 2 or 3 arguments"));
}
let s = arg_str!(0);
let sep = arg_str!(1);
let coll = effective_collation(if args.len() == 3 { Some(arg_str!(2)) } else { None });
let find_pos = if is_ascii_ci_collation(coll.as_deref()) {
ascii_ci_fold(&s).find(&ascii_ci_fold(&sep))
} else {
s.find(sep.as_str())
};
Ok(Value::String(match find_pos {
Some(pos) => s[pos + sep.len()..].to_string(),
None => String::new(),
}))
}
"substring" => {
if args.len() < 2 || args.len() > 3 {
return Err(xpath_err("substring() requires 2 or 3 arguments"));
}
let s = arg_str!(0);
let chars: Vec<char> = s.chars().collect();
let len = chars.len();
let len_f = len as f64;
let start_1based = arg_num!(1).round();
let end_1based = if args.len() == 3 {
let cnt = arg_num!(2).round();
let e = start_1based + cnt;
if e.is_nan() { 0.0 } else { e }
} else {
len_f + 1.0
};
let s_f = (start_1based - 1.0).max(0.0).min(len_f);
let e_f = (end_1based - 1.0).max(0.0).min(len_f);
let (s_idx, e_idx) = if s_f <= e_f {
(s_f as usize, e_f as usize)
} else {
(0, 0)
};
let result: String = chars[s_idx..e_idx].iter().collect();
Ok(Value::String(result))
}
"string-length" => {
let s = if args.is_empty() {
idx.string_value(ctx.context_node)
} else {
check_args!(1);
arg_str!(0)
};
Ok(integer_result(s.chars().count() as i64, ctx.bindings))
}
"normalize-space" => {
let s = if args.is_empty() {
idx.string_value(ctx.context_node)
} else {
check_args!(1);
arg_str!(0)
};
let normalized = s.split_whitespace().collect::<Vec<_>>().join(" ");
Ok(Value::String(normalized))
}
"translate" => {
check_args!(3);
let s = arg_str!(0);
let from = arg_str!(1);
let to: Vec<char> = arg_str!(2).chars().collect();
let mut map: std::collections::HashMap<char, Option<char>> =
std::collections::HashMap::new();
for (i, f) in from.chars().enumerate() {
map.entry(f).or_insert_with(|| to.get(i).copied());
}
let result: String = s
.chars()
.filter_map(|c| match map.get(&c) {
Some(Some(t)) => Some(*t),
Some(None) => None,
None => Some(c),
})
.collect();
Ok(Value::String(result))
}
"number" => {
let v = if args.is_empty() {
Value::String(idx.string_value(ctx.context_node))
} else {
check_args!(1);
arg!(0)
};
if value_is_function(&v)
|| matches!(&v, Value::Sequence(items) if items.iter().any(value_is_function))
{
return Err(xpath_err(
"number(): a function item cannot be atomized (FOTY0013)")
.with_xpath_code("FOTY0013"));
}
if ctx.static_ctx.libxml2_compatible {
if let Value::String(ref s) = v {
if s.trim() == "-" { return Ok(Value::Number(Numeric::Double(-0.0))); }
}
}
Ok(Value::Number(Numeric::Double(value_to_number(&v, idx))))
}
"sum" => {
if args.is_empty() || args.len() > 2 {
return Err(xpath_err(format!(
"sum() requires 1 or 2 arguments (got {})", args.len()
)));
}
let zero_val: Value = if args.len() == 2 { arg!(1) } else { Value::Number(Numeric::Double(0.0)) };
match arg!(0).untyped() {
Value::NodeSet(ns) => {
if ns.is_empty() {
return Ok(zero_val);
}
let total: f64 = ns.iter().map(|&id|
idx.string_value(id).trim().parse::<f64>().unwrap_or(f64::NAN)
).sum();
Ok(Value::Number(Numeric::Double(total)))
}
Value::Number(n) => Ok(Value::Number(n)),
Value::String(s) => Ok(Value::Number(Numeric::Double(s.trim().parse::<f64>().unwrap_or(f64::NAN)))),
Value::Boolean(b) => Ok(Value::Number(Numeric::Double(if b { 1.0 } else { 0.0 }))),
Value::Sequence(items) => {
if items.is_empty() {
return Ok(zero_val);
}
if let Some((kind, total, _)) = duration_seq_total(&items) {
return Ok(duration_value(kind, total));
}
for v in &items {
if let Value::String(s) = v {
if s.trim().parse::<f64>().is_err() {
return Err(xpath_err(format!(
"sum(): non-numeric item '{s}' \
(FORG0006)"
)).with_xpath_code("FORG0006"));
}
}
}
let total: f64 = items.iter()
.map(|v| value_to_number_with(v, idx, ctx.bindings))
.sum();
let kind = items.iter().fold(Some("integer"), |acc, v| {
match (acc, numeric_kind_of(v)) {
(Some(a), Some(b)) => numeric_promote_kind(Some(a), Some(b)),
_ => None,
}
});
Ok(Value::Number(match kind {
Some(k) => Numeric::of_kind(k, total),
None => Numeric::Double(total),
}))
}
Value::ForeignNodeSet(_) => Err(xpath_err(
"sum() over foreign node-sets not supported")),
Value::Typed(_) => unreachable!(),
Value::IntRange { lo, hi } => {
let total = ((hi - lo + 1) as f64) * ((lo + hi) as f64) * 0.5;
Ok(Value::Number(Numeric::of_kind("integer", total)))
}
Value::Map(_) | Value::Array(_) | Value::Function(_) => Ok(zero_val),
}
}
"floor" => {
check_args!(1);
let a = arg!(0);
if ctx.static_ctx.xpath_2_0 {
if let Value::String(s) = &a {
if s.trim().parse::<f64>().is_err() {
return Err(xpath_err(format!(
"floor(): argument '{s}' is not a number (XPTY0004)"
)).with_xpath_code("XPTY0004"));
}
}
}
Ok(preserve_numeric_kind(&a, value_to_number(&a, idx).floor()))
}
"ceiling" => {
check_args!(1);
let a = arg!(0);
if ctx.static_ctx.xpath_2_0 {
if let Value::String(s) = &a {
if s.trim().parse::<f64>().is_err() {
return Err(xpath_err(format!(
"ceiling(): argument '{s}' is not a number (XPTY0004)"
)).with_xpath_code("XPTY0004"));
}
}
}
Ok(preserve_numeric_kind(&a, value_to_number(&a, idx).ceil()))
}
"round" => {
if args.is_empty() || args.len() > 2 {
return Err(xpath_err("round() requires 1 or 2 argument(s)"));
}
if args.len() == 2 && !ctx.static_ctx.xpath_3_0 {
return Err(xpath_err(
"round(): the 2-argument form requires XPath 3.0 (XPST0017)")
.with_xpath_code("XPST0017"));
}
let a = arg!(0);
if ctx.static_ctx.xpath_2_0 {
if let Value::String(s) = &a {
if s.trim().parse::<f64>().is_err() {
return Err(xpath_err(format!(
"round(): argument '{s}' is not a number (XPTY0004)"
)).with_xpath_code("XPTY0004"));
}
}
}
let precision = if args.len() == 2 {
value_to_number(&arg!(1), idx) as i32
} else { 0 };
use rust_decimal::prelude::ToPrimitive;
match &a {
Value::Number(Numeric::Decimal(d)) =>
Ok(Value::Number(Numeric::Decimal(round_decimal_half_to_pos_inf(*d, precision)))),
Value::Number(Numeric::Integer(i)) => {
let d = round_decimal_half_to_pos_inf(rust_decimal::Decimal::from(*i), precision);
Ok(Value::Number(match d.to_i64() {
Some(v) => Numeric::Integer(v),
None => Numeric::Decimal(d),
}))
}
_ => {
let n = value_to_number(&a, idx);
let r = if !n.is_finite() {
n
} else {
let scale = 10f64.powi(precision);
let scaled = n * scale;
let rs = if scaled.is_sign_negative() && scaled >= -0.5 { -0.0 }
else { (scaled + 0.5).floor() };
rs / scale
};
Ok(preserve_numeric_kind(&a, r))
}
}
}
"document" => {
if args.is_empty() || args.len() > 2 {
return Err(xpath_err("document() requires 1 or 2 arguments"));
}
let uri = value_to_string(&arg!(0), idx);
match ctx.bindings.load_document(&uri, None) {
Some(r) => r.map(Value::ForeignNodeSet)
.map_err(|e| e.or_xpath_code("FODC0002")),
None => Err(xpath_err(
"document(): bindings don't support external document loading",
).with_xpath_code("FODC0002")),
}
}
"ends-with" => {
if args.len() < 2 || args.len() > 3 {
return Err(xpath_err("ends-with() requires 2 or 3 arguments"));
}
let s = arg_str!(0);
let suf = arg_str!(1);
let coll = effective_collation(if args.len() == 3 { Some(arg_str!(2)) } else { None });
Ok(Value::Boolean(collation_ends_with(&s, &suf, coll.as_deref())))
}
"exists" => {
check_args!(1);
let v = arg!(0);
Ok(Value::Boolean(match v {
Value::NodeSet(ns) => !ns.is_empty(),
Value::ForeignNodeSet(ns) => !ns.is_empty(),
Value::Sequence(items) => !items.is_empty(),
Value::IntRange { lo, hi } => hi >= lo,
_ => true,
}))
}
"empty" => {
check_args!(1);
let v = arg!(0);
Ok(Value::Boolean(match v {
Value::NodeSet(ns) => ns.is_empty(),
Value::ForeignNodeSet(ns) => ns.is_empty(),
Value::Sequence(items) => items.is_empty(),
Value::IntRange { lo, hi } => hi < lo,
_ => false,
}))
}
"data" => {
check_args!(1);
let v = arg!(0);
match v {
Value::NodeSet(ns) => {
let items: Vec<Value> = ns.iter()
.map(|&id| {
let sv = idx.string_value(id);
ctx.bindings.node_typed_value(id, &sv)
.unwrap_or(Value::String(sv))
})
.collect();
match items.len() {
0 => Ok(Value::NodeSet(Vec::new())),
1 => Ok(items.into_iter().next().unwrap()),
_ => Ok(Value::Sequence(items)),
}
}
Value::ForeignNodeSet(ns) => {
let items: Vec<Value> = ns.iter()
.map(|&p| Value::String(ctx.bindings.foreign_string_value(p)))
.collect();
match items.len() {
0 => Ok(Value::NodeSet(Vec::new())),
1 => Ok(items.into_iter().next().unwrap()),
_ => Ok(Value::Sequence(items)),
}
}
Value::Function(_) => Err(xpath_err(
"data(): a function item cannot be atomized (FOTY0013)")
.with_xpath_code("FOTY0013")),
Value::Sequence(items) if items.iter().any(value_is_function) =>
Err(xpath_err(
"data(): a function item cannot be atomized (FOTY0013)")
.with_xpath_code("FOTY0013")),
other => Ok(other),
}
}
"current-dateTime" => {
check_args!(0);
let now = stable_now();
Ok(Value::Typed(Box::new(TypedAtomic {
kind: "dateTime",
lexical: format_datetime_utc(now),
numeric: None,
boolean: None, user_type: None,
})))
}
"current-date" => {
check_args!(0);
let now = stable_now();
Ok(Value::Typed(Box::new(TypedAtomic {
kind: "date",
lexical: format_date_utc(now),
numeric: None,
boolean: None, user_type: None,
})))
}
"current-time" => {
check_args!(0);
let now = stable_now();
Ok(Value::Typed(Box::new(TypedAtomic {
kind: "time",
lexical: format_time_utc(now),
numeric: None,
boolean: None, user_type: None,
})))
}
"adjust-dateTime-to-timezone"
| "adjust-date-to-timezone"
| "adjust-time-to-timezone" => {
if args.is_empty() || args.len() > 2 {
return Err(xpath_err(format!(
"{name}() requires 1 or 2 arguments (got {})", args.len()
)));
}
let kind = match name {
"adjust-dateTime-to-timezone" => "dateTime",
"adjust-date-to-timezone" => "date",
"adjust-time-to-timezone" => "time",
_ => unreachable!(),
};
if let Value::NodeSet(ns) = &arg!(0) {
if ns.is_empty() { return Ok(Value::NodeSet(Vec::new())); }
}
let lex = value_to_string_with(&arg!(0), idx, ctx.bindings);
let new_tz_minutes: Option<i16> = if args.len() == 2 {
let is_empty = matches!(&arg!(1),
Value::NodeSet(ns) if ns.is_empty());
if is_empty {
None
} else {
let dur = value_to_string_with(&arg!(1), idx, ctx.bindings);
let secs = parse_day_time_duration_secs(&dur).unwrap_or(0);
let mins = secs / 60;
if mins.abs() > 14 * 60 {
return Err(xpath_err(format!(
"adjust timezone exceeds 14 hours: {dur}"
)));
}
Some(mins as i16)
}
} else {
Some(0) };
Ok(Value::Typed(Box::new(TypedAtomic {
kind,
lexical: adjust_timezone(&lex, kind, new_tz_minutes),
numeric: None,
boolean: None, user_type: None,
})))
}
"matches" => {
if args.len() < 2 || args.len() > 3 {
return Err(xpath_err(format!(
"matches() requires 2 or 3 arguments (got {})", args.len()
)));
}
let input = arg_str!(0);
let pattern = arg_str!(1);
let flags = if args.len() == 3 { arg_str!(2) } else { String::new() };
if flags.is_empty() {
return crate::regex::compile_with_cached(
&pattern, ctx.bindings.regex_dialect(),
)
.map(|p| Value::Boolean(p.find_match(&input)))
.map_err(|e| xpath_err(format!("invalid regex: {e}"))
.with_xpath_code("FORX0002"));
}
let re = compile_xpath_regex_dialect(&pattern, &flags,
ctx.bindings.regex_dialect())?;
Ok(Value::Boolean(re.is_match(&input)))
}
"replace" => {
if args.len() < 3 || args.len() > 4 {
return Err(xpath_err(format!(
"replace() requires 3 or 4 arguments (got {})", args.len()
)));
}
let input = arg_str!(0);
let pattern = arg_str!(1);
let replacement = arg_str!(2);
let flags = if args.len() == 4 { arg_str!(3) } else { String::new() };
let re = compile_xpath_regex_dialect(&pattern, &flags,
ctx.bindings.regex_dialect())?;
if re.is_match("") {
return Err(xpath_err(format!(
"replace(): the pattern '{pattern}' matches a \
zero-length string (FORX0003)"
)).with_xpath_code("FORX0003"));
}
let group_count = re.captures_len().saturating_sub(1);
let translated = translate_xpath_replacement(&replacement, group_count)?;
Ok(Value::String(re.replace_all(&input, translated.as_str()).into_owned()))
}
"tokenize" => {
if args.is_empty() || args.len() > 3 {
return Err(xpath_err(format!(
"tokenize() requires 1 to 3 arguments (got {})", args.len()
)));
}
let input = arg_str!(0);
let pattern = if args.len() >= 2 { arg_str!(1) } else { r"\s+".to_string() };
let flags = if args.len() == 3 { arg_str!(2) } else { String::new() };
if input.is_empty() {
return Ok(Value::NodeSet(Vec::new()));
}
let re = compile_xpath_regex_dialect(&pattern, &flags,
ctx.bindings.regex_dialect())?;
if re.is_match("") {
return Err(xpath_err(format!(
"tokenize(): the pattern '{pattern}' matches a \
zero-length string (FORX0003)"
)).with_xpath_code("FORX0003"));
}
let parts: Vec<String> = re.split(&input).map(str::to_string).collect();
match idx.allocate_rtf_text_nodes(parts.clone()) {
Some(ids) => Ok(Value::NodeSet(ids)),
None => Ok(Value::String(parts.join(" "))),
}
}
"lower-case" => {
check_args!(1);
Ok(Value::String(arg_str!(0).to_lowercase()))
}
"upper-case" => {
check_args!(1);
Ok(Value::String(arg_str!(0).to_uppercase()))
}
"string-join" => {
if args.is_empty() || args.len() > 2 {
return Err(xpath_err(format!(
"string-join() requires 1 or 2 arguments (got {})", args.len()
)));
}
let sep = if args.len() == 2 { arg_str!(1) } else { String::new() };
let pieces = sequence_to_strings(&arg!(0), idx);
Ok(Value::String(pieces.join(&sep)))
}
"abs" => {
check_args!(1);
let a = arg!(0);
Ok(preserve_numeric_kind(&a, value_to_number(&a, idx).abs()))
}
"min" | "max" => {
if args.is_empty() || args.len() > 2 {
return Err(xpath_err(format!(
"{name}() requires 1 or 2 arguments (got {})", args.len()
)));
}
let coll = effective_collation(
if args.len() == 2 { Some(arg_str!(1)) } else { None });
let ci = is_ascii_ci_collation(coll.as_deref());
let op = if name == "min" { MinMaxOp::Min } else { MinMaxOp::Max };
min_max_avg(&arg!(0), idx, op, ci)
}
"avg" => {
check_args!(1);
min_max_avg(&arg!(0), idx, MinMaxOp::Avg, false)
}
"distinct-values" => {
if args.is_empty() || args.len() > 2 {
return Err(xpath_err(format!(
"distinct-values() requires 1 or 2 arguments (got {})", args.len()
)));
}
let coll = effective_collation(if args.len() == 2 { Some(arg_str!(1)) } else { None });
let ci = is_ascii_ci_collation(coll.as_deref());
let mut seen = std::collections::HashSet::new();
let mut keep = Vec::new();
for s in sequence_to_strings(&arg!(0), idx) {
let k = if ci { ascii_ci_fold(&s) } else { s.clone() };
if seen.insert(k) { keep.push(s); }
}
match idx.allocate_rtf_text_nodes(keep.clone()) {
Some(ids) => Ok(Value::NodeSet(ids)),
None => Ok(Value::String(keep.join(" "))),
}
}
"index-of" => {
if args.len() < 2 || args.len() > 3 {
return Err(xpath_err(format!(
"index-of() requires 2 or 3 arguments (got {})", args.len()
)));
}
let target = arg_str!(1);
let coll = effective_collation(if args.len() == 3 { Some(arg_str!(2)) } else { None });
let items = sequence_to_strings(&arg!(0), idx);
let (key_target, items_keys): (String, Vec<String>) =
if is_ascii_ci_collation(coll.as_deref()) {
(ascii_ci_fold(&target),
items.iter().map(|s| ascii_ci_fold(s)).collect())
} else {
(target, items)
};
let positions: Vec<String> = items_keys.iter().enumerate()
.filter_map(|(i, s)|
if s == &key_target { Some((i + 1).to_string()) } else { None })
.collect();
match idx.allocate_rtf_text_nodes(positions.clone()) {
Some(ids) => Ok(Value::NodeSet(ids)),
None => Ok(Value::String(positions.join(" "))),
}
}
"subsequence" => {
if args.len() < 2 || args.len() > 3 {
return Err(xpath_err(format!(
"subsequence() requires 2 or 3 arguments (got {})", args.len()
)));
}
let is_empty = |v: &Value| matches!(v,
Value::Sequence(items) if items.is_empty())
|| matches!(v, Value::NodeSet(ns) if ns.is_empty());
if ctx.static_ctx.xpath_2_0 && is_empty(&arg!(1)) {
return Err(xpath_err(
"subsequence(): the start argument must not be the \
empty sequence (XPTY0004)"
).with_xpath_code("XPTY0004"));
}
if ctx.static_ctx.xpath_2_0 && args.len() == 3 && is_empty(&arg!(2)) {
return Err(xpath_err(
"subsequence(): the length argument must not be the \
empty sequence (XPTY0004)"
).with_xpath_code("XPTY0004"));
}
let seq = arg!(0);
let start_f = value_to_number(&arg!(1), idx).round();
let length_f = if args.len() == 3 {
value_to_number(&arg!(2), idx).round()
} else {
f64::INFINITY
};
let slice_bounds = |count: usize| -> (usize, usize) {
if start_f.is_nan() || length_f.is_nan() {
return (0, 0);
}
let end = start_f + length_f; if end.is_nan() { return (0, 0); } let lo_p = start_f.max(1.0); if lo_p >= end { return (0, 0); }
let lo = ((lo_p - 1.0) as usize).min(count);
let hi = if end.is_infinite() && end > 0.0 {
count
} else {
((end - 1.0).max(0.0) as usize).min(count)
};
(lo, hi.max(lo))
};
if let Value::NodeSet(ns) = &seq {
let (lo, hi) = slice_bounds(ns.len());
return Ok(Value::NodeSet(ns[lo..hi].to_vec()));
}
if let Value::Sequence(items) = &seq {
let (lo, hi) = slice_bounds(items.len());
let out: Vec<Value> = items[lo..hi].to_vec();
return Ok(if out.len() == 1 {
out.into_iter().next().unwrap()
} else {
Value::Sequence(out)
});
}
let pieces = sequence_to_strings(&seq, idx);
let (lo, hi) = slice_bounds(pieces.len());
let pieces: Vec<String> = pieces[lo..hi].to_vec();
match idx.allocate_rtf_text_nodes(pieces.clone()) {
Some(ids) => Ok(Value::NodeSet(ids)),
None => Ok(Value::String(pieces.join(""))),
}
}
"reverse" => {
check_args!(1);
if let Value::NodeSet(ns) = arg!(0) {
let mut rev = ns;
rev.reverse();
return Ok(Value::NodeSet(rev));
}
if let Value::Sequence(items) = arg!(0) {
let mut rev = items;
rev.reverse();
return Ok(Value::Sequence(rev));
}
let mut pieces = sequence_to_strings(&arg!(0), idx);
pieces.reverse();
match idx.allocate_rtf_text_nodes(pieces.clone()) {
Some(ids) => Ok(Value::NodeSet(ids)),
None => Ok(Value::String(pieces.join(""))),
}
}
"unordered" => {
check_args!(1);
Ok(arg!(0))
}
"path" => {
if args.len() > 1 {
return Err(xpath_err(format!(
"path() requires 0 or 1 arguments (got {})", args.len()
)));
}
let v = if args.is_empty() {
Value::NodeSet(vec![ctx.context_node])
} else { arg!(0) };
let node = match v {
Value::NodeSet(ref ns) => ns.first().copied(),
_ => None,
};
let Some(node) = node else {
return Ok(Value::NodeSet(Vec::new()));
};
Ok(Value::String(node_path_string(node, idx)))
}
"sort" => {
if args.is_empty() || args.len() > 3 {
return Err(xpath_err(format!(
"sort() requires 1 to 3 arguments (got {})", args.len()
)));
}
if args.len() == 3 {
return Err(xpath_err(
"sort() with key-extractor function not supported"));
}
let input = arg!(0);
let mut items = items_of(&input);
let nums: Option<Vec<f64>> = items.iter().map(|v| {
let s = value_to_string_with(v, idx, ctx.bindings);
s.trim().parse::<f64>().ok()
}).collect();
if let Some(nums) = nums {
let mut indexed: Vec<(usize, f64)> = nums.into_iter().enumerate().collect();
indexed.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
let order: Vec<usize> = indexed.into_iter().map(|(i, _)| i).collect();
let sorted: Vec<Value> = order.into_iter().map(|i| items[i].clone()).collect();
return Ok(if sorted.len() == 1 { sorted.into_iter().next().unwrap() }
else { Value::Sequence(sorted) });
}
let keys: Vec<String> = items.iter()
.map(|v| value_to_string_with(v, idx, ctx.bindings))
.collect();
let mut order: Vec<usize> = (0..items.len()).collect();
order.sort_by(|&a, &b| keys[a].cmp(&keys[b]));
let sorted: Vec<Value> = order.into_iter().map(|i| std::mem::replace(
&mut items[i], Value::NodeSet(Vec::new()))).collect();
Ok(if sorted.len() == 1 { sorted.into_iter().next().unwrap() }
else { Value::Sequence(sorted) })
}
"head" => {
check_args!(1);
if let Value::NodeSet(ns) = arg!(0) {
return Ok(Value::NodeSet(ns.into_iter().take(1).collect()));
}
let pieces = sequence_to_strings(&arg!(0), idx);
Ok(match pieces.into_iter().next() {
Some(s) => Value::String(s),
None => Value::NodeSet(Vec::new()),
})
}
"tail" => {
check_args!(1);
if let Value::NodeSet(ns) = arg!(0) {
return Ok(Value::NodeSet(ns.into_iter().skip(1).collect()));
}
let pieces: Vec<String> = sequence_to_strings(&arg!(0), idx).into_iter().skip(1).collect();
match idx.allocate_rtf_text_nodes(pieces.clone()) {
Some(ids) => Ok(Value::NodeSet(ids)),
None => Ok(Value::String(pieces.join(""))),
}
}
"insert-before" => {
check_args!(3);
let seq = arg!(0);
let pos = value_to_number(&arg!(1), idx).round() as i64;
let ins = arg!(2);
let any_seq = matches!(seq, Value::Sequence(_))
|| matches!(ins, Value::Sequence(_));
if any_seq {
let to_items = |v: Value| -> Vec<Value> {
match v {
Value::Sequence(xs) => xs,
Value::NodeSet(ns) => ns.into_iter()
.map(|n| Value::NodeSet(vec![n])).collect(),
other => vec![other],
}
};
let mut a = to_items(seq);
let mut b = to_items(ins);
let p = if pos < 1 { 0 } else { (pos as usize - 1).min(a.len()) };
let tail = a.split_off(p);
a.append(&mut b);
a.extend(tail);
return Ok(Value::Sequence(a));
}
let into_ids = |v: Value| -> Vec<NodeId> {
match v.untyped() {
Value::NodeSet(ns) => ns,
Value::String(s) => idx.allocate_rtf_text_nodes(vec![s])
.unwrap_or_default(),
Value::Number(n) => idx.allocate_rtf_text_nodes(
vec![value_to_string(&Value::Number(n), idx)])
.unwrap_or_default(),
Value::Boolean(b) => idx.allocate_rtf_text_nodes(
vec![if b { "true".into() } else { "false".into() }])
.unwrap_or_default(),
Value::ForeignNodeSet(_) => Vec::new(),
Value::IntRange { lo, hi } => idx.allocate_rtf_text_nodes(
(lo..=hi).map(|i| i.to_string()).collect()
).unwrap_or_default(),
Value::Typed(_) | Value::Sequence(_) => unreachable!(),
Value::Map(_) | Value::Array(_) | Value::Function(_) => Vec::new(),
}
};
let mut a = into_ids(seq);
let mut b = into_ids(ins);
let p = if pos < 1 { 0 } else { (pos as usize - 1).min(a.len()) };
let tail = a.split_off(p);
a.append(&mut b);
a.extend(tail);
Ok(Value::NodeSet(a))
}
"remove" => {
check_args!(2);
let seq = arg!(0);
let pos = value_to_number(&arg!(1), idx).round() as i64;
if let Value::Sequence(mut items) = seq {
if pos >= 1 && (pos as usize) <= items.len() {
items.remove(pos as usize - 1);
}
return Ok(Value::Sequence(items));
}
let mut items: Vec<NodeId> = match seq.untyped() {
Value::NodeSet(ns) => ns,
Value::String(s) => idx.allocate_rtf_text_nodes(vec![s])
.unwrap_or_default(),
Value::Number(n) => idx.allocate_rtf_text_nodes(
vec![value_to_string(&Value::Number(n), idx)])
.unwrap_or_default(),
Value::Boolean(b) => idx.allocate_rtf_text_nodes(
vec![if b { "true".into() } else { "false".into() }])
.unwrap_or_default(),
Value::ForeignNodeSet(_) => Vec::new(),
Value::IntRange { lo, hi } => idx.allocate_rtf_text_nodes(
(lo..=hi).map(|i| i.to_string()).collect()
).unwrap_or_default(),
Value::Typed(_) | Value::Sequence(_) => unreachable!(),
Value::Map(_) | Value::Array(_) | Value::Function(_) => Vec::new(),
};
if pos >= 1 && (pos as usize) <= items.len() {
items.remove(pos as usize - 1);
}
Ok(Value::NodeSet(items))
}
"empty-sequence" => {
check_args!(0);
Ok(Value::NodeSet(Vec::new()))
}
"format-date" => {
if args.len() < 2 || args.len() > 5 {
return Err(xpath_err(format!(
"format-date() requires 2 to 5 arguments (got {})", args.len()
)));
}
let v = format_date_time_picture(&arg_str!(0), &arg_str!(1), DateKind::Date)?;
let lang = if args.len() > 2 { arg_str!(2) } else { String::new() };
let cal = if args.len() > 3 { arg_str!(3) } else { String::new() };
Ok(Value::String(format!("{}{v}", format_date_locale_prefix(&lang, &cal))))
}
"format-time" => {
if args.len() < 2 || args.len() > 5 {
return Err(xpath_err(format!(
"format-time() requires 2 to 5 arguments (got {})", args.len()
)));
}
let v = format_date_time_picture(&arg_str!(0), &arg_str!(1), DateKind::Time)?;
let lang = if args.len() > 2 { arg_str!(2) } else { String::new() };
let cal = if args.len() > 3 { arg_str!(3) } else { String::new() };
Ok(Value::String(format!("{}{v}", format_date_locale_prefix(&lang, &cal))))
}
"deep-equal" => {
if args.len() < 2 || args.len() > 3 {
return Err(xpath_err(format!(
"deep-equal() requires 2 or 3 arguments (got {})", args.len()
)));
}
let a = arg!(0);
let b = arg!(1);
if value_seq_has_function(&a) || value_seq_has_function(&b) {
return Err(xpath_err(
"deep-equal() cannot compare function items (FOTY0015)")
.with_xpath_code("FOTY0015"));
}
Ok(Value::Boolean(deep_equal_values(&a, &b, idx, ctx.bindings)))
}
"base-uri" => {
if args.len() > 1 {
return Err(xpath_err(format!(
"base-uri() requires 0 or 1 arguments (got {})", args.len()
)));
}
let target = if args.is_empty() {
Some(ctx.context_node)
} else {
match arg!(0) {
Value::NodeSet(ns) => ns.first().copied(),
Value::ForeignNodeSet(_) => None,
_ => return Err(xpath_err(
"base-uri() argument must be a node")),
}
};
let Some(start) = target else {
return Ok(Value::NodeSet(Vec::new()));
};
if idx.kind(start) == XPathNodeKind::Namespace {
return Ok(Value::NodeSet(Vec::new()));
}
let mut chain: Vec<String> = Vec::new();
let mut anchor: Option<String> = None;
let mut n = start;
loop {
if let Some(u) = ctx.bindings.node_base_uri(n) {
anchor = Some(u);
break;
}
if idx.kind(n) == XPathNodeKind::Element {
for a in idx.attr_range(n) {
if idx.local_name(a) == "base" && idx.namespace_uri(a) == "http://www.w3.org/XML/1998/namespace" {
chain.push(idx.string_value(a));
break;
}
}
}
match idx.parent(n) {
Some(p) => n = p,
None => break,
}
}
let mut base = anchor.or_else(|| ctx.bindings.static_base_uri());
for b in chain.into_iter().rev() {
base = Some(match base {
Some(r) => resolve_uri_against(&r, &b),
None => b,
});
}
match base {
Some(u) => Ok(typed_str("anyURI", u)),
None => Ok(Value::NodeSet(Vec::new())),
}
}
"static-base-uri" => {
if !args.is_empty() {
return Err(xpath_err("static-base-uri() takes no arguments"));
}
match ctx.bindings.static_base_uri() {
Some(s) => Ok(typed_str("anyURI", s)),
None => Ok(Value::NodeSet(vec![])),
}
}
"resolve-uri" => {
if args.is_empty() || args.len() > 2 {
return Err(xpath_err(format!(
"resolve-uri() requires 1 or 2 arguments (got {})", args.len()
)));
}
if let Value::NodeSet(ns) = &arg!(0) {
if ns.is_empty() { return Ok(Value::NodeSet(vec![])); }
}
if let Value::Sequence(items) = &arg!(0) {
if items.is_empty() { return Ok(Value::NodeSet(vec![])); }
}
let rel = value_to_string_with(&arg!(0), idx, ctx.bindings);
let base = if args.len() == 2 {
value_to_string_with(&arg!(1), idx, ctx.bindings)
} else {
ctx.bindings.static_base_uri().unwrap_or_default()
};
if args.len() == 2 && !base.is_empty() {
let valid_ref = |u: &str|
!u.contains(char::is_whitespace) && u.matches('#').count() <= 1;
if !valid_ref(&rel) || !valid_ref(&base) || !uri_has_scheme(&base) {
return Err(xpath_err(format!(
"resolve-uri: base '{base}' must be an absolute URI and \
both arguments valid URI references"
)).with_xpath_code("FORG0002"));
}
}
if rel.is_empty() && !base.is_empty() {
return Ok(typed_str("anyURI", base));
}
if base.is_empty() {
return Ok(typed_str("anyURI", rel));
}
Ok(typed_str("anyURI", resolve_uri_rfc3986(&base, &rel)))
}
"root" => {
let v = if args.is_empty() {
Value::NodeSet(vec![ctx.context_node])
} else { arg!(0) };
let node = match v {
Value::NodeSet(ref ns) => ns.first().copied(),
_ => None,
};
let r = node.map(|mut n| {
while let Some(p) = idx.parent(n) { n = p; }
vec![n]
}).unwrap_or_default();
Ok(Value::NodeSet(r))
}
"doc" => {
check_args!(1);
let arg0 = arg!(0);
if matches!(&arg0,
Value::NodeSet(ns) if ns.is_empty())
|| matches!(&arg0,
Value::Sequence(items) if items.is_empty())
{
return Ok(Value::NodeSet(Vec::new()));
}
let uri = value_to_string_with(&arg0, idx, ctx.bindings);
match ctx.bindings.call_function_in(
"", "document",
vec![Value::String(uri)],
ctx.context_node,
) {
Some(r) => r.map_err(|e| e.or_xpath_code("FODC0002")),
None => Ok(Value::NodeSet(Vec::new())),
}
}
"doc-available" => {
check_args!(1);
let arg0 = arg!(0);
if matches!(&arg0,
Value::NodeSet(ns) if ns.is_empty())
|| matches!(&arg0,
Value::Sequence(items) if items.is_empty())
{
return Ok(Value::Boolean(false));
}
let uri = value_to_string_with(&arg0, idx, ctx.bindings);
match ctx.bindings.call_function_in(
"", "document",
vec![Value::String(uri)],
ctx.context_node,
) {
Some(Ok(Value::NodeSet(ns))) => Ok(Value::Boolean(!ns.is_empty())),
_ => Ok(Value::Boolean(false)),
}
}
"document-uri" => {
if args.len() > 1 {
return Err(xpath_err(format!(
"document-uri() takes 0 or 1 arguments (got {})", args.len()
)));
}
let target = if args.is_empty() {
Some(ctx.context_node)
} else {
match arg!(0) {
Value::NodeSet(ns) => ns.first().copied(),
_ => None,
}
};
let uri = target.filter(|&n| idx.kind(n) == XPathNodeKind::Document)
.and_then(|n| ctx.bindings.node_base_uri(n));
match uri {
Some(u) => Ok(typed_str("anyURI", u)),
None => Ok(Value::NodeSet(Vec::new())),
}
}
"round-half-to-even" => {
if args.is_empty() || args.len() > 2 {
return Err(xpath_err(format!(
"round-half-to-even() takes 1 or 2 arguments (got {})", args.len()
)));
}
let a = arg!(0);
let precision = if args.len() == 2 {
value_to_number(&arg!(1), idx) as i32
} else { 0 };
match &a {
Value::Number(Numeric::Decimal(d)) =>
Ok(Value::Number(Numeric::Decimal(round_decimal_half_to_even(*d, precision)))),
Value::Number(Numeric::Integer(i)) => {
use rust_decimal::prelude::ToPrimitive;
let d = round_decimal_half_to_even(rust_decimal::Decimal::from(*i), precision);
Ok(Value::Number(match d.to_i64() {
Some(v) => Numeric::Integer(v),
None => Numeric::Decimal(d),
}))
}
_ => {
let n = value_to_number(&a, idx);
let scale = 10f64.powi(precision);
let scaled = n * scale;
let rounded = if scaled.fract().abs() == 0.5 {
let floor = scaled.floor();
if (floor as i64) % 2 == 0 { floor } else { floor + 1.0 }
} else {
scaled.round()
};
let result = rounded / scale;
let result = if result == 0.0 { 0.0 } else { result };
Ok(preserve_numeric_kind(&a, result))
}
}
}
"collection" => {
if args.len() > 1 {
return Err(xpath_err("collection() takes 0 or 1 arguments"));
}
Ok(Value::NodeSet(Vec::new()))
}
"dateTime" => {
check_args!(2);
let d = value_to_string_with(&arg!(0), idx, ctx.bindings);
let t = value_to_string_with(&arg!(1), idx, ctx.bindings);
match (parse_xsd_date_time(&d, DateKind::Date),
parse_xsd_date_time(&t, DateKind::Time)) {
(Some((y, mo, dd, _, _, _, _, tz_d)),
Some((_, _, _, h, mi, s, frac, tz_t))) => {
let tz = match (tz_d, tz_t) {
(Some(a), Some(b)) if a != b => return Err(xpath_err(
"dateTime(): the date and time have inconsistent timezones (FORG0008)")),
(Some(a), _) => Some(a),
(_, b) => b,
};
Ok(Value::String(format_datetime_lexical(y, mo, dd, h, mi, s, frac, tz)))
}
_ => Ok(Value::String(format!("{}T{}", d.trim(), t.trim()))),
}
}
"local-name-from-QName" => {
check_args!(1);
let a = arg!(0);
if sequence_len(&a) == 0 { return Ok(Value::NodeSet(Vec::new())); }
let s = value_to_string_with(&a, idx, ctx.bindings);
let local = if let Some(i) = s.rfind('}') {
s[i + 1..].to_string()
} else if let Some(i) = s.rfind(':') {
s[i + 1..].to_string()
} else { s };
Ok(typed_str("NCName", local))
}
"prefix-from-QName" => {
check_args!(1);
let s = value_to_string_with(&arg!(0), idx, ctx.bindings);
if s.starts_with('{') {
return Ok(Value::String(String::new()));
}
let prefix = s.split_once(':').map(|(p, _)| p).unwrap_or("");
Ok(Value::String(prefix.to_string()))
}
"namespace-uri-from-QName" => {
check_args!(1);
let a = arg!(0);
if sequence_len(&a) == 0 { return Ok(Value::NodeSet(Vec::new())); }
let s = value_to_string_with(&a, idx, ctx.bindings);
if s.starts_with('{') {
if let Some(end) = s.find('}') {
return Ok(typed_str("anyURI", s[1..end].to_string()));
}
}
Ok(Value::String(String::new()))
}
"QName" => {
check_args!(2);
let uri = value_to_string_with(&arg!(0), idx, ctx.bindings);
let lex = value_to_string_with(&arg!(1), idx, ctx.bindings);
let local = lex.rsplit(':').next().unwrap_or(&lex);
if uri.is_empty() {
Ok(Value::String(lex.clone()))
} else {
Ok(Value::String(format!("{{{uri}}}{local}")))
}
}
"resolve-QName" => {
check_args!(2);
if matches!(&arg!(0), Value::NodeSet(ns) if ns.is_empty()) {
return Ok(Value::NodeSet(Vec::new()));
}
let lex = value_to_string_with(&arg!(0), idx, ctx.bindings);
if !is_valid_lexical_qname(&lex) {
return Err(xpath_err(format!(
"resolve-QName: '{lex}' is not a valid lexical QName"
)).with_xpath_code("FOCA0002"));
}
let (prefix, local) = match lex.split_once(':') {
Some((p, l)) => (p, l),
None => ("", lex.as_str()),
};
let elem = match arg!(1) {
Value::NodeSet(ref ns) => ns.first().copied(),
_ => None,
};
let uri = elem.and_then(|id| {
idx.ns_range(id)
.into_iter()
.find(|&ns_id| {
let p = idx.local_name(ns_id);
(prefix.is_empty() && p.is_empty()) || p == prefix
})
.map(|ns_id| idx.string_value(ns_id))
.or_else(|| (prefix == "xml")
.then(|| "http://www.w3.org/XML/1998/namespace".to_string()))
});
match uri {
Some(u) if !u.is_empty() => Ok(Value::String(format!("{{{u}}}{local}"))),
_ if !prefix.is_empty() => Err(xpath_err(format!(
"resolve-QName: no namespace declaration for prefix '{prefix}'"
)).with_xpath_code("FONS0004")),
_ => Ok(Value::String(local.to_string())),
}
}
"normalize-unicode" => {
if args.is_empty() || args.len() > 2 {
return Err(xpath_err("normalize-unicode() takes 1 or 2 arguments"));
}
let s = value_to_string_with(&arg!(0), idx, ctx.bindings);
Ok(Value::String(s))
}
"type-available" => {
check_args!(1);
let name = value_to_string_with(&arg!(0), idx, ctx.bindings);
let (prefix, local) = match name.split_once(':') {
Some((p, l)) => (Some(p), l),
None => (None, name.as_str()),
};
let uri = match prefix {
Some(p) => ctx.bindings.resolve_prefix(p)
.or_else(|| match p {
"xml" => Some("http://www.w3.org/XML/1998/namespace".into()),
"xs" => Some("http://www.w3.org/2001/XMLSchema".into()),
_ => None,
}),
None => None,
};
let in_xsd = uri.as_deref() == Some("http://www.w3.org/2001/XMLSchema");
if prefix.is_none() {
return Ok(Value::Boolean(false));
}
if !in_xsd {
return Ok(Value::Boolean(false));
}
let known = matches!(local,
"anyType" | "anySimpleType" | "anyAtomicType" | "untyped" | "untypedAtomic"
| "string" | "boolean" | "decimal" | "float" | "double"
| "integer" | "long" | "int" | "short" | "byte"
| "nonNegativeInteger" | "nonPositiveInteger"
| "positiveInteger" | "negativeInteger"
| "unsignedLong" | "unsignedInt" | "unsignedShort" | "unsignedByte"
| "duration" | "dateTime" | "time" | "date"
| "dayTimeDuration" | "yearMonthDuration"
| "gYearMonth" | "gYear" | "gMonth" | "gMonthDay" | "gDay"
| "hexBinary" | "base64Binary" | "anyURI" | "QName" | "NOTATION"
| "normalizedString" | "token" | "language"
| "Name" | "NCName" | "ID" | "IDREF" | "IDREFS"
| "ENTITY" | "ENTITIES" | "NMTOKEN" | "NMTOKENS"
| "numeric"
);
Ok(Value::Boolean(known))
}
"unparsed-entity-public-id" => {
check_args!(1);
Ok(Value::String(String::new()))
}
"in-scope-prefixes" => {
check_args!(1);
let elem = match arg!(0) {
Value::NodeSet(ref ns) => ns.first().copied(),
_ => None,
};
let Some(id) = elem else {
return Ok(Value::NodeSet(Vec::new()));
};
let mut out: Vec<String> = Vec::new();
for ns_id in idx.ns_range(id) {
let p = idx.local_name(ns_id);
out.push(if p.is_empty() { "".into() } else { p.to_string() });
}
if !out.iter().any(|s| s == "xml") {
out.push("xml".into());
}
match idx.allocate_rtf_text_nodes(out.clone()) {
Some(ids) => Ok(Value::NodeSet(ids)),
None => Ok(Value::String(out.join(" "))),
}
}
"namespace-uri-for-prefix" => {
check_args!(2);
let prefix = value_to_string_with(&arg!(0), idx, ctx.bindings);
let elem = match arg!(1) {
Value::NodeSet(ref ns) => ns.first().copied(),
_ => None,
};
let Some(id) = elem else {
return Ok(Value::NodeSet(Vec::new()));
};
for ns_id in idx.ns_range(id) {
let p = idx.local_name(ns_id);
let match_default = prefix.is_empty() && p.is_empty();
if match_default || p == prefix {
return Ok(Value::String(idx.string_value(ns_id)));
}
}
if prefix == "xml" {
return Ok(Value::String("http://www.w3.org/XML/1998/namespace".into()));
}
Ok(Value::NodeSet(Vec::new()))
}
"year-from-dateTime" | "year-from-date" => {
check_args!(1);
let s = value_to_string_with(&arg!(0), idx, ctx.bindings);
let v = s.trim();
if v.is_empty() {
return Ok(Value::NodeSet(Vec::new()));
}
let (sign, rest) = if let Some(r) = v.strip_prefix('-') { (-1i64, r) } else { (1i64, v) };
let yr_end = rest.find('-').unwrap_or(rest.len());
let yr: i64 = rest[..yr_end].parse().map_err(|_|
xpath_err(format!("invalid date-like value: {s:?}")))?;
Ok(Value::Number(Numeric::Double((sign * yr) as f64)))
}
"month-from-dateTime" | "month-from-date" => {
check_args!(1);
let s = value_to_string_with(&arg!(0), idx, ctx.bindings);
let v = s.trim();
if v.is_empty() {
return Ok(Value::NodeSet(Vec::new()));
}
let body = v.strip_prefix('-').unwrap_or(v);
let after_year = body.split_once('-').map(|(_, r)| r).unwrap_or("");
let mm: u32 = after_year.get(..2).and_then(|s| s.parse().ok())
.ok_or_else(|| xpath_err(format!("invalid date-like value: {s:?}")))?;
Ok(Value::Number(Numeric::Double(mm as f64)))
}
"day-from-dateTime" | "day-from-date" => {
check_args!(1);
let s = value_to_string_with(&arg!(0), idx, ctx.bindings);
let v = s.trim();
if v.is_empty() {
return Ok(Value::NodeSet(Vec::new()));
}
let body = v.strip_prefix('-').unwrap_or(v);
let mut parts = body.splitn(3, '-');
parts.next(); parts.next();
let dd_seg = parts.next().unwrap_or("");
let dd: u32 = dd_seg.get(..2).and_then(|s| s.parse().ok())
.ok_or_else(|| xpath_err(format!("invalid date-like value: {s:?}")))?;
Ok(Value::Number(Numeric::Double(dd as f64)))
}
"hours-from-dateTime" | "hours-from-time" => {
check_args!(1);
let s = value_to_string_with(&arg!(0), idx, ctx.bindings);
let t = s.split_once('T').map(|(_, r)| r).unwrap_or(s.trim());
let hh: u32 = t.get(..2).and_then(|s| s.parse().ok())
.ok_or_else(|| xpath_err(format!("invalid time-like value: {s:?}")))?;
Ok(Value::Number(Numeric::Double(hh as f64)))
}
"minutes-from-dateTime" | "minutes-from-time" => {
check_args!(1);
let s = value_to_string_with(&arg!(0), idx, ctx.bindings);
let t = s.split_once('T').map(|(_, r)| r).unwrap_or(s.trim());
let mm: u32 = t.get(3..5).and_then(|s| s.parse().ok())
.ok_or_else(|| xpath_err(format!("invalid time-like value: {s:?}")))?;
Ok(Value::Number(Numeric::Double(mm as f64)))
}
"seconds-from-dateTime" | "seconds-from-time" => {
check_args!(1);
let s = value_to_string_with(&arg!(0), idx, ctx.bindings);
let t = s.split_once('T').map(|(_, r)| r).unwrap_or(s.trim());
let after = t.get(6..).unwrap_or("");
let end = after.find(|c: char| c == 'Z' || c == '+' || c == '-')
.unwrap_or(after.len());
let ss: f64 = after[..end].parse()
.map_err(|_| xpath_err(format!("invalid time-like value: {s:?}")))?;
Ok(Value::Number(Numeric::Double(ss)))
}
"timezone-from-dateTime" | "timezone-from-date" | "timezone-from-time" => {
check_args!(1);
let s = value_to_string_with(&arg!(0), idx, ctx.bindings);
let v = s.trim();
if v.is_empty() {
return Ok(Value::NodeSet(Vec::new()));
}
let tz_start = v.rfind(|c: char| c == 'Z' || c == '+' || c == '-')
.filter(|&i| i > 0);
let tz = match tz_start.map(|i| &v[i..]) {
Some("Z") => "PT0S".to_string(),
Some(off) if off.len() == 6 => {
let sign = &off[0..1];
let hh: i32 = off[1..3].parse().unwrap_or(0);
let mm: i32 = off[4..6].parse().unwrap_or(0);
if hh == 0 && mm == 0 { "PT0S".to_string() }
else if mm == 0 { format!("{sign}PT{hh}H") }
else { format!("{sign}PT{hh}H{mm}M") }
}
_ => return Ok(Value::NodeSet(Vec::new())),
};
Ok(Value::String(tz.replace("+", "")))
}
"years-from-duration" | "months-from-duration"
| "days-from-duration" | "hours-from-duration"
| "minutes-from-duration" | "seconds-from-duration" => {
check_args!(1);
let s = value_to_string_with(&arg!(0), idx, ctx.bindings);
let (sign, rest) = if let Some(r) = s.trim().strip_prefix('-') {
(-1.0_f64, r)
} else { (1.0, s.trim()) };
let body = rest.strip_prefix('P').unwrap_or(rest);
let (date_part, time_part) = match body.find('T') {
Some(i) => (&body[..i], &body[i + 1..]),
None => (body, ""),
};
fn extract(part: &str, marker: char) -> f64 {
if let Some(i) = part.find(marker) {
let start = part[..i].rfind(|c: char| !c.is_ascii_digit() && c != '.')
.map(|n| n + 1).unwrap_or(0);
part[start..i].parse().unwrap_or(0.0)
} else { 0.0 }
}
let years = extract(date_part, 'Y');
let months = extract(date_part, 'M');
let days = extract(date_part, 'D');
let hours = extract(time_part, 'H');
let minutes = extract(time_part, 'M');
let seconds = extract(time_part, 'S');
let v = match name.as_ref() {
"years-from-duration" => years,
"months-from-duration" => months,
"days-from-duration" => days,
"hours-from-duration" => hours,
"minutes-from-duration" => minutes,
"seconds-from-duration" => seconds,
_ => unreachable!(),
};
Ok(Value::Number(Numeric::Double(sign * v)))
}
"nilled" => {
if args.len() != 1 {
return Err(xpath_err("nilled() requires one argument"));
}
let v = arg!(0);
let n = match v {
Value::NodeSet(ref ns) => ns.first().copied(),
_ => return Ok(Value::NodeSet(Vec::new())),
};
let id = match n { Some(id) => id, None => return Ok(Value::NodeSet(Vec::new())) };
if !matches!(idx.kind(id), crate::xpath::XPathNodeKind::Element) {
return Ok(Value::NodeSet(Vec::new()));
}
Ok(Value::Boolean(false))
}
"default-collation" => {
check_args!(0);
Ok(Value::String("http://www.w3.org/2005/xpath-functions/collation/codepoint".into()))
}
"implicit-timezone" => {
check_args!(0);
Ok(Value::String("PT0S".into()))
}
"format-dateTime" => {
if args.len() < 2 || args.len() > 5 {
return Err(xpath_err(format!(
"format-dateTime() requires 2 to 5 arguments (got {})", args.len()
)));
}
let v = format_date_time_picture(&arg_str!(0), &arg_str!(1), DateKind::DateTime)?;
let lang = if args.len() > 2 { arg_str!(2) } else { String::new() };
let cal = if args.len() > 3 { arg_str!(3) } else { String::new() };
Ok(Value::String(format!("{}{v}", format_date_locale_prefix(&lang, &cal))))
}
"compare" => {
if args.len() < 2 || args.len() > 3 {
return Err(xpath_err(format!(
"compare() requires 2 or 3 arguments (got {})", args.len()
)));
}
let a = arg_str!(0);
let b = arg_str!(1);
let coll = effective_collation(if args.len() == 3 { Some(arg_str!(2)) } else { None });
let (ka, kb) = if is_ascii_ci_collation(coll.as_deref()) {
(ascii_ci_fold(&a), ascii_ci_fold(&b))
} else {
(a, b)
};
Ok(Value::Number(Numeric::Double(match ka.cmp(&kb) {
std::cmp::Ordering::Less => -1.0,
std::cmp::Ordering::Equal => 0.0,
std::cmp::Ordering::Greater => 1.0,
})))
}
"codepoint-equal" => {
check_args!(2);
Ok(Value::Boolean(arg_str!(0) == arg_str!(1)))
}
"string-to-codepoints" => {
check_args!(1);
let s = arg_str!(0);
let pieces: Vec<String> = s.chars().map(|c| (c as u32).to_string()).collect();
match idx.allocate_rtf_text_nodes(pieces.clone()) {
Some(ids) => Ok(Value::NodeSet(ids)),
None => Ok(Value::String(pieces.join(" "))),
}
}
"codepoints-to-string" => {
check_args!(1);
let codes = sequence_to_numbers(&arg!(0), idx);
let mut out = String::with_capacity(codes.len());
for n in codes {
if let Some(c) = u32::try_from(n as i64).ok().and_then(char::from_u32) {
out.push(c);
}
}
Ok(Value::String(out))
}
"encode-for-uri" => {
check_args!(1);
let s = arg_str!(0);
let mut out = String::with_capacity(s.len());
for c in s.chars() {
if c.is_ascii_alphanumeric()
|| matches!(c, '-' | '.' | '_' | '~')
{
out.push(c);
} else {
let mut buf = [0u8; 4];
for b in c.encode_utf8(&mut buf).as_bytes() {
let _ = write!(out, "%{:02X}", b);
}
}
}
Ok(Value::String(out))
}
"iri-to-uri" => {
check_args!(1);
let s = arg_str!(0);
let mut out = String::with_capacity(s.len());
for c in s.chars() {
if (c as u32) < 0x80
&& !matches!(c, ' ' | '<' | '>' | '"' | '{' | '}' | '|' | '\\' | '^' | '`')
{
out.push(c);
} else {
let mut buf = [0u8; 4];
for b in c.encode_utf8(&mut buf).as_bytes() {
let _ = write!(out, "%{:02X}", b);
}
}
}
Ok(Value::String(out))
}
"escape-html-uri" => {
check_args!(1);
let s = arg_str!(0);
let mut out = String::with_capacity(s.len());
for c in s.chars() {
if (c as u32) < 0x80 { out.push(c); }
else {
let mut buf = [0u8; 4];
for b in c.encode_utf8(&mut buf).as_bytes() {
let _ = write!(out, "%{:02X}", b);
}
}
}
Ok(Value::String(out))
}
"error" => {
let code_local = if args.is_empty() { None } else {
let s = arg_str!(0);
s.rsplit([':', '}']).next()
.filter(|l| !l.is_empty())
.map(|l| l.to_string())
};
let msg = if args.len() >= 2 { arg_str!(1) }
else if args.is_empty() { "fn:error invoked".to_string() }
else { arg_str!(0) };
let mut e = xpath_err(format!("fn:error: {msg}"));
if let Some(code) = code_local { e = e.with_xpath_code(code); }
Err(e)
}
"trace" => {
if args.is_empty() || args.len() > 2 {
return Err(xpath_err(format!(
"trace() requires 1 or 2 arguments (got {})", args.len()
)));
}
let v = arg!(0);
if args.len() == 2 {
eprintln!("[xpath trace] {}: {}", arg_str!(1), value_to_string(&v, idx));
}
Ok(v)
}
"exactly-one" => {
check_args!(1);
let v = arg!(0);
let n = sequence_len(&v);
if n != 1 {
return Err(xpath_err(format!(
"exactly-one(): expected one item, got {n}"
)));
}
Ok(v)
}
"one-or-more" => {
check_args!(1);
let v = arg!(0);
if sequence_len(&v) < 1 {
return Err(xpath_err("one-or-more(): empty input"));
}
Ok(v)
}
"zero-or-one" => {
check_args!(1);
let v = arg!(0);
let n = sequence_len(&v);
if n > 1 {
return Err(xpath_err(format!(
"zero-or-one(): expected at most one item, got {n}"
)));
}
Ok(v)
}
"node-name" => {
check_args!(1);
match arg!(0) {
Value::NodeSet(ns) => {
let Some(&id) = ns.first() else {
return Ok(Value::NodeSet(Vec::new()));
};
let local = idx.local_name(id);
let uri = idx.namespace_uri(id);
let lex = if uri.is_empty() {
local.to_string()
} else {
format!("{{{uri}}}{local}")
};
Ok(Value::Typed(Box::new(TypedAtomic {
kind: "QName",
lexical: lex,
numeric: None,
boolean: None, user_type: None,
})))
}
_ => Ok(Value::NodeSet(Vec::new())),
}
}
"function-available" if !args.is_empty() => {
let qname = arg_str!(0);
Ok(Value::Boolean(xpath_function_available(&qname, ctx)))
}
"current" if args.is_empty() => {
Ok(Value::NodeSet(vec![ctx.static_ctx.current_node.unwrap_or(ctx.context_node)]))
}
_ => Err(xpath_err(format!("unknown XPath function: {name}()"))),
}
}
fn xpath_function_available(qname: &str, ctx: &EvalCtx<'_>) -> bool {
use super::exslt;
match qname.split_once(':') {
Some((prefix, _local)) => {
match resolve_prefix_or_implicit(ctx.bindings, prefix) {
Some(uri) => matches!(uri.as_str(),
exslt::MATH_NS | exslt::DATE_NS | exslt::STR_NS | exslt::SET_NS
| exslt::REGEXP_NS | exslt::COMMON_NS | exslt::DYN_NS),
None => false,
}
}
None => matches!(qname,
"last" | "position" | "count" | "id" | "local-name" | "namespace-uri"
| "name" | "string" | "concat" | "starts-with" | "contains"
| "substring-before" | "substring-after" | "substring" | "string-length"
| "normalize-space" | "translate" | "boolean" | "not" | "true" | "false"
| "lang" | "number" | "sum" | "floor" | "ceiling" | "round"
| "document" | "key" | "format-number" | "current" | "unparsed-entity-uri"
| "generate-id" | "system-property" | "element-available" | "function-available"),
}
}
fn node_set_of(v: Value) -> Vec<NodeId> {
match v {
Value::NodeSet(ns) => ns,
_ => Vec::new(),
}
}
fn resolve_kind_test_namespaces(
st: &crate::xpath::ast::SequenceType,
bindings: &dyn XPathBindings,
) -> crate::xpath::ast::SequenceType {
use crate::xpath::ast::ItemType;
let resolve = |name: &Option<String>| -> Option<String> {
let n = name.as_ref()?;
let (prefix, local) = n.split_once(':')?;
let uri = resolve_prefix_or_implicit(bindings, prefix)?;
Some(format!("{{{uri}}}{local}"))
};
let item = match &st.item {
ItemType::Element(name @ Some(_)) =>
ItemType::Element(resolve(name).or_else(|| name.clone())),
ItemType::Attribute(name @ Some(_)) =>
ItemType::Attribute(resolve(name).or_else(|| name.clone())),
other => other.clone(),
};
crate::xpath::ast::SequenceType { item, occurrence: st.occurrence }
}
fn kind_test_name_matches<I: DocIndexLike>(
name: Option<&str>, id: NodeId, idx: &I,
) -> bool {
match name {
None => true,
Some(n) => match n.strip_prefix('{').and_then(|r| r.split_once('}')) {
Some((uri, local)) =>
idx.namespace_uri(id) == uri && idx.local_name(id) == local,
None if n.contains(':') => idx.node_name(id) == n,
None => idx.local_name(id) == n,
},
}
}
fn value_matches_sequence_type<I: DocIndexLike>(
v: &Value, st: &crate::xpath::ast::SequenceType, idx: &I,
) -> bool {
use crate::xpath::ast::{ItemType, Occurrence};
let count = sequence_len(v);
let card_ok = match st.occurrence {
Occurrence::One => count == 1,
Occurrence::Optional => count <= 1,
Occurrence::OneOrMore => count >= 1,
Occurrence::ZeroOrMore => true,
};
if !card_ok { return false; }
if count == 0 { return true; }
match &st.item {
ItemType::Any => true,
ItemType::Atomic(name) => {
let try_one = |s: &str| atomic_string_castable(s, name);
match v {
Value::String(_) => matches!(name.as_str(),
"string" | "untypedAtomic" | "anyAtomicType" | "anySimpleType"),
Value::Number(n) => name == "numeric"
|| xsd_is_subtype_of(n.kind(), name),
Value::Boolean(_) => matches!(name.as_str(),
"boolean" | "anyAtomicType"),
Value::NodeSet(ns) => ns.iter().all(|&id|
crate::xpath::is_synthetic_id(id)
&& try_one(&idx.string_value(id))),
Value::ForeignNodeSet(_) => true,
Value::Typed(t) => xsd_is_subtype_of(t.kind, name),
Value::Sequence(items) => items.iter().all(|item|
value_matches_sequence_type(item, st, idx)),
Value::IntRange { .. } => name == "numeric"
|| xsd_is_subtype_of("integer", name),
Value::Map(_) | Value::Array(_) | Value::Function(_) => false,
}
}
ItemType::AnyNode => matches!(v,
Value::NodeSet(_) | Value::ForeignNodeSet(_)),
ItemType::Element(name) => match v {
Value::NodeSet(ns) => ns.iter().all(|&id|
matches!(idx.kind(id), crate::xpath::XPathNodeKind::Element)
&& kind_test_name_matches(name.as_deref(), id, idx)),
_ => false,
},
ItemType::Attribute(name) => match v {
Value::NodeSet(ns) => ns.iter().all(|&id|
matches!(idx.kind(id), crate::xpath::XPathNodeKind::Attribute)
&& kind_test_name_matches(name.as_deref(), id, idx)),
_ => false,
},
ItemType::Text => match v {
Value::NodeSet(ns) => ns.iter().all(|&id|
matches!(idx.kind(id),
crate::xpath::XPathNodeKind::Text |
crate::xpath::XPathNodeKind::CData)),
_ => false,
},
ItemType::Comment => match v {
Value::NodeSet(ns) => ns.iter().all(|&id|
matches!(idx.kind(id), crate::xpath::XPathNodeKind::Comment)),
_ => false,
},
ItemType::PI(target) => match v {
Value::NodeSet(ns) => ns.iter().all(|&id|
matches!(idx.kind(id), crate::xpath::XPathNodeKind::PI)
&& target.as_ref().map_or(true, |t| idx.pi_target(id) == t)),
_ => false,
},
ItemType::Document => match v {
Value::NodeSet(ns) => ns.iter().all(|&id|
matches!(idx.kind(id), crate::xpath::XPathNodeKind::Document)),
_ => false,
},
ItemType::Function(want) => match v {
Value::Function(fi) => match want {
None => true,
Some(want) => fi.arity() == want.params.len()
&& match fi.declared_sig() {
Some(have) => function_sig_subtype_of(have, want),
None => true,
},
},
Value::Sequence(items) => items.iter().all(|item|
value_matches_sequence_type(item, st, idx)),
_ => false,
},
ItemType::Map => match v {
Value::Map(_) => true,
Value::Sequence(items) => items.iter().all(|item|
value_matches_sequence_type(item, st, idx)),
_ => false,
},
ItemType::Array => match v {
Value::Array(_) => true,
Value::Sequence(items) => items.iter().all(|item|
value_matches_sequence_type(item, st, idx)),
_ => false,
},
ItemType::EmptySequence => false,
}
}
fn occurrence_subsumes(sub: Occurrence, sup: Occurrence) -> bool {
let allows_zero = |o: Occurrence| matches!(o, Occurrence::Optional | Occurrence::ZeroOrMore);
let allows_many = |o: Occurrence| matches!(o, Occurrence::OneOrMore | Occurrence::ZeroOrMore);
(!allows_zero(sub) || allows_zero(sup)) && (!allows_many(sub) || allows_many(sup))
}
fn sequence_type_subtype_of(a: &SequenceType, b: &SequenceType) -> bool {
occurrence_subsumes(a.occurrence, b.occurrence)
&& item_type_subtype_of(&a.item, &b.item)
}
fn item_type_subtype_of(a: &ItemType, b: &ItemType) -> bool {
match (a, b) {
(_, ItemType::Any) => true,
(ItemType::EmptySequence, _) => true,
(ItemType::Atomic(x), ItemType::Atomic(y)) => xsd_is_subtype_of(x, y),
(ItemType::AnyNode, ItemType::AnyNode) => true,
(ItemType::Element(_) | ItemType::Attribute(_) | ItemType::Text
| ItemType::Comment | ItemType::PI(_) | ItemType::Document,
ItemType::AnyNode) => true,
(ItemType::Element(x), ItemType::Element(y)) => y.is_none() || x == y,
(ItemType::Attribute(x), ItemType::Attribute(y)) => y.is_none() || x == y,
(ItemType::PI(x), ItemType::PI(y)) => y.is_none() || x == y,
(ItemType::Text, ItemType::Text)
| (ItemType::Comment, ItemType::Comment)
| (ItemType::Document, ItemType::Document) => true,
(ItemType::Function(_), ItemType::Function(None)) => true,
(ItemType::Function(Some(sa)), ItemType::Function(Some(sb))) =>
function_sig_subtype_of(sa, sb),
(ItemType::Map, ItemType::Map) => true,
(ItemType::Array, ItemType::Array) => true,
_ => false,
}
}
fn function_sig_subtype_of(have: &FunctionSig, want: &FunctionSig) -> bool {
have.params.len() == want.params.len()
&& sequence_type_subtype_of(&have.ret, &want.ret)
&& have.params.iter().zip(&want.params)
.all(|(hp, wp)| sequence_type_subtype_of(wp, hp))
}
fn lexical_matches_type(s: &str, name: &str) -> bool {
if s.is_empty() { return false; }
let bytes = s.as_bytes();
match name {
"date" => {
s.len() >= 10
&& parse_xsd_date_time(s, DateKind::Date).is_some()
}
"dateTime" => {
s.contains('T') && parse_xsd_date_time(s, DateKind::DateTime).is_some()
}
"time" => {
bytes.len() >= 8 && bytes[2] == b':' && bytes[5] == b':'
&& parse_xsd_date_time(s, DateKind::Time).is_some()
}
"duration" => {
let rest = s.strip_prefix('-').unwrap_or(s);
rest.starts_with('P') && rest.len() > 1
}
"dayTimeDuration" => {
let rest = s.strip_prefix('-').unwrap_or(s);
rest.starts_with('P')
&& !rest.contains('Y')
&& !rest_contains_month_designator(rest)
}
"yearMonthDuration" => {
let rest = s.strip_prefix('-').unwrap_or(s);
rest.starts_with('P')
&& !rest.contains('D')
&& !rest.contains('T')
}
"gYear" => s.len() >= 4 && bytes.iter().all(|&b| b == b'-' || b.is_ascii_digit() || b == b'+' || b == b'Z' || b == b':'),
"gYearMonth" => s.contains('-') && s.len() >= 7,
"gMonth" => s.starts_with("--") && s.len() >= 4,
"gMonthDay" => s.starts_with("--") && s.len() >= 7,
"gDay" => s.starts_with("---") && s.len() >= 5,
"hexBinary" => s.len() % 2 == 0 && bytes.iter().all(|b| b.is_ascii_hexdigit()),
"base64Binary" => true, _ => true,
}
}
fn date_year_out_of_range(s: &str, name: &str) -> bool {
if !matches!(name, "date" | "dateTime" | "gYear" | "gYearMonth") {
return false;
}
let neg = s.starts_with('-');
let body = s.strip_prefix('-').unwrap_or(s);
let ylen = body.bytes().take_while(u8::is_ascii_digit).count();
if ylen == 0 { return false; }
let ynum = &body[..ylen];
if ynum.parse::<i32>().is_ok() || ynum.parse::<i128>().is_err() {
return false;
}
let probe = format!("{}0001{}", if neg { "-" } else { "" }, &body[ylen..]);
lexical_matches_type(&probe, name)
}
fn rest_contains_month_designator(s: &str) -> bool {
let t_pos = s.find('T');
for (i, c) in s.char_indices() {
if c == 'M' {
match t_pos {
Some(t) if i > t => continue,
_ => return true,
}
}
}
false
}
fn atomic_string_castable(s: &str, name: &str) -> bool {
match name {
"string" | "anyURI" | "anyAtomicType" | "untypedAtomic" => true,
"boolean" => matches!(s.trim(), "true" | "false" | "1" | "0"),
"integer" | "long" | "int" | "short" | "byte"
| "nonNegativeInteger" | "nonPositiveInteger"
| "positiveInteger" | "negativeInteger"
| "unsignedLong" | "unsignedInt" | "unsignedShort" | "unsignedByte"
=> s.trim().parse::<i64>().is_ok(),
"decimal" => s.trim().parse::<f64>().is_ok() && !s.trim().contains(['e', 'E']),
"double" | "float" | "numeric" => s.trim().parse::<f64>().is_ok()
|| matches!(s.trim(), "NaN" | "INF" | "-INF" | "Infinity" | "-Infinity"),
"date" => parse_xsd_date_only(s).is_some(),
"dateTime" => parse_xsd_date_time(s, DateKind::DateTime).is_some(),
"time" => parse_xsd_date_time(s, DateKind::Time).is_some(),
_ => true, }
}
fn canonical_float_lex(n: f64, source: &Value) -> String {
let f = n as f32;
if f.is_nan() { return "NaN".to_string(); }
if f.is_infinite() { return (if f > 0.0 { "INF" } else { "-INF" }).to_string(); }
let is_neg_zero = f == 0.0
&& (f.is_sign_negative()
|| matches!(source, Value::Number(v) if v.as_f64().is_sign_negative())
|| matches!(source, Value::String(s) if s.trim().starts_with('-')));
if f == 0.0 {
return if is_neg_zero { "-0".to_string() } else { "0".to_string() };
}
let abs = f.abs();
if (1e-6..1e7).contains(&abs) {
if f.fract() == 0.0 {
return format!("{}", f as i64);
}
return format!("{f}");
}
let formatted = format!("{f:E}");
let (mantissa, exp) = formatted.split_once('E').unwrap_or((formatted.as_str(), "0"));
let mantissa = if mantissa.contains('.') { mantissa.to_string() }
else { format!("{mantissa}.0") };
let exp = exp.trim_start_matches('+');
format!("{mantissa}E{exp}")
}
fn canonical_double_lex(n: f64, source: &Value) -> String {
if n.is_nan() { return "NaN".to_string(); }
if n.is_infinite() { return (if n > 0.0 { "INF" } else { "-INF" }).to_string(); }
let is_neg_zero = n == 0.0
&& (n.is_sign_negative()
|| matches!(source, Value::Number(v) if v.as_f64().is_sign_negative())
|| matches!(source, Value::String(s) if s.trim().starts_with('-')));
if n == 0.0 {
return if is_neg_zero { "-0".to_string() } else { "0".to_string() };
}
let abs = n.abs();
if abs >= 1e-6 && abs < 1e7 {
if n.fract() == 0.0 {
return format!("{}", n as i64);
}
return format!("{n}");
}
let formatted = format!("{n:E}");
let (mantissa, exp) = match formatted.split_once('E') {
Some((m, e)) => (m.to_string(), e.to_string()),
None => return formatted,
};
let mantissa = if !mantissa.contains('.') {
format!("{mantissa}.0")
} else { mantissa };
format!("{mantissa}E{exp}")
}
fn canonical_decimal_lex(n: f64, src: &str) -> String {
if n == 0.0 { return "0".to_string(); }
if src.contains(['e', 'E']) {
if n.fract() == 0.0 && n.abs() < 1e15 {
return (n as i64).to_string();
}
return format!("{n}");
}
let t = src.trim();
let (sign, body) = match t.strip_prefix('-') {
Some(rest) => ("-", rest),
None => ("", t.strip_prefix('+').unwrap_or(t)),
};
let (whole, frac) = match body.split_once('.') {
Some((w, f)) => (w, f),
None => (body, ""),
};
let whole_trimmed = whole.trim_start_matches('0');
let whole_canon = if whole_trimmed.is_empty() { "0" } else { whole_trimmed };
let frac_trimmed = frac.trim_end_matches('0');
if frac_trimmed.is_empty() {
if whole_canon == "0" { return "0".into(); }
return format!("{sign}{whole_canon}");
}
format!("{sign}{whole_canon}.{frac_trimmed}")
}
pub fn cast_value_to_atomic<I: DocIndexLike>(
v: &Value, st: &crate::xpath::ast::SequenceType, idx: &I,
) -> Result<Value> {
cast_value_to_atomic_impl(v, st, idx).map_err(|e| e.or_xpath_code("FORG0001"))
}
fn cast_value_to_atomic_impl<I: DocIndexLike>(
v: &Value, st: &crate::xpath::ast::SequenceType, idx: &I,
) -> Result<Value> {
use crate::xpath::ast::ItemType;
let s = value_to_string(v, idx);
let make_typed = |kind: &'static str, lex: String,
numeric: Option<f64>, boolean: Option<bool>| -> Value {
Value::Typed(Box::new(TypedAtomic {
kind, lexical: lex, numeric, boolean, user_type: None,
}))
};
match &st.item {
ItemType::Atomic(name) => {
let kind = match atomic_kind_static(name) {
Some(k) => k,
None => return Ok(Value::String(s)),
};
match name.as_str() {
"string" | "anyURI" | "anyAtomicType" | "untypedAtomic"
| "normalizedString" | "token" | "Name" | "NCName"
| "language" | "ID" | "IDREF" | "IDREFS" | "ENTITY"
| "ENTITIES" | "NMTOKEN" | "NMTOKENS" | "NOTATION" | "QName" => {
Ok(make_typed(kind, s, None, None))
}
"boolean" => {
if let Value::Number(n) = v {
let b = !(n.as_f64() == 0.0 || n.as_f64().is_nan());
return Ok(make_typed("boolean",
(if b { "true" } else { "false" }).into(),
None, Some(b)));
}
if let Value::Typed(t) = v {
if let Some(n) = t.numeric {
let b = !(n == 0.0 || n.is_nan());
return Ok(make_typed("boolean",
(if b { "true" } else { "false" }).into(),
None, Some(b)));
}
}
match s.trim() {
"true" | "1" => Ok(make_typed("boolean", "true".into(), None, Some(true))),
"false" | "0" => Ok(make_typed("boolean", "false".into(), None, Some(false))),
_ => Err(xpath_err(format!("cast to xs:boolean failed: {s:?}"))),
}
}
"integer" | "long" | "int" | "short" | "byte"
| "nonNegativeInteger" | "nonPositiveInteger"
| "positiveInteger" | "negativeInteger"
| "unsignedLong" | "unsignedInt" | "unsignedShort" | "unsignedByte" => {
let n: i64 = s.trim().parse().map_err(|_| xpath_err(
format!("cast to xs:{name} failed: {s:?}")))?;
Ok(make_typed(kind, n.to_string(), Some(n as f64), None))
}
"decimal" | "double" | "float" | "numeric" => {
let trimmed = s.trim();
let raw: f64 = match trimmed {
"INF" | "Infinity" => f64::INFINITY,
"-INF" | "-Infinity" => f64::NEG_INFINITY,
"NaN" => f64::NAN,
_ => trimmed.parse().map_err(|_| xpath_err(
format!("cast to xs:{name} failed: {s:?}")))?,
};
let n = if name == "float" { raw as f32 as f64 } else { raw };
let canon = match name.as_str() {
"double" => canonical_double_lex(n, v),
"float" => canonical_float_lex(n, v),
_ => canonical_decimal_lex(n, trimmed),
};
Ok(make_typed(kind, canon, Some(n), None))
}
"date" | "dateTime" | "time"
| "duration" | "dayTimeDuration" | "yearMonthDuration"
| "gYear" | "gYearMonth" | "gMonth" | "gMonthDay" | "gDay"
| "hexBinary" | "base64Binary" => {
if name == "dateTime" {
if let Value::Typed(t) = v {
if t.kind == "date" {
let raw = t.lexical.as_str();
let (date_part, tz) = if let Some(rest) =
raw.strip_suffix('Z')
{
(rest, "Z")
} else if raw.len() >= 6 {
let cand = &raw[raw.len() - 6..];
let b = cand.as_bytes();
if (b[0] == b'+' || b[0] == b'-')
&& b[1].is_ascii_digit() && b[2].is_ascii_digit()
&& b[3] == b':'
&& b[4].is_ascii_digit() && b[5].is_ascii_digit()
{
(&raw[..raw.len() - 6], cand)
} else {
(raw, "")
}
} else {
(raw, "")
};
let lex = format!("{date_part}T00:00:00{tz}");
return Ok(make_typed("dateTime", lex, None, None));
}
}
}
if matches!(name.as_str(),
"gYear" | "gYearMonth" | "gMonth" | "gMonthDay" | "gDay")
{
if let Value::Typed(t) = v {
let dk = match t.kind {
"date" => Some(DateKind::Date),
"dateTime" => Some(DateKind::DateTime),
_ => None,
};
if let Some(dk) = dk {
if let Some((y, mo, d, _, _, _, _, tz)) =
parse_xsd_date_time(&t.lexical, dk)
{
let tzs = tz.map(format_tz_suffix).unwrap_or_default();
let lex = match name.as_str() {
"gYear" => format!("{y:04}{tzs}"),
"gYearMonth" => format!("{y:04}-{mo:02}{tzs}"),
"gMonth" => format!("--{mo:02}{tzs}"),
"gMonthDay" => format!("--{mo:02}-{d:02}{tzs}"),
_ => format!("---{d:02}{tzs}"),
};
return Ok(make_typed(kind, lex, None, None));
}
}
}
}
if matches!(name.as_str(), "hexBinary" | "base64Binary") {
if let Some(lex) = convert_binary_kind(v, name) {
return Ok(make_typed(kind, lex, None, None));
}
}
let trimmed = s.trim();
if !lexical_matches_type(trimmed, name) {
if date_year_out_of_range(trimmed, name) {
return Err(xpath_err(format!(
"cast to xs:{name}: year is outside the \
supported range: {trimmed:?}"
)).with_xpath_code("FODT0001"));
}
return Err(xpath_err(format!(
"cast to xs:{name} failed: {trimmed:?}"
)));
}
if matches!(name.as_str(), "dateTime" | "time")
&& trimmed.contains("24:00:00")
{
let dk = if name == "dateTime" {
DateKind::DateTime } else { DateKind::Time };
if let Some((y, mo, d, h, mi, sec, frac, tz)) =
parse_xsd_date_time(trimmed, dk)
{
let lex = if name == "dateTime" {
format_datetime_lexical(y, mo, d, h, mi, sec, frac, tz)
} else {
let mut l = format!("{h:02}:{mi:02}:{sec:02}");
if frac != 0 {
let mut f = format!(".{frac:06}");
while f.ends_with('0') { f.pop(); }
l.push_str(&f);
}
if let Some(tz_m) = tz { l.push_str(&format_tz_suffix(tz_m)); }
l
};
return Ok(make_typed(kind, lex, None, None));
}
}
if name == "hexBinary" {
return Ok(make_typed(kind, trimmed.to_ascii_uppercase(), None, None));
}
Ok(make_typed(kind, trimmed.to_string(), None, None))
}
_ => Ok(Value::String(s)),
}
}
_ => Ok(v.clone()),
}
}
#[derive(Debug, Clone, Copy)]
pub enum DateKind { Date, Time, DateTime }
fn parse_xsd_date_time(s: &str, kind: DateKind)
-> Option<(i32, u8, u8, u8, u8, u8, u32, Option<i16>)>
{
let s = s.trim();
if matches!(kind, DateKind::Time) {
let (mut h, m, sec, frac, tz) = parse_xsd_time(s)?;
if h == 24 && m == 0 && sec == 0 && frac == 0 { h = 0; }
return Some((0, 0, 0, h, m, sec, frac, tz));
}
let bytes = s.as_bytes();
if bytes.len() < 10 { return None; }
let (neg_year, body) = match s.strip_prefix('-') {
Some(rest) => (true, rest),
None => (false, s),
};
let body_bytes = body.as_bytes();
let first_dash = body_bytes.iter().position(|&c| c == b'-')?;
if first_dash < 4 { return None; }
if body_bytes.len() < first_dash + 6 { return None; }
if body_bytes[first_dash + 3] != b'-' { return None; }
let year_str = &body[..first_dash];
let mut year: i32 = year_str.parse().ok()?;
if neg_year { year = -year; }
let month: u8 = std::str::from_utf8(&body_bytes[first_dash + 1..first_dash + 3])
.ok()?.parse().ok()?;
let day: u8 = std::str::from_utf8(&body_bytes[first_dash + 4..first_dash + 6])
.ok()?.parse().ok()?;
if !(1..=12).contains(&month) { return None; }
let max_day = days_in_month(year, month as u32);
if !(1..=max_day as u8).contains(&day) { return None; }
let rest = &body[first_dash + 6..];
match kind {
DateKind::Date => {
let tz = parse_tz_suffix(rest);
Some((year, month, day, 0, 0, 0, 0, tz))
}
DateKind::DateTime => {
if !rest.starts_with('T') || rest.len() < 9 { return None; }
let (h, m, sec, frac, tz) = parse_xsd_time(&rest[1..])?;
if h == 24 && m == 0 && sec == 0 && frac == 0 {
let (y, mo, d) = add_one_day(year, month, day);
return Some((y, mo, d, 0, 0, 0, 0, tz));
}
Some((year, month, day, h, m, sec, frac, tz))
}
DateKind::Time => unreachable!(),
}
}
fn parse_xsd_time(s: &str) -> Option<(u8, u8, u8, u32, Option<i16>)> {
let bytes = s.as_bytes();
if bytes.len() < 8 { return None; }
let h: u8 = std::str::from_utf8(&bytes[..2]).ok()?.parse().ok()?;
if bytes[2] != b':' { return None; }
let m: u8 = std::str::from_utf8(&bytes[3..5]).ok()?.parse().ok()?;
if bytes[5] != b':' { return None; }
let sec: u8 = std::str::from_utf8(&bytes[6..8]).ok()?.parse().ok()?;
let mut rest_start = 8;
let mut frac_us: u32 = 0;
if s.as_bytes().get(8) == Some(&b'.') {
let mut i = 9;
while i < bytes.len() && bytes[i].is_ascii_digit() { i += 1; }
let digits = &s[9..i];
let take: String = digits.chars().chain(std::iter::repeat('0')).take(6).collect();
frac_us = take.parse().ok().unwrap_or(0);
rest_start = i;
}
let tz = parse_tz_suffix(&s[rest_start..]);
Some((h, m, sec, frac_us, tz))
}
fn parse_tz_suffix(s: &str) -> Option<i16> {
if s.is_empty() { return None; }
if s == "Z" { return Some(0); }
let bytes = s.as_bytes();
if bytes.len() < 6 { return None; }
let sign = match bytes[0] { b'+' => 1, b'-' => -1, _ => return None };
let hh: i16 = std::str::from_utf8(&bytes[1..3]).ok()?.parse().ok()?;
if bytes[3] != b':' { return None; }
let mm: i16 = std::str::from_utf8(&bytes[4..6]).ok()?.parse().ok()?;
Some(sign * (hh * 60 + mm))
}
fn format_date_locale_prefix(lang: &str, calendar: &str) -> String {
let mut prefix = String::new();
let lang = lang.trim();
if !lang.is_empty() && !lang.eq_ignore_ascii_case("en") {
prefix.push_str("[Language: en]");
}
let cal = calendar.trim().rsplit(['}', ':']).next().unwrap_or("").trim();
if !cal.is_empty()
&& !matches!(cal.to_ascii_uppercase().as_str(), "AD" | "ISO" | "CE") {
prefix.push_str("[Calendar: AD]");
}
prefix
}
fn format_date_time_picture(value: &str, picture: &str, kind: DateKind) -> Result<String> {
let (y, mo, d, h, mi, s, frac, tz) = match parse_xsd_date_time(value, kind) {
Some(t) => t,
None => return Ok(value.to_string()), };
let mut out = String::with_capacity(picture.len());
let mut chars = picture.chars().peekable();
while let Some(c) = chars.next() {
match c {
'[' => {
if chars.peek() == Some(&'[') {
chars.next();
out.push('[');
continue;
}
let mut marker = String::new();
while let Some(&nc) = chars.peek() {
if nc == ']' { break; }
marker.push(nc);
chars.next();
}
if chars.peek() != Some(&']') {
return Err(xpath_err(format!(
"format-date/format-time: unterminated picture \
marker '[{marker}' (XTDE1340)"
)));
}
chars.next();
let comp = marker.trim().chars().next().unwrap_or('\0');
let kind_mismatch = match (kind, comp) {
(DateKind::Date, 'H' | 'h' | 'm' | 's' | 'f' | 'P') => true,
(DateKind::Time, 'Y' | 'M' | 'D' | 'd' | 'F' | 'W' | 'w' | 'C' | 'E') => true,
_ => false,
};
if kind_mismatch {
return Err(xpath_err(format!(
"format-date/format-time: component '{comp}' not \
available for this date/time value (XTDE1350)"
)));
}
if !is_known_picture_component(comp) {
return Err(xpath_err(format!(
"format-date/format-time: unknown component '{comp}' \
in picture string (XTDE1340)"
)));
}
out.push_str(&format_picture_marker(&marker, y, mo, d, h, mi, s, frac, tz));
}
']' => {
if chars.peek() == Some(&']') {
chars.next();
out.push(']');
} else {
out.push(']');
}
}
other => out.push(other),
}
}
Ok(out)
}
fn is_known_picture_component(c: char) -> bool {
matches!(c, 'Y' | 'M' | 'D' | 'd' | 'F' | 'W' | 'w' | 'H' | 'h'
| 'm' | 's' | 'f' | 'Z' | 'z' | 'P' | 'C' | 'E')
}
fn format_picture_marker(
marker: &str, y: i32, mo: u8, d: u8, h: u8, mi: u8, s: u8,
frac_us: u32, tz: Option<i16>,
) -> String {
let marker = marker.trim();
let comp = match marker.chars().next() { Some(c) => c, None => return String::new() };
let rest = &marker[comp.len_utf8()..];
let (presentation, width_spec) = match rest.split_once(',') {
Some((p, w)) => (p.trim(), Some(w.trim())),
None => (rest.trim(), None),
};
let (min_w_raw, max_w_raw) = parse_width_spec(width_spec);
let unwrap_star = |v: Option<usize>|
v.and_then(|n| if n == usize::MAX { None } else { Some(n) });
let min_w = unwrap_star(min_w_raw);
let max_w = unwrap_star(max_w_raw);
let pres = if !presentation.is_empty() { presentation }
else { match comp {
'm' | 's' => "01",
_ => "1",
}};
match comp {
'Y' => format_numeric_component((y as i64).abs(), pres, min_w, max_w, true),
'M' if name_presentation(pres) =>
format_named_component(mo as i64, pres, max_w, MONTH_NAMES),
'M' => format_numeric_component(mo as i64, pres, min_w, max_w, false),
'D' => format_numeric_component(d as i64, pres, min_w, max_w, false),
'H' => format_numeric_component(h as i64, pres, min_w, max_w, false),
'h' => {
let hh = if h == 0 { 12 } else if h > 12 { h - 12 } else { h } as i64;
format_numeric_component(hh, pres, min_w, max_w, false)
}
'm' => format_numeric_component(mi as i64, pres, min_w, max_w, false),
's' => format_numeric_component(s as i64, pres, min_w, max_w, false),
'f' => format_fractional_seconds(frac_us, pres, min_w_raw, max_w_raw),
'F' => {
let weekday = day_of_week(y, mo as u32, d as u32);
if name_presentation(pres) {
format_named_component(weekday as i64, pres, max_w, DAY_NAMES)
} else {
format_numeric_component(weekday as i64, pres, min_w, max_w, false)
}
}
'W' => format_numeric_component(
iso_week_of_year(y, mo as u32, d as u32), pres, min_w, max_w, false),
'w' => {
let wd = day_of_week(y, mo as u32, d as u32) as i64; let thursday_dom = d as i64 + 4 - wd;
let wim = (thursday_dom + 6).div_euclid(7);
format_numeric_component(wim, pres, min_w, max_w, false)
}
'd' => {
let day_of_year = month_start_day(y, mo as u32) + d as u32;
format_numeric_component(day_of_year as i64, pres, min_w, max_w, false)
}
'E' => {
if y >= 0 { "AD".to_string() } else { "BC".to_string() }
}
'Z' | 'z' => match tz {
None => String::new(),
Some(off) => {
let sign = if off < 0 { '-' } else { '+' };
let abs = off.unsigned_abs() as i64;
let (hh, mm) = (abs / 60, abs % 60);
let minimal = pres == "0";
let show_min_always = !minimal && max_w.map_or(true, |w| w >= 3);
let mut out = String::new();
if comp == 'z' { out.push_str("GMT"); }
out.push(sign);
if minimal { out.push_str(&hh.to_string()); }
else { out.push_str(&format!("{hh:02}")); }
if show_min_always || mm != 0 {
out.push(':');
out.push_str(&format!("{mm:02}"));
}
out
}
},
'P' => {
let label = if h < 12 { "am" } else { "pm" };
match pres.chars().next() {
Some('n') => label.to_string(), Some('N') => label.to_uppercase(), _ => label.to_string(), }
}
_ => marker.to_string(),
}
}
fn format_numeric_component(
n: i64, presentation: &str,
min_w: Option<usize>, max_w: Option<usize>,
truncate_year: bool,
) -> String {
let (presentation, ordinal) = if let Some(rest) = presentation.strip_suffix('o') {
(rest, true)
} else if let Some(rest) = presentation.strip_suffix('c') {
(rest, false)
} else {
(presentation, false)
};
let worded = match presentation {
"I" => Some(roman_numeral(n, true)),
"i" => Some(roman_numeral(n, false)),
"A" => Some(alpha_label(n, true)),
"a" => Some(alpha_label(n, false)),
"W" => Some(english_words(n, ordinal, WordCase::Upper)),
"w" => Some(english_words(n, ordinal, WordCase::Lower)),
"Ww" => Some(english_words(n, ordinal, WordCase::Title)),
_ => None,
};
if let Some(mut s) = worded {
if let Some(mw) = min_w {
let len = s.chars().count();
if len < mw { s.extend(std::iter::repeat(' ').take(mw - len)); }
}
return s;
}
if let Some((zero_cp, count)) = picture_digit_family(presentation) {
let cap = max_w.unwrap_or(count);
let (value, want) = if truncate_year && n >= 0
&& cap < 5 && n.to_string().len() > cap {
(n % 10_i64.pow(cap as u32), cap)
} else {
(n, min_w.unwrap_or(count))
};
let ascii = if value < 0 { format!("-{:0w$}", -value, w = want) }
else { format!("{:0w$}", value, w = want) };
return ascii.chars()
.map(|c| c.to_digit(10)
.and_then(|v| char::from_u32(zero_cp + v))
.unwrap_or(c))
.collect();
}
let pres_digits = presentation.chars().filter(|c| c.is_ascii_digit()).count().max(1);
let want_width = min_w.unwrap_or(pres_digits);
if truncate_year && n >= 0 {
let bounded_by_pres = pres_digits >= 2;
if max_w.is_some() || bounded_by_pres {
let cap = max_w.unwrap_or(pres_digits);
let raw = n.to_string();
let digits = raw.len();
if cap < 5 && digits > cap {
let modulus = 10_i64.pow(cap as u32);
let truncated = n % modulus;
return format!("{:0width$}", truncated, width = cap);
}
let pad = want_width.max(min_w.unwrap_or(0));
return format!("{:0width$}", n, width = pad);
}
}
let formatted = if n < 0 {
format!("-{:0width$}", -n, width = want_width)
} else {
format!("{:0width$}", n, width = want_width)
};
if ordinal {
format!("{formatted}{}", english_ordinal_suffix(n))
} else {
formatted
}
}
fn english_ordinal_suffix(n: i64) -> &'static str {
let abs = n.unsigned_abs();
if (11..=13).contains(&(abs % 100)) {
return "th";
}
match abs % 10 {
1 => "st",
2 => "nd",
3 => "rd",
_ => "th",
}
}
fn format_fractional_seconds(
frac_us: u32, presentation: &str,
min_w: Option<usize>, max_w: Option<usize>,
) -> String {
let pres_digits = presentation.chars().filter(|c| c.is_ascii_digit()).count();
let unwrap_star = |v: Option<usize>| v.and_then(|n| if n == usize::MAX { None } else { Some(n) });
let min_w_explicit = min_w.map(|n| n != usize::MAX).unwrap_or(false);
let max_w_explicit_star = max_w == Some(usize::MAX);
let min_w = unwrap_star(min_w);
let max_w = unwrap_star(max_w);
let (min_w, max_w) = match (min_w, max_w) {
(Some(mn), Some(mx)) => (mn, Some(mx)),
(Some(mn), None) if max_w_explicit_star => (mn, None),
(Some(mn), None) => (mn, Some(mn.max(pres_digits))),
(None, Some(mx)) => (pres_digits.min(mx).max(1), Some(mx)),
(None, None) if pres_digits > 0 => (pres_digits, Some(pres_digits)),
(None, None) if min_w_explicit => (1, None),
(None, None) => (1, None),
};
let target = max_w.unwrap_or(6).min(6);
if target == 0 { return String::new(); }
let divisor = 10u32.pow((6 - target) as u32);
let half = divisor / 2;
let rounded = (frac_us + half) / divisor;
let max_value = 10u32.pow(target as u32);
let clamped = rounded.min(max_value - 1);
let padded = format!("{:0width$}", clamped, width = target);
let padded_bytes = padded.as_bytes();
let mut end = padded_bytes.len();
while end > min_w && padded_bytes[end - 1] == b'0' { end -= 1; }
let trimmed = &padded[..end];
trimmed.to_string()
}
fn parse_width_spec(spec: Option<&str>) -> (Option<usize>, Option<usize>) {
let spec = match spec { Some(s) => s, None => return (None, None) };
let (min_s, max_s) = match spec.split_once('-') {
Some((a, b)) => (a, Some(b)),
None => (spec, None),
};
let parse = |s: &str| -> Option<usize> {
let s = s.trim();
if s.is_empty() { None }
else if s == "*" { Some(usize::MAX) }
else { s.parse().ok() }
};
(parse(min_s), max_s.and_then(parse))
}
pub fn resolve_uri_against(base: &str, rel: &str) -> String {
resolve_uri_rfc3986(base, rel)
}
fn uri_has_scheme(uri: &str) -> bool {
split_uri(uri).0.is_some()
}
fn resolve_uri_rfc3986(base: &str, rel: &str) -> String {
let (rs, ra, rp, rq, rf) = split_uri(rel);
let (bs, ba, bp, bq, _bf) = split_uri(base);
let (ts, ta, tp, tq) = if let Some(s) = rs {
(s, ra, remove_dot_segments(rp), rq)
} else if ra.is_some() {
(bs.unwrap_or(""), ra, remove_dot_segments(rp), rq)
} else if rp.is_empty() {
let q = if rq.is_some() { rq } else { bq };
(bs.unwrap_or(""), ba, bp.to_string(), q)
} else if rp.starts_with('/') {
(bs.unwrap_or(""), ba, remove_dot_segments(rp), rq)
} else {
let merged = merge_paths(ba.is_some(), bp, rp);
(bs.unwrap_or(""), ba, remove_dot_segments(&merged), rq)
};
let mut out = String::new();
if !ts.is_empty() {
out.push_str(ts);
out.push(':');
}
if let Some(a) = ta {
out.push_str("//");
out.push_str(a);
}
out.push_str(&tp);
if let Some(q) = tq {
out.push('?');
out.push_str(q);
}
if let Some(f) = rf {
out.push('#');
out.push_str(f);
}
out
}
fn split_uri(s: &str) -> (Option<&str>, Option<&str>, &str, Option<&str>, Option<&str>) {
let bytes = s.as_bytes();
let scheme_end = bytes.iter().position(|&b|
!(b.is_ascii_alphanumeric() || b == b'+' || b == b'-' || b == b'.'));
let (scheme, mut rest) = match scheme_end {
Some(i) if i > 0 && bytes[i] == b':' && bytes[0].is_ascii_alphabetic() =>
(Some(&s[..i]), &s[i + 1..]),
_ => (None, s),
};
let fragment = rest.find('#').map(|i| {
let f = &rest[i + 1..];
rest = &rest[..i];
f
});
let query = rest.find('?').map(|i| {
let q = &rest[i + 1..];
rest = &rest[..i];
q
});
let (authority, path) = if let Some(after) = rest.strip_prefix("//") {
let end = after.find(|c: char| c == '/' || c == '?' || c == '#')
.unwrap_or(after.len());
(Some(&after[..end]), &after[end..])
} else {
(None, rest)
};
(scheme, authority, path, query, fragment)
}
fn remove_dot_segments(input: &str) -> String {
let mut in_buf = input.to_string();
let mut out = String::with_capacity(input.len());
while !in_buf.is_empty() {
if in_buf.starts_with("../") {
in_buf.replace_range(..3, "");
} else if in_buf.starts_with("./") {
in_buf.replace_range(..2, "");
} else if in_buf.starts_with("/./") {
in_buf.replace_range(..3, "/");
} else if in_buf == "/." {
in_buf = "/".to_string();
} else if in_buf.starts_with("/../") {
in_buf.replace_range(..4, "/");
if let Some(i) = out.rfind('/') { out.truncate(i); } else { out.clear(); }
} else if in_buf == "/.." {
in_buf = "/".to_string();
if let Some(i) = out.rfind('/') { out.truncate(i); } else { out.clear(); }
} else if in_buf == "." || in_buf == ".." {
in_buf.clear();
} else {
let split = if let Some(stripped) = in_buf.strip_prefix('/') {
stripped.find('/').map(|i| i + 1).unwrap_or(in_buf.len())
} else {
in_buf.find('/').unwrap_or(in_buf.len())
};
out.push_str(&in_buf[..split]);
in_buf.replace_range(..split, "");
}
}
out
}
fn merge_paths(base_has_authority: bool, base_path: &str, rel_path: &str) -> String {
if base_has_authority && base_path.is_empty() {
let mut s = String::with_capacity(rel_path.len() + 1);
s.push('/');
s.push_str(rel_path);
s
} else {
let cut = base_path.rfind('/').map(|i| i + 1).unwrap_or(0);
let mut s = String::with_capacity(cut + rel_path.len());
s.push_str(&base_path[..cut]);
s.push_str(rel_path);
s
}
}
fn adjust_timezone(lex: &str, kind: &str, new_tz_minutes: Option<i16>) -> String {
let dk = match kind {
"date" => DateKind::Date,
"dateTime" => DateKind::DateTime,
"time" => DateKind::Time,
_ => return lex.to_string(),
};
let Some((y, mo, d, mut h, mut mi, s, frac, tz)) = parse_xsd_date_time(lex, dk) else {
return lex.to_string();
};
let (mut yy, mut mm, mut dd) = (y as i64, mo as i64, d as i64);
let needs_shift = match (tz, new_tz_minutes) {
(Some(_), Some(_)) => true, _ => false, };
if needs_shift {
let old_tz = tz.unwrap() as i64;
let new_tz = new_tz_minutes.unwrap() as i64;
let delta_minutes = new_tz - old_tz;
if matches!(dk, DateKind::Time) {
let total = (h as i64) * 60 + (mi as i64) + delta_minutes;
let wrapped = ((total % 1440) + 1440) % 1440;
h = (wrapped / 60) as u8;
mi = (wrapped % 60) as u8;
} else {
let days = ymd_to_days(yy as i32, mm as u32, dd as u32);
let total_min = days * 24 * 60 + (h as i64) * 60 + (mi as i64) + delta_minutes;
let new_days = total_min.div_euclid(24 * 60);
let leftover = total_min.rem_euclid(24 * 60);
h = (leftover / 60) as u8;
mi = (leftover % 60) as u8;
let (ny, nm, nd) = days_to_ymd(new_days);
yy = ny as i64; mm = nm as i64; dd = nd as i64;
}
}
let tz_str = match new_tz_minutes {
None => String::new(),
Some(0) => "Z".to_string(),
Some(m) => format!("{}{:02}:{:02}",
if m < 0 { '-' } else { '+' },
(m.unsigned_abs() / 60), (m.unsigned_abs() % 60)),
};
match dk {
DateKind::Time => {
let mut s_out = format!("{:02}:{:02}:{:02}", h, mi, s);
if frac > 0 {
let mut frac_s = format!("{:06}", frac);
while frac_s.ends_with('0') { frac_s.pop(); }
if !frac_s.is_empty() { s_out.push('.'); s_out.push_str(&frac_s); }
}
s_out.push_str(&tz_str);
s_out
}
DateKind::Date => {
let sign = if yy < 0 { "-" } else { "" };
format!("{}{:04}-{:02}-{:02}{}", sign, yy.unsigned_abs(), mm, dd, tz_str)
}
DateKind::DateTime => {
let sign = if yy < 0 { "-" } else { "" };
let mut s_out = format!("{}{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
sign, yy.unsigned_abs(), mm, dd, h, mi, s);
if frac > 0 {
let mut frac_s = format!("{:06}", frac);
while frac_s.ends_with('0') { frac_s.pop(); }
if !frac_s.is_empty() { s_out.push('.'); s_out.push_str(&frac_s); }
}
s_out.push_str(&tz_str);
s_out
}
}
}
fn node_path_string<I: DocIndexLike>(node: NodeId, idx: &I) -> String {
use crate::xpath::XPathNodeKind;
if matches!(idx.kind(node), XPathNodeKind::Document) {
return "/".to_string();
}
let mut segments: Vec<String> = Vec::new();
let mut cur = node;
loop {
let parent = idx.parent(cur);
let segment = match idx.kind(cur) {
XPathNodeKind::Element => {
let local = idx.local_name(cur).to_string();
let uri = idx.namespace_uri(cur).to_string();
let pos = match parent {
Some(p) => element_index_among_siblings(p, cur, &local, &uri, idx),
None => 1,
};
format!("Q{{{uri}}}{local}[{pos}]")
}
XPathNodeKind::Attribute => {
let local = idx.local_name(cur).to_string();
let uri = idx.namespace_uri(cur).to_string();
format!("@Q{{{uri}}}{local}")
}
XPathNodeKind::Text | XPathNodeKind::CData => {
let pos = match parent {
Some(p) => kind_index_among_siblings(p, cur, XPathNodeKind::Text, idx),
None => 1,
};
format!("text()[{pos}]")
}
XPathNodeKind::Comment => {
let pos = match parent {
Some(p) => kind_index_among_siblings(p, cur, XPathNodeKind::Comment, idx),
None => 1,
};
format!("comment()[{pos}]")
}
XPathNodeKind::PI => {
let target = idx.pi_target(cur).to_string();
let pos = match parent {
Some(p) => pi_index_among_siblings(p, cur, &target, idx),
None => 1,
};
format!("processing-instruction({target})[{pos}]")
}
XPathNodeKind::Namespace => {
let prefix = idx.local_name(cur);
format!("namespace::{prefix}")
}
XPathNodeKind::Document => break,
};
segments.push(segment);
match parent {
Some(p) if matches!(idx.kind(p), XPathNodeKind::Document) => break,
Some(p) => { cur = p; }
None => {
segments.push("Q{}root()".to_string());
return segments.into_iter().rev().collect::<Vec<_>>().join("/");
}
}
}
let mut parts: Vec<String> = Vec::with_capacity(segments.len() + 1);
parts.push(String::new());
parts.extend(segments.into_iter().rev());
parts.join("/")
}
fn element_index_among_siblings<I: DocIndexLike>(
parent: NodeId, child: NodeId, local: &str, uri: &str, idx: &I,
) -> usize {
let mut count = 0usize;
for &sib in idx.children(parent) {
if matches!(idx.kind(sib), crate::xpath::XPathNodeKind::Element)
&& idx.local_name(sib) == local
&& idx.namespace_uri(sib) == uri
{
count += 1;
if sib == child { return count; }
}
}
count
}
fn kind_index_among_siblings<I: DocIndexLike>(
parent: NodeId, child: NodeId,
kind: crate::xpath::XPathNodeKind, idx: &I,
) -> usize {
let mut count = 0usize;
for &sib in idx.children(parent) {
if idx.kind(sib) == kind {
count += 1;
if sib == child { return count; }
}
}
count
}
fn pi_index_among_siblings<I: DocIndexLike>(
parent: NodeId, child: NodeId, target: &str, idx: &I,
) -> usize {
let mut count = 0usize;
for &sib in idx.children(parent) {
if matches!(idx.kind(sib), crate::xpath::XPathNodeKind::PI)
&& idx.pi_target(sib) == target
{
count += 1;
if sib == child { return count; }
}
}
count
}
fn deep_equal_values<I: DocIndexLike>(
a: &Value, b: &Value, idx: &I, bindings: &dyn XPathBindings,
) -> bool {
let a_items = items_of(a);
let b_items = items_of(b);
if a_items.len() != b_items.len() { return false; }
a_items.iter().zip(b_items.iter()).all(|(x, y)|
deep_equal_one(x, y, idx, bindings))
}
fn seq_from_items(mut items: Vec<Value>) -> Value {
match items.len() {
0 => Value::NodeSet(Vec::new()),
1 => items.pop().unwrap(),
_ => Value::Sequence(items),
}
}
fn key_number(v: &Value) -> Option<f64> {
match v {
Value::Number(n) => Some(n.as_f64()),
Value::Typed(t) => t.numeric,
_ => None,
}
}
pub fn map_key_eq<I: DocIndexLike>(a: &Value, b: &Value, idx: &I) -> bool {
match (key_number(a), key_number(b)) {
(Some(x), Some(y)) => (x.is_nan() && y.is_nan()) || x == y,
(None, None) => value_to_string(a, idx) == value_to_string(b, idx),
_ => false,
}
}
pub fn first_atomic_key<I: DocIndexLike>(v: &Value, idx: &I) -> Value {
match v {
Value::NodeSet(ns) => Value::String(
ns.first().map(|&id| idx.string_value(id)).unwrap_or_default()),
Value::ForeignNodeSet(_) => Value::String(value_to_string(v, idx)),
Value::Sequence(items) => items.first()
.map(|x| first_atomic_key(x, idx))
.unwrap_or(Value::String(String::new())),
Value::IntRange { lo, .. } => Value::Number(Numeric::Integer(*lo)),
other => other.clone(),
}
}
fn eval_lookup<I: DocIndexLike>(
base: &Value, key: &crate::xpath::ast::LookupKey,
ctx: &EvalCtx<'_>, idx: &I,
) -> Result<Value> {
use crate::xpath::ast::LookupKey;
let keys: Option<Vec<Value>> = match key {
LookupKey::Wildcard => None,
LookupKey::Name(s) => Some(vec![Value::String(s.clone())]),
LookupKey::Integer(i) => Some(vec![Value::Number(Numeric::Integer(*i))]),
LookupKey::Expr(e) => Some(items_of(&eval_expr(e, ctx, idx)?)),
};
let mut out: Vec<Value> = Vec::new();
for item in items_of(base) {
match &item {
Value::Map(entries) => match &keys {
None => for (_, val) in entries.iter() { out.extend(items_of(val)); },
Some(ks) => for k in ks {
for (mk, mv) in entries.iter() {
if map_key_eq(mk, k, idx) { out.extend(items_of(mv)); }
}
},
},
Value::Array(members) => match &keys {
None => for m in members.iter() { out.extend(items_of(m)); },
Some(ks) => for k in ks {
let pos = value_to_number(k, idx);
if pos.fract() == 0.0 && pos >= 1.0
&& (pos as usize) <= members.len()
{
out.extend(items_of(&members[pos as usize - 1]));
}
},
},
_ => return Err(xpath_err(
"the '?' lookup operator requires a map or array (XPTY0004)")),
}
}
Ok(seq_from_items(out))
}
fn items_of(v: &Value) -> Vec<Value> {
match v {
Value::NodeSet(ns) => ns.iter()
.map(|&id| Value::NodeSet(vec![id])).collect(),
Value::Sequence(items) => items.clone(),
Value::ForeignNodeSet(ns) => ns.iter()
.map(|&p| Value::ForeignNodeSet(vec![p])).collect(),
Value::IntRange { lo, hi } =>
(*lo..=*hi).map(|i| Value::Number(Numeric::Double(i as f64))).collect(),
other => vec![other.clone()],
}
}
fn sequence_len(v: &Value) -> usize {
match v {
Value::NodeSet(ns) => ns.len(),
Value::ForeignNodeSet(ns) => ns.len(),
Value::Sequence(items) => items.iter().map(sequence_len).sum(),
Value::IntRange { lo, hi } => ((hi - lo) as usize).saturating_add(1),
_ => 1,
}
}
fn iter_items<'a>(v: &'a Value) -> Box<dyn Iterator<Item = Value> + 'a> {
match v {
Value::NodeSet(ns) => Box::new(ns.iter().map(|&id| Value::NodeSet(vec![id]))),
Value::ForeignNodeSet(ns) => Box::new(ns.iter().map(|&p| Value::ForeignNodeSet(vec![p]))),
Value::Sequence(items) => Box::new(items.iter().flat_map(iter_items)),
Value::IntRange { lo, hi } => Box::new((*lo..=*hi).map(|i| Value::Number(Numeric::Double(i as f64)))),
other => Box::new(std::iter::once(other.clone())),
}
}
fn deep_equal_one<I: DocIndexLike>(
a: &Value, b: &Value, idx: &I, bindings: &dyn XPathBindings,
) -> bool {
match (a, b) {
(Value::NodeSet(an), Value::NodeSet(bn))
if an.len() == 1 && bn.len() == 1 =>
{
deep_equal_node(an[0], bn[0], idx)
}
(Value::NodeSet(ns), other) | (other, Value::NodeSet(ns))
if ns.len() == 1 && matches!(idx.kind(ns[0]), XPathNodeKind::Text) =>
{
value_to_string_with(other, idx, bindings) == idx.string_value(ns[0])
}
(Value::NodeSet(_), _) | (_, Value::NodeSet(_)) => false,
_ if !deep_equal_types_comparable(a, b) => false,
_ => values_eq(a, b, idx, bindings),
}
}
fn deep_equal_types_comparable(a: &Value, b: &Value) -> bool {
#[derive(PartialEq, Eq, Clone, Copy)]
enum Family { Numeric, String, Boolean, Date, Duration, Other }
fn family_of(v: &Value) -> Family {
match v {
Value::Number(_) => Family::Numeric,
Value::Boolean(_) => Family::Boolean,
Value::String(_) => Family::String,
Value::Typed(t) => match t.kind {
"string" | "anyURI" | "untypedAtomic" | "normalizedString"
| "token" | "Name" | "NCName" | "QName"
| "language" | "NMTOKEN" | "ID" | "IDREF" | "ENTITY"
=> Family::String,
"boolean" => Family::Boolean,
"date" | "dateTime" | "time"
| "gYear" | "gYearMonth" | "gMonth" | "gMonthDay" | "gDay"
=> Family::Date,
"duration" | "dayTimeDuration" | "yearMonthDuration"
=> Family::Duration,
k if matches!(k,
"integer" | "decimal" | "double" | "float"
| "long" | "int" | "short" | "byte"
| "unsignedLong" | "unsignedInt" | "unsignedShort" | "unsignedByte"
| "nonNegativeInteger" | "nonPositiveInteger"
| "positiveInteger" | "negativeInteger" | "numeric"
) => Family::Numeric,
_ => Family::Other,
},
_ => Family::Other,
}
}
let fa = family_of(a);
let fb = family_of(b);
if fa == Family::Other || fb == Family::Other { return true; }
fa == fb
}
fn deep_equal_node<I: DocIndexLike>(a: NodeId, b: NodeId, idx: &I) -> bool {
if idx.kind(a) != idx.kind(b) { return false; }
match idx.kind(a) {
XPathNodeKind::Element => {
if idx.namespace_uri(a) != idx.namespace_uri(b) { return false; }
if idx.local_name(a) != idx.local_name(b) { return false; }
let mut a_attrs: Vec<NodeId> = idx.attr_range(a).collect();
let mut b_attrs: Vec<NodeId> = idx.attr_range(b).collect();
if a_attrs.len() != b_attrs.len() { return false; }
a_attrs.sort_by_key(|&id| (idx.namespace_uri(id).to_string(),
idx.local_name(id).to_string()));
b_attrs.sort_by_key(|&id| (idx.namespace_uri(id).to_string(),
idx.local_name(id).to_string()));
for (&aa, &bb) in a_attrs.iter().zip(b_attrs.iter()) {
if idx.namespace_uri(aa) != idx.namespace_uri(bb) { return false; }
if idx.local_name(aa) != idx.local_name(bb) { return false; }
if idx.string_value(aa) != idx.string_value(bb) { return false; }
}
let a_kids = idx.children(a);
let b_kids = idx.children(b);
if a_kids.len() != b_kids.len() { return false; }
a_kids.iter().zip(b_kids.iter())
.all(|(&x, &y)| deep_equal_node(x, y, idx))
}
XPathNodeKind::Document => {
let a_kids = idx.children(a);
let b_kids = idx.children(b);
if a_kids.len() != b_kids.len() { return false; }
a_kids.iter().zip(b_kids.iter())
.all(|(&x, &y)| deep_equal_node(x, y, idx))
}
XPathNodeKind::Attribute => {
idx.namespace_uri(a) == idx.namespace_uri(b)
&& idx.local_name(a) == idx.local_name(b)
&& idx.string_value(a) == idx.string_value(b)
}
XPathNodeKind::Text | XPathNodeKind::CData => {
idx.string_value(a) == idx.string_value(b)
}
XPathNodeKind::Comment => {
idx.string_value(a) == idx.string_value(b)
}
XPathNodeKind::PI => {
idx.pi_target(a) == idx.pi_target(b)
&& idx.string_value(a) == idx.string_value(b)
}
XPathNodeKind::Namespace => {
idx.local_name(a) == idx.local_name(b)
&& idx.string_value(a) == idx.string_value(b)
}
}
}
const MONTH_NAMES: &[&str] = &[
"", "January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December",
];
const DAY_NAMES: &[&str] = &[
"", "Monday", "Tuesday", "Wednesday", "Thursday",
"Friday", "Saturday", "Sunday",
];
fn name_presentation(pres: &str) -> bool {
matches!(pres, "N" | "n" | "Nn"
| "NN" | "nn" )
}
fn format_named_component(value: i64, pres: &str, max_w: Option<usize>, table: &[&str]) -> String {
let idx = value as usize;
let name = table.get(idx).copied().unwrap_or("");
let chosen: String = match max_w {
Some(max) if max < name.chars().count() => {
let abbrev3: String = name.chars().take(3).collect();
if max >= 3 { abbrev3 } else { name.chars().take(max).collect() }
}
_ => name.to_string(),
};
match pres {
"N" => chosen.to_uppercase(),
"n" => chosen.to_lowercase(),
_ => chosen,
}
}
const DIGIT_FAMILY_ZEROS: &[u32] = &[
0x0660, 0x06F0, 0x0966, 0x09E6, 0x0A66, 0x0AE6, 0x0B66, 0x0BE6, 0x0C66, 0x0CE6, 0x0D66, 0x0E50, 0x0ED0, 0x0F20, 0x1040, 0x17E0, 0x1810, 0xFF10, 0x104A0, 0x1D7CE, ];
fn picture_digit_family(pres: &str) -> Option<(u32, usize)> {
if pres.is_empty() || pres.is_ascii() { return None; }
let mut zero = None;
let mut count = 0;
for c in pres.chars() {
let cp = c as u32;
let z = DIGIT_FAMILY_ZEROS.iter().copied()
.find(|&z| cp >= z && cp <= z + 9)?;
match zero {
Some(prev) if prev != z => return None, _ => zero = Some(z),
}
count += 1;
}
zero.map(|z| (z, count))
}
fn iso_week_of_year(y: i32, m: u32, d: u32) -> i64 {
let ord = (month_start_day(y, m) + d) as i64; let wd = day_of_week(y, m, d) as i64; let week = (ord - wd + 10).div_euclid(7);
if week < 1 {
iso_weeks_in_year(y - 1)
} else if week > iso_weeks_in_year(y) {
1
} else {
week
}
}
fn iso_weeks_in_year(y: i32) -> i64 {
let p = |yy: i32| (yy + yy.div_euclid(4) - yy.div_euclid(100) + yy.div_euclid(400))
.rem_euclid(7);
if p(y) == 4 || p(y - 1) == 3 { 53 } else { 52 }
}
fn day_of_week(y: i32, m: u32, d: u32) -> u32 {
let days = ymd_to_days(y, m, d);
let wd = ((days + 3).rem_euclid(7) as u32) + 1;
wd
}
fn month_start_day(y: i32, m: u32) -> u32 {
let mut total = 0;
let leap = is_leap_year(y);
for mm in 1..m {
total += match mm {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 => if leap { 29 } else { 28 },
_ => 0,
};
}
total
}
fn roman_numeral(n: i64, upper: bool) -> String {
if !(1..=3999).contains(&n) { return n.to_string(); }
const PAIRS: &[(i64, &str, &str)] = &[
(1000, "M", "m"),
(900, "CM", "cm"),
(500, "D", "d"),
(400, "CD", "cd"),
(100, "C", "c"),
(90, "XC", "xc"),
(50, "L", "l"),
(40, "XL", "xl"),
(10, "X", "x"),
(9, "IX", "ix"),
(5, "V", "v"),
(4, "IV", "iv"),
(1, "I", "i"),
];
let mut n = n;
let mut out = String::new();
for &(val, hi, lo) in PAIRS {
while n >= val {
out.push_str(if upper { hi } else { lo });
n -= val;
}
}
out
}
fn alpha_label(n: i64, upper: bool) -> String {
if n < 1 { return n.to_string(); }
let base = if upper { b'A' } else { b'a' };
let mut buf = Vec::new();
let mut n = n;
while n > 0 {
n -= 1;
buf.push(base + (n % 26) as u8);
n /= 26;
}
buf.reverse();
String::from_utf8(buf).unwrap_or_default()
}
#[derive(Clone, Copy)]
pub enum WordCase { Upper, Lower, Title }
pub fn english_words(n: i64, ordinal: bool, case: WordCase) -> String {
if n < 0 {
let s = english_words(-n, ordinal, case);
return format!("MINUS {s}");
}
let words = if ordinal { english_ordinal(n) } else { english_cardinal(n) };
case_transform(&words, case)
}
pub fn case_transform(s: &str, case: WordCase) -> String {
match case {
WordCase::Upper => s.to_uppercase(),
WordCase::Lower => s.to_lowercase(),
WordCase::Title => {
let mut out = String::with_capacity(s.len());
let mut start_of_word = true;
for c in s.chars() {
if c.is_whitespace() || c == '-' {
out.push(c);
start_of_word = true;
} else if start_of_word {
for u in c.to_uppercase() { out.push(u); }
start_of_word = false;
} else {
for l in c.to_lowercase() { out.push(l); }
}
}
out
}
}
}
fn english_cardinal(n: i64) -> String {
const UNDER_20: &[&str] = &[
"zero", "one", "two", "three", "four", "five", "six", "seven",
"eight", "nine", "ten", "eleven", "twelve", "thirteen",
"fourteen", "fifteen", "sixteen", "seventeen", "eighteen",
"nineteen",
];
const TENS: &[&str] = &[
"", "", "twenty", "thirty", "forty", "fifty", "sixty",
"seventy", "eighty", "ninety",
];
if (0..20).contains(&n) { return UNDER_20[n as usize].to_string(); }
if n < 100 {
let t = (n / 10) as usize;
let r = (n % 10) as usize;
if r == 0 { return TENS[t].to_string(); }
return format!("{} {}", TENS[t], UNDER_20[r]);
}
if n < 1000 {
let h = (n / 100) as i64;
let r = n % 100;
let head = format!("{} hundred", UNDER_20[h as usize]);
if r == 0 { head }
else { format!("{head} and {}", english_cardinal(r)) }
} else if n < 1_000_000 {
let th = n / 1000;
let r = n % 1000;
let head = format!("{} thousand", english_cardinal(th));
if r == 0 { head }
else if r < 100 { format!("{head} and {}", english_cardinal(r)) }
else { format!("{head} {}", english_cardinal(r)) }
} else if n < 1_000_000_000 {
let mil = n / 1_000_000;
let r = n % 1_000_000;
let head = format!("{} million", english_cardinal(mil));
if r == 0 { head }
else if r < 100 { format!("{head} and {}", english_cardinal(r)) }
else { format!("{head} {}", english_cardinal(r)) }
} else {
n.to_string()
}
}
fn english_ordinal(n: i64) -> String {
const ORDINAL_UNDER_20: &[&str] = &[
"zeroth", "first", "second", "third", "fourth", "fifth",
"sixth", "seventh", "eighth", "ninth", "tenth", "eleventh",
"twelfth", "thirteenth", "fourteenth", "fifteenth",
"sixteenth", "seventeenth", "eighteenth", "nineteenth",
];
const ORDINAL_TENS: &[&str] = &[
"", "", "twentieth", "thirtieth", "fortieth", "fiftieth",
"sixtieth", "seventieth", "eightieth", "ninetieth",
];
const TENS: &[&str] = &[
"", "", "twenty", "thirty", "forty", "fifty", "sixty",
"seventy", "eighty", "ninety",
];
if (0..20).contains(&n) { return ORDINAL_UNDER_20[n as usize].to_string(); }
if n < 100 {
let t = (n / 10) as usize;
let r = (n % 10) as usize;
if r == 0 { return ORDINAL_TENS[t].to_string(); }
return format!("{} {}", TENS[t], ORDINAL_UNDER_20[r]);
}
let head_div = if n < 1000 { 100 }
else if n < 1_000_000 { 1000 }
else { 1_000_000 };
let head_word = if n < 1000 {
format!("{} hundred", english_cardinal(n / 100))
} else if n < 1_000_000 {
format!("{} thousand", english_cardinal(n / 1000))
} else {
format!("{} million", english_cardinal(n / 1_000_000))
};
let r = n % head_div;
if r == 0 {
format!("{head_word}th")
} else if n < 1000 {
format!("{head_word} and {}", english_ordinal(r))
} else if r < 100 {
format!("{head_word} and {}", english_ordinal(r))
} else {
format!("{head_word} {}", english_ordinal(r))
}
}
#[derive(Clone, Copy)]
enum MinMaxOp { Min, Max, Avg }
fn min_max_avg<I: DocIndexLike>(
v: &Value, idx: &I, op: MinMaxOp, ci: bool,
) -> Result<Value> {
let strs = sequence_to_strings(v, idx);
if strs.is_empty() {
return Ok(Value::NodeSet(Vec::new()));
}
if let Value::Sequence(items) = v {
if matches!(op, MinMaxOp::Avg) {
if let Some((kind, total, count)) = duration_seq_total(items) {
let units = if kind == "yearMonthDuration" {
round_months_half_up(total as f64 / count as f64)
} else {
total / count
};
return Ok(duration_value(kind, units));
}
}
if matches!(op, MinMaxOp::Min | MinMaxOp::Max)
&& items.iter().all(|x| matches!(x, Value::Typed(_)))
{
use std::cmp::Ordering;
let mut best = &items[0];
let mut comparable = true;
for x in &items[1..] {
match compare_typed_values(best, x) {
Some(ord) => {
let take = match op {
MinMaxOp::Max => ord == Ordering::Less,
_ => ord == Ordering::Greater,
};
if take { best = x; }
}
None => { comparable = false; break; }
}
}
if comparable { return Ok(best.clone()); }
}
}
fn is_string_item(v: &Value) -> bool {
match v {
Value::String(_) => true,
Value::Typed(t) => matches!(t.kind,
"string" | "anyURI" | "normalizedString" | "token"
| "Name" | "NCName" | "language"),
_ => false,
}
}
let all_typed_string = match v {
Value::Typed(_) | Value::String(_) => is_string_item(v),
Value::Sequence(items) => !items.is_empty()
&& items.iter().all(is_string_item),
_ => false,
};
if all_typed_string {
if matches!(op, MinMaxOp::Avg) {
return Ok(Value::Number(Numeric::Double(f64::NAN)));
}
let key = |s: &String| if ci { ascii_ci_fold(s) } else { s.clone() };
let chosen = match op {
MinMaxOp::Min => strs.iter().min_by(|a, b| key(a).cmp(&key(b))).cloned(),
MinMaxOp::Max => strs.iter().max_by(|a, b| key(a).cmp(&key(b))).cloned(),
MinMaxOp::Avg => unreachable!(),
};
return Ok(chosen.map(Value::String).unwrap_or(Value::NodeSet(Vec::new())));
}
let numeric: Option<Vec<f64>> = strs.iter()
.map(|s| s.trim().parse::<f64>().ok())
.collect();
if let Some(nums) = numeric {
let n = nums.len() as f64;
let result = match op {
MinMaxOp::Min => nums.into_iter().fold(f64::INFINITY, f64::min),
MinMaxOp::Max => nums.into_iter().fold(f64::NEG_INFINITY, f64::max),
MinMaxOp::Avg => nums.into_iter().sum::<f64>() / n,
};
let kind = match v {
Value::Sequence(items) => items.iter().fold(Some("integer"), |acc, it| {
match (acc, numeric_kind_of(it)) {
(Some(a), Some(b)) => numeric_promote_kind(Some(a), Some(b)),
_ => None,
}
}),
Value::IntRange { .. } => Some("integer"),
other => numeric_kind_of(other),
}.unwrap_or("double");
let kind = if matches!(op, MinMaxOp::Avg) && kind == "integer" { "decimal" } else { kind };
return Ok(Value::Number(Numeric::of_kind(kind, result)));
}
if matches!(op, MinMaxOp::Avg) {
return Ok(Value::Number(Numeric::Double(f64::NAN)));
}
let key = |s: &String| if ci { ascii_ci_fold(s) } else { s.clone() };
let chosen = match op {
MinMaxOp::Min => strs.iter().min_by(|a, b| key(a).cmp(&key(b))).cloned(),
MinMaxOp::Max => strs.iter().max_by(|a, b| key(a).cmp(&key(b))).cloned(),
MinMaxOp::Avg => unreachable!(),
};
Ok(chosen.map(Value::String).unwrap_or(Value::NodeSet(Vec::new())))
}
#[derive(Clone, Copy)]
enum ValueCmp { Eq, Ne, Lt, Gt, Le, Ge }
fn is_unordered_atomic_kind(kind: &str) -> bool {
matches!(kind,
"gYear" | "gYearMonth" | "gMonth" | "gMonthDay" | "gDay"
| "duration" | "hexBinary" | "base64Binary"
| "QName" | "NOTATION")
}
fn value_compare<I: DocIndexLike>(
l: &Expr, r: &Expr, ctx: &EvalCtx<'_>, idx: &I, op: ValueCmp,
) -> Result<Value> {
let lv = atomise_singleton(eval_expr(l, ctx, idx)?, idx, ctx.bindings)?;
let rv = atomise_singleton(eval_expr(r, ctx, idx)?, idx, ctx.bindings)?;
let (lv, rv) = match (lv, rv) {
(None, _) | (_, None) => return Ok(Value::NodeSet(Vec::new())),
(Some(a), Some(b)) => (a, b),
};
if value_is_function(&lv) || value_is_function(&rv) {
return Err(xpath_err(
"a function item cannot appear in a value comparison (FOTY0013)")
.with_xpath_code("FOTY0013"));
}
let coll = DEFAULT_COLLATION.with(|c| c.borrow().clone());
let collated = coll.as_deref().filter(|u|
*u == "http://www.w3.org/2005/xpath-functions/collation/html-ascii-case-insensitive");
if let Some(_) = collated {
if values_both_stringy(&lv, &rv) {
let a = ascii_ci_fold(&value_to_string_with(&lv, idx, ctx.bindings));
let b = ascii_ci_fold(&value_to_string_with(&rv, idx, ctx.bindings));
let result = match op {
ValueCmp::Eq => a == b,
ValueCmp::Ne => a != b,
ValueCmp::Lt => a < b,
ValueCmp::Gt => a > b,
ValueCmp::Le => a <= b,
ValueCmp::Ge => a >= b,
};
return Ok(Value::Boolean(result));
}
}
let result = match op {
ValueCmp::Eq => values_eq(&lv, &rv, idx, ctx.bindings),
ValueCmp::Ne => values_ne(&lv, &rv, idx, ctx.bindings),
ValueCmp::Lt | ValueCmp::Gt | ValueCmp::Le | ValueCmp::Ge => {
for v in [&lv, &rv] {
if let Value::Typed(t) = v {
if is_unordered_atomic_kind(t.kind) {
return Err(xpath_err(format!(
"value comparison operator is not defined on xs:{}",
t.kind,
)).with_xpath_code("XPTY0004"));
}
}
}
if let Some(ord) = compare_typed_values(&lv, &rv) {
use std::cmp::Ordering;
match (op, ord) {
(ValueCmp::Lt, Ordering::Less) => true,
(ValueCmp::Gt, Ordering::Greater) => true,
(ValueCmp::Le, Ordering::Less | Ordering::Equal) => true,
(ValueCmp::Ge, Ordering::Greater | Ordering::Equal) => true,
_ => false,
}
} else {
let a = value_to_number_with(&lv, idx, ctx.bindings);
let b = value_to_number_with(&rv, idx, ctx.bindings);
match op {
ValueCmp::Lt => a < b,
ValueCmp::Gt => a > b,
ValueCmp::Le => a <= b,
ValueCmp::Ge => a >= b,
_ => unreachable!(),
}
}
}
};
Ok(Value::Boolean(result))
}
fn values_both_stringy(a: &Value, b: &Value) -> bool {
fn is_str(v: &Value) -> bool {
match v {
Value::String(_) => true,
Value::Typed(t) => matches!(t.kind,
"string" | "untypedAtomic" | "anyURI"
| "normalizedString" | "token" | "Name" | "NCName"
| "language" | "ID" | "IDREF" | "ENTITY" | "NMTOKEN"
| "NOTATION" | "QName"),
_ => false,
}
}
is_str(a) && is_str(b)
}
pub fn date_value_to_utc_micros(lex: &str, kind: DateKind) -> Option<i128> {
let (y, mo, d, h, mi, sec, frac, tz) = parse_xsd_date_time(lex, kind)?;
let tz_min = tz.unwrap_or(0) as i64;
let day_count = match kind {
DateKind::Time => 0,
_ => ymd_to_days(y, mo as u32, d as u32),
};
let secs = day_count * 86_400
+ (h as i64) * 3600 + (mi as i64) * 60 + (sec as i64)
- tz_min * 60;
Some((secs as i128) * 1_000_000 + (frac as i128))
}
fn compare_typed_values(a: &Value, b: &Value) -> Option<std::cmp::Ordering> {
fn date_kind(v: &Value) -> Option<(&'static str, String)> {
match v {
Value::Typed(t) => {
let kind = match t.kind {
"date" => "date",
"dateTime" => "dateTime",
"time" => "time",
"gYear" | "gYearMonth"
| "gMonth" | "gMonthDay" | "gDay" => "date",
"dayTimeDuration" => "dayTime",
"yearMonthDuration"=> "yearMonth",
"duration" => "duration",
_ => return None,
};
Some((kind, t.lexical.clone()))
}
_ => None,
}
}
let (ka, la) = date_kind(a)?;
let (kb, lb) = date_kind(b)?;
if ka != kb { return None; }
match ka {
"date" | "dateTime" | "time" => {
let dk = match ka {
"date" => DateKind::Date,
"dateTime" => DateKind::DateTime,
"time" => DateKind::Time,
_ => unreachable!(),
};
let a_us = date_value_to_utc_micros(&la, dk)?;
let b_us = date_value_to_utc_micros(&lb, dk)?;
Some(a_us.cmp(&b_us))
}
"dayTime" => {
let a_s = parse_day_time_duration_secs(&la)?;
let b_s = parse_day_time_duration_secs(&lb)?;
Some(a_s.cmp(&b_s))
}
"yearMonth" => {
let a_m = parse_year_month_duration_months(&la)?;
let b_m = parse_year_month_duration_months(&lb)?;
Some(a_m.cmp(&b_m))
}
_ => None,
}
}
fn atomise_singleton<I: DocIndexLike>(
v: Value, idx: &I, _bindings: &dyn XPathBindings,
) -> Result<Option<Value>> {
match v {
Value::NodeSet(ns) => match ns.len() {
0 => Ok(None),
1 => Ok(Some(Value::String(idx.string_value(ns[0])))),
_ => Err(xpath_err(
"value-comparison operand must have at most one item")),
}
Value::ForeignNodeSet(_) => Ok(Some(v)),
Value::Sequence(items) => match items.len() {
0 => Ok(None),
1 => Ok(Some(items.into_iter().next().unwrap())),
_ => Err(xpath_err(
"value-comparison operand must have at most one item")),
}
other => Ok(Some(other)),
}
}
fn try_membership_filter<I: DocIndexLike>(
pred: &Expr,
items: &[Value],
ctx: &EvalCtx<'_>,
idx: &I,
) -> Result<Option<Vec<Value>>> {
let Some((rhs, negated)) = classify_membership_pred(pred) else {
return Ok(None);
};
if expr_references_context_item(rhs) {
return Ok(None);
}
let rhs_val = eval_expr(rhs, ctx, idx)?;
let set = match build_membership_set(&rhs_val, idx) {
Some(s) => s,
None => return Ok(None),
};
let mut kept = Vec::with_capacity(items.len());
for item in items {
let hit = match &set {
MembershipSet::Int(s) => match item_as_int(item) {
Some(n) => s.contains(&n),
None => false,
},
MembershipSet::Str(s) => {
let item_s = match item {
Value::String(t) => t.clone(),
Value::NodeSet(ns) if ns.len() == 1 => idx.string_value(ns[0]),
_ => continue,
};
s.contains(&item_s)
}
};
if hit ^ negated {
kept.push(item.clone());
}
}
Ok(Some(kept))
}
fn classify_membership_pred(pred: &Expr) -> Option<(&Expr, bool)> {
if let Expr::FunctionCall(name, args) = pred {
if name == "not" && args.len() == 1 {
return classify_membership_pred(&args[0]).map(|(rhs, neg)| (rhs, !neg));
}
}
match pred {
Expr::Eq(a, b) => match (is_bare_dot(a), is_bare_dot(b)) {
(true, false) => Some((b.as_ref(), false)),
(false, true) => Some((a.as_ref(), false)),
_ => None,
},
Expr::Ne(a, b) => match (is_bare_dot(a), is_bare_dot(b)) {
(true, false) => Some((b.as_ref(), true)),
(false, true) => Some((a.as_ref(), true)),
_ => None,
},
_ => None,
}
}
fn is_bare_dot(e: &Expr) -> bool {
if let Expr::Path(p) = e {
return is_bare_dot_path(p);
}
false
}
pub fn expr_references_context_item(e: &Expr) -> bool {
use Expr::*;
match e {
Path(p) => path_uses_context_item(p),
ContextItem => true,
Variable(_) | Literal(_) | Integer(_) | Decimal(_) | Double(_) => false,
Or(l, r) | And(l, r)
| Eq(l, r) | Ne(l, r) | Lt(l, r) | Gt(l, r) | Le(l, r) | Ge(l, r)
| ValueEq(l, r) | ValueNe(l, r)
| ValueLt(l, r) | ValueGt(l, r) | ValueLe(l, r) | ValueGe(l, r)
| Add(l, r) | Sub(l, r) | Mul(l, r) | Div(l, r) | Mod(l, r)
| Union(l, r) | IDiv(l, r) | Intersect(l, r) | Except(l, r)
| Range(l, r) | SimpleMap(l, r) | NodeBefore(l, r) | NodeAfter(l, r)
| NodeIs(l, r) =>
expr_references_context_item(l) || expr_references_context_item(r),
Neg(x) | InstanceOf(x, _) | CastAs(x, _)
| CastableAs(x, _) | TreatAs(x, _) => expr_references_context_item(x),
IfThenElse { cond, then_branch, else_branch } =>
expr_references_context_item(cond)
|| expr_references_context_item(then_branch)
|| expr_references_context_item(else_branch),
For { bindings, body } | Let { bindings, body }
| Quantified { bindings, test: body, .. } =>
bindings.iter().any(|(_, e)| expr_references_context_item(e))
|| expr_references_context_item(body),
Sequence(items) => items.iter().any(expr_references_context_item),
FilterPath { primary, predicates, steps } => {
expr_references_context_item(primary)
|| predicates.iter().any(expr_references_context_item)
|| steps.iter().any(|s| s.predicates.iter().any(expr_references_context_item))
}
FunctionCall(_, args) => args.iter().any(expr_references_context_item),
TryCatch { body, catches } =>
expr_references_context_item(body)
|| catches.iter().any(|c| expr_references_context_item(&c.body)),
WithDefaultCollation(_, inner) => expr_references_context_item(inner),
BackwardsCompat(inner) => expr_references_context_item(inner),
MapConstructor(entries) => entries.iter()
.any(|(k, v)| expr_references_context_item(k) || expr_references_context_item(v)),
ArrayConstructor { members, .. } =>
members.iter().any(expr_references_context_item),
Lookup(base, key) => expr_references_context_item(base)
|| lookup_key_references_context_item(key),
UnaryLookup(_) => true,
InlineFunction { .. } | NamedFunctionRef { .. } | Placeholder => false,
DynamicCall { func, args } => expr_references_context_item(func)
|| args.iter().any(expr_references_context_item),
}
}
fn lookup_key_references_context_item(key: &crate::xpath::ast::LookupKey) -> bool {
matches!(key, crate::xpath::ast::LookupKey::Expr(e)
if expr_references_context_item(e))
}
fn path_uses_context_item(path: &crate::xpath::ast::LocationPath) -> bool {
use crate::xpath::ast::LocationPath;
match path {
LocationPath::Absolute(_) => false,
LocationPath::Relative(_) => true,
}
}
enum MembershipSet {
Int(HashSet<i64>),
Str(HashSet<String>),
}
fn build_membership_set<I: DocIndexLike>(v: &Value, idx: &I) -> Option<MembershipSet> {
let mut all_int = true;
let mut all_str = true;
for item in iter_items(v) {
if item_as_int(&item).is_none() { all_int = false; }
if !item_as_string_kind(&item) { all_str = false; }
if !all_int && !all_str { return None; }
}
if all_int {
let s: HashSet<i64> = iter_items(v)
.filter_map(|it| item_as_int(&it))
.collect();
return Some(MembershipSet::Int(s));
}
if all_str {
let s: HashSet<String> = iter_items(v)
.filter_map(|it| item_as_string(&it, idx))
.collect();
return Some(MembershipSet::Str(s));
}
None
}
fn item_as_int(v: &Value) -> Option<i64> {
match v {
Value::Number(n) if n.as_f64().is_finite() && n.as_f64().fract() == 0.0 => Some(n.as_f64() as i64),
Value::Typed(t) => t.numeric.and_then(|n|
if n.is_finite() && n.fract() == 0.0 { Some(n as i64) } else { None }),
_ => None,
}
}
fn item_as_string_kind(v: &Value) -> bool {
matches!(v, Value::String(_) | Value::NodeSet(_)) || matches!(v, Value::Typed(_))
}
fn item_as_string<I: DocIndexLike>(v: &Value, idx: &I) -> Option<String> {
match v {
Value::String(s) => Some(s.clone()),
Value::NodeSet(ns) if ns.len() == 1 => Some(idx.string_value(ns[0])),
Value::Typed(t) => Some(t.lexical.clone()),
_ => None,
}
}
fn filter_sequence_by_predicates<I: DocIndexLike>(
items: Vec<Value>, predicates: &[Expr], ctx: &EvalCtx<'_>, idx: &I,
) -> Result<Vec<Value>> {
let needs_flatten = items.iter().any(|v|
matches!(v, Value::IntRange { .. } | Value::Sequence(_)));
let mut surviving: Vec<Value> = if needs_flatten {
items.iter().flat_map(iter_items).collect()
} else {
items
};
for pred in predicates {
if let Some(fast) = try_membership_filter(pred, &surviving, ctx, idx)? {
surviving = fast;
continue;
}
let size = surviving.len();
let mut next: Vec<Value> = Vec::with_capacity(size);
for (i, item) in surviving.into_iter().enumerate() {
let pos = i + 1;
let (ctx_node, ctx_item) = match &item {
Value::NodeSet(ns) if ns.len() == 1 =>
(ns[0], None),
_ => (ctx.context_node, Some(item.clone())),
};
let inner_ctx = EvalCtx {
context_node: ctx_node,
pos, size,
bindings: ctx.bindings,
static_ctx: ctx.static_ctx,
};
let pv = with_context_item(ctx_item, ||
eval_expr(pred, &inner_ctx, idx)
)?;
let keep = match pv {
Value::Number(n) => (n.as_f64() as usize) == pos && n.as_f64().fract() == 0.0,
Value::Typed(ref t) if t.numeric.is_some() => {
let n = t.numeric.unwrap();
(n as usize) == pos && n.fract() == 0.0
}
Value::NodeSet(ref ns) if ns.len() == 1
&& matches!(idx.kind(ns[0]),
crate::xpath::XPathNodeKind::Text)
&& idx.parent(ns[0]).is_none()
=> {
let s = idx.string_value(ns[0]);
match s.trim().parse::<f64>() {
Ok(n) if n.fract() == 0.0 => (n as usize) == pos,
_ => value_to_bool(&pv, idx),
}
}
v => value_to_bool(&v, idx),
};
if keep { next.push(item); }
}
surviving = next;
}
Ok(surviving)
}
fn sequence_to_strings<I: DocIndexLike>(v: &Value, idx: &I) -> Vec<String> {
match v {
Value::NodeSet(ns) => ns.iter().map(|&id| idx.string_value(id)).collect(),
Value::ForeignNodeSet(_) => vec![value_to_string(v, idx)],
Value::String(s) => vec![s.clone()],
Value::Number(_) | Value::Boolean(_) => vec![value_to_string(v, idx)],
Value::Typed(t) => vec![t.lexical.clone()],
Value::Sequence(items) => items.iter()
.flat_map(|item| sequence_to_strings(item, idx))
.collect(),
Value::IntRange { lo, hi } => (*lo..=*hi).map(|i| i.to_string()).collect(),
Value::Map(_) | Value::Array(_) | Value::Function(_) => Vec::new(),
}
}
fn sequence_to_numbers<I: DocIndexLike>(v: &Value, idx: &I) -> Vec<f64> {
sequence_to_strings(v, idx).into_iter()
.map(|s| s.trim().parse().unwrap_or(f64::NAN))
.collect()
}
pub fn compile_xpath_2_0_regex(pattern: &str, flags: &str) -> Result<regex::Regex> {
compile_xpath_regex(pattern, flags)
}
fn is_ascii_ci_collation(uri: Option<&str>) -> bool {
matches!(uri, Some(u) if u ==
"http://www.w3.org/2005/xpath-functions/collation/html-ascii-case-insensitive")
}
fn ascii_ci_fold(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
if c.is_ascii_uppercase() { out.push(c.to_ascii_lowercase()); }
else { out.push(c); }
}
out
}
fn collation_starts_with(s: &str, pre: &str, uri: Option<&str>) -> bool {
if is_ascii_ci_collation(uri) {
ascii_ci_fold(s).starts_with(ascii_ci_fold(pre).as_str())
} else {
s.starts_with(pre)
}
}
fn collation_contains(s: &str, sub: &str, uri: Option<&str>) -> bool {
if is_ascii_ci_collation(uri) {
ascii_ci_fold(s).contains(ascii_ci_fold(sub).as_str())
} else {
s.contains(sub)
}
}
fn collation_ends_with(s: &str, suf: &str, uri: Option<&str>) -> bool {
if is_ascii_ci_collation(uri) {
ascii_ci_fold(s).ends_with(ascii_ci_fold(suf).as_str())
} else {
s.ends_with(suf)
}
}
fn translate_xpath_replacement(s: &str, group_count: usize) -> Result<String> {
let mut out = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
match c {
'\\' => {
match chars.next() {
Some('\\') => out.push('\\'),
Some('$') => out.push('$'),
Some(other) => return Err(xpath_err(format!(
"replace(): replacement contains illegal escape '\\{other}'"
)).with_xpath_code("FORX0004")),
None => return Err(xpath_err(
"replace(): replacement ends with a trailing backslash"
).with_xpath_code("FORX0004")),
}
}
'$' => {
match chars.peek() {
Some(d) if d.is_ascii_digit() => {
let mut digits = String::new();
while let Some(&d) = chars.peek() {
if d.is_ascii_digit() { digits.push(d); chars.next(); }
else { break; }
}
let mut take = digits.len();
while take > 0 {
let n: usize = digits[..take].parse().unwrap_or(usize::MAX);
if n <= group_count { break; }
take -= 1;
}
if take == 0 {
out.push_str("$$");
out.push_str(&digits);
} else if take == digits.len() {
out.push('$');
out.push_str(&digits);
} else {
out.push_str("${");
out.push_str(&digits[..take]);
out.push('}');
out.push_str(&digits[take..]);
}
}
_ => return Err(xpath_err(
"replace(): unescaped '$' in replacement"
).with_xpath_code("FORX0004")),
}
}
other => out.push(other),
}
}
Ok(out)
}
fn compile_xpath_regex(pattern: &str, flags: &str) -> Result<regex::Regex> {
compile_xpath_regex_dialect(pattern, flags, crate::regex::Dialect::Xpath)
}
fn compile_xpath_regex_dialect(
pattern: &str, flags: &str, dialect: crate::regex::Dialect,
) -> Result<regex::Regex> {
let literal = flags.contains('q');
if !literal && dialect == crate::regex::Dialect::Xpath20 {
crate::regex::parser::parse_with(pattern, dialect)
.map_err(|e| xpath_err(format!("invalid regex: {e}"))
.with_xpath_code("FORX0002"))?;
}
let mut inline = String::new();
if !flags.is_empty() {
let mut seen = [false; 128];
inline.push_str("(?");
for c in flags.chars() {
if matches!(c, 's' | 'm' | 'i' | 'x') {
let idx = c as usize;
if !seen[idx] {
seen[idx] = true;
inline.push(c);
}
}
}
inline.push(')');
if inline.ends_with("(?)") { inline.clear(); }
}
let body = if literal {
regex::escape(pattern)
} else {
translate_xsd_regex_escapes(pattern)
};
let full = format!("{inline}{body}");
regex::Regex::new(&full)
.map_err(|e| xpath_err(format!("invalid regex: {e}")).with_xpath_code("FORX0002"))
}
fn translate_xsd_regex_escapes(pattern: &str) -> String {
const NAME_CHAR: &str = "A-Za-z0-9._\\-:\u{00B7}\u{C0}-\u{D6}\u{D8}-\u{F6}\u{F8}-\u{2FF}\u{370}-\u{37D}\u{37F}-\u{1FFF}\u{200C}-\u{200D}\u{2070}-\u{218F}\u{2C00}-\u{2FEF}\u{3001}-\u{D7FF}\u{F900}-\u{FDCF}\u{FDF0}-\u{FFFD}";
const NAME_CHAR_NEG: &str = "^A-Za-z0-9._\\-:\u{00B7}\u{C0}-\u{D6}\u{D8}-\u{F6}\u{F8}-\u{2FF}\u{370}-\u{37D}\u{37F}-\u{1FFF}\u{200C}-\u{200D}\u{2070}-\u{218F}\u{2C00}-\u{2FEF}\u{3001}-\u{D7FF}\u{F900}-\u{FDCF}\u{FDF0}-\u{FFFD}";
const NAME_START: &str = "A-Za-z_:\u{C0}-\u{D6}\u{D8}-\u{F6}\u{F8}-\u{2FF}\u{370}-\u{37D}\u{37F}-\u{1FFF}\u{200C}-\u{200D}\u{2070}-\u{218F}\u{2C00}-\u{2FEF}\u{3001}-\u{D7FF}\u{F900}-\u{FDCF}\u{FDF0}-\u{FFFD}";
const NAME_START_NEG: &str = "^A-Za-z_:\u{C0}-\u{D6}\u{D8}-\u{F6}\u{F8}-\u{2FF}\u{370}-\u{37D}\u{37F}-\u{1FFF}\u{200C}-\u{200D}\u{2070}-\u{218F}\u{2C00}-\u{2FEF}\u{3001}-\u{D7FF}\u{F900}-\u{FDCF}\u{FDF0}-\u{FFFD}";
let mut out = String::with_capacity(pattern.len());
let mut chars = pattern.chars().peekable();
let mut in_class = false;
while let Some(c) = chars.next() {
match c {
'\\' => {
let next = chars.next().unwrap_or('\0');
match next {
'c' => if in_class { out.push_str(NAME_CHAR) } else { out.push_str(&format!("[{NAME_CHAR}]")) },
'C' => if in_class { out.push_str(NAME_CHAR_NEG) } else { out.push_str(&format!("[{NAME_CHAR_NEG}]")) },
'i' => if in_class { out.push_str(NAME_START) } else { out.push_str(&format!("[{NAME_START}]")) },
'I' => if in_class { out.push_str(NAME_START_NEG) } else { out.push_str(&format!("[{NAME_START_NEG}]")) },
other => { out.push('\\'); out.push(other); }
}
}
'[' => { out.push('['); in_class = true; }
']' => { out.push(']'); in_class = false; }
_ => out.push(c),
}
}
out
}
fn xs_constructor<I: DocIndexLike>(
local: &str,
args: &[Value],
idx: &I,
bindings: &dyn XPathBindings,
) -> Result<Value> {
if args.len() != 1 {
return Err(xpath_err(format!("xs:{local}(): requires exactly 1 argument")));
}
if let Value::NodeSet(ref ns) = args[0] {
if ns.is_empty() {
return Ok(Value::NodeSet(Vec::new()));
}
}
if local == "date" {
if let Value::Typed(t) = &args[0] {
if t.kind == "dateTime" {
if let Some(t_pos) = t.lexical.find('T') {
let after = &t.lexical[t_pos + 1..];
let tz_start = after.rfind(|c| c == 'Z' || c == '+' || c == '-')
.filter(|&i| {
i >= after.len().saturating_sub(6)
});
let tz = tz_start.map(|i| &after[i..]).unwrap_or("");
let lex = format!("{}{}", &t.lexical[..t_pos], tz);
return Ok(Value::Typed(Box::new(TypedAtomic {
kind: "date", lexical: lex, numeric: None, boolean: None, user_type: None,
})));
}
}
}
}
if matches!(local, "hexBinary" | "base64Binary") {
if let Some(lex) = convert_binary_kind(&args[0], local) {
let kind = atomic_kind_static(local).unwrap();
return Ok(Value::Typed(Box::new(TypedAtomic {
kind, lexical: lex, numeric: None, boolean: None, user_type: None,
})));
}
}
if matches!(local, "gYear" | "gYearMonth" | "gMonth" | "gMonthDay" | "gDay") {
if let Value::Typed(t) = &args[0] {
let dk = match t.kind {
"date" => Some(DateKind::Date),
"dateTime" => Some(DateKind::DateTime),
_ => None,
};
if let Some(dk) = dk {
if let Some((y, mo, d, _, _, _, _, tz)) = parse_xsd_date_time(&t.lexical, dk) {
let tzs = tz.map(format_tz_suffix).unwrap_or_default();
let lex = match local {
"gYear" => format!("{y:04}{tzs}"),
"gYearMonth" => format!("{y:04}-{mo:02}{tzs}"),
"gMonth" => format!("--{mo:02}{tzs}"),
"gMonthDay" => format!("--{mo:02}-{d:02}{tzs}"),
_ => format!("---{d:02}{tzs}"),
};
let kind = atomic_kind_static(local).unwrap();
return Ok(Value::Typed(Box::new(TypedAtomic {
kind, lexical: lex, numeric: None, boolean: None, user_type: None,
})));
}
}
}
}
let s = value_to_string_with(&args[0], idx, bindings);
let trimmed = s.trim();
let kind = atomic_kind_static(local).ok_or_else(|| xpath_err(format!(
"xs:{local}(): unknown XSD constructor function"
)))?;
let lexically_invalid = match kind {
"date" | "dateTime" | "time" | "duration" | "dayTimeDuration"
| "yearMonthDuration" | "gYear" | "gYearMonth" | "gMonth"
| "gMonthDay" | "gDay" | "hexBinary"
=> !lexical_matches_type(trimmed, kind),
"decimal" => trimmed.contains(['e', 'E'])
|| trimmed.parse::<f64>().is_err(),
_ => false,
};
if lexically_invalid {
if date_year_out_of_range(trimmed, kind) {
return Err(xpath_err(format!(
"xs:{local}: year is outside the supported range: '{trimmed}'"
)).with_xpath_code("FODT0001"));
}
return Err(xpath_err(format!(
"xs:{local}: '{trimmed}' is not a valid lexical xs:{local}"
)).with_xpath_code("FORG0001"));
}
let numeric = if is_numeric_kind(kind) {
let raw = match trimmed {
"INF" | "Infinity" => f64::INFINITY,
"-INF" | "-Infinity" => f64::NEG_INFINITY,
"NaN" => f64::NAN,
_ => match trimmed.parse::<f64>() {
Ok(n) => n,
Err(_) => return Err(xpath_err(format!(
"xs:{local}: '{trimmed}' is not a valid numeric \
lexical form"
)).with_xpath_code("FORG0001")),
},
};
Some(if kind == "float" { raw as f32 as f64 } else { raw })
} else { None };
let boolean = if kind == "boolean" {
let from_num = match &args[0] {
Value::Number(n) => Some(!(n.as_f64() == 0.0 || n.as_f64().is_nan())),
Value::Typed(t) if t.numeric.is_some() => {
let n = t.numeric.unwrap();
Some(!(n == 0.0 || n.is_nan()))
}
_ => None,
};
Some(from_num.unwrap_or_else(|| matches!(trimmed, "true" | "1")))
} else { None };
let lexical = if kind == "boolean" {
if boolean == Some(true) { "true".into() } else { "false".into() }
} else if kind == "double" {
canonical_double_lex(numeric.unwrap_or(f64::NAN), &args[0])
} else if kind == "float" {
canonical_float_lex(numeric.unwrap_or(f64::NAN), &args[0])
} else if kind == "decimal" {
canonical_decimal_lex(numeric.unwrap_or(f64::NAN), trimmed)
} else if is_integer_subkind(kind) && source_is_numeric(&args[0]) {
let n = numeric.unwrap_or(f64::NAN);
if !n.is_finite() {
return Err(xpath_err(format!(
"xs:{local}: cannot cast non-finite numeric to {kind}"
)).with_xpath_code("FOCA0002"));
}
(n.trunc() as i64).to_string()
} else if matches!(kind, "date" | "dateTime" | "time") {
canonical_date_time_lex(trimmed, kind)
} else if matches!(kind, "gYear" | "gYearMonth" | "gMonth" | "gMonthDay" | "gDay") {
if let Some(stripped) = trimmed.strip_suffix("+00:00")
.or_else(|| trimmed.strip_suffix("-00:00")) {
format!("{stripped}Z")
} else {
trimmed.to_string()
}
} else if kind == "yearMonthDuration" {
canonical_year_month_duration_lex(trimmed)
} else if kind == "dayTimeDuration" {
canonical_day_time_duration_lex(trimmed)
} else if kind == "hexBinary" {
trimmed.to_ascii_uppercase()
} else { trimmed.to_string() };
Ok(Value::Typed(Box::new(TypedAtomic { kind, lexical, numeric, boolean, user_type: None })))
}
pub fn atomic_kind_static(local: &str) -> Option<&'static str> {
Some(match local {
"string" => "string",
"normalizedString" => "normalizedString",
"token" => "token",
"Name" => "Name",
"NCName" => "NCName",
"QName" => "QName",
"ID" => "ID",
"IDREF" => "IDREF",
"IDREFS" => "IDREFS",
"ENTITY" => "ENTITY",
"ENTITIES" => "ENTITIES",
"NMTOKEN" => "NMTOKEN",
"NMTOKENS" => "NMTOKENS",
"anyURI" => "anyURI",
"language" => "language",
"NOTATION" => "NOTATION",
"boolean" => "boolean",
"integer" => "integer",
"int" => "int",
"long" => "long",
"short" => "short",
"byte" => "byte",
"unsignedInt" => "unsignedInt",
"unsignedLong" => "unsignedLong",
"unsignedShort" => "unsignedShort",
"unsignedByte" => "unsignedByte",
"nonNegativeInteger" => "nonNegativeInteger",
"nonPositiveInteger" => "nonPositiveInteger",
"positiveInteger" => "positiveInteger",
"negativeInteger" => "negativeInteger",
"decimal" => "decimal",
"double" => "double",
"float" => "float",
"date" => "date",
"dateTime" => "dateTime",
"time" => "time",
"duration" => "duration",
"dayTimeDuration" => "dayTimeDuration",
"yearMonthDuration" => "yearMonthDuration",
"gYear" => "gYear",
"gYearMonth" => "gYearMonth",
"gMonth" => "gMonth",
"gMonthDay" => "gMonthDay",
"gDay" => "gDay",
"hexBinary" => "hexBinary",
"base64Binary" => "base64Binary",
"untypedAtomic" => "untypedAtomic",
"anyAtomicType" => "anyAtomicType",
_ => return None,
})
}
fn is_numeric_kind(k: &str) -> bool {
matches!(k,
"integer" | "int" | "long" | "short" | "byte"
| "unsignedInt" | "unsignedLong" | "unsignedShort" | "unsignedByte"
| "nonNegativeInteger" | "nonPositiveInteger"
| "positiveInteger" | "negativeInteger"
| "decimal" | "double" | "float")
}
fn is_integer_subkind(k: &str) -> bool {
matches!(k,
"integer" | "int" | "long" | "short" | "byte"
| "unsignedInt" | "unsignedLong" | "unsignedShort" | "unsignedByte"
| "nonNegativeInteger" | "nonPositiveInteger"
| "positiveInteger" | "negativeInteger")
}
fn source_is_numeric(v: &Value) -> bool {
match v {
Value::Number(_) => true,
Value::Typed(t) => t.numeric.is_some(),
_ => false,
}
}
fn canonical_year_month_duration_lex(s: &str) -> String {
let months = match parse_year_month_duration_months(s) {
Some(m) => m,
None => return s.to_string(),
};
if months == 0 { return "P0M".into(); }
let mut out = String::with_capacity(8);
let total = if months < 0 { out.push('-'); -months } else { months };
out.push('P');
let y = total / 12;
let m = total % 12;
if y > 0 { out.push_str(&y.to_string()); out.push('Y'); }
if m > 0 { out.push_str(&m.to_string()); out.push('M'); }
out
}
fn negate_duration_lex(s: &str) -> String {
let t = s.trim();
if t == "PT0S" || t == "P0M" { return t.to_string(); }
if let Some(rest) = t.strip_prefix('-') {
rest.to_string()
} else {
format!("-{t}")
}
}
fn format_day_time_duration_micros(total_us: i64) -> String {
if total_us == 0 { return "PT0S".into(); }
let neg = total_us < 0;
let mut rem = total_us.unsigned_abs() as u64;
let mut out = String::with_capacity(16);
if neg { out.push('-'); }
out.push('P');
let us_per_day = 86_400u64 * 1_000_000;
let days = rem / us_per_day;
rem %= us_per_day;
if days > 0 { out.push_str(&days.to_string()); out.push('D'); }
if rem == 0 { return out; }
out.push('T');
let us_per_hour = 3600u64 * 1_000_000;
let h = rem / us_per_hour;
rem %= us_per_hour;
if h > 0 { out.push_str(&h.to_string()); out.push('H'); }
let us_per_min = 60u64 * 1_000_000;
let m = rem / us_per_min;
rem %= us_per_min;
if m > 0 { out.push_str(&m.to_string()); out.push('M'); }
if rem > 0 {
let secs = rem / 1_000_000;
let frac = rem % 1_000_000;
out.push_str(&secs.to_string());
if frac != 0 {
let frac_str = format!("{frac:06}");
let trimmed = frac_str.trim_end_matches('0');
out.push('.');
out.push_str(trimmed);
}
out.push('S');
}
out
}
fn canonical_day_time_duration_lex(s: &str) -> String {
let total_us = match parse_day_time_duration_micros(s) {
Some(u) => u,
None => return s.to_string(),
};
if total_us == 0 { return "PT0S".into(); }
let mut out = String::with_capacity(16);
let mut rem = if total_us < 0 { out.push('-'); -total_us } else { total_us };
out.push('P');
let us_per_day = 86_400 * 1_000_000;
let days = rem / us_per_day;
rem %= us_per_day;
if days > 0 { out.push_str(&days.to_string()); out.push('D'); }
if rem == 0 { return out; }
out.push('T');
let us_per_hour = 3600 * 1_000_000;
let h = rem / us_per_hour;
rem %= us_per_hour;
if h > 0 { out.push_str(&h.to_string()); out.push('H'); }
let us_per_min = 60 * 1_000_000;
let m = rem / us_per_min;
rem %= us_per_min;
if m > 0 { out.push_str(&m.to_string()); out.push('M'); }
if rem > 0 {
let secs = rem / 1_000_000;
let frac = rem % 1_000_000;
out.push_str(&secs.to_string());
if frac != 0 {
let frac_str = format!("{frac:06}");
let trimmed = frac_str.trim_end_matches('0');
out.push('.');
out.push_str(trimmed);
}
out.push('S');
}
out
}
fn parse_day_time_duration_micros(s: &str) -> Option<i128> {
let s = s.trim();
let (sign, body) = match s.strip_prefix('-') {
Some(rest) => (-1i128, rest),
None => (1i128, s),
};
let body = body.strip_prefix('P')?;
let (day_part, time_part) = match body.find('T') {
Some(i) => (&body[..i], &body[i + 1..]),
None => (body, ""),
};
let pull = |part: &str, marker: char| -> i128 {
let Some(i) = part.find(marker) else { return 0; };
let start = part[..i].rfind(|c: char| !c.is_ascii_digit() && c != '.')
.map(|n| n + 1).unwrap_or(0);
part[start..i].parse().unwrap_or(0)
};
let pull_secs = |part: &str| -> i128 {
let Some(i) = part.find('S') else { return 0; };
let start = part[..i].rfind(|c: char| !c.is_ascii_digit() && c != '.')
.map(|n| n + 1).unwrap_or(0);
let lex = &part[start..i];
if let Some((whole, frac)) = lex.split_once('.') {
let w: i128 = whole.parse().unwrap_or(0);
let take: String = frac.chars().chain(std::iter::repeat('0')).take(6).collect();
let f: i128 = take.parse().unwrap_or(0);
w * 1_000_000 + f
} else {
lex.parse::<i128>().unwrap_or(0) * 1_000_000
}
};
let days = pull(day_part, 'D');
let hours = pull(time_part, 'H');
let mins = pull(time_part, 'M');
let secs_us = pull_secs(time_part);
Some(sign * (days * 86_400 * 1_000_000
+ hours * 3600 * 1_000_000
+ mins * 60 * 1_000_000
+ secs_us))
}
fn hex_to_bytes(s: &str) -> Option<Vec<u8>> {
let s = s.trim();
if s.len() % 2 != 0 { return None; }
(0..s.len()).step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok())
.collect()
}
fn bytes_to_hex_upper(bytes: &[u8]) -> String {
use std::fmt::Write;
let mut s = String::with_capacity(bytes.len() * 2);
for b in bytes { let _ = write!(s, "{b:02X}"); }
s
}
const BASE64_ALPHABET: &[u8; 64] =
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
fn bytes_to_base64(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
for chunk in bytes.chunks(3) {
let b0 = chunk[0] as u32;
let b1 = *chunk.get(1).unwrap_or(&0) as u32;
let b2 = *chunk.get(2).unwrap_or(&0) as u32;
let n = (b0 << 16) | (b1 << 8) | b2;
out.push(BASE64_ALPHABET[(n >> 18 & 63) as usize] as char);
out.push(BASE64_ALPHABET[(n >> 12 & 63) as usize] as char);
out.push(if chunk.len() > 1 { BASE64_ALPHABET[(n >> 6 & 63) as usize] as char } else { '=' });
out.push(if chunk.len() > 2 { BASE64_ALPHABET[(n & 63) as usize] as char } else { '=' });
}
out
}
fn base64_to_bytes(s: &str) -> Option<Vec<u8>> {
let clean: Vec<u8> = s.bytes().filter(|b| !b.is_ascii_whitespace()).collect();
if clean.is_empty() || clean.len() % 4 != 0 { return None; }
let val = |c: u8| -> Option<u32> {
match c {
b'A'..=b'Z' => Some((c - b'A') as u32),
b'a'..=b'z' => Some((c - b'a' + 26) as u32),
b'0'..=b'9' => Some((c - b'0' + 52) as u32),
b'+' => Some(62),
b'/' => Some(63),
_ => None,
}
};
let mut out = Vec::with_capacity(clean.len() / 4 * 3);
for chunk in clean.chunks(4) {
let pad = chunk.iter().filter(|&&c| c == b'=').count();
let c0 = val(chunk[0])?;
let c1 = val(chunk[1])?;
let c2 = if pad >= 2 { 0 } else { val(chunk[2])? };
let c3 = if pad >= 1 { 0 } else { val(chunk[3])? };
let n = (c0 << 18) | (c1 << 12) | (c2 << 6) | c3;
out.push((n >> 16) as u8);
if pad < 2 { out.push((n >> 8) as u8); }
if pad < 1 { out.push(n as u8); }
}
Some(out)
}
fn convert_binary_kind(src: &Value, target: &str) -> Option<String> {
let t = match src { Value::Typed(t) => t, _ => return None };
let bytes = match t.kind {
"hexBinary" => hex_to_bytes(&t.lexical)?,
"base64Binary" => base64_to_bytes(&t.lexical)?,
_ => return None,
};
Some(match target {
"hexBinary" => bytes_to_hex_upper(&bytes),
"base64Binary" => bytes_to_base64(&bytes),
_ => return None,
})
}
fn canonical_date_time_lex(s: &str, kind: &str) -> String {
let s = if let Some(stripped) = s.strip_suffix("+00:00")
.or_else(|| s.strip_suffix("-00:00")) {
format!("{stripped}Z")
} else {
s.to_string()
};
let s = s.as_str();
if s.contains("24:00:00") {
let dk = match kind {
"dateTime" => DateKind::DateTime,
"time" => DateKind::Time,
_ => DateKind::Date,
};
if !matches!(dk, DateKind::Date) {
if let Some((y, mo, d, h, mi, sec, frac, tz)) = parse_xsd_date_time(s, dk) {
return if matches!(dk, DateKind::DateTime) {
format_datetime_lexical(y, mo, d, h, mi, sec, frac, tz)
} else {
let mut l = format!("{h:02}:{mi:02}:{sec:02}");
if frac != 0 {
let mut f = format!(".{frac:06}");
while f.ends_with('0') { f.pop(); }
l.push_str(&f);
}
if let Some(tz_m) = tz { l.push_str(&format_tz_suffix(tz_m)); }
l
};
}
}
}
let dot = match s.find('.') { Some(i) => i, None => return s.to_string() };
let after = &s[dot + 1..];
let tz_off = after.find(|c: char| c == 'Z' || c == '+' || c == '-')
.unwrap_or(after.len());
let (digits, tz) = (&after[..tz_off], &after[tz_off..]);
let trimmed = digits.trim_end_matches('0');
if trimmed.is_empty() {
let mut out = String::with_capacity(s.len());
out.push_str(&s[..dot]);
out.push_str(tz);
out
} else {
let mut out = String::with_capacity(s.len());
out.push_str(&s[..dot]);
out.push('.');
out.push_str(trimmed);
out.push_str(tz);
out
}
}
fn format_date_utc(secs: i64) -> String {
let (y, m, d) = days_to_ymd(secs.div_euclid(86_400));
format!("{y:04}-{m:02}-{d:02}Z")
}
fn format_datetime_utc(secs: i64) -> String {
let days = secs.div_euclid(86_400);
let day_sec = secs.rem_euclid(86_400);
let (y, m, d) = days_to_ymd(days);
let h = day_sec / 3600;
let mi = (day_sec % 3600) / 60;
let s = day_sec % 60;
format!("{y:04}-{m:02}-{d:02}T{h:02}:{mi:02}:{s:02}Z")
}
fn format_time_utc(secs: i64) -> String {
let day_sec = secs.rem_euclid(86_400);
let h = day_sec / 3600;
let mi = (day_sec % 3600) / 60;
let s = day_sec % 60;
format!("{h:02}:{mi:02}:{s:02}Z")
}
fn days_to_ymd(days: i64) -> (i32, u32, u32) {
let z = days + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = (z - era * 146_097) as u64;
let yoe = (doe - doe/1460 + doe/36_524 - doe/146_096) / 365;
let y = yoe as i64 + era * 400;
let doy = doe - (365 * yoe + yoe/4 - yoe/100);
let mp = (5 * doy + 2) / 153;
let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
let m = (if mp < 10 { mp + 3 } else { mp - 9 }) as u32;
let y = if m <= 2 { y + 1 } else { y };
(y as i32, m, d)
}
fn dedup_sort(nodes: &mut Vec<NodeId>) {
nodes.sort_unstable();
nodes.dedup();
}
fn dedup_foreign(nodes: &mut Vec<ForeignNodePtr>) {
let mut seen = HashSet::new();
nodes.retain(|&p| seen.insert(p as usize));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn out_of_range_year_is_overflow_not_malformed() {
assert!(!date_year_out_of_range("2024-05-01T00:00:00", "dateTime"));
assert!(!date_year_out_of_range("21999-05-01", "date"));
assert!(!date_year_out_of_range("-1999-05-01", "date"));
assert!(!date_year_out_of_range("99999", "gYear"));
assert!(!date_year_out_of_range("123456-05", "gYearMonth"));
assert!(date_year_out_of_range("999999999999-05-01T00:00:00", "dateTime"));
assert!(date_year_out_of_range("-999999999999-05-01", "date"));
assert!(date_year_out_of_range("12345678901-05", "gYearMonth"));
assert!(date_year_out_of_range("999999999999", "gYear"));
assert!(!date_year_out_of_range("999999999999-13-99", "date"));
assert!(!date_year_out_of_range("not-a-date", "date"));
assert!(!date_year_out_of_range("999999999999", "gMonth"));
assert!(!date_year_out_of_range("999999999999", "time"));
}
#[test]
fn duration_combine_dispatches_by_family() {
let dur = |k: &'static str, lex: &str| TypedAtomic {
kind: k, lexical: lex.to_string(), numeric: None, boolean: None, user_type: None,
};
let lex = |v: Option<Value>| match v {
Some(Value::Typed(t)) => t.lexical.clone(),
other => panic!("expected typed duration, got {other:?}"),
};
assert_eq!(
lex(duration_combine(&dur("yearMonthDuration", "P2Y6M"),
&dur("yearMonthDuration", "P6M"), false)),
"P3Y");
assert_eq!(
lex(duration_combine(&dur("yearMonthDuration", "P0M"),
&dur("yearMonthDuration", "P2Y"), true)),
"-P2Y");
assert_eq!(
lex(duration_combine(&dur("dayTimeDuration", "P2D"),
&dur("dayTimeDuration", "PT10.03S"), false)),
"P2DT10.03S");
assert!(duration_combine(&dur("yearMonthDuration", "P1Y"),
&dur("dayTimeDuration", "PT1H"), false).is_none());
}
#[test]
fn format_number_preserves_negative_zero() {
assert_eq!(format_number(-0.0), "-0");
assert_eq!(format_number(0.0), "0");
}
#[test]
fn format_number_handles_special_values() {
assert_eq!(format_number(f64::INFINITY), "Infinity");
assert_eq!(format_number(f64::NEG_INFINITY), "-Infinity");
assert_eq!(format_number(f64::NAN), "NaN");
}
#[test]
fn format_number_integers_are_decimal_no_dot() {
assert_eq!(format_number(42.0), "42");
assert_eq!(format_number(-7.0), "-7");
assert_eq!(format_number(0.5), "0.5");
}
#[test]
fn ordinal_suffix_picks_st_nd_rd_th() {
assert_eq!(english_ordinal_suffix(1), "st");
assert_eq!(english_ordinal_suffix(2), "nd");
assert_eq!(english_ordinal_suffix(3), "rd");
assert_eq!(english_ordinal_suffix(4), "th");
assert_eq!(english_ordinal_suffix(21), "st");
assert_eq!(english_ordinal_suffix(22), "nd");
assert_eq!(english_ordinal_suffix(23), "rd");
}
#[test]
fn ordinal_suffix_teens_are_all_th() {
assert_eq!(english_ordinal_suffix(11), "th");
assert_eq!(english_ordinal_suffix(12), "th");
assert_eq!(english_ordinal_suffix(13), "th");
assert_eq!(english_ordinal_suffix(111), "th");
assert_eq!(english_ordinal_suffix(112), "th");
assert_eq!(english_ordinal_suffix(113), "th");
}
#[test]
fn duration_split_pure_year_month() {
assert_eq!(parse_duration_split("P1Y"), Some((12, 0)));
assert_eq!(parse_duration_split("P0Y12M"), Some((12, 0)));
assert_eq!(parse_duration_split("P12M"), Some((12, 0)));
assert_eq!(parse_duration_split("P1Y6M"), Some((18, 0)));
}
#[test]
fn duration_split_pure_day_time() {
assert_eq!(parse_duration_split("P1D"), Some((0, 86_400)));
assert_eq!(parse_duration_split("PT24H"), Some((0, 86_400)));
assert_eq!(parse_duration_split("PT1H30M"), Some((0, 5400)));
assert_eq!(parse_duration_split("-PT1H"), Some((0, -3600)));
assert_eq!(parse_duration_split("-P1DT12H"), Some((0, -129_600)));
assert_eq!(parse_duration_split("-PT36H"), Some((0, -129_600)));
}
#[test]
fn duration_split_mixed_components() {
assert_eq!(parse_duration_split("P1Y2M3DT4H5M6S"),
Some((14, 3 * 86_400 + 4 * 3600 + 5 * 60 + 6)));
}
#[test]
fn duration_split_rejects_malformed() {
assert_eq!(parse_duration_split("1Y"), None); assert_eq!(parse_duration_split("PY"), None); assert_eq!(parse_duration_split("P1X"), None); assert_eq!(parse_duration_split(""), None);
}
#[test]
fn canonical_double_zero_and_neg_zero() {
assert_eq!(canonical_double_lex( 0.0, &Value::Number(Numeric::Double( 0.0))), "0");
assert_eq!(canonical_double_lex(-0.0, &Value::Number(Numeric::Double(-0.0))), "-0");
assert_eq!(canonical_double_lex(0.0, &Value::String("-0".into())), "-0");
}
#[test]
fn canonical_double_special_values() {
assert_eq!(canonical_double_lex(f64::INFINITY, &Value::Number(Numeric::Double(0.0))), "INF");
assert_eq!(canonical_double_lex(f64::NEG_INFINITY, &Value::Number(Numeric::Double(0.0))), "-INF");
assert_eq!(canonical_double_lex(f64::NAN, &Value::Number(Numeric::Double(0.0))), "NaN");
}
#[test]
fn canonical_double_fixed_point_window() {
assert_eq!(canonical_double_lex(1.5, &Value::Number(Numeric::Double(1.5))), "1.5");
assert_eq!(canonical_double_lex(0.001, &Value::Number(Numeric::Double(0.001))), "0.001");
assert_eq!(canonical_double_lex(42.0, &Value::Number(Numeric::Double(42.0))), "42");
assert_eq!(canonical_double_lex(9_999_999.0,
&Value::Number(Numeric::Double(9_999_999.0))),
"9999999");
}
#[test]
fn canonical_double_scientific_outside_window() {
assert_eq!(canonical_double_lex(1e7, &Value::Number(Numeric::Double(1e7))), "1.0E7");
assert_eq!(canonical_double_lex(1e-8, &Value::Number(Numeric::Double(1e-8))), "1.0E-8");
assert_eq!(canonical_double_lex(1.5e10, &Value::Number(Numeric::Double(1.5e10))), "1.5E10");
}
#[test]
fn canonical_decimal_drops_negative_zero() {
assert_eq!(canonical_decimal_lex(-0.0, "-0.0"), "0");
assert_eq!(canonical_decimal_lex( 0.0, "0"), "0");
}
#[test]
fn canonical_decimal_passes_fixed_point_through() {
assert_eq!(canonical_decimal_lex(1.5, "1.5"), "1.5");
assert_eq!(canonical_decimal_lex(42.0, "42"), "42");
}
#[test]
fn canonical_decimal_strips_scientific_input() {
assert_eq!(canonical_decimal_lex(1e3, "1e3"), "1000");
}
#[test]
fn split_uri_full_form() {
let (s, a, p, q, f) = split_uri("http://host/path?q#frag");
assert_eq!(s, Some("http"));
assert_eq!(a, Some("host"));
assert_eq!(p, "/path");
assert_eq!(q, Some("q"));
assert_eq!(f, Some("frag"));
}
#[test]
fn split_uri_relative_no_scheme() {
let (s, a, p, q, f) = split_uri("path/to/file");
assert_eq!(s, None);
assert_eq!(a, None);
assert_eq!(p, "path/to/file");
assert_eq!(q, None);
assert_eq!(f, None);
}
#[test]
fn remove_dot_segments_matches_rfc3986_examples() {
assert_eq!(remove_dot_segments("/a/b/c/./../../g"), "/a/g");
assert_eq!(remove_dot_segments("mid/content=5/../6"), "mid/6");
assert_eq!(remove_dot_segments("/./a/b"), "/a/b");
assert_eq!(remove_dot_segments(""), "");
}
#[test]
fn resolve_uri_handles_rfc3986_normal_examples() {
let base = "http://a/b/c/d;p?q";
assert_eq!(resolve_uri_rfc3986(base, "g:h"), "g:h");
assert_eq!(resolve_uri_rfc3986(base, "g"), "http://a/b/c/g");
assert_eq!(resolve_uri_rfc3986(base, "./g"), "http://a/b/c/g");
assert_eq!(resolve_uri_rfc3986(base, "g/"), "http://a/b/c/g/");
assert_eq!(resolve_uri_rfc3986(base, "/g"), "http://a/g");
assert_eq!(resolve_uri_rfc3986(base, "?y"), "http://a/b/c/d;p?y");
assert_eq!(resolve_uri_rfc3986(base, "g?y"), "http://a/b/c/g?y");
assert_eq!(resolve_uri_rfc3986(base, "#s"), "http://a/b/c/d;p?q#s");
assert_eq!(resolve_uri_rfc3986(base, "g#s"), "http://a/b/c/g#s");
assert_eq!(resolve_uri_rfc3986(base, "../g"), "http://a/b/g");
assert_eq!(resolve_uri_rfc3986(base, "../../g"),"http://a/g");
}
#[test]
fn resolve_uri_empty_rel_yields_base() {
let base = "http://www.baseuri.exmpl/tests/";
assert_eq!(resolve_uri_rfc3986(base, ""), "http://www.baseuri.exmpl/tests/");
}
}
#[cfg(kani)]
mod proofs {
use super::*;
use std::ops::Range;
const MAX_NODES: usize = 3;
const MAX_CHILDREN: usize = 2;
struct AnyIndex {
parents: [Option<NodeId>; MAX_NODES],
child_lens: [usize; MAX_NODES],
child_buf: [[NodeId; MAX_CHILDREN]; MAX_NODES],
}
impl AnyIndex {
fn any() -> Self {
let mut idx = AnyIndex {
parents: [None; MAX_NODES],
child_lens: [0; MAX_NODES],
child_buf: [[0; MAX_CHILDREN]; MAX_NODES],
};
for i in 0..MAX_NODES {
let p: Option<NodeId> = kani::any();
if let Some(pid) = p {
kani::assume(pid < i);
}
idx.parents[i] = p;
let n: usize = kani::any();
kani::assume(n <= MAX_CHILDREN);
idx.child_lens[i] = n;
for j in 0..MAX_CHILDREN {
let c: NodeId = kani::any();
kani::assume(c > i && c < MAX_NODES);
idx.child_buf[i][j] = c;
}
}
idx
}
}
impl DocIndexLike for AnyIndex {
fn children(&self, id: NodeId) -> &[NodeId] {
if id >= MAX_NODES { return &[]; }
&self.child_buf[id][..self.child_lens[id]]
}
fn parent(&self, id: NodeId) -> Option<NodeId> {
if id >= MAX_NODES { None } else { self.parents[id] }
}
fn attr_range(&self, _: NodeId) -> Range<NodeId> { 0..0 }
fn kind(&self, _: NodeId) -> XPathNodeKind { unreachable!() }
fn pi_target(&self, _: NodeId) -> &str { unreachable!() }
fn string_value(&self, _: NodeId) -> String { unreachable!() }
fn node_name(&self, _: NodeId) -> &str { unreachable!() }
fn local_name(&self, _: NodeId) -> &str { unreachable!() }
fn namespace_uri(&self, _: NodeId) -> &str { unreachable!() }
}
fn any_node() -> NodeId {
let n: NodeId = kani::any();
kani::assume(n < MAX_NODES);
n
}
#[kani::proof]
#[kani::unwind(4)]
fn following_siblings_never_panics() {
let _ = following_siblings(any_node(), &AnyIndex::any());
}
#[kani::proof]
#[kani::unwind(4)]
fn preceding_siblings_never_panics() {
let _ = preceding_siblings(any_node(), &AnyIndex::any());
}
}