use std::borrow::Cow;
use std::cell::{Cell, RefCell};
use std::collections::{HashMap, HashSet};
use std::fmt::Write as _;
use std::rc::Rc;
use std::sync::{Arc, OnceLock};
#[cfg(feature = "embedded")]
use crate::{
sxd_document as sxd_document_no_unsafe, sxd_xpath as sxd_xpath_no_unsafe,
xml_input_shared as xml_sec_xml_input,
};
use sxd_document_no_unsafe::dom::{Document as SxdDocument, Element as SxdElement};
use sxd_document_no_unsafe::{Package, QName, StorageRequirements};
#[cfg(test)]
use sxd_xpath_no_unsafe::Context;
use sxd_xpath_no_unsafe::{Factory, Value as SxdValue, XPath, function, nodeset};
use super::budget::{
Meter, ParseBudget, XINCLUDE_RECURSION_DEPTH_CEILING, reconcile_replacement_growth,
reserve_retained_hash_map_slot, reserve_retained_hash_set_slot, reserve_retained_vec_slot,
reserve_temporary_vec_slot, retained_hash_storage,
};
use super::compiler::{
DecimalFormat, Expression, KeyDeclaration, ModuleDocument, NameTest, Pattern,
StylesheetDocumentId, normalize_xpath_for_sxd,
};
use super::expression::innermost_namespaced_call;
use super::lexical::{
ValidatedXPointerFragment, is_ncname, is_ncname_char, is_xml_whitespace, trim_xml_whitespace,
};
use super::model::parser_workspace_bytes;
use super::resolver::decode_resource;
use super::runtime::{apply_source_whitespace_rules, expanded_name_owned_bytes};
use super::{
Attribute, BudgetKind, Clock, Document, Error, ErrorKind, ExpandedName, Node, NodeId, NodeKind,
NodeReference, ResolvePurpose, ResolveRequest, ResolvedResource, Resolver, ResourceIdentity,
Result, Value,
};
pub(crate) type SourceNode = NodeReference;
fn projected_node_storage(count: usize, meter: &Meter) -> Result<Vec<SourceNode>> {
let requested = count.saturating_mul(std::mem::size_of::<SourceNode>());
meter.check_additional(BudgetKind::OwnedBytes, requested)?;
let mut nodes = Vec::new();
nodes.try_reserve_exact(count).map_err(|error| {
Error::Dynamic(format!("failed to reserve XPath node projection: {error}"))
})?;
meter.check_additional(
BudgetKind::OwnedBytes,
nodes
.capacity()
.saturating_mul(std::mem::size_of::<SourceNode>()),
)?;
Ok(nodes)
}
pub(crate) trait VariableBindings {
fn get(&self, name: &ExpandedName) -> Option<&Value>;
fn result_tree_fragment(&self, identity: u64) -> Option<Arc<Document>>;
}
fn result_tree_fragment_handle(document: &Arc<Document>) -> u64 {
Arc::as_ptr(document) as usize as u64
}
impl VariableBindings for HashMap<ExpandedName, Value> {
fn get(&self, name: &ExpandedName) -> Option<&Value> {
HashMap::get(self, name)
}
fn result_tree_fragment(&self, identity: u64) -> Option<Arc<Document>> {
self.values().find_map(|value| match value {
Value::ResultTreeFragment(document)
if result_tree_fragment_handle(document) == identity =>
{
Some(Arc::clone(document))
}
_ => None,
})
}
}
struct VariableOverlay<'a> {
base: &'a dyn VariableBindings,
additions: HashMap<ExpandedName, Value>,
}
impl<'a> VariableOverlay<'a> {
fn new(base: &'a dyn VariableBindings) -> Self {
Self {
base,
additions: HashMap::new(),
}
}
fn insert(&mut self, name: ExpandedName, value: Value) {
self.additions.insert(name, value);
}
}
impl VariableBindings for VariableOverlay<'_> {
fn get(&self, name: &ExpandedName) -> Option<&Value> {
self.additions.get(name).or_else(|| self.base.get(name))
}
fn result_tree_fragment(&self, identity: u64) -> Option<Arc<Document>> {
self.additions
.values()
.find_map(|value| match value {
Value::ResultTreeFragment(document)
if result_tree_fragment_handle(document) == identity =>
{
Some(Arc::clone(document))
}
_ => None,
})
.or_else(|| self.base.result_tree_fragment(identity))
}
}
enum PreparedExtensionCalls<'a> {
Borrowed {
expression: &'a Expression,
variables: &'a dyn VariableBindings,
},
Rewritten {
expression: Expression,
variables: VariableOverlay<'a>,
reserved_owned_bytes: usize,
},
}
impl<'a> PreparedExtensionCalls<'a> {
fn parts(&'a self) -> (&'a Expression, &'a dyn VariableBindings) {
match self {
Self::Borrowed {
expression,
variables,
} => (expression, *variables),
Self::Rewritten {
expression,
variables,
..
} => (expression, variables),
}
}
fn reserved_owned_bytes(&self) -> usize {
match self {
Self::Borrowed { .. } => 0,
Self::Rewritten {
reserved_owned_bytes,
..
} => *reserved_owned_bytes,
}
}
}
const CONTEXT_NS: &str = "urn:structured-world:xml-sec:xslt:context";
const DOCUMENTS_ELEMENT: &str = "__xml_sec_documents";
const DOCUMENT_ELEMENT: &str = "__xml_sec_document";
const EXTENSION_CONTEXT_NS: &str = "urn:structured-world:xml-sec:xslt:extensions";
pub(crate) const EXSLT_COMMON_NS: &str = "http://exslt.org/common";
const EXSLT_STRINGS_NS: &str = "http://exslt.org/strings";
const EXSLT_MATH_NS: &str = "http://exslt.org/math";
const EXSLT_SETS_NS: &str = "http://exslt.org/sets";
const EXSLT_DYNAMIC_NS: &str = "http://exslt.org/dynamic";
const EXSLT_CRYPTO_NS: &str = "http://exslt.org/crypto";
const LIBXSLT_TEST_NS: &str = "http://xmlsoft.org/XSLT/";
const LIBXSLT_TEST_PLUGIN_NS: &str = "http://xmlsoft.org/xslt/testplugin";
const SAXON_NS: &str = "http://icl.com/saxon";
const LIBXSLT_NS: &str = "http://xmlsoft.org/XSLT/namespace";
const XT_NS: &str = "http://www.jclark.com/xt";
const XALAN_REDIRECT_NS: &str = "org.apache.xalan.xslt.extensions.Redirect";
fn is_prepared_extension_call(namespace: &str, local: &str) -> bool {
matches!(
(namespace, local),
(EXSLT_COMMON_NS | LIBXSLT_NS | SAXON_NS | XT_NS, "node-set")
| (EXSLT_COMMON_NS, "object-type")
| (EXSLT_STRINGS_NS, "split")
| (EXSLT_STRINGS_NS, "tokenize")
| (EXSLT_STRINGS_NS, "replace")
| (EXSLT_DYNAMIC_NS, "evaluate")
| (EXSLT_DYNAMIC_NS, "map")
| (SAXON_NS, "expression")
| (SAXON_NS, "eval")
| (SAXON_NS, "evaluate")
| (SAXON_NS, "line-number")
)
}
pub(crate) struct EvaluatorSourceOptions {
pub(crate) process_xinclude: bool,
pub(crate) whitespace: Arc<[(NameTest, bool, usize, usize)]>,
pub(crate) clock: Option<Arc<dyn Clock>>,
}
struct OperationClock {
source: Arc<dyn Clock>,
value: OnceLock<time::OffsetDateTime>,
}
impl OperationClock {
fn new(source: Arc<dyn Clock>) -> Self {
Self {
source,
value: OnceLock::new(),
}
}
}
impl Clock for OperationClock {
fn now_local(&self) -> Result<time::OffsetDateTime> {
if let Some(value) = self.value.get().copied() {
return Ok(value);
}
let current = self.source.now_local()?;
Ok(*self.value.get_or_init(|| current))
}
}
pub(crate) struct PreparedEvaluatorSource {
pub(crate) document: Document,
pub(crate) remap: Option<HashMap<NodeId, NodeId>>,
pub(crate) remap_owned_bytes: usize,
resource_identities: HashMap<ResourceIdentity, ResolvedResource>,
}
pub(crate) fn prepare_evaluator_source(
source: &Document,
resolver: &dyn Resolver,
meter: &mut Meter,
options: &EvaluatorSourceOptions,
) -> Result<PreparedEvaluatorSource> {
let mut resource_identities = HashMap::new();
let (mut document, include_remap) = if options.process_xinclude {
let expanded = expand_xinclude_document(
source,
&XIncludeDocumentIdentity::InMemory(source as *const Document as usize),
resolver,
meter,
&mut resource_identities,
0,
None,
)?;
(
expanded.document,
expanded
.principal_mapping
.map(|mapping| (mapping, expanded.principal_mapping_owned_bytes)),
)
} else {
meter.charge(BudgetKind::OwnedBytes, source.estimated_clone_bytes())?;
(source.clone(), None)
};
let (whitespace_remap, whitespace_remap_owned_bytes) =
apply_source_whitespace_rules(&mut document, &options.whitespace, meter)?
.map_or((None, 0), |(remap, bytes)| (Some(remap), bytes));
let (include_remap, include_remap_owned_bytes) =
include_remap.map_or((None, 0), |(remap, bytes)| (Some(remap), bytes));
let (remap, remap_owned_bytes) = compose_node_remaps(
include_remap,
include_remap_owned_bytes,
whitespace_remap,
whitespace_remap_owned_bytes,
meter,
)?;
Ok(PreparedEvaluatorSource {
document,
remap,
remap_owned_bytes,
resource_identities,
})
}
fn compose_node_remaps(
first: Option<HashMap<NodeId, NodeId>>,
first_owned_bytes: usize,
second: Option<HashMap<NodeId, NodeId>>,
second_owned_bytes: usize,
meter: &mut Meter,
) -> Result<(Option<HashMap<NodeId, NodeId>>, usize)> {
match (first, second) {
(None, None) => Ok((None, 0)),
(Some(remap), None) => Ok((Some(remap), first_owned_bytes)),
(None, Some(remap)) => Ok((Some(remap), second_owned_bytes)),
(Some(mut first), Some(second)) => {
first.retain(|_, intermediate| {
let Some(final_id) = second.get(intermediate).copied() else {
return false;
};
*intermediate = final_id;
true
});
drop(second);
meter.release_owned_bytes(second_owned_bytes);
Ok((Some(first), first_owned_bytes))
}
}
}
fn source_node_owner(node: &SourceNode) -> NodeId {
match node {
SourceNode::Node(node) => *node,
SourceNode::Attribute { owner, .. } | SourceNode::Namespace { owner, .. } => *owner,
}
}
fn source_base_uri(source: &Document, node: &SourceNode) -> Option<String> {
source
.node(source_node_owner(node))
.and_then(|node| node.base_uri.clone())
}
type PatternCacheKey = (String, Vec<(String, String)>, NodeId);
fn pattern_cache_key_owned_bytes(key: &PatternCacheKey) -> usize {
let namespace_bytes = key.1.iter().fold(0usize, |total, (prefix, uri)| {
total
.saturating_add(prefix.capacity())
.saturating_add(uri.capacity())
});
key.0
.capacity()
.saturating_add(
key.1
.capacity()
.saturating_mul(std::mem::size_of::<(String, String)>()),
)
.saturating_add(namespace_bytes)
}
fn clone_pattern_cache_key(
expression: &str,
namespaces: &[(String, String)],
logical_root: NodeId,
meter: &mut Meter,
) -> Result<(PatternCacheKey, usize)> {
let requested = expression.len().saturating_add(
namespaces
.len()
.saturating_mul(std::mem::size_of::<(String, String)>())
.saturating_add(namespaces.iter().fold(0usize, |total, (prefix, uri)| {
total.saturating_add(prefix.len()).saturating_add(uri.len())
})),
);
meter.charge(BudgetKind::OwnedBytes, requested)?;
let key = (expression.to_owned(), namespaces.to_vec(), logical_root);
let actual = pattern_cache_key_owned_bytes(&key);
if actual > requested {
if let Err(error) = meter.charge(BudgetKind::OwnedBytes, actual - requested) {
meter.release_owned_bytes(requested);
return Err(error);
}
} else {
meter.release_owned_bytes(requested - actual);
}
Ok((key, actual))
}
fn pattern_cache_retained_owned_bytes(
cache: &HashMap<PatternCacheKey, HashSet<SourceNode>>,
) -> usize {
cache.iter().fold(
retained_hash_storage::<(PatternCacheKey, HashSet<SourceNode>)>(cache.capacity()),
|total, (key, nodes)| {
total
.saturating_add(pattern_cache_key_owned_bytes(key))
.saturating_add(retained_hash_storage::<SourceNode>(nodes.capacity()))
},
)
}
fn clear_pattern_cache(
cache: &mut HashMap<PatternCacheKey, HashSet<SourceNode>>,
index_owned_bytes: &mut usize,
meter: &mut Meter,
) {
let retired = std::mem::take(cache);
let released = pattern_cache_retained_owned_bytes(&retired);
drop(retired);
*index_owned_bytes = 0;
meter.release_owned_bytes(released);
}
fn insert_pattern_cache_entry(
cache: &mut HashMap<PatternCacheKey, HashSet<SourceNode>>,
index_owned_bytes: &mut usize,
key: PatternCacheKey,
key_owned_bytes: usize,
nodes: impl IntoIterator<Item = SourceNode>,
meter: &mut Meter,
) -> Result<()> {
if let Err(error) = reserve_retained_hash_map_slot(cache, meter, index_owned_bytes) {
meter.release_owned_bytes(key_owned_bytes);
return Err(error);
}
let mut retained_nodes = HashSet::new();
let mut node_index_owned_bytes = 0usize;
for node in nodes {
if retained_nodes.contains(&node) {
continue;
}
if let Err(error) =
reserve_retained_hash_set_slot(&mut retained_nodes, meter, &mut node_index_owned_bytes)
{
meter.release_owned_bytes(key_owned_bytes.saturating_add(node_index_owned_bytes));
return Err(error);
}
retained_nodes.insert(node);
}
cache.insert(key, retained_nodes);
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct DocumentRequest {
href: String,
base_uri: Option<String>,
empty_document: Option<EmptyDocumentId>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum EmptyDocumentId {
Logical(usize),
Stylesheet(StylesheetDocumentId),
}
impl DocumentRequest {
fn relative_to(
href: String,
base_uri: Option<String>,
empty_document: Option<EmptyDocumentId>,
) -> Self {
let empty_resource = href
.find('#')
.map_or(href.is_empty(), |fragment| fragment == 0);
let empty_document = empty_resource.then_some(empty_document).flatten();
Self {
href,
base_uri: if empty_document.is_some() {
None
} else {
base_uri
},
empty_document,
}
}
fn payload_owned_bytes(&self) -> usize {
self.href
.capacity()
.saturating_add(self.base_uri.as_ref().map_or(0, String::capacity))
}
}
#[derive(Default)]
struct PendingDocumentRequests {
items: HashSet<DocumentRequest>,
payload_bytes: usize,
}
impl PendingDocumentRequests {
fn retained_owned_bytes(&self) -> usize {
retained_hash_storage::<DocumentRequest>(self.items.capacity())
.saturating_add(self.payload_bytes)
}
}
fn document_cache_key_owned_bytes(request: &DocumentRequest) -> usize {
request.href.capacity().saturating_mul(2).saturating_add(
request
.base_uri
.as_ref()
.map_or(0, |base_uri| base_uri.capacity().saturating_mul(2)),
)
}
fn charge_document_cache_keys(request: &DocumentRequest, meter: &mut Meter) -> Result<()> {
meter.charge(
BudgetKind::OwnedBytes,
document_cache_key_owned_bytes(request),
)
}
#[derive(Default)]
struct DocumentCacheIndexBytes {
documents: usize,
roots: usize,
}
struct CachedDocumentRoots {
paths: Vec<NodePath>,
identity: Option<Arc<ResourceIdentity>>,
}
fn seed_document_cache(
request: DocumentRequest,
root: NodeId,
maps: &NodeMaps,
documents: &mut HashMap<DocumentRequest, Vec<SourceNode>>,
document_roots: &mut HashMap<DocumentRequest, CachedDocumentRoots>,
index_bytes: &mut DocumentCacheIndexBytes,
meter: &mut Meter,
) -> Result<()> {
charge_document_cache_keys(&request, meter)?;
let root = SourceNode::Node(root);
reserve_retained_hash_map_slot(documents, meter, &mut index_bytes.documents)?;
reserve_retained_hash_map_slot(document_roots, meter, &mut index_bytes.roots)?;
let mut roots = Vec::new();
let mut roots_bytes = 0;
if let Some(path) = maps.forward.get(&root) {
reserve_temporary_vec_slot(&mut roots, meter, &mut roots_bytes)?;
meter.charge(BudgetKind::OwnedBytes, path.owned_bytes())?;
roots.push(path.clone());
}
let nodes = vec![root];
meter.charge(
BudgetKind::OwnedBytes,
nodes
.capacity()
.saturating_mul(std::mem::size_of::<SourceNode>()),
)?;
document_roots.insert(
request.clone(),
CachedDocumentRoots {
paths: roots,
identity: None,
},
);
documents.insert(request, nodes);
Ok(())
}
#[derive(Debug, Clone)]
pub(crate) struct CustomFunctionCall {
pub(crate) name: ExpandedName,
pub(crate) node: SourceNode,
pub(crate) position: usize,
pub(crate) size: usize,
pub(crate) arguments: Vec<Value>,
}
#[derive(Default)]
pub(crate) struct CustomCallSession {
state: Rc<RefCell<CustomCallState>>,
}
#[derive(Default)]
struct CustomCallState {
cursor: usize,
pending: Option<Rc<DeferredCustomCall>>,
last_requested: Option<Rc<DeferredCustomCall>>,
completed: Vec<CompletedCustomCall>,
retained_bytes: usize,
}
impl CustomCallState {
fn reserve_completed_slot(&mut self, meter: &mut Meter) -> Result<()> {
let old_capacity = self.completed.capacity();
reserve_retained_vec_slot(&mut self.completed, meter)?;
let capacity_growth = self
.completed
.capacity()
.saturating_sub(old_capacity)
.saturating_mul(std::mem::size_of::<CompletedCustomCall>());
self.retained_bytes = self.retained_bytes.saturating_add(capacity_growth);
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct DeferredCustomCall {
name: ExpandedName,
node: NodePath,
position: usize,
size: usize,
arguments: Vec<DeferredXPathValue>,
}
struct CompletedCustomCall {
call: Rc<DeferredCustomCall>,
result: DeferredXPathValue,
fragment: Option<Arc<Document>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum DeferredXPathValue {
Boolean(bool),
Number(u64),
String(String),
ResultTreeFragment { identity: u64, value: Arc<String> },
NodeSet(Vec<NodePath>),
}
impl DeferredCustomCall {
fn owned_bytes(&self) -> usize {
std::mem::size_of::<Self>()
.saturating_add(self.name.local.len())
.saturating_add(self.name.namespace.as_deref().map_or(0, str::len))
.saturating_add(self.node.owned_bytes())
.saturating_add(
self.arguments
.iter()
.map(DeferredXPathValue::owned_bytes)
.sum::<usize>(),
)
}
}
impl DeferredXPathValue {
fn owned_bytes(&self) -> usize {
std::mem::size_of::<Self>().saturating_add(match self {
Self::Boolean(_) | Self::Number(_) => 0,
Self::String(value) => value.len(),
Self::ResultTreeFragment { value, .. } => value
.len()
.saturating_add(std::mem::size_of::<String>() + 2 * std::mem::size_of::<usize>()),
Self::NodeSet(paths) => paths.iter().map(NodePath::owned_bytes).sum(),
})
}
}
impl CustomCallSession {
fn begin_attempt(&self) {
let mut state = self.state.borrow_mut();
state.cursor = 0;
state.pending = None;
}
fn fragment(&self, identity: u64) -> Option<Arc<Document>> {
let state = self.state.borrow();
state
.completed
.iter()
.filter_map(|completed| completed.fragment.as_ref())
.find(|document| result_tree_fragment_handle(document) == identity)
.cloned()
}
pub(crate) fn retained_bytes(&self) -> usize {
self.state.borrow().retained_bytes
}
}
struct DeferredStylesheetFunction {
name: ExpandedName,
session: Rc<RefCell<CustomCallState>>,
}
impl function::Function for DeferredStylesheetFunction {
fn evaluate<'c, 'd>(
&self,
context: &sxd_xpath_no_unsafe::context::Evaluation<'c, 'd>,
arguments: Vec<SxdValue<'d>>,
) -> std::result::Result<SxdValue<'d>, function::Error> {
context.reserve_string_allocation(deferred_call_size(&self.name, &arguments))?;
let call = DeferredCustomCall {
name: self.name.clone(),
node: typed_path_to_with_context(&context.node, context)?,
position: context.position,
size: context.size,
arguments: arguments
.into_iter()
.map(|value| defer_sxd_value(value, context))
.collect::<std::result::Result<Vec<_>, _>>()?,
};
let mut state = self.session.borrow_mut();
let cursor = state.cursor;
state.cursor = state.cursor.saturating_add(1);
if let Some(completed) = state.completed.get(cursor) {
if *completed.call != call {
return Err(function::Error::Other {
what: "stylesheet function continuation diverged".into(),
});
}
return restore_sxd_value(&completed.result, context);
}
if state.pending.is_some() {
return Err(function::Error::Other {
what: "multiple pending stylesheet function calls".into(),
});
}
state.pending = Some(Rc::new(call));
Err(function::Error::Other {
what: "stylesheet function evaluation suspended".into(),
})
}
}
fn deferred_call_size(name: &ExpandedName, arguments: &[SxdValue<'_>]) -> usize {
std::mem::size_of::<DeferredCustomCall>()
.saturating_add(name.local.len())
.saturating_add(name.namespace.as_deref().map_or(0, str::len))
.saturating_add(arguments.iter().map(deferred_sxd_value_size).sum::<usize>())
}
fn deferred_sxd_value_size(value: &SxdValue<'_>) -> usize {
std::mem::size_of::<DeferredXPathValue>().saturating_add(match value {
SxdValue::Boolean(_) | SxdValue::Number(_) => 0,
SxdValue::String(_) | SxdValue::ResultTreeFragment(_, _) => 0,
#[cfg(feature = "embedded")]
SxdValue::AccountedString(text) => text.len(),
SxdValue::Nodeset(nodes) => nodes.size().saturating_mul(std::mem::size_of::<NodePath>()),
})
}
fn defer_sxd_value(
value: SxdValue<'_>,
context: &sxd_xpath_no_unsafe::context::Evaluation<'_, '_>,
) -> std::result::Result<DeferredXPathValue, function::Error> {
Ok(match value {
SxdValue::Boolean(value) => DeferredXPathValue::Boolean(value),
SxdValue::Number(value) => DeferredXPathValue::Number(value.to_bits()),
SxdValue::String(value) => DeferredXPathValue::String(value),
#[cfg(feature = "embedded")]
SxdValue::AccountedString(text) => DeferredXPathValue::String(text.as_str().to_owned()),
SxdValue::ResultTreeFragment(identity, value) => {
DeferredXPathValue::ResultTreeFragment { identity, value }
}
SxdValue::Nodeset(nodes) => DeferredXPathValue::NodeSet(
nodes
.document_order_with_context(context)?
.iter()
.map(|node| typed_path_to_with_context(node, context))
.collect::<std::result::Result<Vec<_>, _>>()?,
),
})
}
fn restore_sxd_value<'d>(
value: &DeferredXPathValue,
context: &sxd_xpath_no_unsafe::context::Evaluation<'_, 'd>,
) -> std::result::Result<SxdValue<'d>, function::Error> {
Ok(match value {
DeferredXPathValue::Boolean(value) => SxdValue::Boolean(*value),
DeferredXPathValue::Number(bits) => SxdValue::Number(f64::from_bits(*bits)),
DeferredXPathValue::String(value) => {
context.reserve_string_allocation(value.len())?;
SxdValue::String(value.clone())
}
DeferredXPathValue::ResultTreeFragment { identity, value } => {
SxdValue::ResultTreeFragment(*identity, value.clone())
}
DeferredXPathValue::NodeSet(paths) => {
let mut nodes = nodeset::Nodeset::new();
for path in paths {
let node =
resolve_node_path(context, path)?.ok_or_else(|| function::Error::Other {
what: "stylesheet function continuation node is stale".into(),
})?;
nodes.add_metered(context, node)?;
}
SxdValue::Nodeset(nodes)
}
})
}
pub(crate) struct Evaluator {
pub(crate) source: Document,
package: Package,
maps: NodeMaps,
node_base_uris: Rc<RefCell<HashMap<Vec<usize>, Option<String>>>>,
expressions: RefCell<HashMap<String, XPath>>,
pattern_matches: HashMap<PatternCacheKey, HashSet<SourceNode>>,
pattern_match_index_bytes: usize,
generated_ids: Rc<RefCell<GeneratedIdCache>>,
id_index: Rc<RefCell<IdIndex>>,
key_index: Rc<RefCell<KeyIndex>>,
key_index_bytes: usize,
key_declarations: Arc<[KeyDeclaration]>,
key_name_indices: Arc<[usize]>,
ready_key_indexes: Rc<RefCell<HashSet<(usize, usize)>>>,
ready_key_index_bytes: usize,
pending_dynamic_key_request: Rc<RefCell<Option<(usize, usize)>>>,
unparsed_entity_index: Rc<RefCell<Vec<HashMap<String, String>>>>,
decimal_formats: Arc<[DecimalFormat]>,
stylesheet_functions: Arc<HashSet<ExpandedName>>,
resolver: Arc<dyn Resolver>,
documents: HashMap<DocumentRequest, Vec<SourceNode>>,
document_cache_index_bytes: DocumentCacheIndexBytes,
document_roots: Rc<RefCell<HashMap<DocumentRequest, CachedDocumentRoots>>>,
pending_document_requests: Rc<RefCell<PendingDocumentRequests>>,
pending_dynamic_variables: HashSet<ExpandedName>,
pending_dynamic_variable_index_bytes: usize,
pending_dynamic_variable_payload_bytes: usize,
resource_identities: HashMap<ResourceIdentity, ResolvedResource>,
resource_documents: HashMap<ResourceIdentity, (SourceNode, Arc<ResourceIdentity>)>,
result_tree_fragments: HashMap<u64, (SourceNode, Arc<Document>)>,
result_tree_fragment_index_bytes: usize,
node_base_uri_index_bytes: usize,
dynamic_evaluation_depth: usize,
process_xinclude: bool,
whitespace: Arc<[(NameTest, bool, usize, usize)]>,
clock: Option<Arc<dyn Clock>>,
}
struct TemporaryStrings {
values: Vec<String>,
reserved_owned_bytes: usize,
}
pub(crate) struct DynamicVariableRequests {
pub(crate) names: HashSet<ExpandedName>,
index_bytes: usize,
payload_bytes: usize,
}
impl DynamicVariableRequests {
pub(crate) fn release(self, meter: &mut Meter) {
meter.release_owned_bytes(self.index_bytes.saturating_add(self.payload_bytes));
}
}
impl TemporaryStrings {
fn release(self, meter: &mut Meter) {
meter.release_owned_bytes(self.reserved_owned_bytes);
}
}
impl Evaluator {
pub(crate) fn take_dynamic_variable_requests(&mut self) -> DynamicVariableRequests {
DynamicVariableRequests {
names: std::mem::take(&mut self.pending_dynamic_variables),
index_bytes: std::mem::take(&mut self.pending_dynamic_variable_index_bytes),
payload_bytes: std::mem::take(&mut self.pending_dynamic_variable_payload_bytes),
}
}
pub(crate) fn take_dynamic_key_request(&mut self) -> Option<(usize, NodeId)> {
let (key_slot, document_index) = self.pending_dynamic_key_request.borrow_mut().take()?;
self.source
.logical_roots()
.get(document_index)
.copied()
.map(|root| (key_slot, root))
}
pub(crate) fn new<R: Resolver + 'static>(
prepared_source: PreparedEvaluatorSource,
principal_stylesheet: &Document,
_principal_base_uri: Option<&str>,
module_documents: &[ModuleDocument],
resolver: Arc<R>,
meter: &mut Meter,
source_options: EvaluatorSourceOptions,
) -> Result<Self> {
let PreparedEvaluatorSource {
document,
remap: _,
remap_owned_bytes: _,
resource_identities,
} = prepared_source;
let mut source = document;
reserve_stylesheet_imports(principal_stylesheet, module_documents, meter)?;
let stylesheet_root = import_stylesheet_document(
&mut source,
principal_stylesheet,
&source_options.whitespace,
meter,
)?;
let module_roots_start = source.logical_roots().len();
for module in module_documents {
import_stylesheet_document(
&mut source,
&module.document,
&source_options.whitespace,
meter,
)?;
}
let package = project_semantic_document(&source, meter)?;
let maps = NodeMaps::new(&source, meter)?;
let mut id_index = Vec::new();
extend_id_index(&source, &maps, 0, &mut id_index, meter)?;
let unparsed_entity_index = build_unparsed_entity_index(&source, meter)?;
let mut node_base_uris = HashMap::new();
let mut node_base_uri_index_bytes = 0;
extend_node_base_uri_entries(
&source,
maps.reverse.iter(),
&mut node_base_uris,
&mut node_base_uri_index_bytes,
meter,
)?;
let node_base_uris = Rc::new(RefCell::new(node_base_uris));
let principal_request = DocumentRequest {
href: String::new(),
base_uri: None,
empty_document: Some(EmptyDocumentId::Stylesheet(StylesheetDocumentId::PRINCIPAL)),
};
let mut document_root_entries = HashMap::new();
let mut documents = HashMap::new();
let mut document_cache_index_bytes = DocumentCacheIndexBytes::default();
seed_document_cache(
principal_request,
stylesheet_root,
&maps,
&mut documents,
&mut document_root_entries,
&mut document_cache_index_bytes,
meter,
)?;
let source_root = source.root();
let source_request = DocumentRequest::relative_to(
String::new(),
source_base_uri(&source, &SourceNode::Node(source_root)),
source
.logical_roots()
.binary_search(&source_root)
.ok()
.map(EmptyDocumentId::Logical),
);
seed_document_cache(
source_request,
source_root,
&maps,
&mut documents,
&mut document_root_entries,
&mut document_cache_index_bytes,
meter,
)?;
for (module, &root) in module_documents
.iter()
.zip(&source.logical_roots()[module_roots_start..])
{
let request = DocumentRequest {
href: String::new(),
base_uri: None,
empty_document: Some(EmptyDocumentId::Stylesheet(module.id)),
};
seed_document_cache(
request,
root,
&maps,
&mut documents,
&mut document_root_entries,
&mut document_cache_index_bytes,
meter,
)?;
}
let document_roots = Rc::new(RefCell::new(document_root_entries));
let pending_document_requests = Rc::new(RefCell::new(PendingDocumentRequests::default()));
Ok(Self {
source,
package,
maps,
node_base_uris,
expressions: RefCell::new(HashMap::new()),
pattern_matches: HashMap::new(),
pattern_match_index_bytes: 0,
generated_ids: Rc::new(RefCell::new(GeneratedIdCache::default())),
id_index: Rc::new(RefCell::new(id_index)),
key_index: Rc::new(RefCell::new(HashMap::new())),
key_index_bytes: 0,
key_declarations: Arc::from([]),
key_name_indices: Arc::from([]),
ready_key_indexes: Rc::new(RefCell::new(HashSet::new())),
ready_key_index_bytes: 0,
pending_dynamic_key_request: Rc::new(RefCell::new(None)),
unparsed_entity_index: Rc::new(RefCell::new(unparsed_entity_index)),
decimal_formats: Arc::from([]),
stylesheet_functions: Arc::new(HashSet::new()),
resolver,
documents,
document_cache_index_bytes,
document_roots,
pending_document_requests,
pending_dynamic_variables: HashSet::new(),
pending_dynamic_variable_index_bytes: 0,
pending_dynamic_variable_payload_bytes: 0,
resource_identities,
resource_documents: HashMap::new(),
result_tree_fragments: HashMap::new(),
result_tree_fragment_index_bytes: 0,
node_base_uri_index_bytes,
dynamic_evaluation_depth: 0,
process_xinclude: source_options.process_xinclude,
whitespace: source_options.whitespace,
clock: source_options
.clock
.map(|clock| Arc::new(OperationClock::new(clock)) as Arc<dyn Clock>),
})
}
pub(crate) fn initialize_xslt(
&mut self,
decimal_formats: Arc<[DecimalFormat]>,
stylesheet_functions: Arc<HashSet<ExpandedName>>,
key_declarations: Arc<[KeyDeclaration]>,
key_name_indices: Arc<[usize]>,
) {
self.decimal_formats = decimal_formats;
self.stylesheet_functions = stylesheet_functions;
self.key_declarations = key_declarations;
self.key_name_indices = key_name_indices;
}
pub(crate) fn key_index_ready(&self, key_slot: usize, logical_root: NodeId) -> bool {
let Some(document_index) = self
.source
.logical_roots()
.iter()
.position(|candidate| *candidate == logical_root)
else {
return false;
};
self.ready_key_indexes
.borrow()
.contains(&(key_slot, document_index))
}
pub(crate) fn finish_key_index(
&mut self,
key_slot: usize,
logical_root: NodeId,
meter: &mut Meter,
) -> Result<()> {
let document_index = self
.source
.logical_roots()
.iter()
.position(|candidate| *candidate == logical_root)
.ok_or_else(|| Error::Dynamic("key index has no logical document".into()))?;
let mut ready = self.ready_key_indexes.borrow_mut();
reserve_retained_hash_set_slot(&mut ready, meter, &mut self.ready_key_index_bytes)?;
ready.insert((key_slot, document_index));
drop(ready);
clear_pattern_cache(
&mut self.pattern_matches,
&mut self.pattern_match_index_bytes,
meter,
);
Ok(())
}
pub(crate) fn append_key_entry(
&mut self,
key_slot: usize,
value: String,
value_reservation: usize,
node: &SourceNode,
meter: &mut Meter,
) -> Result<()> {
debug_assert_eq!(value_reservation, value.capacity());
let Some(sxd) = self
.maps
.to_sxd(self.package.as_document().root().into(), node)
else {
meter.release_owned_bytes(value_reservation);
return Ok(());
};
let document_index = self
.source
.logical_root_for(node)
.and_then(|root| {
self.source
.logical_roots()
.iter()
.position(|candidate| *candidate == root)
})
.ok_or_else(|| Error::Dynamic("key node has no logical document".into()));
let document_index = match document_index {
Ok(document_index) => document_index,
Err(error) => {
meter.release_owned_bytes(value_reservation);
return Err(error);
}
};
let (path, path_reservation) = match typed_path_to_metered(&sxd, meter) {
Ok(path) => path,
Err(error) => {
meter.release_owned_bytes(value_reservation);
return Err(error);
}
};
let result = (|| {
meter.charge(BudgetKind::KeyEntries, 1)?;
let mut index = self.key_index.borrow_mut();
let group = (key_slot, document_index);
if !index.contains_key(&group) {
reserve_retained_hash_map_slot(&mut index, meter, &mut self.key_index_bytes)?;
}
let values = index.entry(group).or_default();
if let Some(paths) = values.get_mut(value.as_str()) {
reserve_retained_vec_slot(paths, meter)?;
paths.push(path);
meter.release_owned_bytes(value_reservation);
} else {
reserve_retained_hash_map_slot(values, meter, &mut self.key_index_bytes)?;
let mut paths = Vec::new();
reserve_retained_vec_slot(&mut paths, meter)?;
paths.push(path);
values.insert(value, paths);
}
Ok(())
})();
if result.is_err() {
meter.release_owned_bytes(value_reservation.saturating_add(path_reservation));
}
result
}
#[expect(
clippy::too_many_arguments,
reason = "XPath dynamic context and its budget/continuation state are independent inputs"
)]
pub(crate) fn evaluate(
&mut self,
expression: &Expression,
node: &SourceNode,
position: usize,
size: usize,
variables: &dyn VariableBindings,
meter: &mut Meter,
custom_calls: Option<&CustomCallSession>,
) -> Result<XPathValue> {
loop {
if let Some(custom_calls) = custom_calls {
custom_calls.begin_attempt();
}
let prepared = self.prepare_extension_calls(
expression,
node,
position,
size,
variables,
meter,
custom_calls,
)?;
let outcome = (|| {
let (prepared_expression, augmented) = prepared.parts();
let value = if let Some(name) = variable_reference_name(
trim_xml_whitespace(&prepared_expression.source),
&prepared_expression.namespaces,
) && let Some(Value::StoredExpression(source)) = augmented.get(&name)
{
XPathValue::StoredExpression(source.clone())
} else {
self.evaluate_core(
prepared_expression,
node,
position,
size,
augmented,
meter,
custom_calls,
)?
};
let requested = std::mem::take(&mut *self.pending_document_requests.borrow_mut());
if requested.items.is_empty() {
return Ok(Some(value));
}
let requested_owned_bytes = requested.retained_owned_bytes();
meter.charge(BudgetKind::OwnedBytes, requested_owned_bytes)?;
let prepared_count = self.documents.len();
let prepared = self.prepare_document_requests(requested.items, augmented, meter);
meter.release_owned_bytes(requested_owned_bytes);
prepared?;
if self.documents.len() == prepared_count {
return Err(Error::Dynamic(
"document() resolution made no progress".into(),
));
}
Ok(None)
})();
let reserved_owned_bytes = prepared.reserved_owned_bytes();
drop(prepared);
meter.release_owned_bytes(reserved_owned_bytes);
if outcome.is_err() {
*self.pending_document_requests.borrow_mut() = PendingDocumentRequests::default();
}
if let Some(value) = outcome? {
return Ok(value);
}
}
}
pub(crate) fn document_order(&self, mut nodes: Vec<SourceNode>) -> Vec<SourceNode> {
nodes.sort_by_key(|node| self.maps.order.get(node).copied());
nodes.dedup();
nodes
}
pub(crate) fn take_custom_function_call(
&self,
session: &CustomCallSession,
variables: &dyn VariableBindings,
meter: &mut Meter,
) -> Result<Option<CustomFunctionCall>> {
let pending = {
let mut state = session.state.borrow_mut();
let pending = state.pending.take();
if let Some(pending) = pending.as_ref() {
let retained = pending.owned_bytes();
meter.charge(BudgetKind::OwnedBytes, retained)?;
state.retained_bytes = state.retained_bytes.saturating_add(retained);
}
state.last_requested = pending.clone();
pending
};
pending
.map(|pending| {
let node = self
.maps
.reverse
.get(&pending.node)
.cloned()
.ok_or_else(|| Error::Dynamic("stylesheet function context is stale".into()))?;
let arguments = pending
.arguments
.iter()
.map(|argument| self.project_deferred_value(argument, session, variables))
.collect::<Result<Vec<_>>>()?;
Ok(CustomFunctionCall {
name: pending.name.clone(),
node,
position: pending.position,
size: pending.size,
arguments,
})
})
.transpose()
}
pub(crate) fn complete_custom_function_call(
&self,
session: &CustomCallSession,
value: Value,
meter: &mut Meter,
) -> Result<()> {
let retained = self.deferred_public_value_size(&value)?;
meter.charge(BudgetKind::OwnedBytes, retained)?;
let (result, fragment) = match self.defer_public_value(value) {
Ok(value) => value,
Err(error) => {
meter.release_owned_bytes(retained);
return Err(error);
}
};
let mut state = session.state.borrow_mut();
let Some(call) = state.last_requested.take() else {
meter.release_owned_bytes(retained);
return Err(Error::Dynamic(
"stylesheet function continuation is missing".into(),
));
};
if let Err(error) = state.reserve_completed_slot(meter) {
meter.release_owned_bytes(retained);
return Err(error);
}
state.retained_bytes = state.retained_bytes.saturating_add(retained);
state.completed.push(CompletedCustomCall {
call,
result,
fragment,
});
Ok(())
}
fn deferred_public_value_size(&self, value: &Value) -> Result<usize> {
let payload = match value {
Value::Boolean(_) | Value::Number(_) => 0,
Value::String(value) | Value::StoredExpression(value) => value.capacity(),
Value::NodeSet(nodes) => nodes.iter().try_fold(0usize, |total, node| {
let path = self.maps.forward.get(node).ok_or_else(|| {
Error::Dynamic("stylesheet function result node is stale".into())
})?;
Ok::<_, Error>(total.saturating_add(path.owned_bytes()))
})?,
Value::ResultTreeFragment(document) => {
let mut string_bytes = 0usize;
document.visit_string_value(document.root(), |value| {
string_bytes = string_bytes.saturating_add(value.len());
});
std::mem::size_of::<Arc<Document>>().saturating_add(string_bytes)
}
};
Ok(std::mem::size_of::<DeferredXPathValue>().saturating_add(payload))
}
fn project_deferred_value(
&self,
value: &DeferredXPathValue,
session: &CustomCallSession,
variables: &dyn VariableBindings,
) -> Result<Value> {
Ok(match value {
DeferredXPathValue::Boolean(value) => Value::Boolean(*value),
DeferredXPathValue::Number(bits) => Value::Number(f64::from_bits(*bits)),
DeferredXPathValue::String(value) => Value::String(value.clone()),
DeferredXPathValue::NodeSet(paths) => Value::NodeSet(
paths
.iter()
.map(|path| {
self.maps.reverse.get(path).cloned().ok_or_else(|| {
Error::Dynamic("stylesheet function argument node is stale".into())
})
})
.collect::<Result<Vec<_>>>()?,
),
DeferredXPathValue::ResultTreeFragment { identity, .. } => variables
.result_tree_fragment(*identity)
.or_else(|| session.fragment(*identity))
.map(Value::ResultTreeFragment)
.ok_or_else(|| Error::Dynamic("stale result-tree-fragment identity".into()))?,
})
}
fn defer_public_value(
&self,
value: Value,
) -> Result<(DeferredXPathValue, Option<Arc<Document>>)> {
Ok(match value {
Value::Boolean(value) => (DeferredXPathValue::Boolean(value), None),
Value::Number(value) => (DeferredXPathValue::Number(value.to_bits()), None),
Value::String(value) | Value::StoredExpression(value) => {
(DeferredXPathValue::String(value), None)
}
Value::NodeSet(nodes) => (
DeferredXPathValue::NodeSet(
nodes
.iter()
.map(|node| {
self.maps.forward.get(node).cloned().ok_or_else(|| {
Error::Dynamic("stylesheet function result node is stale".into())
})
})
.collect::<Result<Vec<_>>>()?,
),
None,
),
Value::ResultTreeFragment(document) => {
let deferred = DeferredXPathValue::ResultTreeFragment {
identity: result_tree_fragment_handle(&document),
value: Arc::new(document.string_value(document.root())),
};
(deferred, Some(document))
}
})
}
fn import_result_tree_fragment(
&mut self,
fragment: &Arc<Document>,
meter: &mut Meter,
) -> Result<SourceNode> {
let identity = result_tree_fragment_handle(fragment);
if let Some((root, _owner)) = self.result_tree_fragments.get(&identity) {
return Ok(root.clone());
}
let root = self.import_document(fragment, meter)?;
reserve_retained_hash_map_slot(
&mut self.result_tree_fragments,
meter,
&mut self.result_tree_fragment_index_bytes,
)?;
self.result_tree_fragments
.insert(identity, (root.clone(), Arc::clone(fragment)));
Ok(root)
}
fn import_document(&mut self, document: &Document, meter: &mut Meter) -> Result<SourceNode> {
meter.charge(BudgetKind::OwnedBytes, semantic_projection_size(document))?;
let first_new_node = self.source.nodes().len();
let logical_root = self.source.import(document);
let sxd_document = self.package.as_document();
let documents = sxd_document
.root()
.children()
.into_iter()
.find_map(|child| child.element())
.ok_or_else(|| Error::Dynamic("semantic projection root is missing".into()))?;
project_logical_root(&self.source, logical_root, sxd_document, documents, meter)?;
self.maps.extend(&self.source, first_new_node, meter)?;
extend_id_index(
&self.source,
&self.maps,
first_new_node,
&mut self.id_index.borrow_mut(),
meter,
)?;
append_unparsed_entity_document(
&self.source,
logical_root,
&mut self.unparsed_entity_index.borrow_mut(),
meter,
)?;
extend_node_base_uri_entries(
&self.source,
self.maps
.reverse
.iter()
.filter(|(_, node)| source_node_owner(node).0 >= first_new_node),
&mut self.node_base_uris.borrow_mut(),
&mut self.node_base_uri_index_bytes,
meter,
)?;
let root = SourceNode::Node(logical_root);
let logical_document = self.source.logical_roots().len() - 1;
let empty_uri = DocumentRequest::relative_to(
String::new(),
source_base_uri(&self.source, &root),
Some(EmptyDocumentId::Logical(logical_document)),
);
self.cache_document(empty_uri, vec![root.clone()], None, meter)?;
Ok(root)
}
fn cache_document(
&mut self,
request: DocumentRequest,
nodes: Vec<SourceNode>,
identity: Option<&Arc<ResourceIdentity>>,
meter: &mut Meter,
) -> Result<()> {
if self.documents.contains_key(&request) {
return Ok(());
}
charge_document_cache_keys(&request, meter)?;
reserve_retained_hash_map_slot(
&mut self.documents,
meter,
&mut self.document_cache_index_bytes.documents,
)?;
reserve_retained_hash_map_slot(
&mut self.document_roots.borrow_mut(),
meter,
&mut self.document_cache_index_bytes.roots,
)?;
meter.charge(
BudgetKind::OwnedBytes,
nodes
.capacity()
.saturating_mul(std::mem::size_of::<SourceNode>()),
)?;
let mut roots = Vec::new();
let mut roots_index_bytes = 0;
for node in &nodes {
let Some(path) = self.maps.forward.get(node) else {
continue;
};
reserve_temporary_vec_slot(&mut roots, meter, &mut roots_index_bytes)?;
meter.charge(BudgetKind::OwnedBytes, path.owned_bytes())?;
roots.push(path.clone());
}
self.document_roots.borrow_mut().insert(
request.clone(),
CachedDocumentRoots {
paths: roots,
identity: identity.map(Arc::clone),
},
);
self.documents.insert(request, nodes);
Ok(())
}
#[expect(
clippy::too_many_arguments,
reason = "extension preparation preserves the complete XPath dynamic context"
)]
fn prepare_extension_calls<'a>(
&mut self,
expression: &'a Expression,
node: &SourceNode,
position: usize,
size: usize,
variables: &'a dyn VariableBindings,
meter: &mut Meter,
custom_calls: Option<&CustomCallSession>,
) -> Result<PreparedExtensionCalls<'a>> {
let mut reserved_owned_bytes = 0usize;
let prepared = self.prepare_extension_calls_inner(
expression,
node,
position,
size,
variables,
meter,
custom_calls,
&mut reserved_owned_bytes,
);
if prepared.is_err() {
meter.release_owned_bytes(reserved_owned_bytes);
}
prepared
}
#[expect(
clippy::too_many_arguments,
reason = "extension preparation also tracks temporary storage for fail-closed cleanup"
)]
fn prepare_extension_calls_inner<'a>(
&mut self,
expression: &'a Expression,
node: &SourceNode,
position: usize,
size: usize,
variables: &'a dyn VariableBindings,
meter: &mut Meter,
custom_calls: Option<&CustomCallSession>,
reserved_owned_bytes: &mut usize,
) -> Result<PreparedExtensionCalls<'a>> {
if innermost_namespaced_call(
&expression.source,
&expression.namespaces,
is_prepared_extension_call,
)
.is_none()
{
return Ok(PreparedExtensionCalls::Borrowed {
expression,
variables,
});
}
let extension_prefix_bytes = unused_internal_name_owned_len(
expression
.namespaces
.iter()
.map(|(prefix, _)| prefix.as_str()),
"__xml_sec_ext",
'_',
);
let extension_namespace_bytes = unused_internal_name_owned_len(
expression
.namespaces
.iter()
.map(|(_, namespace)| namespace.as_str()),
EXTENSION_CONTEXT_NS,
':',
);
let rewrite_bytes = expression
.source
.len()
.saturating_add(
expression
.namespaces
.len()
.saturating_add(1)
.saturating_mul(std::mem::size_of::<(String, String)>()),
)
.saturating_add(expression.namespaces.iter().fold(
0usize,
|total, (prefix, namespace)| {
total
.saturating_add(prefix.len())
.saturating_add(namespace.len())
},
))
.saturating_add(extension_prefix_bytes)
.saturating_add(extension_namespace_bytes)
.saturating_add(std::mem::size_of::<Vec<(String, String)>>())
.saturating_add(2 * std::mem::size_of::<usize>());
meter.charge(BudgetKind::OwnedBytes, rewrite_bytes)?;
*reserved_owned_bytes = reserved_owned_bytes.saturating_add(rewrite_bytes);
let (extension_prefix, extension_namespace) = unused_internal_namespace(
&expression.namespaces,
"__xml_sec_ext",
EXTENSION_CONTEXT_NS,
);
let mut namespaces = Vec::with_capacity(expression.namespaces.len().saturating_add(1));
namespaces.extend(expression.namespaces.iter().cloned());
namespaces.push((
extension_prefix.into_owned(),
extension_namespace.into_owned(),
));
let prepared_expression = Expression {
source: String::new(),
namespaces: Arc::new(namespaces),
variable_references: Arc::clone(&expression.variable_references),
static_base_uri: expression.static_base_uri.clone(),
stylesheet_document: expression.stylesheet_document,
};
let mut source = expression.source.clone();
let expression = &prepared_expression;
let (extension_prefix, extension_namespace) = expression
.namespaces
.last()
.map(|(prefix, namespace)| (prefix.as_str(), namespace.as_str()))
.expect("generated extension namespace was appended");
let mut augmented = VariableOverlay::new(variables);
let mut stored_expressions = HashSet::new();
let mut variable_index = 0usize;
while let Some(call) =
innermost_namespaced_call(&source, &expression.namespaces, is_prepared_extension_call)
{
let kind = match (call.namespace.as_str(), call.local.as_str()) {
(EXSLT_COMMON_NS | LIBXSLT_NS | SAXON_NS | XT_NS, "node-set") => {
ExtensionCallKind::NodeSet
}
(EXSLT_COMMON_NS, "object-type") => ExtensionCallKind::ObjectType,
(EXSLT_STRINGS_NS, "split") => ExtensionCallKind::Split,
(EXSLT_STRINGS_NS, "tokenize") => ExtensionCallKind::Tokenize,
(EXSLT_STRINGS_NS, "replace") => ExtensionCallKind::Replace,
(EXSLT_DYNAMIC_NS, "evaluate") => ExtensionCallKind::DynamicEvaluate,
(EXSLT_DYNAMIC_NS, "map") => ExtensionCallKind::DynamicMap,
(SAXON_NS, "expression") => ExtensionCallKind::SaxonExpression,
(SAXON_NS, "eval") => ExtensionCallKind::SaxonEval,
(SAXON_NS, "evaluate") => ExtensionCallKind::SaxonEvaluate,
(SAXON_NS, "line-number") => ExtensionCallKind::SaxonLineNumber,
_ => unreachable!("extension call predicate admits only known functions"),
};
let is_stored_expression = matches!(kind, ExtensionCallKind::SaxonExpression);
let value = match kind {
ExtensionCallKind::NodeSet => {
if call.arguments.len() != 1 {
return Err(Error::Dynamic(
"exsl:node-set() requires one argument".into(),
));
}
let argument = trim_xml_whitespace(&call.arguments[0]);
if let Some(name) = variable_reference_name(argument, &expression.namespaces)
&& let Some(Value::ResultTreeFragment(fragment)) = augmented.get(&name)
{
let fragment = fragment.clone();
let root = self.import_result_tree_fragment(&fragment, meter)?;
Value::NodeSet(self.singleton(&root, true, meter)?)
} else {
let value = self.evaluate_core(
&expression.derived(argument),
node,
position,
size,
&augmented,
meter,
custom_calls,
)?;
match value {
XPathValue::NodeSet(nodes) => Value::NodeSet(nodes),
value => {
let (text, text_bytes) =
value.into_temporary_string(self, meter)?;
let fragment = text_document(&text, meter);
drop(text);
meter.release_owned_bytes(text_bytes);
let fragment = fragment?;
let root = self.import_document(&fragment, meter)?;
let nodes = self.children(&root, meter)?;
Value::NodeSet(nodes)
}
}
}
}
ExtensionCallKind::ObjectType => {
if call.arguments.len() != 1 {
return Err(Error::Dynamic(
"exsl:object-type() requires one argument".into(),
));
}
let argument = trim_xml_whitespace(&call.arguments[0]);
let declared_name = variable_reference_name(argument, &expression.namespaces);
let declared = declared_name.as_ref().and_then(|name| augmented.get(name));
let object_type = if declared_name.as_ref().is_some_and(|name| {
stored_expressions.contains(name)
|| matches!(augmented.get(name), Some(Value::StoredExpression(_)))
}) {
"external"
} else {
match declared {
Some(Value::ResultTreeFragment(_)) => "RTF",
Some(Value::NodeSet(_)) => "node-set",
Some(Value::Boolean(_)) => "boolean",
Some(Value::Number(_)) => "number",
Some(Value::String(_)) => "string",
Some(Value::StoredExpression(_)) => "external",
None => match self.evaluate_core(
&expression.derived(argument),
node,
position,
size,
&augmented,
meter,
custom_calls,
)? {
XPathValue::NodeSet(_) => "node-set",
XPathValue::ResultTreeFragment(_) => "RTF",
XPathValue::Boolean(_) => "boolean",
XPathValue::Number(_) => "number",
XPathValue::String(_) => "string",
XPathValue::StoredExpression(_) => "external",
},
}
};
Value::String(object_type.into())
}
ExtensionCallKind::Split | ExtensionCallKind::Tokenize => {
if !(1..=2).contains(&call.arguments.len()) {
return Err(Error::Dynamic(format!(
"{}() requires one or two arguments",
call.display_name
)));
}
let input = self
.evaluate_core(
&expression.derived(call.arguments[0].clone()),
node,
position,
size,
&augmented,
meter,
custom_calls,
)?
.into_temporary_string(self, meter)?;
let delimiter = if let Some(argument) = call.arguments.get(1) {
match self
.evaluate_core(
&expression.derived(argument.clone()),
node,
position,
size,
&augmented,
meter,
custom_calls,
)?
.into_temporary_string(self, meter)
{
Ok((value, reserved)) => (Cow::Owned(value), reserved),
Err(error) => {
meter.release_owned_bytes(input.1);
return Err(error);
}
}
} else {
(
Cow::Borrowed(match kind {
ExtensionCallKind::Tokenize => " \t\r\n",
ExtensionCallKind::Split => " ",
_ => unreachable!(),
}),
0,
)
};
let nodes = (|| {
let fragment = token_document(&input.0, &delimiter.0, kind, meter)?;
let root = self.import_document(&fragment, meter)?;
self.children(&root, meter)
})();
debug_assert!(input.1.checked_add(delimiter.1).is_some());
meter.release_owned_bytes(input.1 + delimiter.1);
let nodes = nodes?;
Value::NodeSet(nodes)
}
ExtensionCallKind::Replace => {
if call.arguments.len() != 3 {
return Err(Error::Dynamic(
"str:replace() requires three arguments".into(),
));
}
let (input, input_owned_bytes) = self
.evaluate_core(
&expression.derived(call.arguments[0].clone()),
node,
position,
size,
&augmented,
meter,
custom_calls,
)?
.into_temporary_string(self, meter)?;
let searches = match self.evaluate_extension_strings(
expression.derived(call.arguments[1].clone()),
node,
position,
size,
&augmented,
meter,
custom_calls,
) {
Ok(searches) => searches,
Err(error) => {
meter.release_owned_bytes(input_owned_bytes);
return Err(error);
}
};
let replacements = match self.evaluate_extension_strings(
expression.derived(call.arguments[2].clone()),
node,
position,
size,
&augmented,
meter,
custom_calls,
) {
Ok(replacements) => replacements,
Err(error) => {
meter.release_owned_bytes(input_owned_bytes);
searches.release(meter);
return Err(error);
}
};
let replaced =
replace_exslt_string(&input, &searches.values, &replacements.values, meter);
meter.release_owned_bytes(input_owned_bytes);
searches.release(meter);
replacements.release(meter);
let replaced = replaced?;
let fragment = text_document(&replaced, meter);
meter.release_owned_bytes(replaced.len());
let fragment = fragment?;
let root = self.import_document(&fragment, meter)?;
let nodes = self.children(&root, meter)?;
Value::NodeSet(nodes)
}
ExtensionCallKind::DynamicEvaluate | ExtensionCallKind::SaxonEvaluate => {
if call.arguments.len() != 1 {
return Err(Error::Dynamic(format!(
"{}() requires one argument",
call.display_name
)));
}
let (dynamic_source, dynamic_source_bytes) = self
.evaluate_core(
&expression.derived(call.arguments[0].clone()),
node,
position,
size,
&augmented,
meter,
custom_calls,
)?
.into_fully_metered_temporary_string(self, meter)?;
let result = if dynamic_source.is_empty() {
Ok(Value::NodeSet(Vec::new()))
} else {
let dynamic_expression = expression.derived(dynamic_source);
for name in dynamic_expression.variable_references.iter() {
if augmented.get(name).is_some()
|| self.pending_dynamic_variables.contains(name)
{
continue;
}
let payload_bytes = expanded_name_owned_bytes(name);
meter.charge(BudgetKind::OwnedBytes, payload_bytes)?;
if let Err(error) = reserve_retained_hash_set_slot(
&mut self.pending_dynamic_variables,
meter,
&mut self.pending_dynamic_variable_index_bytes,
) {
meter.release_owned_bytes(payload_bytes);
return Err(error);
}
self.pending_dynamic_variables.insert(name.clone());
self.pending_dynamic_variable_payload_bytes = self
.pending_dynamic_variable_payload_bytes
.saturating_add(payload_bytes);
}
match self.evaluate_dynamic(
&dynamic_expression,
node,
position,
size,
&augmented,
meter,
custom_calls,
) {
Ok(value) => Ok(xpath_value_to_public(value)),
Err(error)
if kind == ExtensionCallKind::DynamicEvaluate
&& dynamic_expression_error_is_recoverable(&error) =>
{
Ok(Value::NodeSet(Vec::new()))
}
Err(error) => Err(error),
}
};
meter.release_owned_bytes(dynamic_source_bytes);
result?
}
ExtensionCallKind::SaxonExpression => {
if call.arguments.len() != 1 {
return Err(Error::Dynamic(
"saxon:expression() requires one argument".into(),
));
}
let (stored, stored_bytes) = self
.evaluate_core(
&expression.derived(call.arguments[0].clone()),
node,
position,
size,
&augmented,
meter,
custom_calls,
)?
.into_fully_metered_temporary_string(self, meter)?;
*reserved_owned_bytes = reserved_owned_bytes.saturating_add(stored_bytes);
validate_dynamic_expression(&stored, &expression.namespaces)?;
Value::StoredExpression(stored)
}
ExtensionCallKind::SaxonEval => {
if call.arguments.len() != 1 {
return Err(Error::Dynamic("saxon:eval() requires one argument".into()));
}
let Some(name) = variable_reference_name(
trim_xml_whitespace(&call.arguments[0]),
&expression.namespaces,
) else {
return Err(Error::Dynamic(
"saxon:eval() requires a stored expression".into(),
));
};
let Some(Value::StoredExpression(dynamic_source)) = augmented.get(&name) else {
return Err(Error::Dynamic("stored XPath expression is missing".into()));
};
xpath_value_to_public(self.evaluate_dynamic(
&expression.derived(dynamic_source.clone()),
node,
position,
size,
&augmented,
meter,
custom_calls,
)?)
}
ExtensionCallKind::SaxonLineNumber => {
if call.arguments.len() != 1 {
return Err(Error::Dynamic(
"saxon:line-number() requires one argument".into(),
));
}
let value = self.evaluate_core(
&expression.derived(call.arguments[0].clone()),
node,
position,
size,
&augmented,
meter,
custom_calls,
)?;
let line = match value {
XPathValue::NodeSet(nodes) => nodes
.first()
.and_then(|node| self.source.source_line(node))
.unwrap_or(0),
_ => 0,
};
Value::Number(line as f64)
}
ExtensionCallKind::DynamicMap => {
if call.arguments.len() != 2 {
return Err(Error::Dynamic("dyn:map() requires two arguments".into()));
}
let XPathValue::NodeSet(nodes) = self.evaluate_core(
&expression.derived(call.arguments[0].clone()),
node,
position,
size,
&augmented,
meter,
custom_calls,
)?
else {
return Err(Error::Dynamic("dyn:map() requires a node-set".into()));
};
let (dynamic_source, dynamic_source_bytes) = self
.evaluate_core(
&expression.derived(call.arguments[1].clone()),
node,
position,
size,
&augmented,
meter,
custom_calls,
)?
.into_fully_metered_temporary_string(self, meter)?;
*reserved_owned_bytes =
reserved_owned_bytes.saturating_add(dynamic_source_bytes);
let dynamic_expression = expression.derived(dynamic_source);
let nodes = self.document_order(nodes);
let mut result_nodes = Vec::new();
let mut scalars = Vec::new();
let total = nodes.len();
for (index, mapped_node) in nodes.iter().enumerate() {
let value = match self.evaluate_dynamic(
&dynamic_expression,
mapped_node,
index + 1,
total,
&augmented,
meter,
custom_calls,
) {
Ok(value) => value,
Err(error) if dynamic_expression_error_is_recoverable(&error) => {
continue;
}
Err(error) => return Err(error),
};
match value {
XPathValue::NodeSet(nodes) => {
for node in nodes {
reserve_temporary_vec_slot(
&mut result_nodes,
meter,
reserved_owned_bytes,
)?;
result_nodes.push(node);
}
}
value => {
let kind = match value {
XPathValue::Boolean(_) => "boolean",
XPathValue::Number(_) => "number",
XPathValue::String(_)
| XPathValue::StoredExpression(_)
| XPathValue::ResultTreeFragment(_) => "string",
XPathValue::NodeSet(_) => unreachable!(),
};
reserve_temporary_vec_slot(
&mut scalars,
meter,
reserved_owned_bytes,
)?;
let (value, value_bytes) =
value.into_fully_metered_temporary_string(self, meter)?;
*reserved_owned_bytes =
reserved_owned_bytes.saturating_add(value_bytes);
scalars.push((kind, value));
}
}
}
if !scalars.is_empty() {
let (fragment, fragment_owned_bytes) =
dynamic_map_document(&scalars, meter)?;
let imported = self.import_document(&fragment, meter);
meter.release_owned_bytes(fragment_owned_bytes);
let root = imported?;
if let SourceNode::Node(root) = root
&& let Some(root) = self.source.node(root)
{
for child in root.children.iter().copied() {
reserve_temporary_vec_slot(
&mut result_nodes,
meter,
reserved_owned_bytes,
)?;
result_nodes.push(SourceNode::Node(child));
}
}
}
Value::NodeSet(self.document_order(result_nodes))
}
};
let local = format!("value{variable_index}");
variable_index += 1;
augmented.insert(
ExpandedName::new(Some(extension_namespace), local.clone()),
value,
);
if is_stored_expression {
stored_expressions
.insert(ExpandedName::new(Some(extension_namespace), local.clone()));
}
source.replace_range(
call.start..call.end,
&format!("${extension_prefix}:{local}"),
);
}
let rewritten = expression.derived(source);
Ok(PreparedExtensionCalls::Rewritten {
expression: rewritten,
variables: augmented,
reserved_owned_bytes: *reserved_owned_bytes,
})
}
#[expect(
clippy::too_many_arguments,
reason = "extension arguments preserve the complete XPath dynamic context"
)]
fn evaluate_extension_strings(
&self,
expression: Expression,
node: &SourceNode,
position: usize,
size: usize,
variables: &dyn VariableBindings,
meter: &mut Meter,
custom_calls: Option<&CustomCallSession>,
) -> Result<TemporaryStrings> {
let value = self.evaluate_core(
&expression,
node,
position,
size,
variables,
meter,
custom_calls,
)?;
let mut values = Vec::new();
let mut reserved_owned_bytes = 0usize;
let materialized = (|| {
match value {
XPathValue::NodeSet(nodes) => {
for node in self.document_order(nodes) {
reserve_temporary_vec_slot(&mut values, meter, &mut reserved_owned_bytes)?;
let (value, value_bytes) =
self.materialize_temporary_string_value(&node, meter)?;
reserved_owned_bytes = reserved_owned_bytes.saturating_add(value_bytes);
values.push(value);
}
}
value => {
reserve_temporary_vec_slot(&mut values, meter, &mut reserved_owned_bytes)?;
let (value, temporary_bytes) = value.into_temporary_string(self, meter)?;
reserved_owned_bytes = reserved_owned_bytes.saturating_add(temporary_bytes);
values.push(value);
}
}
Ok(())
})();
if let Err(error) = materialized {
meter.release_owned_bytes(reserved_owned_bytes);
return Err(error);
}
Ok(TemporaryStrings {
values,
reserved_owned_bytes,
})
}
#[expect(
clippy::too_many_arguments,
reason = "dynamic XPath preserves the caller's complete evaluation context"
)]
fn evaluate_dynamic(
&mut self,
expression: &Expression,
node: &SourceNode,
position: usize,
size: usize,
variables: &dyn VariableBindings,
meter: &mut Meter,
custom_calls: Option<&CustomCallSession>,
) -> Result<XPathValue> {
validate_dynamic_expression(&expression.source, &expression.namespaces)?;
self.dynamic_evaluation_depth = self.dynamic_evaluation_depth.saturating_add(1);
let depth = self.dynamic_evaluation_depth;
let result = meter.recursion(depth.saturating_mul(32)).and_then(|()| {
self.evaluate(
expression,
node,
position,
size,
variables,
meter,
custom_calls,
)
});
self.dynamic_evaluation_depth -= 1;
result
}
#[expect(
clippy::too_many_arguments,
reason = "the XPath engine requires every component of its dynamic context"
)]
fn evaluate_core(
&self,
expression: &Expression,
node: &SourceNode,
position: usize,
size: usize,
variables: &dyn VariableBindings,
meter: &mut Meter,
custom_calls: Option<&CustomCallSession>,
) -> Result<XPathValue> {
let document = self.package.as_document();
let context_path = self
.maps
.forward
.get(node)
.ok_or_else(|| Error::Dynamic("XPath context node is stale".into()))?;
meter.charge(BudgetKind::XPathOperations, context_path.ordinary().len())?;
let context_node = self
.maps
.resolve(document.root().into(), context_path)
.ok_or_else(|| Error::Dynamic("XPath context node is stale".into()))?;
let uses_current =
super::expression::has_unprefixed_function_call(&expression.source, "current");
let context_workspace = xpath_context_workspace_upper_bound(
expression,
custom_calls
.is_some()
.then_some(self.stylesheet_functions.as_ref()),
);
let adapter_workspace = xpath_adapter_workspace_upper_bound(expression)
.saturating_add(context_workspace)
.saturating_add(if uses_current {
context_path.owned_bytes()
} else {
0
});
meter.check_additional(BudgetKind::OwnedBytes, adapter_workspace)?;
let normalized = normalize_xpath_for_sxd(&expression.source);
let isolated = hide_projection_elements_from_axes(&normalized);
let context_namespace =
(isolated.contains("position") || isolated.contains("last")).then(|| {
unused_internal_namespace(&expression.namespaces, "__xml_sec_ctx", CONTEXT_NS)
});
let rewritten = context_namespace.as_ref().map_or_else(
|| Cow::Borrowed(isolated.as_ref()),
|(prefix, _)| rewrite_outer_context_functions(&isolated, prefix),
);
if !self.expressions.borrow().contains_key(rewritten.as_ref()) {
let retained_bytes = rewritten
.len()
.saturating_mul(128)
.saturating_add(std::mem::size_of::<XPath>());
meter.check_additional(
BudgetKind::OwnedBytes,
adapter_workspace.saturating_add(retained_bytes),
)?;
meter.charge(BudgetKind::OwnedBytes, retained_bytes)?;
let xpath = Factory::new().build(&rewritten).map_err(|error| {
Error::Static(format!(
"invalid XPath expression `{}`: {error}",
expression.source
))
})?;
self.expressions
.borrow_mut()
.insert(rewritten.as_ref().to_owned(), xpath);
}
let document_access_error = RefCell::new(None);
let work_remaining = meter.work_remaining();
let admitted_work = Cell::new(0usize);
let ceiling_crossed = Cell::new(None);
let admit_work = |units: usize| -> std::result::Result<(), function::Error> {
match admitted_work
.get()
.checked_add(units)
.filter(|total| *total <= work_remaining)
{
Some(total) => {
admitted_work.set(total);
Ok(())
}
None => {
if ceiling_crossed.get().is_none() {
ceiling_crossed.set(Some(admitted_work.get().saturating_add(units)));
}
Err(function::Error::Other {
what: "XSLT work ceiling exceeded".into(),
})
}
}
};
let ceiling_refusal = |meter: &Meter, attempted: usize| Error::Budget {
kind: BudgetKind::AggregateWork,
limit: meter.work_ceiling(),
actual: (meter.work_ceiling() - work_remaining).saturating_add(attempted),
};
let mut context = sxd_xpath_no_unsafe::context::ScopedContext::new();
context.set_work_admission(&admit_work);
context.set_document_root_resolver(projected_document_root);
let (owned_bytes, owned_bytes_limit) = meter.usage(BudgetKind::OwnedBytes)?;
context.set_string_allocation_limit(
owned_bytes_limit
.saturating_sub(owned_bytes)
.saturating_sub(adapter_workspace),
);
let (xpath_operations, xpath_operations_limit) =
meter.usage(BudgetKind::XPathOperations)?;
context.set_evaluation_work_limit(xpath_operations_limit.saturating_sub(xpath_operations));
let (extension_operations, extension_operations_limit) =
meter.usage(BudgetKind::ExtensionOperations)?;
context.set_extension_work_limit(
extension_operations_limit.saturating_sub(extension_operations),
);
for (prefix, uri) in expression.namespaces.iter() {
context.set_namespace(prefix, uri);
}
context.set_namespace("xml", "http://www.w3.org/XML/1998/namespace");
if let Some((prefix, namespace)) = &context_namespace {
context.set_namespace(prefix, namespace);
context.set_variable((namespace.as_ref(), "position"), position as f64);
context.set_variable((namespace.as_ref(), "last"), size as f64);
}
if uses_current {
context.set_function(
"current",
CurrentNode {
path: context_path.clone(),
},
);
}
let function_namespaces = Arc::clone(&expression.namespaces);
context.set_function(
"key",
KeyFunction {
index: Rc::clone(&self.key_index),
namespaces: Arc::clone(&function_namespaces),
declarations: Arc::clone(&self.key_declarations),
name_indices: Arc::clone(&self.key_name_indices),
ready: Rc::clone(&self.ready_key_indexes),
pending: Rc::clone(&self.pending_dynamic_key_request),
},
);
context.set_function(
"format-number",
FormatNumberFunction {
formats: Arc::clone(&self.decimal_formats),
namespaces: Arc::clone(&function_namespaces),
},
);
context.set_function(
"generate-id",
GenerateId {
assigned: self.generated_ids.clone(),
},
);
context.set_function(
"id",
IdFunction {
nodes_by_document: Rc::clone(&self.id_index),
},
);
context.set_function("name", NodeNameFunction::Qualified);
context.set_function("local-name", NodeNameFunction::Local);
context.set_function("namespace-uri", NodeNameFunction::NamespaceUri);
context.set_function(
"system-property",
SystemProperty {
namespaces: Arc::clone(&function_namespaces),
},
);
context.set_function(
"element-available",
ElementAvailable {
namespaces: Arc::clone(&function_namespaces),
},
);
context.set_function(
"unparsed-entity-uri",
UnparsedEntityUriFunction {
documents: Rc::clone(&self.unparsed_entity_index),
},
);
context.set_function("lang", LangFunction);
register_exslt_functions(&mut context, self.clock.as_ref().map(Arc::clone));
context.set_function(
"function-available",
FunctionAvailable {
namespaces: function_namespaces,
stylesheet_functions: Arc::clone(&self.stylesheet_functions),
},
);
context.set_function(
"document",
DocumentFunction {
resolver: self.resolver.as_ref(),
access_error: &document_access_error,
static_base_uri: expression.static_base_uri.clone(),
static_document: expression.stylesheet_document,
roots: Rc::clone(&self.document_roots),
pending: Rc::clone(&self.pending_document_requests),
node_base_uris: Rc::clone(&self.node_base_uris),
},
);
if let Some(custom_calls) = custom_calls {
for name in self.stylesheet_functions.iter() {
let qname: sxd_xpath_no_unsafe::OwnedQName = name.namespace.as_deref().map_or_else(
|| name.local.as_str().into(),
|namespace| (namespace, name.local.as_str()).into(),
);
context.set_function(
qname,
DeferredStylesheetFunction {
name: name.clone(),
session: Rc::clone(&custom_calls.state),
},
);
}
}
debug_assert!(
context.function_count()
<= 96usize.saturating_add(if custom_calls.is_some() {
self.stylesheet_functions.len()
} else {
0
}),
"fixed XPath function registrations exceeded their preflighted representation bound"
);
let mut variable_projection_error = false;
for name in expression.variable_references.iter() {
if variable_projection_error {
break;
}
let Some(value) = variables.get(name) else {
continue;
};
let qname: sxd_xpath_no_unsafe::OwnedQName = name.namespace.as_deref().map_or_else(
|| name.local.as_str().into(),
|namespace| (namespace, name.local.as_str()).into(),
);
match value {
Value::Boolean(value) => context.set_variable(qname, *value),
Value::Number(value) => context.set_variable(qname, *value),
Value::String(value) => {
let reservation = context.reserve_temporary_allocation(value.len());
if reservation.is_err() {
variable_projection_error = true;
continue;
}
context.set_variable(qname, value.clone());
}
Value::StoredExpression(value) => {
let reservation = context.reserve_temporary_allocation(value.len());
if reservation.is_err() {
variable_projection_error = true;
continue;
}
context.set_variable(qname, value.clone())
}
Value::ResultTreeFragment(document) => {
let nodes = document.node_count();
for _ in 0..2 {
if context.charge_work(nodes).is_err() {
if let Some(attempted) = ceiling_crossed.get() {
return Err(ceiling_refusal(meter, attempted));
}
let attempted = context
.evaluation_work_exceeded()
.expect("failed XPath work charge records its attempted total");
return Err(Error::Budget {
kind: BudgetKind::XPathOperations,
limit: xpath_operations_limit,
actual: xpath_operations.saturating_add(attempted),
});
}
}
let length = document.string_value_len(document.root());
let reservation = context.reserve_temporary_allocation(length.saturating_add(
std::mem::size_of::<String>() + 2 * std::mem::size_of::<usize>(),
));
if reservation.is_err() {
variable_projection_error = true;
continue;
}
context.set_variable(
qname,
SxdValue::ResultTreeFragment(
result_tree_fragment_handle(document),
Arc::new(document.string_value_with_capacity(document.root(), length)),
),
);
}
Value::NodeSet(nodes) => {
let mut set = nodeset::Nodeset::new();
for reference in nodes {
if let Some(node) = self.maps.to_sxd(document.root().into(), reference) {
if !set.contains(node.clone()) {
let reservation = context
.reserve_temporary_allocation(std::mem::size_of_val(&node));
if reservation.is_err() {
variable_projection_error = true;
break;
}
}
set.add(node);
}
}
context.set_variable(qname, set);
}
}
}
if variable_projection_error {
meter.charge(BudgetKind::XPathOperations, context.evaluation_work_used())?;
let attempted = context
.string_allocation_exceeded()
.expect("failed XPath allocation records its attempted total");
return Err(Error::Budget {
kind: BudgetKind::OwnedBytes,
limit: owned_bytes_limit,
actual: owned_bytes
.saturating_add(adapter_workspace)
.saturating_add(attempted),
});
}
let expressions = self.expressions.borrow();
let xpath = expressions
.get(rewritten.as_ref())
.expect("compiled XPath was inserted into the execution cache");
let generated_id_bytes_before = self.generated_ids.borrow().owned_bytes;
let evaluation = xpath.evaluate(&context, context_node);
meter.charge(BudgetKind::XPathOperations, context.evaluation_work_used())?;
meter.charge(
BudgetKind::ExtensionOperations,
context.extension_work_used(),
)?;
if let Some(error) = document_access_error.borrow_mut().take() {
return Err(error);
}
if let Some(attempted) = ceiling_crossed.get() {
return Err(ceiling_refusal(meter, attempted));
}
if let Some(attempted) = context.evaluation_work_exceeded() {
return Err(Error::Budget {
kind: BudgetKind::XPathOperations,
limit: xpath_operations_limit,
actual: xpath_operations.saturating_add(attempted),
});
}
if let Some(attempted) = context.extension_work_exceeded() {
return Err(Error::Budget {
kind: BudgetKind::ExtensionOperations,
limit: extension_operations_limit,
actual: extension_operations.saturating_add(attempted),
});
}
let generated_id_bytes = self
.generated_ids
.borrow()
.owned_bytes
.saturating_sub(generated_id_bytes_before);
meter.charge(BudgetKind::OwnedBytes, generated_id_bytes)?;
let value = match evaluation {
Ok(value) => value,
Err(error) => {
if let Some(attempted) = context.string_allocation_exceeded() {
return Err(Error::Budget {
kind: BudgetKind::OwnedBytes,
limit: owned_bytes_limit,
actual: owned_bytes
.saturating_add(adapter_workspace)
.saturating_add(attempted),
});
}
let message = error.to_string();
return Err(
if message.starts_with("unknown function") && message.contains("prefix: Some(")
{
Error::Unsupported(format!(
"XPath extension function in `{}`: {message}",
expression.source
))
} else {
Error::Dynamic(format!("XPath `{}` failed: {message}", expression.source))
},
);
}
};
if let SxdValue::ResultTreeFragment(identity, _) = value {
return variables
.result_tree_fragment(identity)
.map(XPathValue::ResultTreeFragment)
.or_else(|| {
custom_calls
.and_then(|session| session.fragment(identity))
.map(XPathValue::ResultTreeFragment)
})
.ok_or_else(|| Error::Dynamic("stale result-tree-fragment identity".into()));
}
let live_context_bytes = context_workspace.saturating_add(context.string_allocation_used());
meter.charge(BudgetKind::OwnedBytes, live_context_bytes)?;
let projected = self.maps.project_value(
&self.source,
self.package.as_document().root().into(),
value,
meter,
);
meter.release_owned_bytes(live_context_bytes);
projected
}
fn prepare_document_requests(
&mut self,
requested: HashSet<DocumentRequest>,
variables: &dyn VariableBindings,
meter: &mut Meter,
) -> Result<()> {
let mut fragments = Vec::new();
let mut fragments_owned_bytes = 0usize;
let result = (|| {
for request in requested {
if self.documents.contains_key(&request) {
continue;
}
let fragment_offset = request.href.find('#').map(|index| index + 1);
let resource_uri = fragment_offset
.map_or(request.href.as_str(), |offset| &request.href[..offset - 1]);
let resource_request = DocumentRequest::relative_to(
resource_uri.to_owned(),
request.base_uri.clone(),
request.empty_document,
);
if !self.documents.contains_key(&resource_request) {
meter.check_additional(BudgetKind::ExternalDocuments, 1)?;
}
let cached_identity = self
.document_roots
.borrow()
.get(&resource_request)
.and_then(|cached| cached.identity.as_ref().map(Arc::clone));
authorize_document_request(
self.resolver.as_ref(),
&request,
cached_identity.as_deref(),
)?;
let (root, identity) = if let Some(nodes) = self.documents.get(&resource_request) {
(nodes.first().cloned(), cached_identity)
} else {
meter.charge(BudgetKind::ExternalDocuments, 1)?;
let resource = match self.resolver.resolve(ResolveRequest::new(
resource_uri,
request.base_uri.as_deref(),
ResolvePurpose::Document,
)) {
Ok(resource) => resource,
Err(Error::ResourceNotFound { .. }) => {
self.cache_document(resource_request.clone(), Vec::new(), None, meter)?;
self.cache_document(request, Vec::new(), None, meter)?;
continue;
}
Err(error) => return Err(error),
};
authorize_document_request(
self.resolver.as_ref(),
&request,
Some(&resource.identity),
)?;
let identity_is_new = match self.resource_identities.get(&resource.identity) {
Some(previous) if previous != &resource => {
return Err(Error::StaleResource {
identity: resource.identity,
});
}
Some(_) => false,
None => true,
};
if let Some((root, identity)) =
self.resource_documents.get(&resource.identity).cloned()
{
self.cache_document(
resource_request.clone(),
vec![root.clone()],
Some(&identity),
meter,
)?;
(Some(root), Some(identity))
} else {
let xml = decode_resource_for_xml_parse(&resource, meter)?;
let (document, parsed_reservation) = parse_external_document_metered(
&xml.value,
Some(&resource.canonical_uri),
meter,
)?;
let (mut document, expanded_reservation) = if self.process_xinclude {
let expanded = expand_xinclude_document(
&document,
&XIncludeDocumentIdentity::External(resource.identity.clone()),
self.resolver.as_ref(),
meter,
&mut self.resource_identities,
1,
None,
)?;
(expanded.document, Some(expanded.retained_owned_bytes))
} else {
(document, None)
};
if let Some((remap, remap_owned_bytes)) =
apply_source_whitespace_rules(&mut document, &self.whitespace, meter)?
{
drop(remap);
meter.release_owned_bytes(remap_owned_bytes);
}
let root = self.import_document(&document, meter)?;
meter.release_owned_bytes(parsed_reservation);
if let Some(reservation) = expanded_reservation {
meter.release_owned_bytes(reservation);
}
meter.release_owned_bytes(xml.temporary_bytes);
let resource_identity = if identity_is_new {
cache_new_resource_identity(
&mut self.resource_identities,
resource,
meter,
)?
} else {
let resource_identity = resource.identity.clone();
meter.release_owned_bytes(resource.bytes.capacity());
resource_identity
};
meter.charge(
BudgetKind::OwnedBytes,
resource_document_cache_entry_owned_bytes(&resource_identity),
)?;
meter.charge(
BudgetKind::OwnedBytes,
resource_identity.0.len().saturating_add(
std::mem::size_of::<ResourceIdentity>()
+ 2 * std::mem::size_of::<usize>(),
),
)?;
let identity = Arc::new(resource_identity.clone());
self.resource_documents
.insert(resource_identity, (root.clone(), Arc::clone(&identity)));
self.cache_document(
resource_request.clone(),
vec![root.clone()],
Some(&identity),
meter,
)?;
(Some(root), Some(identity))
}
};
let Some(root) = root else {
self.cache_document(request, Vec::new(), identity.as_ref(), meter)?;
continue;
};
if let Some(fragment_offset) = fragment_offset {
reserve_temporary_vec_slot(&mut fragments, meter, &mut fragments_owned_bytes)?;
fragments.push((request, root, fragment_offset, identity));
} else if request != resource_request {
self.cache_document(request, vec![root], identity.as_ref(), meter)?;
}
}
for (request, root, fragment_offset, identity) in fragments.drain(..) {
let raw_fragment = &request.href[fragment_offset..];
let (fragment, reserved_bytes) = decode_document_fragment(raw_fragment, meter)?;
let Some(expression) = fragment
.strip_prefix("xpointer(")
.and_then(|fragment| fragment.strip_suffix(')'))
else {
if !is_ncname(&fragment) {
meter.release_owned_bytes(reserved_bytes);
return Err(Error::Unsupported(format!(
"document fragment `{fragment}`"
)));
}
let SourceNode::Node(logical_root) = root else {
meter.release_owned_bytes(reserved_bytes);
return Err(Error::Dynamic(
"document fragment root is not a document node".into(),
));
};
let selected = self
.source
.ids()
.find_map(|(value, root, owner)| {
(root == logical_root && value == fragment)
.then_some(SourceNode::Node(owner))
})
.into_iter()
.collect();
meter.release_owned_bytes(reserved_bytes);
self.cache_document(request, selected, identity.as_ref(), meter)?;
continue;
};
let selected = self.evaluate_core(
&Expression::generated(expression, Vec::new()),
&root,
1,
1,
variables,
meter,
None,
);
meter.release_owned_bytes(reserved_bytes);
let selected = selected?;
let XPathValue::NodeSet(nodes) = selected else {
return Err(Error::Dynamic(format!(
"document fragment `{fragment}` did not select nodes"
)));
};
self.cache_document(request, nodes, identity.as_ref(), meter)?;
}
Ok(())
})();
drop(fragments);
meter.release_owned_bytes(fragments_owned_bytes);
result
}
pub(crate) fn matches(
&mut self,
pattern: &Pattern,
node: &SourceNode,
variables: &HashMap<ExpandedName, Value>,
meter: &mut Meter,
) -> Result<bool> {
let source = trim_xml_whitespace(&pattern.source);
if source == "/" {
return Ok(self
.source
.logical_root_for(node)
.is_some_and(|root| matches!(node, SourceNode::Node(id) if *id == root)));
}
let logical_root = self
.source
.logical_root_for(node)
.ok_or_else(|| Error::Dynamic("template candidate has no logical document".into()))?;
for branch in split_pattern_branches(source) {
let branch = trim_xml_whitespace(branch);
if let Some(matches) =
self.matches_simple_node_pattern(branch, &pattern.namespaces, node)?
{
if matches {
return Ok(true);
}
continue;
}
if let Some(matches) =
self.matches_simple_attribute_pattern(branch, &pattern.namespaces, node)?
{
if matches {
return Ok(true);
}
continue;
}
let expression = if branch.starts_with('/')
|| branch.starts_with("id(")
|| branch.starts_with("key(")
{
branch.to_owned()
} else {
format!("//{branch}")
};
let uses_current = super::expression::has_unprefixed_function_call(branch, "current");
let cache_key = if !branch.contains('$') && !uses_current {
Some(clone_pattern_cache_key(
&expression,
&pattern.namespaces,
logical_root,
meter,
)?)
} else {
None
};
if let Some((_, key_owned_bytes, cached)) =
cache_key.as_ref().and_then(|(key, key_owned_bytes)| {
self.pattern_matches
.get(key)
.map(|cached| (key, *key_owned_bytes, cached))
})
{
let matches = cached.contains(node);
meter.release_owned_bytes(key_owned_bytes);
if matches {
return Ok(true);
}
continue;
}
let root_node = SourceNode::Node(logical_root);
let evaluation_node = if uses_current { node } else { &root_node };
if let Err(error) = meter.charge(BudgetKind::XPathEvaluations, 1) {
if let Some((_, key_owned_bytes)) = cache_key {
meter.release_owned_bytes(key_owned_bytes);
}
return Err(error);
}
let value = match self.evaluate(
&Expression::generated(expression, pattern.namespaces.clone()),
evaluation_node,
1,
1,
variables,
meter,
None,
) {
Ok(value) => value,
Err(error) => {
if let Some((_, key_owned_bytes)) = cache_key {
meter.release_owned_bytes(key_owned_bytes);
}
return Err(error);
}
};
if let XPathValue::NodeSet(nodes) = value {
let matches = nodes.contains(node);
if let Some((cache_key, key_owned_bytes)) = cache_key {
insert_pattern_cache_entry(
&mut self.pattern_matches,
&mut self.pattern_match_index_bytes,
cache_key,
key_owned_bytes,
nodes,
meter,
)?;
}
if matches {
return Ok(true);
}
} else {
if let Some((_, key_owned_bytes)) = cache_key {
meter.release_owned_bytes(key_owned_bytes);
}
return Err(Error::Dynamic(format!(
"template pattern `{branch}` did not select a node-set"
)));
}
}
Ok(false)
}
pub(crate) fn pattern_terminal_rejects(
&self,
pattern: &Pattern,
node: &SourceNode,
) -> Result<bool> {
let mut saw_definitive_branch = false;
for branch in split_pattern_branches(trim_xml_whitespace(&pattern.source)) {
let terminal = terminal_pattern_node_test(branch);
if terminal.is_empty() {
return Ok(false);
}
let candidate = if terminal.starts_with('@') {
self.matches_simple_attribute_pattern(terminal, &pattern.namespaces, node)?
} else {
self.matches_simple_node_pattern(terminal, &pattern.namespaces, node)?
};
match candidate {
Some(true) => return Ok(false),
Some(false) => saw_definitive_branch = true,
None => return Ok(false),
}
}
Ok(saw_definitive_branch)
}
fn matches_simple_node_pattern(
&self,
pattern: &str,
namespaces: &[(String, String)],
node: &SourceNode,
) -> Result<Option<bool>> {
let SourceNode::Node(id) = node else {
return Ok(None);
};
let kind = &self
.source
.node(*id)
.ok_or_else(|| Error::Dynamic("template node candidate is stale".into()))?
.kind;
if let Some(path) = simple_absolute_element_pattern_path(pattern) {
let mut current = Some(*id);
for segment in path.rsplit('/') {
let segment = segment.trim_matches(super::lexical::is_xml_whitespace);
let Some(current_id) = current else {
return Ok(Some(false));
};
let Some(current_node) = self.source.node(current_id) else {
return Err(Error::Dynamic("template node candidate is stale".into()));
};
let NodeKind::Element { name, .. } = ¤t_node.kind else {
return Ok(Some(false));
};
if !element_pattern_name_matches(segment, name, namespaces)? {
return Ok(Some(false));
}
current = current_node.parent;
}
let matches_root = current
.and_then(|parent| self.source.node(parent))
.is_some_and(|node| matches!(node.kind, NodeKind::Root));
return Ok(Some(matches_root));
}
if let Some((lexical, predicate)) = pattern.strip_suffix(']').and_then(|value| {
let (lexical, predicate) = value.split_once("[@")?;
(!predicate.contains(['[', ']'])).then_some((lexical, predicate))
}) && let Some((attribute_name, expected)) = predicate.split_once('=')
&& let attribute_name = trim_xml_whitespace(attribute_name)
&& is_ncname(attribute_name)
&& let Some(expected) = quoted_pattern_literal(trim_xml_whitespace(expected))
{
let NodeKind::Element {
name, attributes, ..
} = kind
else {
return Ok(Some(false));
};
let lexical = lexical.trim_matches(super::lexical::is_xml_whitespace);
return Ok(Some(
(lexical == "*" || element_pattern_name_matches(lexical, name, namespaces)?)
&& attributes.iter().any(|attribute| {
attribute.name.namespace.is_none()
&& attribute.name.local == attribute_name
&& attribute.value == expected
}),
));
}
let result = match pattern {
"node()" => Some(!matches!(kind, NodeKind::Root)),
"text()" => Some(matches!(kind, NodeKind::Text { .. })),
"comment()" => Some(matches!(kind, NodeKind::Comment(_))),
"processing-instruction()" => {
Some(matches!(kind, NodeKind::ProcessingInstruction { .. }))
}
"*" => Some(matches!(kind, NodeKind::Element { .. })),
lexical
if !lexical.contains(['/', '[', '(', ')', '|', '@']) && !lexical.contains("::") =>
{
let NodeKind::Element { name, .. } = kind else {
return Ok(Some(false));
};
Some(element_pattern_name_matches(lexical, name, namespaces)?)
}
_ => None,
};
Ok(result)
}
fn matches_simple_attribute_pattern(
&self,
pattern: &str,
namespaces: &[(String, String)],
node: &SourceNode,
) -> Result<Option<bool>> {
let Some(lexical) = super::lexical::strip_xpath_attribute_axis(pattern) else {
return Ok(None);
};
if lexical.contains(['/', '[', '(', ')']) {
return Ok(None);
}
let SourceNode::Attribute { owner, index } = node else {
return Ok(Some(false));
};
let attribute = self
.source
.node(*owner)
.and_then(|node| match &node.kind {
NodeKind::Element { attributes, .. } => attributes.get(*index),
_ => None,
})
.ok_or_else(|| Error::Dynamic("template attribute candidate is stale".into()))?;
if lexical == "*" {
return Ok(Some(true));
}
if let Some((prefix, local)) = lexical.split_once(':') {
let namespace = namespaces
.iter()
.find(|(candidate, _)| candidate == prefix)
.map(|(_, namespace)| namespace.as_str())
.ok_or_else(|| Error::Static(format!("unbound pattern prefix {prefix}")))?;
return Ok(Some(
attribute.name.namespace.as_deref() == Some(namespace)
&& (local == "*" || attribute.name.local == local),
));
}
Ok(Some(
attribute.name.namespace.is_none() && attribute.name.local == lexical,
))
}
pub(crate) fn children(&self, node: &SourceNode, meter: &mut Meter) -> Result<Vec<SourceNode>> {
let children = match node {
SourceNode::Node(id) => self
.source
.node(*id)
.map(|node| node.children.as_slice())
.unwrap_or_default(),
SourceNode::Attribute { .. } | SourceNode::Namespace { .. } => &[],
};
meter.charge(BudgetKind::XPathOperations, children.len())?;
let mut projected = projected_node_storage(children.len(), meter)?;
projected.extend(children.iter().copied().map(SourceNode::Node));
Ok(projected)
}
pub(crate) fn attributes(
&self,
node: &SourceNode,
meter: &mut Meter,
) -> Result<Vec<SourceNode>> {
let SourceNode::Node(owner) = node else {
return Ok(Vec::new());
};
let count = self
.source
.node(*owner)
.and_then(|node| match &node.kind {
NodeKind::Element { attributes, .. } => Some(attributes.len()),
_ => None,
})
.unwrap_or_default();
meter.charge(BudgetKind::XPathOperations, count)?;
let mut projected = projected_node_storage(count, meter)?;
projected.extend((0..count).map(|index| SourceNode::Attribute {
owner: *owner,
index,
}));
Ok(projected)
}
pub(crate) fn attributes_and_children(
&self,
node: &SourceNode,
meter: &mut Meter,
) -> Result<Vec<SourceNode>> {
let SourceNode::Node(owner) = node else {
return Ok(Vec::new());
};
let Some(source) = self.source.node(*owner) else {
return Ok(Vec::new());
};
let attribute_count = match &source.kind {
NodeKind::Element { attributes, .. } => attributes.len(),
_ => 0,
};
let count = attribute_count.saturating_add(source.children.len());
meter.charge(BudgetKind::XPathOperations, count)?;
let mut projected = projected_node_storage(count, meter)?;
projected.extend((0..attribute_count).map(|index| SourceNode::Attribute {
owner: *owner,
index,
}));
projected.extend(source.children.iter().copied().map(SourceNode::Node));
Ok(projected)
}
pub(crate) fn singleton(
&self,
node: &SourceNode,
include: bool,
meter: &mut Meter,
) -> Result<Vec<SourceNode>> {
meter.charge(BudgetKind::XPathOperations, usize::from(include))?;
let mut projected = projected_node_storage(usize::from(include), meter)?;
if include {
projected.push(node.clone());
}
Ok(projected)
}
pub(crate) fn preceding_nonempty_comment(
&self,
node: &SourceNode,
meter: &mut Meter,
) -> Result<Vec<SourceNode>> {
let SourceNode::Node(id) = node else {
return Ok(Vec::new());
};
let Some(parent) = self.source.node(*id).and_then(|node| node.parent) else {
return Ok(Vec::new());
};
let Some(parent) = self.source.node(parent) else {
return Ok(Vec::new());
};
meter.charge(BudgetKind::XPathOperations, parent.children.len())?;
let Some(position) = parent.children.iter().position(|child| child == id) else {
return Ok(Vec::new());
};
let mut candidate = None;
for id in parent.children[..position].iter().rev().copied() {
meter.charge(BudgetKind::XPathOperations, 1)?;
let mut nonempty = false;
self.visit_string_value_metered(&SourceNode::Node(id), meter, |text| {
if !nonempty {
nonempty = text
.bytes()
.any(|byte| !matches!(byte, b' ' | b'\t' | b'\r' | b'\n'));
}
})?;
if nonempty {
if self
.source
.node(id)
.is_some_and(|node| matches!(node.kind, NodeKind::Comment(_)))
{
candidate = Some(id);
}
break;
}
}
let mut projected = projected_node_storage(usize::from(candidate.is_some()), meter)?;
projected.extend(candidate.map(SourceNode::Node));
Ok(projected)
}
pub(crate) fn select_child_axis(
&self,
expression: &Expression,
node: &SourceNode,
meter: &mut Meter,
) -> Result<Option<(Vec<SourceNode>, usize)>> {
let source = trim_xml_whitespace(&expression.source);
if source.contains('|') && source != "*|text()" {
return Ok(None);
}
let steps = source.split('/');
if source.is_empty()
|| steps.clone().any(|step| {
step.is_empty()
|| (step.contains(['[', ']', '(', ')', '@'])
&& !matches!(step, "text()" | "*|text()"))
|| step.contains("::")
})
{
return Ok(None);
}
if steps
.clone()
.any(|step| !matches!(step, "*" | "text()" | "*|text()") && !is_lexical_qname(step))
{
return Ok(None);
}
for step in steps
.clone()
.filter(|step| !matches!(*step, "*" | "text()" | "*|text()"))
{
pattern_name_namespace(step, &expression.namespaces)?;
}
let mut selected = Vec::new();
let mut selected_bytes = 0;
reserve_temporary_vec_slot(&mut selected, meter, &mut selected_bytes)?;
selected.push(node.clone());
for step in steps {
let mut next = Vec::new();
let mut next_bytes = 0;
for parent in &selected {
let SourceNode::Node(parent) = parent else {
continue;
};
let Some(children) = self
.source
.node(*parent)
.map(|node| node.children.as_slice())
else {
continue;
};
for id in children.iter().copied() {
meter.charge(BudgetKind::XPathOperations, 1)?;
let Some(candidate) = self.source.node(id) else {
continue;
};
let matches = match step {
"*" => matches!(candidate.kind, NodeKind::Element { .. }),
"text()" => matches!(candidate.kind, NodeKind::Text { .. }),
"*|text()" => matches!(
candidate.kind,
NodeKind::Element { .. } | NodeKind::Text { .. }
),
lexical => match &candidate.kind {
NodeKind::Element { name, .. } => {
element_pattern_name_matches(lexical, name, &expression.namespaces)?
}
_ => false,
},
};
if matches {
if let Err(error) =
reserve_temporary_vec_slot(&mut next, meter, &mut next_bytes)
{
meter.release_owned_bytes(selected_bytes);
meter.release_owned_bytes(next_bytes);
return Err(error);
}
next.push(SourceNode::Node(id));
}
}
}
meter.release_owned_bytes(selected_bytes);
selected = next;
selected_bytes = next_bytes;
}
Ok(Some((selected, selected_bytes)))
}
pub(crate) fn materialize_temporary_string_value(
&self,
node: &SourceNode,
meter: &mut Meter,
) -> Result<(String, usize)> {
let mut length = 0usize;
self.visit_string_value_metered(node, meter, |value| {
length = length.saturating_add(value.len());
})?;
meter.charge(BudgetKind::OwnedBytes, length)?;
let mut output = String::with_capacity(length);
let retained = reconcile_owned_string_capacity(meter, length, output.capacity())?;
if let Err(error) = self.visit_string_value_metered(node, meter, |value| {
output.push_str(value);
}) {
meter.release_owned_bytes(retained);
return Err(error);
}
Ok((output, retained))
}
pub(crate) fn visit_string_value(&self, node: &SourceNode, mut visit: impl FnMut(&str)) {
match node {
SourceNode::Node(id) => self.source.visit_string_value(*id, visit),
SourceNode::Attribute { owner, index } => {
if let Some(value) = self.source.node(*owner).and_then(|node| match &node.kind {
NodeKind::Element { attributes, .. } => attributes
.get(*index)
.map(|attribute| attribute.value.as_str()),
_ => None,
}) {
visit(value);
}
}
SourceNode::Namespace { owner, index } => {
if let Some(value) = self.source.node(*owner).and_then(|node| match &node.kind {
NodeKind::Element { namespaces, .. } => namespaces
.get(*index)
.map(|namespace| namespace.uri.as_str()),
_ => None,
}) {
visit(value);
}
}
}
}
pub(crate) fn visit_string_value_metered(
&self,
node: &SourceNode,
meter: &mut Meter,
mut visit: impl FnMut(&str),
) -> Result<()> {
match node {
SourceNode::Node(id) => self.source.try_visit_string_value(
*id,
|work| meter.charge(BudgetKind::XPathOperations, work),
visit,
),
SourceNode::Attribute { .. } | SourceNode::Namespace { .. } => {
meter.charge(BudgetKind::XPathOperations, 1)?;
let mut bytes = 0;
self.visit_string_value(node, |value| bytes = value.len());
meter.charge(BudgetKind::XPathOperations, bytes)?;
self.visit_string_value(node, &mut visit);
Ok(())
}
}
}
pub(crate) fn borrowed_leaf_string_value(&self, node: &SourceNode) -> Option<&str> {
match node {
SourceNode::Node(id) => self.source.node(*id).and_then(|node| match &node.kind {
NodeKind::Text { value, .. } => Some(value.as_str()),
_ => None,
}),
SourceNode::Attribute { owner, index } => {
self.source.node(*owner).and_then(|node| match &node.kind {
NodeKind::Element { attributes, .. } => attributes
.get(*index)
.map(|attribute| attribute.value.as_str()),
_ => None,
})
}
SourceNode::Namespace { owner, index } => {
self.source.node(*owner).and_then(|node| match &node.kind {
NodeKind::Element { namespaces, .. } => namespaces
.get(*index)
.map(|namespace| namespace.uri.as_str()),
_ => None,
})
}
}
}
pub(crate) fn is_text_node(&self, node: &SourceNode) -> bool {
matches!(node, SourceNode::Node(id) if self.source.node(*id).is_some_and(|node| matches!(node.kind, NodeKind::Text { .. })))
}
pub(crate) fn is_element_node(&self, node: &SourceNode) -> bool {
matches!(node, SourceNode::Node(id) if self.source.node(*id).is_some_and(|node| matches!(node.kind, NodeKind::Element { .. })))
}
pub(crate) fn visit_qualified_name(&self, node: &SourceNode, mut visit: impl FnMut(&str)) {
if let SourceNode::Namespace { owner, index } = node {
if let Some(prefix) = self.source.node(*owner).and_then(|node| match &node.kind {
NodeKind::Element { namespaces, .. } => namespaces
.get(*index)
.and_then(|namespace| namespace.prefix.as_deref()),
_ => None,
}) {
visit(prefix);
}
return;
}
let named = match node {
SourceNode::Node(id) => self.source.node(*id).and_then(|node| match &node.kind {
NodeKind::Element { name, prefix, .. } => Some((name, prefix.as_deref())),
_ => None,
}),
SourceNode::Attribute { owner, index } => {
self.source.node(*owner).and_then(|node| match &node.kind {
NodeKind::Element { attributes, .. } => attributes
.get(*index)
.map(|attribute| (&attribute.name, attribute.prefix.as_deref())),
_ => None,
})
}
SourceNode::Namespace { .. } => None,
};
if let SourceNode::Node(id) = node
&& let Some(node) = self.source.node(*id)
&& let NodeKind::ProcessingInstruction { target, .. } = &node.kind
{
visit(target);
return;
}
let Some((name, prefix)) = named else {
return;
};
if let Some(prefix) = prefix {
visit(prefix);
visit(":");
}
visit(&name.local);
}
pub(crate) fn relative_nodes(
&self,
path: &str,
node: &SourceNode,
namespaces: &[(String, String)],
meter: &mut Meter,
) -> Result<Option<(Vec<SourceNode>, usize)>> {
let mut reserved_owned_bytes = 0usize;
let result = (|| {
let mut selected = Vec::new();
reserve_temporary_vec_slot(&mut selected, meter, &mut reserved_owned_bytes)?;
selected.push(node.clone());
for step in path.split('/') {
if step == "." {
continue;
}
meter.charge(BudgetKind::XPathOperations, selected.len())?;
let mut next = Vec::new();
match step {
".." => {
for parent in selected.iter().filter_map(|node| self.parent_node(node)) {
reserve_temporary_vec_slot(
&mut next,
meter,
&mut reserved_owned_bytes,
)?;
next.push(parent);
}
}
"text()" => {
for parent in &selected {
let SourceNode::Node(parent) = parent else {
continue;
};
let Some(parent) = self.source.node(*parent) else {
continue;
};
meter.charge(BudgetKind::XPathOperations, parent.children.len())?;
for child in &parent.children {
let candidate = SourceNode::Node(*child);
if self.is_text_node(&candidate) {
reserve_temporary_vec_slot(
&mut next,
meter,
&mut reserved_owned_bytes,
)?;
next.push(candidate);
}
}
}
}
attribute if attribute.starts_with('@') => {
let lexical = &attribute[1..];
for parent in &selected {
let SourceNode::Node(owner) = parent else {
continue;
};
let owner = *owner;
let attribute_count =
self.source.node(owner).map_or(0, |node| match &node.kind {
NodeKind::Element { attributes, .. } => attributes.len(),
_ => 0,
});
meter.charge(BudgetKind::XPathOperations, attribute_count)?;
for index in 0..attribute_count {
let candidate = SourceNode::Attribute { owner, index };
if self.attribute_name_matches(&candidate, lexical, namespaces)? {
reserve_temporary_vec_slot(
&mut next,
meter,
&mut reserved_owned_bytes,
)?;
next.push(candidate);
}
}
}
}
lexical if is_lexical_qname(lexical) => {
for parent in &selected {
let SourceNode::Node(parent) = parent else {
continue;
};
let Some(parent) = self.source.node(*parent) else {
continue;
};
meter.charge(BudgetKind::XPathOperations, parent.children.len())?;
for child in &parent.children {
let matches = self.source.node(*child).is_some_and(|node| {
matches!(&node.kind, NodeKind::Element { name, .. } if element_pattern_name_matches(lexical, name, namespaces).unwrap_or(false))
});
if matches {
reserve_temporary_vec_slot(
&mut next,
meter,
&mut reserved_owned_bytes,
)?;
next.push(SourceNode::Node(*child));
}
}
}
}
_ => return Ok(None),
}
let retired_bytes = selected
.capacity()
.saturating_mul(std::mem::size_of::<SourceNode>());
drop(selected);
meter.release_owned_bytes(retired_bytes);
reserved_owned_bytes -= retired_bytes;
selected = next;
}
Ok(Some((selected, reserved_owned_bytes)))
})();
if result.is_err() || matches!(result, Ok(None)) {
meter.release_owned_bytes(reserved_owned_bytes);
}
result
}
fn parent_node(&self, node: &SourceNode) -> Option<SourceNode> {
match node {
SourceNode::Node(id) => self.source.node(*id)?.parent.map(SourceNode::Node),
SourceNode::Attribute { owner, .. } | SourceNode::Namespace { owner, .. } => {
self.source.node(*owner).map(|_| SourceNode::Node(*owner))
}
}
}
fn attribute_name_matches(
&self,
node: &SourceNode,
lexical: &str,
namespaces: &[(String, String)],
) -> Result<bool> {
let SourceNode::Attribute { owner, index } = node else {
return Ok(false);
};
let Some(attribute) = self.source.node(*owner).and_then(|node| match &node.kind {
NodeKind::Element { attributes, .. } => attributes.get(*index),
_ => None,
}) else {
return Ok(false);
};
element_pattern_name_matches(lexical, &attribute.name, namespaces)
}
}
fn xpath_adapter_workspace_upper_bound(expression: &Expression) -> usize {
let source = expression.source.as_str();
let source_bytes = source.len();
let normalizes =
source.chars().any(super::lexical::is_xml_whitespace) || source.as_bytes().contains(&b'*');
let normalized_bytes = if normalizes {
source_bytes.saturating_mul(16)
} else {
source_bytes
};
let normalization_peak = if normalizes {
source_bytes.saturating_mul(20)
} else {
0
};
let axis_rewrites = [
"ancestor-or-self::",
"ancestor::",
"following::",
"following-sibling::",
"preceding::",
"preceding-sibling::",
"parent::",
"self::",
]
.into_iter()
.fold(source.matches("..").count(), |count, axis| {
count.saturating_add(source.matches(axis).count())
});
let isolated_bytes = normalized_bytes
.saturating_add(axis_rewrites.saturating_mul(192))
.saturating_mul(2);
let internal_prefix_bytes = expression
.namespaces
.iter()
.map(|(prefix, _)| prefix.len())
.max()
.unwrap_or(0)
.max("__xml_sec_ctx".len())
.saturating_add(1);
let context_calls = source
.matches("position")
.count()
.saturating_add(source.matches("last").count());
let rewritten_bytes = isolated_bytes
.saturating_add(context_calls.saturating_mul(internal_prefix_bytes.saturating_add(2)))
.saturating_mul(2);
let namespace_bytes = expression
.namespaces
.iter()
.fold(0usize, |bytes, (prefix, uri)| {
bytes
.saturating_add(hash_table_entry_bytes::<(String, String)>())
.saturating_add(prefix.len())
.saturating_add(uri.len())
})
.saturating_add(hash_table_entry_bytes::<(String, String)>())
.saturating_add("xml".len())
.saturating_add("http://www.w3.org/XML/1998/namespace".len())
.saturating_add(if context_calls != 0 {
hash_table_entry_bytes::<(String, String)>()
.saturating_add(internal_prefix_bytes)
.saturating_add(CONTEXT_NS.len())
} else {
0
});
normalization_peak.max(
normalized_bytes
.saturating_add(isolated_bytes)
.saturating_add(rewritten_bytes)
.saturating_add(namespace_bytes)
.saturating_add(internal_prefix_bytes.saturating_mul(2)),
)
}
fn xpath_context_workspace_upper_bound(
expression: &Expression,
stylesheet_functions: Option<&HashSet<ExpandedName>>,
) -> usize {
const FIXED_FUNCTION_SLOTS: usize = 96;
const FIXED_FUNCTION_NAME_BYTES: usize = 8 * 1024;
const FUNCTION_OBJECT_BYTES: usize = 128;
let stylesheet_function_count = stylesheet_functions.map_or(0, HashSet::len);
let function_slots = FIXED_FUNCTION_SLOTS.saturating_add(stylesheet_function_count);
let variable_slots = expression.variable_references.len().saturating_add(2);
let namespace_slots = expression.namespaces.len().saturating_add(2);
let dynamic_name_bytes = expression
.variable_references
.iter()
.chain(stylesheet_functions.into_iter().flatten())
.fold(0usize, |bytes, name| {
bytes
.saturating_add(name.namespace.as_deref().map_or(0, str::len))
.saturating_add(name.local.len())
});
let namespace_bytes = expression
.namespaces
.iter()
.fold(0usize, |bytes, (prefix, uri)| {
bytes.saturating_add(prefix.len()).saturating_add(uri.len())
});
function_slots
.saturating_mul(
std::mem::size_of::<(sxd_xpath_no_unsafe::OwnedQName, Box<dyn function::Function>)>()
.saturating_add(std::mem::size_of::<Vec<()>>())
.saturating_mul(2)
.saturating_add(FUNCTION_OBJECT_BYTES),
)
.saturating_add(
variable_slots.saturating_mul(
std::mem::size_of::<(sxd_xpath_no_unsafe::OwnedQName, SxdValue<'static>)>()
.saturating_add(std::mem::size_of::<Vec<()>>())
.saturating_mul(2),
),
)
.saturating_add(
namespace_slots.saturating_mul(hash_table_entry_bytes::<(String, String)>()),
)
.saturating_add(FIXED_FUNCTION_NAME_BYTES)
.saturating_add(dynamic_name_bytes)
.saturating_add(namespace_bytes)
}
fn hash_table_entry_bytes<T>() -> usize {
std::mem::size_of::<T>().saturating_mul(2)
}
fn reserve_stylesheet_imports(
principal_stylesheet: &Document,
module_documents: &[ModuleDocument],
meter: &mut Meter,
) -> Result<()> {
let clone_bytes = module_documents.iter().fold(
principal_stylesheet.estimated_clone_bytes(),
|total, module| total.saturating_add(module.document.estimated_clone_bytes()),
);
meter.charge(BudgetKind::OwnedBytes, clone_bytes)
}
fn import_stylesheet_document(
target: &mut Document,
source: &Document,
whitespace: &[(NameTest, bool, usize, usize)],
meter: &mut Meter,
) -> Result<NodeId> {
let clone_bytes = source.estimated_clone_bytes();
meter.charge(BudgetKind::OwnedBytes, clone_bytes)?;
let mut prepared = source.clone();
if let Some((_, remap_owned_bytes)) =
apply_source_whitespace_rules(&mut prepared, whitespace, meter)?
{
meter.release_owned_bytes(remap_owned_bytes);
}
let root = target.import(&prepared);
meter.release_owned_bytes(clone_bytes);
Ok(root)
}
fn terminal_pattern_node_test(branch: &str) -> &str {
let branch = trim_xml_whitespace(branch);
let mut quote = None;
let mut predicate_depth = 0usize;
let mut parenthesis_depth = 0usize;
let mut terminal_start = 0usize;
let mut terminal_end = branch.len();
for (index, character) in branch.char_indices() {
if let Some(active) = quote {
if character == active {
quote = None;
}
continue;
}
match character {
'\'' | '"' => quote = Some(character),
'[' => {
if predicate_depth == 0 && parenthesis_depth == 0 {
terminal_end = terminal_end.min(index);
}
predicate_depth += 1;
}
']' => predicate_depth = predicate_depth.saturating_sub(1),
'(' if predicate_depth == 0 => parenthesis_depth += 1,
')' if predicate_depth == 0 => parenthesis_depth = parenthesis_depth.saturating_sub(1),
'/' if predicate_depth == 0 && parenthesis_depth == 0 => {
terminal_start = index + 1;
terminal_end = branch.len();
}
_ => {}
}
}
trim_xml_whitespace(&branch[terminal_start..terminal_end])
}
fn simple_absolute_element_pattern_path(pattern: &str) -> Option<&str> {
let remainder = pattern.strip_prefix('/')?;
if remainder.is_empty() || remainder.starts_with('/') {
return None;
}
remainder
.split('/')
.map(|segment| segment.trim_matches(super::lexical::is_xml_whitespace))
.all(|segment| {
!segment.is_empty()
&& !segment.contains(['[', ']', '(', ')', '|', '@', '*'])
&& !segment.contains("::")
})
.then_some(remainder)
}
fn project_semantic_document(source: &Document, meter: &mut Meter) -> Result<Package> {
meter.charge(BudgetKind::OwnedBytes, semantic_projection_size(source))?;
let package = Package::new();
let document = package.as_document();
let documents = document.create_element(QName::new(DOCUMENTS_ELEMENT));
document.root().append_child(documents);
for logical_root in source.logical_roots() {
project_logical_root(source, *logical_root, document, documents, meter)?;
}
Ok(package)
}
fn project_logical_root<'d>(
source: &Document,
logical_root: NodeId,
document: SxdDocument<'d>,
documents: SxdElement<'d>,
meter: &mut Meter,
) -> Result<()> {
let wrapper = document.create_element(QName::new(DOCUMENT_ELEMENT));
documents.append_child(wrapper);
let root = source
.node(logical_root)
.ok_or_else(|| Error::Dynamic("missing logical document root".into()))?;
let mut pending = Vec::new();
let mut pending_reservation = 0usize;
let projected = (|| {
for child in root.children.iter().rev() {
reserve_temporary_vec_slot(&mut pending, meter, &mut pending_reservation)?;
pending.push((*child, wrapper));
}
while let Some((id, parent)) = pending.pop() {
let node = source
.node(id)
.ok_or_else(|| Error::Dynamic(format!("stale semantic node {id:?}")))?;
match &node.kind {
NodeKind::Root => {
for child in node.children.iter().rev() {
reserve_temporary_vec_slot(&mut pending, meter, &mut pending_reservation)?;
pending.push((*child, parent));
}
}
NodeKind::Element {
name,
prefix,
attributes,
namespaces,
} => {
let element = document.create_element(QName::with_namespace_uri(
name.namespace.as_deref(),
&name.local,
));
element.set_preferred_prefix(prefix.as_deref());
for namespace in namespaces.iter() {
if let Some(prefix) = &namespace.prefix {
element.register_prefix(prefix, &namespace.uri);
} else {
element.set_default_namespace_uri(Some(&namespace.uri));
element.register_prefix("", &namespace.uri);
}
}
for attribute in attributes {
let projected = element.set_attribute_value(
QName::with_namespace_uri(
attribute.name.namespace.as_deref(),
&attribute.name.local,
),
&attribute.value,
);
projected.set_preferred_prefix(attribute.prefix.as_deref());
}
parent.append_child(element);
for child in node.children.iter().rev() {
reserve_temporary_vec_slot(&mut pending, meter, &mut pending_reservation)?;
pending.push((*child, element));
}
}
NodeKind::Text { value, .. } => parent.append_child(document.create_text(value)),
NodeKind::Comment(value) => parent.append_child(document.create_comment(value)),
NodeKind::ProcessingInstruction { target, value } => parent
.append_child(document.create_processing_instruction(target, value.as_deref())),
}
}
Ok(())
})();
drop(pending);
meter.release_owned_bytes(pending_reservation);
projected
}
fn semantic_projection_size(source: &Document) -> usize {
let mut requirements = StorageRequirements {
roots: 1,
elements: 1usize.saturating_add(source.logical_roots().len()),
child_edges: 1usize.saturating_add(source.logical_roots().len()),
interned_strings: 1usize.saturating_add(source.logical_roots().len()),
string_bytes: DOCUMENTS_ELEMENT.len().saturating_add(
source
.logical_roots()
.len()
.saturating_mul(DOCUMENT_ELEMENT.len()),
),
..StorageRequirements::default()
};
for (_, node) in source.nodes() {
match &node.kind {
NodeKind::Root => {}
NodeKind::Text { value, .. } => {
requirements.texts = requirements.texts.saturating_add(1);
requirements.child_edges = requirements.child_edges.saturating_add(1);
requirements.interned_strings = requirements.interned_strings.saturating_add(1);
requirements.string_bytes = requirements.string_bytes.saturating_add(value.len());
}
NodeKind::Comment(value) => {
requirements.comments = requirements.comments.saturating_add(1);
requirements.child_edges = requirements.child_edges.saturating_add(1);
requirements.interned_strings = requirements.interned_strings.saturating_add(1);
requirements.string_bytes = requirements.string_bytes.saturating_add(value.len());
}
NodeKind::ProcessingInstruction { target, value } => {
requirements.processing_instructions =
requirements.processing_instructions.saturating_add(1);
requirements.child_edges = requirements.child_edges.saturating_add(1);
requirements.interned_strings = requirements
.interned_strings
.saturating_add(1 + usize::from(value.is_some()));
requirements.string_bytes = requirements
.string_bytes
.saturating_add(target.len())
.saturating_add(value.as_deref().map_or(0, str::len));
}
NodeKind::Element {
name,
prefix,
attributes,
namespaces,
} => {
requirements.elements = requirements.elements.saturating_add(1);
requirements.attributes = requirements.attributes.saturating_add(attributes.len());
requirements.namespace_bindings = requirements
.namespace_bindings
.saturating_add(namespaces.len());
requirements.child_edges = requirements.child_edges.saturating_add(1);
requirements.interned_strings = requirements.interned_strings.saturating_add(
1 + usize::from(name.namespace.is_some()) + usize::from(prefix.is_some()),
);
requirements.string_bytes = requirements
.string_bytes
.saturating_add(name.local.len())
.saturating_add(name.namespace.as_deref().map_or(0, str::len))
.saturating_add(prefix.as_deref().map_or(0, str::len));
for attribute in attributes {
requirements.interned_strings = requirements.interned_strings.saturating_add(
2 + usize::from(attribute.name.namespace.is_some())
+ usize::from(attribute.prefix.is_some()),
);
requirements.string_bytes = requirements
.string_bytes
.saturating_add(attribute.name.local.len())
.saturating_add(attribute.name.namespace.as_deref().map_or(0, str::len))
.saturating_add(attribute.prefix.as_deref().map_or(0, str::len))
.saturating_add(attribute.value.len());
}
for namespace in namespaces.iter() {
requirements.interned_strings = requirements
.interned_strings
.saturating_add(1 + usize::from(namespace.prefix.is_some()));
requirements.string_bytes = requirements
.string_bytes
.saturating_add(namespace.prefix.as_deref().map_or(0, str::len))
.saturating_add(namespace.uri.len());
}
}
}
}
sxd_document_no_unsafe::estimated_storage_bytes(requirements)
}
fn split_pattern_branches(source: &str) -> Vec<&str> {
let mut branches = Vec::new();
let mut start = 0;
let mut depth = 0usize;
let mut quote = None;
for (index, character) in source.char_indices() {
if let Some(active) = quote {
if character == active {
quote = None;
}
continue;
}
match character {
'\'' | '"' => quote = Some(character),
'(' | '[' => depth += 1,
')' | ']' => depth = depth.saturating_sub(1),
'|' if depth == 0 => {
branches.push(&source[start..index]);
start = index + 1;
}
_ => {}
}
}
branches.push(&source[start..]);
branches
}
#[derive(Debug, Clone)]
pub(crate) enum XPathValue {
NodeSet(Vec<SourceNode>),
ResultTreeFragment(Arc<Document>),
Boolean(bool),
Number(f64),
String(String),
StoredExpression(String),
}
impl XPathValue {
pub(crate) fn boolean(&self) -> bool {
match self {
Self::NodeSet(nodes) => !nodes.is_empty(),
Self::ResultTreeFragment(_) => true,
Self::Boolean(value) => *value,
Self::Number(value) => *value != 0.0 && !value.is_nan(),
Self::String(value) => !value.is_empty(),
Self::StoredExpression(value) => !value.is_empty(),
}
}
pub(crate) fn into_temporary_string(
self,
evaluator: &Evaluator,
meter: &mut Meter,
) -> Result<(String, usize)> {
match self {
Self::String(value) | Self::StoredExpression(value) => Ok((value, 0)),
Self::NodeSet(nodes) => {
let Some(node) = nodes.first() else {
return Ok((String::new(), 0));
};
evaluator.materialize_temporary_string_value(node, meter)
}
Self::ResultTreeFragment(document) => temporary_document_string(&document, meter),
Self::Boolean(value) => {
let value = if value { "true" } else { "false" };
meter.charge(BudgetKind::OwnedBytes, value.len())?;
let value = String::from(value);
let retained =
reconcile_owned_string_capacity(meter, value.len(), value.capacity())?;
Ok((value, retained))
}
Self::Number(value) => {
const MAX_XPATH_F64_BYTES: usize = 327;
meter.charge(BudgetKind::OwnedBytes, MAX_XPATH_F64_BYTES)?;
let value = super::value::format_xpath_number(value);
let retained =
reconcile_owned_string_capacity(meter, MAX_XPATH_F64_BYTES, value.capacity())?;
Ok((value, retained))
}
}
}
pub(crate) fn into_fully_metered_temporary_string(
self,
evaluator: &Evaluator,
meter: &mut Meter,
) -> Result<(String, usize)> {
match self {
Self::String(value) | Self::StoredExpression(value) => {
let retained = value.capacity();
meter.charge(BudgetKind::OwnedBytes, retained)?;
Ok((value, retained))
}
value => value.into_temporary_string(evaluator, meter),
}
}
pub(crate) fn into_number(self, evaluator: &Evaluator, meter: &mut Meter) -> Result<f64> {
match self {
Self::Number(value) => Ok(value),
Self::Boolean(true) => Ok(1.0),
Self::Boolean(false) => Ok(0.0),
Self::String(value) | Self::StoredExpression(value) => {
metered_xpath_number(&value, meter)
}
Self::NodeSet(nodes) => {
let Some(node) = nodes.first() else {
return Ok(f64::NAN);
};
if let Some(value) = evaluator.borrowed_leaf_string_value(node) {
return metered_xpath_number(value, meter);
}
let (value, temporary_bytes) =
evaluator.materialize_temporary_string_value(node, meter)?;
let number = metered_xpath_number(&value, meter);
meter.release_owned_bytes(temporary_bytes);
number
}
Self::ResultTreeFragment(document) => {
let (value, temporary_bytes) = temporary_document_string(&document, meter)?;
let number = metered_xpath_number(&value, meter);
meter.release_owned_bytes(temporary_bytes);
number
}
}
}
}
fn temporary_document_string(document: &Document, meter: &mut Meter) -> Result<(String, usize)> {
let mut length = 0usize;
document.try_visit_string_value(
document.root(),
|work| meter.charge(BudgetKind::XPathOperations, work),
|value| length = length.saturating_add(value.len()),
)?;
meter.charge(BudgetKind::OwnedBytes, length)?;
let mut output = String::with_capacity(length);
let retained = reconcile_owned_string_capacity(meter, length, output.capacity())?;
if let Err(error) = document.try_visit_string_value(
document.root(),
|work| meter.charge(BudgetKind::XPathOperations, work),
|value| output.push_str(value),
) {
meter.release_owned_bytes(retained);
return Err(error);
}
Ok((output, retained))
}
fn reconcile_owned_string_capacity(
meter: &mut Meter,
expected: usize,
actual: usize,
) -> Result<usize> {
if actual < expected {
meter.release_owned_bytes(expected - actual);
} else if actual > expected
&& let Err(error) = meter.charge(BudgetKind::OwnedBytes, actual - expected)
{
meter.release_owned_bytes(expected);
return Err(error);
}
Ok(actual)
}
fn metered_xpath_number(value: &str, meter: &mut Meter) -> Result<f64> {
meter.charge(BudgetKind::XPathOperations, value.len())?;
Ok(xpath_number(value))
}
pub(crate) fn xpath_number(value: &str) -> f64 {
parse_xpath_number(value).unwrap_or(f64::NAN)
}
pub(crate) fn parse_xpath_number(value: &str) -> Option<f64> {
let trimmed = value.trim_matches(|c| matches!(c, ' ' | '\t' | '\r' | '\n'));
trimmed.strip_prefix('-').map_or_else(
|| parse_xpath_number_token(trimmed),
|magnitude| parse_xpath_number_token(magnitude).map(|value| -value),
)
}
pub(crate) fn parse_xpath_number_token(value: &str) -> Option<f64> {
let valid = !value.is_empty()
&& value.split_once('.').map_or_else(
|| value.chars().all(|character| character.is_ascii_digit()),
|(integer, fraction)| {
(integer.is_empty() || integer.chars().all(|c| c.is_ascii_digit()))
&& !(integer.is_empty() && fraction.is_empty())
&& fraction.chars().all(|c| c.is_ascii_digit())
},
);
valid.then(|| value.parse().ok()).flatten()
}
const XINCLUDE_NS: &str = "http://www.w3.org/2001/XInclude";
struct ExpandedXIncludeDocument {
document: Document,
principal_mapping: Option<HashMap<NodeId, NodeId>>,
principal_mapping_owned_bytes: usize,
retained_owned_bytes: usize,
retained_namespace_bytes: usize,
}
#[derive(Clone, Copy)]
struct PendingXIncludeNode {
source: NodeId,
target_parent: NodeId,
preserve_context: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum XIncludeDocumentIdentity {
InMemory(usize),
External(ResourceIdentity),
}
#[derive(Debug)]
struct XIncludeChainEntry {
document: XIncludeDocumentIdentity,
document_owned_bytes: usize,
selected_root: Vec<usize>,
selected_root_owned_bytes: usize,
}
impl PartialEq for XIncludeChainEntry {
fn eq(&self, other: &Self) -> bool {
self.document == other.document && self.selected_root == other.selected_root
}
}
impl Eq for XIncludeChainEntry {}
#[derive(Default)]
struct XIncludeChain {
entries: Vec<XIncludeChainEntry>,
reserved_owned_bytes: usize,
}
impl XIncludeChain {
fn contains(&self, document: &XIncludeDocumentIdentity, selected_root: &[usize]) -> bool {
self.entries
.iter()
.any(|entry| &entry.document == document && entry.selected_root == selected_root)
}
fn contains_external(&self, identity: &ResourceIdentity, selected_root: &[usize]) -> bool {
self.entries.iter().any(|entry| {
matches!(
&entry.document,
XIncludeDocumentIdentity::External(candidate) if candidate == identity
) && entry.selected_root == selected_root
})
}
fn push(&mut self, entry: XIncludeChainEntry, meter: &mut Meter) -> Result<()> {
if let Err(error) =
reserve_temporary_vec_slot(&mut self.entries, meter, &mut self.reserved_owned_bytes)
{
meter.release_owned_bytes(
entry
.document_owned_bytes
.saturating_add(entry.selected_root_owned_bytes),
);
return Err(error);
}
self.entries.push(entry);
Ok(())
}
fn pop(&mut self, meter: &mut Meter) {
let entry = self.entries.pop().expect("XInclude chain entry was pushed");
meter.release_owned_bytes(
entry
.document_owned_bytes
.saturating_add(entry.selected_root_owned_bytes),
);
}
}
fn clone_xinclude_identity(
identity: &XIncludeDocumentIdentity,
meter: &mut Meter,
) -> Result<(XIncludeDocumentIdentity, usize)> {
if let XIncludeDocumentIdentity::External(identity) = identity {
let (identity, retained) = clone_resource_identity(identity, meter)?;
return Ok((XIncludeDocumentIdentity::External(identity), retained));
}
Ok((identity.clone(), 0))
}
fn clone_resource_identity(
identity: &ResourceIdentity,
meter: &mut Meter,
) -> Result<(ResourceIdentity, usize)> {
let estimated = identity.0.len();
meter.charge(BudgetKind::OwnedBytes, estimated)?;
let cloned = identity.clone();
let retained = cloned.0.capacity();
if retained > estimated {
if let Err(error) = meter.charge(BudgetKind::OwnedBytes, retained - estimated) {
meter.release_owned_bytes(estimated);
return Err(error);
}
} else {
meter.release_owned_bytes(estimated - retained);
}
Ok((cloned, retained))
}
fn xinclude_selected_path(
document: &Document,
mut selected: NodeId,
meter: &mut Meter,
) -> Result<(Vec<usize>, usize)> {
let mut path = Vec::new();
let mut reserved_owned_bytes = 0usize;
let result = (|| {
while let Some(parent) = document
.node(selected)
.ok_or_else(|| Error::Xml("XInclude selected node is stale".into()))?
.parent
{
let index = document
.node(parent)
.and_then(|node| node.children.iter().position(|child| *child == selected))
.ok_or_else(|| Error::Xml("XInclude selected node has a stale parent".into()))?;
reserve_temporary_vec_slot(&mut path, meter, &mut reserved_owned_bytes)?;
path.push(index);
selected = parent;
}
path.reverse();
Ok(())
})();
if let Err(error) = result {
meter.release_owned_bytes(reserved_owned_bytes);
return Err(error);
}
Ok((path, reserved_owned_bytes))
}
struct XIncludeTraversal<'a> {
source_identity: &'a XIncludeDocumentIdentity,
chain: &'a mut XIncludeChain,
}
fn expand_xinclude_document(
source: &Document,
source_identity: &XIncludeDocumentIdentity,
resolver: &dyn Resolver,
meter: &mut Meter,
identities: &mut HashMap<ResourceIdentity, ResolvedResource>,
depth: usize,
selected_root: Option<NodeId>,
) -> Result<ExpandedXIncludeDocument> {
let mut chain = XIncludeChain::default();
let result = expand_xinclude_document_in_chain(
source,
resolver,
meter,
identities,
XIncludeTraversal {
source_identity,
chain: &mut chain,
},
depth,
selected_root,
);
meter.release_owned_bytes(chain.reserved_owned_bytes);
result
}
fn expand_xinclude_document_in_chain(
source: &Document,
resolver: &dyn Resolver,
meter: &mut Meter,
identities: &mut HashMap<ResourceIdentity, ResolvedResource>,
traversal: XIncludeTraversal<'_>,
depth: usize,
selected_root: Option<NodeId>,
) -> Result<ExpandedXIncludeDocument> {
meter.recursion_with_ceiling(depth, XINCLUDE_RECURSION_DEPTH_CEILING)?;
let (source_nodes, source_clone_bytes) = selected_root.map_or_else(
|| (source.node_count(), source.estimated_clone_bytes()),
|root| source.estimated_subtree_projection(root),
);
let mapping_bytes = xinclude_remap_bytes(source_nodes);
let pending_bytes = xinclude_pending_bytes(source_nodes);
meter.charge(
BudgetKind::OwnedBytes,
source_clone_bytes
.saturating_add(mapping_bytes)
.saturating_add(pending_bytes),
)?;
let mut retained_owned_bytes = source_clone_bytes.saturating_add(mapping_bytes);
let mut retained_namespace_bytes = 0usize;
let base_uri = source
.node(source.root())
.and_then(|node| node.base_uri.clone());
let mut output = Document::empty(base_uri);
let mut principal_mapping = HashMap::with_capacity(source_nodes);
principal_mapping.insert(source.root(), output.root());
let mut pending = Vec::with_capacity(source_nodes.saturating_sub(1));
if let Some(selected_root) = selected_root {
pending.push(PendingXIncludeNode {
source: selected_root,
target_parent: output.root(),
preserve_context: false,
});
} else {
pending.extend(
source
.node(source.root())
.into_iter()
.flat_map(|root| root.children.iter().rev())
.map(|child| PendingXIncludeNode {
source: *child,
target_parent: output.root(),
preserve_context: false,
}),
);
}
while let Some(PendingXIncludeNode {
source: source_id,
target_parent,
preserve_context,
}) = pending.pop()
{
let node = source
.node(source_id)
.ok_or_else(|| Error::Xml("XInclude source node is stale".into()))?;
let is_include = matches!(
&node.kind,
NodeKind::Element { name, .. }
if name.namespace.as_deref() == Some(XINCLUDE_NS) && name.local == "include"
);
if matches!(
&node.kind,
NodeKind::Element { name, .. }
if name.namespace.as_deref() == Some(XINCLUDE_NS) && name.local == "fallback"
) {
return Err(Error::Xml(
"xi:fallback must be a direct child of xi:include".into(),
));
}
if !is_include {
let Some(target) = output.append_node_from(target_parent, node) else {
continue;
};
if preserve_context {
preserve_xinclude_context(
source,
source_id,
&mut output,
target_parent,
target,
meter,
)?
.apply(&mut retained_owned_bytes);
}
principal_mapping.insert(source_id, target);
pending.extend(node.children.iter().rev().map(|child| PendingXIncludeNode {
source: *child,
target_parent: target,
preserve_context: false,
}));
continue;
}
let fallback = validate_xinclude_children(source, node)?;
let result = resolve_xinclude(
source,
source_id,
resolver,
meter,
identities,
XIncludeTraversal {
source_identity: traversal.source_identity,
chain: traversal.chain,
},
depth,
);
match result {
Ok(XIncludeContent::Xml(included)) => {
let included_nodes = included.document.node_count();
let copied_bytes = included.document.estimated_clone_bytes();
let remap_bytes = xinclude_remap_bytes(included_nodes);
meter.charge(
BudgetKind::OwnedBytes,
copied_bytes.saturating_add(remap_bytes),
)?;
let mut included_mapping = HashMap::with_capacity(included_nodes);
if let Some(root) = included.document.node(included.document.root()) {
for child in root.children.iter().copied() {
let target = output.append_subtree_from(
target_parent,
&included.document,
child,
&mut included_mapping,
meter,
)?;
preserve_xinclude_context(
&included.document,
child,
&mut output,
target_parent,
target,
meter,
)?
.apply(&mut retained_owned_bytes);
}
}
output.remap_ids_from(&included.document, &included_mapping)?;
output.merge_unparsed_entities_from(&included.document, output.root())?;
let disposable_bytes = included
.retained_owned_bytes
.saturating_sub(included.retained_namespace_bytes);
meter.release_owned_bytes(remap_bytes.saturating_add(disposable_bytes));
retained_owned_bytes = retained_owned_bytes
.saturating_add(copied_bytes)
.saturating_add(included.retained_namespace_bytes);
retained_namespace_bytes =
retained_namespace_bytes.saturating_add(included.retained_namespace_bytes);
}
Ok(XIncludeContent::Text {
value,
value_reservation,
base_uri,
base_uri_reservation,
}) => {
retained_owned_bytes =
retained_owned_bytes.saturating_add(output.push_precharged_text_coalesced(
target_parent,
value,
value_reservation,
base_uri,
base_uri_reservation,
meter,
)?);
}
Err(XIncludeFailure::Fatal(error)) => return Err(error),
Err(XIncludeFailure::Resource(error)) => {
let Some(fallback) = fallback else {
return Err(error);
};
if let Some(fallback) = source.node(fallback) {
pending.extend(fallback.children.iter().rev().map(|child| {
PendingXIncludeNode {
source: *child,
target_parent,
preserve_context: true,
}
}));
}
}
}
}
output.remap_ids_from(source, &principal_mapping)?;
output.merge_unparsed_entities_from(source, output.root())?;
meter.release_owned_bytes(pending_bytes);
Ok(ExpandedXIncludeDocument {
document: output,
principal_mapping: Some(principal_mapping),
principal_mapping_owned_bytes: mapping_bytes,
retained_owned_bytes,
retained_namespace_bytes,
})
}
fn effective_xml_language(document: &Document, mut id: NodeId) -> Option<&str> {
loop {
let node = document.node(id)?;
if let NodeKind::Element { attributes, .. } = &node.kind
&& let Some(language) = attributes.iter().find(|attribute| {
attribute.name.namespace.as_deref() == Some("http://www.w3.org/XML/1998/namespace")
&& attribute.name.local == "lang"
})
{
return Some(language.value.as_str());
}
id = node.parent?;
}
}
#[derive(Default)]
struct RetainedBytesDelta {
added: usize,
removed: usize,
}
impl RetainedBytesDelta {
fn apply(self, retained: &mut usize) {
*retained = retained
.checked_sub(self.removed)
.and_then(|value| value.checked_add(self.added))
.expect("XInclude retained-byte delta matches live cloned storage");
}
}
fn preserve_xinclude_context(
source: &Document,
source_id: NodeId,
output: &mut Document,
target_parent: NodeId,
target: NodeId,
meter: &mut Meter,
) -> Result<RetainedBytesDelta> {
let mut delta =
preserve_xinclude_base(source, source_id, output, target_parent, target, meter)?;
delta.added = delta.added.saturating_add(preserve_xinclude_language(
source,
source_id,
output,
target_parent,
target,
meter,
)?);
Ok(delta)
}
fn preserve_xinclude_base(
source: &Document,
source_id: NodeId,
output: &mut Document,
target_parent: NodeId,
target: NodeId,
meter: &mut Meter,
) -> Result<RetainedBytesDelta> {
if !matches!(
output.node(target).map(|node| &node.kind),
Some(NodeKind::Element { .. })
) {
return Ok(RetainedBytesDelta::default());
}
let Some(acquired_base) = source
.node(source_id)
.and_then(|node| node.base_uri.as_deref())
else {
return Ok(RetainedBytesDelta::default());
};
let parent_base = output
.node(target_parent)
.and_then(|node| node.base_uri.as_deref());
if parent_base == Some(acquired_base) {
return Ok(RetainedBytesDelta::default());
}
let existing = output.node(target).and_then(|node| match &node.kind {
NodeKind::Element { attributes, .. } => attributes.iter().position(|attribute| {
attribute.name.namespace.as_deref() == Some("http://www.w3.org/XML/1998/namespace")
&& attribute.name.local == "base"
}),
_ => None,
});
if let Some(index) = existing {
meter.charge(BudgetKind::OwnedBytes, acquired_base.len())?;
let value = acquired_base.to_owned();
let node = output
.node_mut(target)
.expect("XInclude fixup target remains present");
let NodeKind::Element { attributes, .. } = &mut node.kind else {
unreachable!("XInclude base fixup target was checked as an element");
};
let retired = std::mem::replace(&mut attributes[index].value, value);
let removed = retired.len();
drop(retired);
meter.release_owned_bytes(removed);
return Ok(RetainedBytesDelta {
added: acquired_base.len(),
removed,
});
}
let bytes = std::mem::size_of::<Attribute>()
.saturating_add("http://www.w3.org/XML/1998/namespace".len())
.saturating_add("base".len())
.saturating_add("xml".len())
.saturating_add(acquired_base.len());
meter.charge(BudgetKind::OwnedBytes, bytes)?;
output.add_default_attribute(
target,
Attribute {
name: ExpandedName::new(Some("http://www.w3.org/XML/1998/namespace"), "base"),
prefix: Some("xml".into()),
value: acquired_base.to_owned(),
},
)?;
Ok(RetainedBytesDelta {
added: bytes,
removed: 0,
})
}
fn preserve_xinclude_language(
source: &Document,
source_id: NodeId,
output: &mut Document,
target_parent: NodeId,
target: NodeId,
meter: &mut Meter,
) -> Result<usize> {
let explicit_language = output.node(target).and_then(|node| match &node.kind {
NodeKind::Element { attributes, .. } => attributes.iter().find(|attribute| {
attribute.name.namespace.as_deref() == Some("http://www.w3.org/XML/1998/namespace")
&& attribute.name.local == "lang"
}),
_ => None,
});
if explicit_language.is_some() {
return Ok(0);
}
if !matches!(
output.node(target).map(|node| &node.kind),
Some(NodeKind::Element { .. })
) {
return Ok(0);
}
let original = effective_xml_language(source, source_id);
if original == effective_xml_language(output, target_parent) {
return Ok(0);
}
let value = original.unwrap_or_default();
let bytes = std::mem::size_of::<Attribute>()
.saturating_add("http://www.w3.org/XML/1998/namespace".len())
.saturating_add("lang".len())
.saturating_add("xml".len())
.saturating_add(value.len());
meter.charge(BudgetKind::OwnedBytes, bytes)?;
let value = value.to_owned();
output.add_default_attribute(
target,
Attribute {
name: ExpandedName::new(Some("http://www.w3.org/XML/1998/namespace"), "lang"),
prefix: Some("xml".into()),
value,
},
)?;
Ok(bytes)
}
fn validate_xinclude_children(source: &Document, include: &Node) -> Result<Option<NodeId>> {
let mut fallback = None;
for child in &include.children {
let Some(Node {
kind: NodeKind::Element { name, .. },
..
}) = source.node(*child)
else {
continue;
};
if name.namespace.as_deref() == Some(XINCLUDE_NS) {
if name.local != "fallback" {
return Err(Error::Xml(format!(
"XInclude namespace child xi:{} is not permitted in xi:include",
name.local
)));
}
if fallback.replace(*child).is_some() {
return Err(Error::Xml(
"XInclude xi:include permits at most one xi:fallback child".into(),
));
}
continue;
}
}
Ok(fallback)
}
fn select_xpointer_element(document: &Document, pointer: &str) -> Result<Option<NodeId>> {
let pointer = pointer.trim_matches(is_xml_whitespace);
if is_ncname(pointer) {
return Ok(xpointer_id(document, pointer));
}
let bytes = pointer.as_bytes();
let mut cursor = 0usize;
let mut saw_part = false;
while cursor < bytes.len() {
while bytes
.get(cursor)
.is_some_and(|byte| byte_is_xml_whitespace(*byte))
{
cursor += 1;
}
if cursor == bytes.len() {
break;
}
let scheme_start = cursor;
while bytes.get(cursor).is_some_and(|byte| *byte != b'(') {
if byte_is_xml_whitespace(bytes[cursor]) {
return Err(Error::Unsupported(
"XInclude xpointer has whitespace before scheme data".into(),
));
}
cursor += 1;
}
let scheme = pointer.get(scheme_start..cursor).ok_or_else(|| {
Error::Unsupported("XInclude xpointer scheme boundary is invalid UTF-8".into())
})?;
if cursor == bytes.len() || !is_xpointer_scheme_name(scheme) {
return Err(Error::Unsupported(
"XInclude xpointer has an invalid scheme name".into(),
));
}
cursor += 1;
let data_start = cursor;
let mut nested = 0usize;
let data_end = loop {
let byte = *bytes.get(cursor).ok_or_else(|| {
Error::Unsupported("XInclude xpointer has unbalanced scheme data".into())
})?;
match byte {
b'^' => {
let escaped = *bytes.get(cursor + 1).ok_or_else(|| {
Error::Unsupported("XInclude xpointer ends with an escape".into())
})?;
if !matches!(escaped, b'^' | b'(' | b')') {
return Err(Error::Unsupported(
"XInclude xpointer has an invalid scheme-data escape".into(),
));
}
cursor += 2;
}
b'(' => {
nested += 1;
cursor += 1;
}
b')' if nested == 0 => break cursor,
b')' => {
nested -= 1;
cursor += 1;
}
_ => cursor += 1,
}
};
let data = pointer.get(data_start..data_end).ok_or_else(|| {
Error::Unsupported("XInclude xpointer data boundary is invalid UTF-8".into())
})?;
cursor += 1;
saw_part = true;
if scheme == "element"
&& let Some(selected) = select_xpointer_element_part(document, data)
{
return Ok(Some(selected));
}
}
if !saw_part {
return Err(Error::Unsupported("XInclude xpointer is empty".into()));
}
Ok(None)
}
fn select_xpointer_element_part(document: &Document, data: &str) -> Option<NodeId> {
let (mut current, mut sequence) = if data.starts_with('/') {
(document.root(), data)
} else {
let (anchor, suffix) = data
.find('/')
.map_or((data, ""), |slash| (&data[..slash], &data[slash..]));
if !is_ncname(anchor) {
return None;
}
(xpointer_id(document, anchor)?, suffix)
};
while !sequence.is_empty() {
sequence = sequence.strip_prefix('/')?;
let end = sequence.find('/').unwrap_or(sequence.len());
let step = sequence.get(..end)?;
if step.starts_with('0') || !step.bytes().all(|byte| byte.is_ascii_digit()) {
return None;
}
let position = step.parse::<usize>().ok()?;
current = document
.node(current)?
.children
.iter()
.filter(|child| {
matches!(
document.node(**child).map(|node| &node.kind),
Some(NodeKind::Element { .. })
)
})
.nth(position - 1)
.copied()?;
sequence = sequence.get(end..)?;
}
matches!(
document.node(current).map(|node| &node.kind),
Some(NodeKind::Element { .. })
)
.then_some(current)
}
fn xpointer_id(document: &Document, value: &str) -> Option<NodeId> {
document
.ids()
.find(|(candidate, root, _)| *candidate == value && *root == document.root())
.map(|(_, _, owner)| owner)
}
fn is_xpointer_scheme_name(value: &str) -> bool {
value.split_once(':').map_or_else(
|| is_ncname(value),
|(prefix, local)| is_ncname(prefix) && is_ncname(local) && !local.contains(':'),
)
}
const fn byte_is_xml_whitespace(byte: u8) -> bool {
matches!(byte, b' ' | b'\t' | b'\r' | b'\n')
}
fn decode_document_fragment<'a>(
fragment: &'a str,
meter: &mut Meter,
) -> Result<(Cow<'a, str>, usize)> {
let fragment = ValidatedXPointerFragment::new(fragment)?;
let Some(decoded_len) = fragment.decoded_len() else {
return Ok((Cow::Borrowed(fragment.source()), 0));
};
meter.charge(BudgetKind::OwnedBytes, decoded_len)?;
let mut decoded = Vec::with_capacity(decoded_len);
fragment.write_decoded_bytes(&mut decoded);
let decoded = String::from_utf8(decoded).map_err(|_| {
meter.release_owned_bytes(decoded_len);
Error::Unsupported("validated URI fragment became invalid UTF-8".into())
})?;
Ok((Cow::Owned(decoded), decoded_len))
}
fn xinclude_pending_bytes(node_count: usize) -> usize {
node_count.saturating_mul(std::mem::size_of::<PendingXIncludeNode>())
}
fn xinclude_remap_bytes(node_count: usize) -> usize {
node_count.saturating_mul(
std::mem::size_of::<(NodeId, NodeId)>().saturating_add(std::mem::size_of::<usize>()),
)
}
enum XIncludeContent {
Xml(ExpandedXIncludeDocument),
Text {
value: String,
value_reservation: usize,
base_uri: String,
base_uri_reservation: usize,
},
}
enum XIncludeFailure {
Resource(Error),
Fatal(Error),
}
fn resolve_xinclude(
source: &Document,
include: NodeId,
resolver: &dyn Resolver,
meter: &mut Meter,
identities: &mut HashMap<ResourceIdentity, ResolvedResource>,
traversal: XIncludeTraversal<'_>,
depth: usize,
) -> std::result::Result<XIncludeContent, XIncludeFailure> {
let node = source
.node(include)
.ok_or_else(|| XIncludeFailure::Fatal(Error::Xml("XInclude node is stale".into())))?;
let attributes = match &node.kind {
NodeKind::Element { attributes, .. } => attributes,
_ => {
return Err(XIncludeFailure::Fatal(Error::Xml(
"XInclude node is not an element".into(),
)));
}
};
let attribute = |local: &str| {
attributes
.iter()
.find(|attribute| attribute.name.namespace.is_none() && attribute.name.local == local)
.map(|attribute| attribute.value.as_str())
};
let href_attribute = attribute("href");
for name in ["accept", "accept-language"] {
if let Some(value) = attribute(name) {
meter
.charge(BudgetKind::XPathOperations, value.len())
.map_err(XIncludeFailure::Fatal)?;
if !value.bytes().all(|byte| (0x20..=0x7e).contains(&byte)) {
return Err(XIncludeFailure::Fatal(Error::Xml(format!(
"XInclude {name} must contain only printable ASCII"
))));
}
}
}
let href = href_attribute.unwrap_or_default();
let parse = attribute("parse").unwrap_or("xml");
if !matches!(parse, "xml" | "text") {
return Err(XIncludeFailure::Fatal(Error::Xml(format!(
"unsupported XInclude parse mode {parse}"
))));
}
let xpointer = attribute("xpointer");
let encoding = attribute("encoding");
if parse == "text" && xpointer.is_some() {
return Err(XIncludeFailure::Fatal(Error::Xml(
"XInclude xpointer is not permitted with parse=\"text\"".into(),
)));
}
if href.contains('#') {
return Err(XIncludeFailure::Fatal(Error::Xml(
"XInclude href must not contain a fragment identifier".into(),
)));
}
if href_attribute.is_none() && xpointer.is_none() {
return Err(XIncludeFailure::Fatal(Error::Xml(
"XInclude without href must provide xpointer".into(),
)));
}
if href.is_empty() && parse == "xml" {
let selected_root = match xpointer {
Some(pointer) => select_xpointer_element(source, pointer)
.map_err(XIncludeFailure::Resource)?
.ok_or_else(|| {
XIncludeFailure::Resource(Error::Unsupported(
"XInclude xpointer did not identify an element".into(),
))
})?,
None => source.root(),
};
let (selected_path, selected_path_owned_bytes) =
xinclude_selected_path(source, selected_root, meter).map_err(XIncludeFailure::Fatal)?;
if traversal
.chain
.contains(traversal.source_identity, &selected_path)
{
meter.release_owned_bytes(selected_path_owned_bytes);
return Err(XIncludeFailure::Fatal(Error::Xml(
"XInclude same-document cycle detected".into(),
)));
}
let (document, document_owned_bytes) =
clone_xinclude_identity(traversal.source_identity, meter).map_err(|error| {
meter.release_owned_bytes(selected_path_owned_bytes);
XIncludeFailure::Fatal(error)
})?;
let entry = XIncludeChainEntry {
document,
document_owned_bytes,
selected_root: selected_path,
selected_root_owned_bytes: selected_path_owned_bytes,
};
traversal
.chain
.push(entry, meter)
.map_err(XIncludeFailure::Fatal)?;
let expanded = expand_xinclude_document_in_chain(
source,
resolver,
meter,
identities,
XIncludeTraversal {
source_identity: traversal.source_identity,
chain: traversal.chain,
},
depth.saturating_add(1),
Some(selected_root),
);
traversal.chain.pop(meter);
return expanded
.map(XIncludeContent::Xml)
.map_err(XIncludeFailure::Fatal);
}
meter
.charge(BudgetKind::ExternalDocuments, 1)
.map_err(XIncludeFailure::Fatal)?;
let request = ResolveRequest {
uri: href,
base_uri: node.base_uri.as_deref(),
purpose: ResolvePurpose::XInclude,
accept: attribute("accept"),
accept_language: attribute("accept-language"),
};
resolver
.authorize(super::ResourceAccess::Request(request))
.map_err(XIncludeFailure::Fatal)?;
let resource = resolver.resolve(request).map_err(|error| match error {
Error::ResourceNotFound { .. } | Error::Resolver { .. } => XIncludeFailure::Resource(error),
error => XIncludeFailure::Fatal(error),
})?;
resolver
.authorize(super::ResourceAccess::Retained {
request,
identity: &resource.identity,
})
.map_err(XIncludeFailure::Fatal)?;
let identity_is_new = match identities.get(&resource.identity) {
Some(previous) if previous != &resource => {
return Err(XIncludeFailure::Fatal(Error::StaleResource {
identity: resource.identity,
}));
}
Some(_) => false,
None => true,
};
if parse == "text" {
let external_encoding = resource
.encoding
.as_deref()
.or_else(|| resource.media_type.as_deref().and_then(media_type_charset));
let xml_media_type = external_encoding.is_none()
&& resource
.media_type
.as_deref()
.is_some_and(is_xml_media_type);
let mode = if xml_media_type {
XIncludeParseMode::TextXmlMediaType
} else {
XIncludeParseMode::Text
};
let mut value =
decode_xinclude_resource(&resource, external_encoding.or(encoding), meter, mode)?;
normalize_xinclude_text_line_endings(&mut value, meter).map_err(XIncludeFailure::Fatal)?;
if let Some(character) = value
.value
.chars()
.find(|character| !super::lexical::is_xml10_character(*character))
{
return Err(XIncludeFailure::Fatal(Error::Xml(format!(
"XInclude text resource contains forbidden XML character U+{:04X}",
u32::from(character)
))));
}
let expected_uri_bytes = resource.canonical_uri.len();
meter
.charge(BudgetKind::OwnedBytes, expected_uri_bytes)
.map_err(XIncludeFailure::Fatal)?;
let canonical_uri = resource.canonical_uri.clone();
let canonical_uri_reservation =
reconcile_owned_string_capacity(meter, expected_uri_bytes, canonical_uri.capacity())
.map_err(XIncludeFailure::Fatal)?;
if identity_is_new {
charge_resource_identity_cache_entry(&resource, meter)
.map_err(XIncludeFailure::Fatal)?;
identities.insert(resource.identity.clone(), resource);
} else {
meter.release_owned_bytes(resource.bytes.capacity());
}
return Ok(XIncludeContent::Text {
value: value.value,
value_reservation: value.temporary_bytes,
base_uri: canonical_uri,
base_uri_reservation: canonical_uri_reservation,
});
}
let xml_encoding = resource
.encoding
.as_deref()
.or_else(|| resource.media_type.as_deref().and_then(media_type_charset));
let xml = decode_xinclude_resource(&resource, xml_encoding, meter, XIncludeParseMode::Xml)?;
let (document, parsed_reservation) =
parse_external_document_metered(&xml.value, Some(&resource.canonical_uri), meter)
.map_err(XIncludeFailure::Fatal)?;
let selected_root = if let Some(pointer) = xpointer {
match select_xpointer_element(&document, pointer).and_then(|selected| {
selected.ok_or_else(|| {
Error::Unsupported("XInclude xpointer did not identify an element".into())
})
}) {
Ok(selected) => Some(selected),
Err(error) => {
meter.release_owned_bytes(parsed_reservation);
meter.release_owned_bytes(xml.temporary_bytes);
meter.release_owned_bytes(resource.bytes.capacity());
return Err(XIncludeFailure::Resource(error));
}
}
} else {
None
};
let selected_node = selected_root.unwrap_or_else(|| document.root());
let (selected_path, selected_path_owned_bytes) =
match xinclude_selected_path(&document, selected_node, meter) {
Ok(selected) => selected,
Err(error) => {
meter.release_owned_bytes(parsed_reservation);
meter.release_owned_bytes(xml.temporary_bytes);
meter.release_owned_bytes(resource.bytes.capacity());
return Err(XIncludeFailure::Fatal(error));
}
};
if traversal
.chain
.contains_external(&resource.identity, &selected_path)
{
meter.release_owned_bytes(selected_path_owned_bytes);
meter.release_owned_bytes(parsed_reservation);
meter.release_owned_bytes(xml.temporary_bytes);
meter.release_owned_bytes(resource.bytes.capacity());
return Err(XIncludeFailure::Fatal(Error::Resolver {
uri: resource.canonical_uri,
message: "XInclude cycle detected".into(),
}));
}
let retained_namespace_bytes =
match document.retained_namespace_arena_bytes(selected_root, meter) {
Ok(bytes) => bytes,
Err(error) => {
meter.release_owned_bytes(selected_path_owned_bytes);
meter.release_owned_bytes(parsed_reservation);
meter.release_owned_bytes(xml.temporary_bytes);
meter.release_owned_bytes(resource.bytes.capacity());
return Err(XIncludeFailure::Fatal(error));
}
};
let (resource_identity, resource_identity_owned_bytes) =
match clone_resource_identity(&resource.identity, meter) {
Ok(identity) => identity,
Err(error) => {
meter.release_owned_bytes(selected_path_owned_bytes);
meter.release_owned_bytes(parsed_reservation);
meter.release_owned_bytes(xml.temporary_bytes);
meter.release_owned_bytes(resource.bytes.capacity());
return Err(XIncludeFailure::Fatal(error));
}
};
let external_identity = XIncludeDocumentIdentity::External(resource_identity);
let (document_identity, document_owned_bytes) =
match clone_xinclude_identity(&external_identity, meter) {
Ok(identity) => identity,
Err(error) => {
meter.release_owned_bytes(resource_identity_owned_bytes);
meter.release_owned_bytes(selected_path_owned_bytes);
meter.release_owned_bytes(parsed_reservation);
meter.release_owned_bytes(xml.temporary_bytes);
meter.release_owned_bytes(resource.bytes.capacity());
return Err(XIncludeFailure::Fatal(error));
}
};
let chain_entry = XIncludeChainEntry {
document: document_identity,
document_owned_bytes,
selected_root: selected_path,
selected_root_owned_bytes: selected_path_owned_bytes,
};
if identity_is_new {
if let Err(error) = charge_resource_identity_cache_entry(&resource, meter) {
meter.release_owned_bytes(
resource_identity_owned_bytes
.saturating_add(chain_entry.document_owned_bytes)
.saturating_add(chain_entry.selected_root_owned_bytes),
);
meter.release_owned_bytes(parsed_reservation);
meter.release_owned_bytes(xml.temporary_bytes);
meter.release_owned_bytes(resource.bytes.capacity());
return Err(XIncludeFailure::Fatal(error));
}
let XIncludeDocumentIdentity::External(resource_identity) = &external_identity else {
unreachable!("external XInclude identity")
};
identities.insert(resource_identity.clone(), resource);
} else {
meter.release_owned_bytes(resource.bytes.capacity());
}
if let Err(error) = traversal.chain.push(chain_entry, meter) {
meter.release_owned_bytes(resource_identity_owned_bytes);
meter.release_owned_bytes(parsed_reservation);
meter.release_owned_bytes(xml.temporary_bytes);
return Err(XIncludeFailure::Fatal(error));
}
let expanded = expand_xinclude_document_in_chain(
&document,
resolver,
meter,
identities,
XIncludeTraversal {
source_identity: &external_identity,
chain: traversal.chain,
},
depth.saturating_add(1),
selected_root,
);
traversal.chain.pop(meter);
meter.release_owned_bytes(resource_identity_owned_bytes);
match expanded {
Ok(mut expanded) => {
let transferred_from_parse = retained_namespace_bytes.min(parsed_reservation);
let transferred_from_decode = retained_namespace_bytes - transferred_from_parse;
if transferred_from_decode > xml.temporary_bytes {
meter.release_owned_bytes(parsed_reservation);
meter.release_owned_bytes(xml.temporary_bytes);
return Err(XIncludeFailure::Fatal(Error::Xml(
"XInclude namespace retention exceeds parsed document storage".into(),
)));
}
meter.release_owned_bytes(parsed_reservation - transferred_from_parse);
meter.release_owned_bytes(xml.temporary_bytes - transferred_from_decode);
expanded.retained_owned_bytes = expanded
.retained_owned_bytes
.saturating_add(retained_namespace_bytes);
expanded.retained_namespace_bytes = expanded
.retained_namespace_bytes
.saturating_add(retained_namespace_bytes);
Ok(XIncludeContent::Xml(expanded))
}
Err(error) => {
meter.release_owned_bytes(parsed_reservation);
meter.release_owned_bytes(xml.temporary_bytes);
Err(XIncludeFailure::Fatal(error))
}
}
}
fn normalize_xinclude_text_line_endings(
value: &mut MeteredDecodedResource,
meter: &mut Meter,
) -> Result<()> {
if !value.value.as_bytes().contains(&b'\r') {
return Ok(());
}
let requested = value.value.len();
meter.charge(BudgetKind::OwnedBytes, requested)?;
let mut normalized = String::new();
if let Err(error) = normalized.try_reserve_exact(requested) {
meter.release_owned_bytes(requested);
return Err(Error::Dynamic(format!(
"failed to reserve XInclude line-normalization storage: {error}"
)));
}
reconcile_replacement_growth(meter, requested, normalized.capacity())?;
let mut characters = value.value.chars().peekable();
while let Some(character) = characters.next() {
if character == '\r' {
if characters.peek() == Some(&'\n') {
characters.next();
}
normalized.push('\n');
} else {
normalized.push(character);
}
}
let old_capacity = value.value.capacity();
value.temporary_bytes = value
.temporary_bytes
.saturating_sub(old_capacity)
.saturating_add(normalized.capacity());
value.value = normalized;
meter.release_owned_bytes(old_capacity);
Ok(())
}
fn parse_external_document_metered(
xml: &str,
base_uri: Option<&str>,
meter: &mut Meter,
) -> Result<(Document, usize)> {
let workspace_reservation = parser_workspace_bytes(xml);
meter.charge(BudgetKind::OwnedBytes, workspace_reservation)?;
let structural_node_bytes = std::mem::size_of::<Node>()
.saturating_add(std::mem::size_of::<NodeId>())
.max(1);
let remaining = meter.remaining_owned_bytes();
let budget = ParseBudget::new(
xml.len(),
remaining / structural_node_bytes,
meter.recursion_limit(),
usize::MAX,
remaining,
remaining,
);
let document = match Document::parse_with_budget(xml, base_uri, budget) {
Ok(document) => document,
Err(error) => {
meter.release_owned_bytes(workspace_reservation);
return Err(error);
}
};
let retained_reservation = document.estimated_owned_bytes().saturating_sub(xml.len());
if retained_reservation < workspace_reservation {
meter.release_owned_bytes(workspace_reservation - retained_reservation);
} else if retained_reservation > workspace_reservation
&& let Err(error) = meter.charge(
BudgetKind::OwnedBytes,
retained_reservation - workspace_reservation,
)
{
meter.release_owned_bytes(workspace_reservation);
return Err(error);
}
Ok((document, retained_reservation))
}
#[derive(Clone, Copy)]
enum XIncludeParseMode {
Xml,
Text,
TextXmlMediaType,
}
#[derive(Clone, Copy)]
enum ResourceDecodeMode {
Xml,
Text,
XmlText,
}
fn decode_xinclude_resource(
resource: &ResolvedResource,
encoding: Option<&str>,
meter: &mut Meter,
mode: XIncludeParseMode,
) -> std::result::Result<MeteredDecodedResource, XIncludeFailure> {
let source_owned_bytes = resource.bytes.capacity();
let bytes = resource.bytes.as_slice();
let (bytes, encoding, decode_mode) = match mode {
XIncludeParseMode::Text => {
let (bytes, encoding) = xinclude_text_payload(bytes, encoding);
(bytes, Some(encoding), ResourceDecodeMode::Text)
}
XIncludeParseMode::TextXmlMediaType => (bytes, None, ResourceDecodeMode::XmlText),
XIncludeParseMode::Xml => (bytes, encoding, ResourceDecodeMode::Xml),
};
decode_resource_metered_inner(bytes, source_owned_bytes, encoding, meter, decode_mode).map_err(
|error| match error {
MeteredDecodeError::Budget(error) => XIncludeFailure::Fatal(error),
MeteredDecodeError::Decode(error) => classify_xinclude_decode_error(error, mode),
},
)
}
fn classify_xinclude_decode_error(
error: xml_sec_xml_input::Error,
mode: XIncludeParseMode,
) -> XIncludeFailure {
let unsupported_encoding = matches!(
error,
xml_sec_xml_input::Error::UnsupportedByteEncoding(_)
| xml_sec_xml_input::Error::UnsupportedEncoding(_)
);
let error = Error::Xml(error.to_string());
match (mode, unsupported_encoding) {
(
XIncludeParseMode::Xml | XIncludeParseMode::Text | XIncludeParseMode::TextXmlMediaType,
true,
) => XIncludeFailure::Resource(error),
(
XIncludeParseMode::Xml | XIncludeParseMode::Text | XIncludeParseMode::TextXmlMediaType,
false,
) => XIncludeFailure::Fatal(error),
}
}
fn xinclude_text_payload<'a>(bytes: &'a [u8], encoding: Option<&'a str>) -> (&'a [u8], &'a str) {
let encoding = match encoding {
Some(encoding) => encoding,
None if bytes.starts_with(&[0x00, 0x00, 0xFE, 0xFF]) => "UTF-32",
None if bytes.starts_with(&[0xFF, 0xFE, 0x00, 0x00]) => "UTF-32",
None if bytes.starts_with(&[0xFE, 0xFF]) => "UTF-16",
None if bytes.starts_with(&[0xFF, 0xFE]) => "UTF-16",
None => "UTF-8",
};
if (encoding.eq_ignore_ascii_case("UTF-8") || encoding.eq_ignore_ascii_case("UTF8"))
&& let Some(payload) = bytes.strip_prefix(&[0xEF, 0xBB, 0xBF])
{
return (payload, encoding);
}
if encoding.eq_ignore_ascii_case("UTF-16") || encoding.eq_ignore_ascii_case("UTF16") {
if let Some(payload) = bytes.strip_prefix(&[0xFE, 0xFF]) {
return (payload, "UTF-16BE");
}
if let Some(payload) = bytes.strip_prefix(&[0xFF, 0xFE]) {
return (payload, "UTF-16LE");
}
}
if encoding.eq_ignore_ascii_case("UTF-32") || encoding.eq_ignore_ascii_case("UTF32") {
if let Some(payload) = bytes.strip_prefix(&[0x00, 0x00, 0xFE, 0xFF]) {
return (payload, "UTF-32BE");
}
if let Some(payload) = bytes.strip_prefix(&[0xFF, 0xFE, 0x00, 0x00]) {
return (payload, "UTF-32LE");
}
}
(bytes, encoding)
}
fn media_type_charset(media_type: &str) -> Option<&str> {
media_type.split(';').skip(1).find_map(|parameter| {
let (name, value) = parameter.trim().split_once('=')?;
if !name.trim().eq_ignore_ascii_case("charset") {
return None;
}
let value = value.trim().trim_matches('"');
(!value.is_empty()).then_some(value)
})
}
fn is_xml_media_type(media_type: &str) -> bool {
let kind = media_type.split(';').next().unwrap_or(media_type).trim();
if kind.eq_ignore_ascii_case("text/xml") || kind.eq_ignore_ascii_case("application/xml") {
return true;
}
let Some((type_name, subtype)) = kind.split_once('/') else {
return false;
};
!type_name.is_empty()
&& subtype.len() > 4
&& subtype
.get(subtype.len() - 4..)
.is_some_and(|suffix| suffix.eq_ignore_ascii_case("+xml"))
}
fn decode_resource_for_xml_parse(
resource: &ResolvedResource,
meter: &mut Meter,
) -> Result<MeteredDecodedResource> {
decode_resource_metered(
&resource.bytes,
resource.bytes.capacity(),
resource.encoding.as_deref(),
meter,
true,
)
}
fn hash_entry_storage<K, V>() -> usize {
std::mem::size_of::<(K, V)>().saturating_mul(2)
}
fn resource_identity_cache_entry_owned_bytes(resource: &ResolvedResource) -> usize {
hash_entry_storage::<ResourceIdentity, ResolvedResource>()
.saturating_add(resource.identity.0.len())
.saturating_add(resource.canonical_uri.capacity())
.saturating_add(resource.identity.0.capacity())
.saturating_add(resource.bytes.capacity())
.saturating_add(resource.media_type.as_ref().map_or(0, String::capacity))
.saturating_add(resource.encoding.as_ref().map_or(0, String::capacity))
}
fn charge_resource_identity_cache_entry(
resource: &ResolvedResource,
meter: &mut Meter,
) -> Result<()> {
let additional = resource_identity_cache_entry_owned_bytes(resource)
.saturating_sub(resource.bytes.capacity());
meter.charge(BudgetKind::OwnedBytes, additional)
}
fn cache_new_resource_identity(
identities: &mut HashMap<ResourceIdentity, ResolvedResource>,
resource: ResolvedResource,
meter: &mut Meter,
) -> Result<ResourceIdentity> {
charge_resource_identity_cache_entry(&resource, meter)?;
let identity = resource.identity.clone();
identities.insert(identity.clone(), resource);
Ok(identity)
}
fn resource_document_cache_entry_owned_bytes(identity: &ResourceIdentity) -> usize {
hash_entry_storage::<ResourceIdentity, (SourceNode, Arc<ResourceIdentity>)>()
.saturating_add(identity.0.len())
}
fn decode_resource_metered(
bytes: &[u8],
source_owned_bytes: usize,
encoding: Option<&str>,
meter: &mut Meter,
parsed_xml: bool,
) -> Result<MeteredDecodedResource> {
let mode = if parsed_xml {
ResourceDecodeMode::Xml
} else {
ResourceDecodeMode::Text
};
decode_resource_metered_inner(bytes, source_owned_bytes, encoding, meter, mode).map_err(
|error| match error {
MeteredDecodeError::Budget(error) => error,
MeteredDecodeError::Decode(error) => Error::Xml(error.to_string()),
},
)
}
struct MeteredDecodedResource {
value: String,
temporary_bytes: usize,
}
enum MeteredDecodeError {
Budget(Error),
Decode(xml_sec_xml_input::Error),
}
fn decode_resource_metered_inner(
bytes: &[u8],
source_owned_bytes: usize,
encoding: Option<&str>,
meter: &mut Meter,
mode: ResourceDecodeMode,
) -> std::result::Result<MeteredDecodedResource, MeteredDecodeError> {
let parsed_xml = matches!(mode, ResourceDecodeMode::Xml);
let decoded_copies = if parsed_xml { 2 } else { 1 };
let (used, limit) = meter
.usage(BudgetKind::OwnedBytes)
.map_err(MeteredDecodeError::Budget)?;
debug_assert!(source_owned_bytes >= bytes.len());
let Some(source_total) = used.checked_add(source_owned_bytes) else {
return Err(MeteredDecodeError::Budget(Error::Budget {
kind: BudgetKind::OwnedBytes,
limit,
actual: usize::MAX,
}));
};
if source_total > limit {
return Err(MeteredDecodeError::Budget(Error::Budget {
kind: BudgetKind::OwnedBytes,
limit,
actual: source_total,
}));
}
let available = limit - source_total;
let maximum_decoded = available / decoded_copies;
let decoded = match mode {
ResourceDecodeMode::XmlText => {
xml_sec_xml_input::decode_xml_text_bounded(bytes, encoding, maximum_decoded)
.map(|value| value.into_owned())
}
ResourceDecodeMode::Xml | ResourceDecodeMode::Text => {
decode_resource(bytes, encoding, parsed_xml, maximum_decoded)
}
}
.map_err(|error| match error {
xml_sec_xml_input::Error::DecodedLimit { actual, .. } => {
MeteredDecodeError::Budget(Error::Budget {
kind: BudgetKind::OwnedBytes,
limit,
actual: used
.saturating_add(source_owned_bytes)
.saturating_add(actual.saturating_mul(decoded_copies)),
})
}
error => MeteredDecodeError::Decode(error),
})?;
let parser_owned_bytes = if parsed_xml { decoded.len() } else { 0 };
let temporary_bytes = decoded.capacity().saturating_add(parser_owned_bytes);
let retained = source_owned_bytes.saturating_add(temporary_bytes);
meter
.charge(BudgetKind::OwnedBytes, retained)
.map_err(MeteredDecodeError::Budget)?;
Ok(MeteredDecodedResource {
value: decoded,
temporary_bytes,
})
}
struct NodeMaps {
forward: HashMap<SourceNode, NodePath>,
reverse: HashMap<NodePath, SourceNode>,
order: HashMap<SourceNode, NodeOrder>,
forward_index_bytes: usize,
reverse_index_bytes: usize,
order_index_bytes: usize,
next_order: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
struct NodeOrder(usize, u8, usize);
impl NodeMaps {
fn new(source: &Document, meter: &mut Meter) -> Result<Self> {
let mut maps = Self {
forward: HashMap::new(),
reverse: HashMap::new(),
order: HashMap::new(),
forward_index_bytes: 0,
reverse_index_bytes: 0,
order_index_bytes: 0,
next_order: 0,
};
maps.extend(source, 0, meter)?;
Ok(maps)
}
fn extend(&mut self, source: &Document, first_node: usize, meter: &mut Meter) -> Result<()> {
let MeteredSemanticPaths {
paths,
reserved_owned_bytes,
} = semantic_node_paths_from(source, first_node, meter)?;
let result = (|| {
let order_base = self.next_order;
self.next_order = self.next_order.saturating_add(paths.len());
for (id, node) in source.nodes().skip(first_node) {
let (path, rank) = paths.get(&id).ok_or_else(|| {
Error::Dynamic(format!("semantic node {id:?} has no document path"))
})?;
self.reserve_entry(
path.len().saturating_mul(std::mem::size_of::<usize>()),
meter,
)?;
let key = SourceNode::Node(id);
self.forward
.insert(key.clone(), NodePath::Ordinary(path.to_vec()));
self.reverse
.insert(NodePath::Ordinary(path.to_vec()), key.clone());
let rank = order_base.saturating_add(*rank);
self.order
.insert(key.clone(), NodeOrder(rank.saturating_mul(3), 0, 0));
if let NodeKind::Element {
attributes,
namespaces,
..
} = &node.kind
{
for (index, source_attribute) in attributes.iter().enumerate() {
let path_owned_bytes = path
.len()
.saturating_mul(std::mem::size_of::<usize>())
.saturating_add(
source_attribute
.name
.namespace
.as_deref()
.map_or(0, str::len),
)
.saturating_add(source_attribute.name.local.len());
self.reserve_entry(path_owned_bytes, meter)?;
let attribute_path = NodePath::Attribute {
parent: path.to_vec(),
namespace: source_attribute.name.namespace.clone(),
local: source_attribute.name.local.clone(),
};
let key = SourceNode::Attribute { owner: id, index };
self.forward.insert(key.clone(), attribute_path.clone());
self.reverse.insert(attribute_path, key.clone());
self.order
.insert(key.clone(), NodeOrder(rank.saturating_mul(3), 2, index));
}
for (index, namespace) in namespaces
.iter()
.enumerate()
.filter(|(_, namespace)| xpath_namespace_is_visible(namespace))
{
let path_owned_bytes = path
.len()
.saturating_mul(std::mem::size_of::<usize>())
.saturating_add(namespace.prefix.as_deref().map_or(0, str::len))
.saturating_add(namespace.uri.len());
self.reserve_entry(path_owned_bytes, meter)?;
let namespace_path = NodePath::Namespace {
parent: path.to_vec(),
prefix: namespace.prefix.clone().unwrap_or_default(),
uri: namespace.uri.clone(),
};
let key = SourceNode::Namespace { owner: id, index };
self.forward.insert(key.clone(), namespace_path.clone());
self.reverse.insert(namespace_path, key.clone());
let namespace_order = if namespace.prefix.as_deref() == Some("xml") {
0
} else {
namespaces.len().saturating_sub(index).saturating_add(1)
};
self.order.insert(
key.clone(),
NodeOrder(rank.saturating_mul(3), 1, namespace_order),
);
}
}
}
Ok(())
})();
drop(paths);
meter.release_owned_bytes(reserved_owned_bytes);
result
}
fn reserve_entry(&mut self, path_owned_bytes: usize, meter: &mut Meter) -> Result<()> {
reserve_retained_hash_map_slot(&mut self.forward, meter, &mut self.forward_index_bytes)?;
reserve_retained_hash_map_slot(&mut self.reverse, meter, &mut self.reverse_index_bytes)?;
reserve_retained_hash_map_slot(&mut self.order, meter, &mut self.order_index_bytes)?;
meter.charge(BudgetKind::OwnedBytes, path_owned_bytes.saturating_mul(2))
}
fn to_sxd<'d>(&self, root: nodeset::Node<'d>, node: &SourceNode) -> Option<nodeset::Node<'d>> {
self.resolve(root, self.forward.get(node)?)
}
fn resolve<'d>(&self, root: nodeset::Node<'d>, path: &NodePath) -> Option<nodeset::Node<'d>> {
let mut node = root;
for index in path.ordinary() {
node = node.child_at(*index)?;
}
match path {
NodePath::Ordinary(_) => Some(node),
NodePath::Attribute {
namespace, local, ..
} => resolve_attribute_node(node, namespace.as_deref(), local),
NodePath::Namespace { prefix, uri, .. } => resolve_namespace_node(node, prefix, uri),
}
}
fn project_value(
&self,
source: &Document,
root: nodeset::Node<'_>,
value: SxdValue<'_>,
meter: &mut Meter,
) -> Result<XPathValue> {
Ok(match value {
SxdValue::Boolean(value) => XPathValue::Boolean(value),
SxdValue::Number(value) => XPathValue::Number(value),
SxdValue::String(value) => XPathValue::String(value),
#[cfg(feature = "embedded")]
SxdValue::AccountedString(text) => {
let mut value = String::new();
super::budget::append_metered_string(&mut value, text.as_str(), meter)?;
XPathValue::String(value)
}
SxdValue::ResultTreeFragment(_, _) => {
return Err(Error::Dynamic(
"result-tree fragment escaped without its owning document".into(),
));
}
SxdValue::Nodeset(mut nodes) => {
let node_count = nodes.size();
if node_count <= 64 {
let mut projected = projected_node_storage(node_count, meter)?;
for node in nodes {
let (path, path_bytes) = typed_path_to_metered(&node, meter)?;
if let Some(source) = self.reverse.get(&path) {
projected.push(source.clone());
}
meter.release_owned_bytes(path_bytes);
}
projected.sort_by_key(|node| self.order.get(node).copied());
return Ok(XPathValue::NodeSet(projected));
}
let mut projected = Vec::new();
let mut traversal = Vec::<(usize, Option<NodeId>)>::new();
let mut temporary_bytes = 0usize;
let result = (|| {
let mut node = root.clone();
let mut source_id = None;
loop {
meter.charge(BudgetKind::XPathOperations, 1)?;
if nodes.remove(&node)
&& let Some(id) = source_id
{
reserve_temporary_vec_slot(
&mut projected,
meter,
&mut temporary_bytes,
)?;
projected.push(SourceNode::Node(id));
}
if let (Some(element), Some(owner)) = (node.element(), source_id) {
let attributes = source.node(owner).and_then(|node| match &node.kind {
NodeKind::Element { attributes, .. } => Some(attributes.as_slice()),
_ => None,
});
for (index, attribute) in
attributes.unwrap_or_default().iter().enumerate()
{
meter.charge(BudgetKind::XPathOperations, 1)?;
let Some(attribute) = element.attribute(QName::with_namespace_uri(
attribute.name.namespace.as_deref(),
&attribute.name.local,
)) else {
continue;
};
let attribute_node = nodeset::Node::Attribute(attribute);
if !nodes.remove(&attribute_node) {
continue;
}
reserve_temporary_vec_slot(
&mut projected,
meter,
&mut temporary_bytes,
)?;
projected.push(SourceNode::Attribute { owner, index });
}
}
if let Some(child) = node.child_at(0) {
source_id = projected_child_source(source, &traversal, source_id, 0);
reserve_temporary_vec_slot(
&mut traversal,
meter,
&mut temporary_bytes,
)?;
traversal.push((0, source_id));
node = child;
continue;
}
while let Some((index, _)) = traversal.last_mut() {
let parent = node
.parent()
.expect("a projected non-root node retains its parent");
if let Some(sibling) = parent.child_at(*index + 1) {
*index += 1;
source_id = projected_sibling_source(source, &traversal);
traversal.last_mut().expect("current frame exists").1 = source_id;
node = sibling;
break;
}
traversal.pop();
source_id = traversal.last().and_then(|(_, source)| *source);
node = parent;
}
if traversal.is_empty() && node == root {
break;
}
}
for node in nodes {
let (path, path_bytes) = typed_path_to_metered(&node, meter)?;
let result = (|| {
if let Some(source) = self.reverse.get(&path) {
reserve_temporary_vec_slot(
&mut projected,
meter,
&mut temporary_bytes,
)?;
projected.push(source.clone());
}
Ok(())
})();
meter.release_owned_bytes(path_bytes);
result?;
}
projected.sort_by_key(|node| self.order.get(node).copied());
Ok(XPathValue::NodeSet(projected))
})();
meter.release_owned_bytes(temporary_bytes);
return result;
}
})
}
}
fn projected_child_source(
source: &Document,
traversal: &[(usize, Option<NodeId>)],
parent: Option<NodeId>,
index: usize,
) -> Option<NodeId> {
match traversal.len() {
0 => None,
1 => source.logical_roots().get(index).copied(),
_ => parent.and_then(|parent| source.node(parent)?.children.get(index).copied()),
}
}
fn projected_sibling_source(
source: &Document,
traversal: &[(usize, Option<NodeId>)],
) -> Option<NodeId> {
let (index, _) = *traversal.last()?;
match traversal.len() {
1 => None,
2 => source.logical_roots().get(index).copied(),
_ => traversal
.get(traversal.len() - 2)
.and_then(|(_, parent)| *parent)
.and_then(|parent| source.node(parent)?.children.get(index).copied()),
}
}
fn xpath_namespace_is_visible(namespace: &super::Namespace) -> bool {
!namespace.uri.is_empty()
}
struct MeteredSemanticPaths {
paths: HashMap<NodeId, (Vec<usize>, usize)>,
reserved_owned_bytes: usize,
}
fn semantic_node_paths_from(
source: &Document,
first_node: usize,
meter: &mut Meter,
) -> Result<MeteredSemanticPaths> {
let mut paths = HashMap::new();
let mut reserved_owned_bytes = 0usize;
let result = (|| {
let mut rank = 0usize;
for (document_index, logical_root) in source.logical_roots().iter().copied().enumerate() {
if logical_root.0 < first_node {
continue;
}
let mut root_path = Vec::new();
for component in [0, document_index] {
reserve_temporary_vec_slot(&mut root_path, meter, &mut reserved_owned_bytes)?;
root_path.push(component);
}
let mut pending = Vec::new();
reserve_temporary_vec_slot(&mut pending, meter, &mut reserved_owned_bytes)?;
pending.push((logical_root, root_path));
while let Some((parent, parent_path)) = pending.pop() {
let node = source.node(parent).ok_or_else(|| {
Error::Dynamic(format!("stale semantic node {parent:?} in document path"))
})?;
for (index, child) in node.children.iter().copied().enumerate().rev() {
let mut child_path = Vec::new();
for component in parent_path.iter().copied() {
reserve_temporary_vec_slot(
&mut child_path,
meter,
&mut reserved_owned_bytes,
)?;
child_path.push(component);
}
reserve_temporary_vec_slot(&mut child_path, meter, &mut reserved_owned_bytes)?;
child_path.push(index);
reserve_temporary_vec_slot(&mut pending, meter, &mut reserved_owned_bytes)?;
pending.push((child, child_path));
}
reserve_retained_hash_map_slot(&mut paths, meter, &mut reserved_owned_bytes)?;
paths.insert(parent, (parent_path, rank));
rank = rank.saturating_add(1);
}
}
Ok(())
})();
if let Err(error) = result {
drop(paths);
meter.release_owned_bytes(reserved_owned_bytes);
return Err(error);
}
Ok(MeteredSemanticPaths {
paths,
reserved_owned_bytes,
})
}
fn extend_node_base_uri_entries<'a>(
source: &Document,
entries: impl Iterator<Item = (&'a NodePath, &'a SourceNode)>,
index: &mut HashMap<Vec<usize>, Option<String>>,
index_bytes: &mut usize,
meter: &mut Meter,
) -> Result<()> {
for (path, node) in entries {
let SourceNode::Node(id) = node else {
continue;
};
let semantic_node = source
.node(*id)
.ok_or_else(|| Error::Dynamic(format!("stale semantic node {id:?}")))?;
let base_uri = semantic_node.base_uri.as_deref();
let inherited = semantic_node
.parent
.and_then(|parent| source.node(parent))
.and_then(|parent| parent.base_uri.as_deref());
let is_logical_root = source.logical_roots().binary_search(id).is_ok();
if !is_logical_root && base_uri == inherited {
continue;
}
let key = path.ordinary();
meter.charge(
BudgetKind::OwnedBytes,
key.len()
.saturating_mul(std::mem::size_of::<usize>())
.saturating_add(base_uri.map_or(0, str::len)),
)?;
reserve_retained_hash_map_slot(index, meter, index_bytes)?;
index.insert(key.to_vec(), base_uri.map(str::to_owned));
}
Ok(())
}
fn node_base_uri_at<'a>(
index: &'a HashMap<Vec<usize>, Option<String>>,
path: &NodePath,
) -> Option<&'a str> {
let mut ordinary = path.ordinary();
loop {
if let Some(base_uri) = index.get(ordinary) {
return base_uri.as_deref();
}
if ordinary.len() <= 2 {
return None;
}
ordinary = &ordinary[..ordinary.len() - 1];
}
}
fn element_pattern_name_matches(
lexical: &str,
name: &super::ExpandedName,
namespaces: &[(String, String)],
) -> Result<bool> {
if let Some((_, local)) = lexical.split_once(':') {
let namespace = pattern_name_namespace(lexical, namespaces)?
.expect("a prefixed lexical name resolves a namespace");
Ok(name.namespace.as_deref() == Some(namespace) && (local == "*" || name.local == local))
} else {
Ok(name.namespace.is_none() && name.local == lexical)
}
}
fn pattern_name_namespace<'a>(
lexical: &str,
namespaces: &'a [(String, String)],
) -> Result<Option<&'a str>> {
let Some((prefix, _)) = lexical.split_once(':') else {
return Ok(None);
};
namespaces
.iter()
.find_map(|(candidate, uri)| (candidate == prefix).then_some(uri.as_str()))
.map(Some)
.ok_or_else(|| Error::Static(format!("unbound pattern namespace prefix {prefix}")))
}
fn quoted_pattern_literal(value: &str) -> Option<&str> {
let quote = value.as_bytes().first().copied()?;
if !matches!(quote, b'\'' | b'"') || value.as_bytes().last().copied() != Some(quote) {
return None;
}
let literal = &value[1..value.len().checked_sub(1)?];
(!literal.as_bytes().contains("e)).then_some(literal)
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum ExtensionCallKind {
NodeSet,
ObjectType,
Replace,
Split,
Tokenize,
DynamicEvaluate,
DynamicMap,
SaxonExpression,
SaxonEval,
SaxonEvaluate,
SaxonLineNumber,
}
fn variable_reference_name(source: &str, namespaces: &[(String, String)]) -> Option<ExpandedName> {
let lexical = trim_xml_whitespace(source).strip_prefix('$')?;
if let Some((prefix, local)) = lexical.split_once(':') {
let namespace = namespaces
.iter()
.find_map(|(candidate, namespace)| (candidate == prefix).then(|| namespace.clone()))?;
Some(ExpandedName::new(Some(namespace), local))
} else {
Some(ExpandedName::new(None::<String>, lexical))
}
}
fn validate_dynamic_expression(source: &str, namespaces: &[(String, String)]) -> Result<()> {
let normalized = normalize_xpath_for_sxd(source);
Factory::new()
.build(&normalized)
.map_err(|error| Error::Dynamic(format!("invalid dynamic XPath `{source}`: {error}")))?;
let bytes = source.as_bytes();
let mut quote = None;
let mut cursor = 0usize;
while cursor < bytes.len() {
let character = bytes[cursor] as char;
if let Some(active) = quote {
if character == active {
quote = None;
}
cursor += 1;
continue;
}
if matches!(character, '\'' | '"') {
quote = Some(character);
cursor += 1;
continue;
}
if !is_xpath_name_start(character) {
cursor += 1;
continue;
}
let start = cursor;
cursor += 1;
while cursor < bytes.len()
&& ((bytes[cursor] as char).is_ascii_alphanumeric()
|| matches!(bytes[cursor], b'_' | b'-' | b'.'))
{
cursor += 1;
}
if cursor >= bytes.len() || bytes[cursor] != b':' || bytes.get(cursor + 1) == Some(&b':') {
continue;
}
let prefix = &source[start..cursor];
if prefix != "xml" && !namespaces.iter().any(|(candidate, _)| candidate == prefix) {
return Err(Error::Dynamic(format!(
"dynamic XPath `{source}` uses undeclared namespace prefix `{prefix}`"
)));
}
cursor += 1;
}
Ok(())
}
fn is_xpath_name_start(character: char) -> bool {
character == '_' || character.is_ascii_alphabetic()
}
fn xpath_value_to_public(value: XPathValue) -> Value {
match value {
XPathValue::NodeSet(nodes) => Value::NodeSet(nodes),
XPathValue::ResultTreeFragment(document) => Value::ResultTreeFragment(document),
XPathValue::Boolean(value) => Value::Boolean(value),
XPathValue::Number(value) => Value::Number(value),
XPathValue::String(value) => Value::String(value),
XPathValue::StoredExpression(value) => Value::StoredExpression(value),
}
}
fn token_document(
input: &str,
delimiters: &str,
kind: ExtensionCallKind,
meter: &mut Meter,
) -> Result<Document> {
let scan_work = match kind {
ExtensionCallKind::Tokenize => input.len().saturating_mul(delimiters.len().max(1)),
ExtensionCallKind::Split => input.len().saturating_add(delimiters.len()),
_ => unreachable!("token documents are produced only by split and tokenize"),
};
meter.charge(BudgetKind::ExtensionOperations, scan_work)?;
let mut document = Document::empty(None);
meter.charge(
BudgetKind::OwnedBytes,
document.retained_tree_container_bytes(),
)?;
visit_exslt_tokens(input, delimiters, kind, |token| {
meter.charge(BudgetKind::ResultNodes, 2)?;
meter.charge(BudgetKind::OwnedBytes, "token".len() + token.len())?;
document.reserve_metered_push_containers(document.root(), meter)?;
let element = document.push(
document.root(),
NodeKind::Element {
name: ExpandedName::new(None::<String>, "token"),
prefix: None,
attributes: Vec::new(),
namespaces: Arc::new(Vec::new()),
},
None,
);
document.reserve_metered_push_containers(element, meter)?;
document.push(
element,
NodeKind::Text {
value: token.to_owned(),
disable_output_escaping: false,
},
None,
);
Ok(())
})?;
Ok(document)
}
fn visit_exslt_tokens(
input: &str,
delimiters: &str,
kind: ExtensionCallKind,
mut visit: impl FnMut(&str) -> Result<()>,
) -> Result<()> {
if delimiters.is_empty() {
for (offset, character) in input.char_indices() {
visit(&input[offset..offset + character.len_utf8()])?;
}
return Ok(());
}
match kind {
ExtensionCallKind::Split => {
for token in input.split(delimiters).filter(|token| !token.is_empty()) {
visit(token)?;
}
}
ExtensionCallKind::Tokenize => {
for token in input
.split(|character| delimiters.contains(character))
.filter(|token| !token.is_empty())
{
visit(token)?;
}
}
_ => unreachable!("token documents are produced only by split and tokenize"),
}
Ok(())
}
fn dynamic_map_document(values: &[(&str, String)], meter: &mut Meter) -> Result<(Document, usize)> {
const RESULT_PREFIX: &str = "exsl";
let owned_bytes_before = meter.usage(BudgetKind::OwnedBytes)?.0;
let mut document = Document::empty(None);
meter.charge(
BudgetKind::OwnedBytes,
document.retained_tree_container_bytes(),
)?;
for (local, value) in values {
meter.charge(BudgetKind::ResultNodes, 2)?;
meter.charge(
BudgetKind::OwnedBytes,
local
.len()
.saturating_add(value.len())
.saturating_add(2usize.saturating_mul(RESULT_PREFIX.len()))
.saturating_add(2usize.saturating_mul(EXSLT_COMMON_NS.len()))
.saturating_add(std::mem::size_of::<Vec<super::Namespace>>())
.saturating_add(2usize.saturating_mul(std::mem::size_of::<usize>()))
.saturating_add(std::mem::size_of::<super::Namespace>()),
)?;
document.reserve_metered_push_containers(document.root(), meter)?;
let element = document.push(
document.root(),
NodeKind::Element {
name: ExpandedName::new(Some(EXSLT_COMMON_NS), *local),
prefix: Some(RESULT_PREFIX.into()),
attributes: Vec::new(),
namespaces: Arc::new(vec![super::Namespace {
prefix: Some(RESULT_PREFIX.into()),
uri: EXSLT_COMMON_NS.into(),
}]),
},
None,
);
document.reserve_metered_push_containers(element, meter)?;
document.push(
element,
NodeKind::Text {
value: value.clone(),
disable_output_escaping: false,
},
None,
);
}
let owned_bytes_after = meter.usage(BudgetKind::OwnedBytes)?.0;
Ok((
document,
owned_bytes_after
.checked_sub(owned_bytes_before)
.expect("dynamic-map owned-byte usage is monotonic while constructing its document"),
))
}
fn text_document(value: &str, meter: &mut Meter) -> Result<Document> {
let mut document = Document::empty(None);
meter.charge(BudgetKind::ResultNodes, 1)?;
meter.charge(
BudgetKind::OwnedBytes,
document
.retained_tree_container_bytes()
.saturating_add(value.len()),
)?;
document.reserve_metered_push_containers(document.root(), meter)?;
document.push(
document.root(),
NodeKind::Text {
value: value.to_owned(),
disable_output_escaping: false,
},
None,
);
Ok(document)
}
fn replace_exslt_string(
input: &str,
searches: &[String],
replacements: &[String],
meter: &mut Meter,
) -> Result<String> {
let mut output_bytes = 0usize;
for_each_exslt_replacement_segment(input, searches, replacements, meter, |segment| {
output_bytes = output_bytes
.checked_add(segment.len())
.ok_or_else(|| Error::Dynamic("str:replace() result length overflow".into()))?;
Ok(())
})?;
meter.charge(BudgetKind::OwnedBytes, output_bytes)?;
let mut output = String::with_capacity(output_bytes);
for_each_exslt_replacement_segment(input, searches, replacements, meter, |segment| {
output.push_str(segment);
Ok(())
})?;
Ok(output)
}
fn for_each_exslt_replacement_segment(
input: &str,
searches: &[String],
replacements: &[String],
meter: &mut Meter,
mut emit: impl FnMut(&str) -> Result<()>,
) -> Result<()> {
let mut cursor = 0;
let empty_search = searches.iter().position(String::is_empty);
while cursor < input.len() {
let remaining = &input[cursor..];
let mut matched = None;
for (index, search) in searches.iter().enumerate() {
if search.is_empty() {
continue;
}
meter.charge(BudgetKind::ExtensionOperations, 1)?;
if remaining.starts_with(search.as_str())
&& matched.is_none_or(|(_, best): (usize, &String)| search.len() > best.len())
{
matched = Some((index, search));
}
}
if let Some((index, search)) = matched {
emit(replacements.get(index).map_or("", String::as_str))?;
cursor += search.len();
continue;
}
let character = remaining
.chars()
.next()
.expect("cursor remains inside the input");
emit(&remaining[..character.len_utf8()])?;
cursor += character.len_utf8();
if let Some(index) = empty_search
&& cursor < input.len()
{
emit(replacements.get(index).map_or("", String::as_str))?;
}
}
Ok(())
}
fn unused_internal_namespace<'a>(
namespaces: &[(String, String)],
preferred_prefix: &'a str,
preferred_namespace: &'a str,
) -> (Cow<'a, str>, Cow<'a, str>) {
(
unused_internal_name(
namespaces.iter().map(|(prefix, _)| prefix.as_str()),
preferred_prefix,
'_',
),
unused_internal_name(
namespaces.iter().map(|(_, namespace)| namespace.as_str()),
preferred_namespace,
':',
),
)
}
fn unused_internal_name<'existing, 'preferred>(
existing: impl Iterator<Item = &'existing str> + Clone,
preferred: &'preferred str,
padding: char,
) -> Cow<'preferred, str> {
if existing.clone().all(|value| value != preferred) {
return Cow::Borrowed(preferred);
}
let longest = existing.map(str::len).max().unwrap_or(0);
let mut unique = String::with_capacity(longest + padding.len_utf8());
unique.push_str(preferred);
while unique.len() <= longest {
unique.push(padding);
}
Cow::Owned(unique)
}
fn unused_internal_name_owned_len<'existing>(
existing: impl Iterator<Item = &'existing str> + Clone,
preferred: &str,
padding: char,
) -> usize {
if existing.clone().all(|value| value != preferred) {
return preferred.len();
}
existing
.map(str::len)
.max()
.unwrap_or(0)
.saturating_add(padding.len_utf8())
}
fn rewrite_outer_context_functions<'a>(source: &'a str, prefix: &str) -> Cow<'a, str> {
if !source.contains("position") && !source.contains("last") {
return std::borrow::Cow::Borrowed(source);
}
let mut output = String::with_capacity(source.len());
let mut quote = None;
let mut predicate_depth = 0usize;
let mut cursor = 0;
while cursor < source.len() {
let tail = &source[cursor..];
let character = tail
.chars()
.next()
.expect("cursor is at a character boundary");
if let Some(active) = quote {
output.push(character);
cursor += character.len_utf8();
if character == active {
quote = None;
}
continue;
}
match character {
'\'' | '"' => quote = Some(character),
'[' => predicate_depth += 1,
']' => predicate_depth = predicate_depth.saturating_sub(1),
_ => {}
}
if predicate_depth == 0 {
let replacement = [("position", "position"), ("last", "last")]
.into_iter()
.find_map(|(function, local)| {
outer_context_call_len(tail, function).and_then(|length| {
source[..cursor]
.chars()
.next_back()
.is_none_or(|before| !is_xpath_name_character(before))
.then_some((length, local))
})
});
if let Some((length, local)) = replacement {
output.push('$');
output.push_str(prefix);
output.push(':');
output.push_str(local);
cursor += length;
continue;
}
}
output.push(character);
cursor += character.len_utf8();
}
if output == source {
std::borrow::Cow::Borrowed(source)
} else {
std::borrow::Cow::Owned(output)
}
}
fn outer_context_call_len(source: &str, function: &str) -> Option<usize> {
let mut cursor = function.len();
if !source.starts_with(function)
|| source[cursor..]
.chars()
.next()
.is_some_and(is_xpath_name_character)
{
return None;
}
cursor += source[cursor..]
.find(|character: char| !is_xpath_whitespace(character))
.unwrap_or(source.len() - cursor);
if source.as_bytes().get(cursor) != Some(&b'(') {
return None;
}
cursor += 1;
cursor += source[cursor..]
.find(|character: char| !is_xpath_whitespace(character))
.unwrap_or(source.len() - cursor);
(source.as_bytes().get(cursor) == Some(&b')')).then_some(cursor + 1)
}
fn is_xpath_whitespace(character: char) -> bool {
matches!(character, ' ' | '\t' | '\r' | '\n')
}
fn is_xpath_name_character(character: char) -> bool {
character == ':' || is_ncname_char(character)
}
fn is_lexical_qname(value: &str) -> bool {
let mut parts = value.split(':');
let Some(first) = parts.next() else {
return false;
};
is_ncname(first) && parts.next().is_none_or(is_ncname) && parts.next().is_none()
}
#[derive(Default)]
struct GeneratedIdCache {
assigned: HashMap<GeneratedNodeIdentity, usize>,
owned_bytes: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct GeneratedNodeIdentity {
document: sxd_document_no_unsafe::dom::DocumentIdentity,
path: NodePath,
}
struct GenerateId {
assigned: Rc<RefCell<GeneratedIdCache>>,
}
impl function::Function for GenerateId {
fn evaluate<'c, 'd>(
&self,
context: &sxd_xpath_no_unsafe::context::Evaluation<'c, 'd>,
args: Vec<SxdValue<'d>>,
) -> std::result::Result<SxdValue<'d>, function::Error> {
if args.len() != 1 {
return Err(function::Error::Other {
what: "generate-id() requires one argument".into(),
});
}
let SxdValue::Nodeset(nodes) = &args[0] else {
return Err(function::Error::Other {
what: "generate-id() requires a node-set".into(),
});
};
let Some(node) = nodes.document_order_first_with_context(context)? else {
return Ok(SxdValue::String(String::new()));
};
let id = assign_generated_id(
context,
&self.assigned,
GeneratedNodeIdentity {
document: node.document().identity(),
path: typed_path_to_with_context(&node, context)?,
},
)?;
let digits = id.checked_ilog10().unwrap_or(0) as usize + 1;
let output_len = 2usize.saturating_add(digits);
context.reserve_string_allocation(output_len)?;
let mut output = String::new();
if output.try_reserve_exact(output_len).is_err() {
context.release_temporary_allocation(output_len)?;
return Err(function::Error::Other {
what: "failed to reserve generated-ID result storage".into(),
});
}
let actual_bytes = output.capacity();
if actual_bytes > output_len
&& let Err(error) =
context.reserve_string_allocation(actual_bytes.saturating_sub(output_len))
{
context.release_temporary_allocation(output_len)?;
return Err(error);
}
write!(&mut output, "id{id}").expect("writing to String cannot fail");
Ok(SxdValue::String(output))
}
}
fn assign_generated_id(
context: &sxd_xpath_no_unsafe::context::Evaluation<'_, '_>,
assigned: &RefCell<GeneratedIdCache>,
identity: GeneratedNodeIdentity,
) -> std::result::Result<usize, function::Error> {
let mut cache = assigned.borrow_mut();
if let Some(id) = cache.assigned.get(&identity) {
return Ok(*id);
}
let entry_bytes = identity
.path
.owned_bytes()
.saturating_add(std::mem::size_of::<(GeneratedNodeIdentity, usize)>());
context.reserve_string_allocation(entry_bytes)?;
if let Err(error) = reserve_generated_id_slot(context, &mut cache) {
context.release_temporary_allocation(entry_bytes)?;
return Err(error);
}
let id = cache.assigned.len() + 1;
cache.assigned.insert(identity, id);
cache.owned_bytes = cache.owned_bytes.saturating_add(entry_bytes);
Ok(id)
}
fn reserve_generated_id_slot(
context: &sxd_xpath_no_unsafe::context::Evaluation<'_, '_>,
cache: &mut GeneratedIdCache,
) -> std::result::Result<(), function::Error> {
if cache.assigned.len() < cache.assigned.capacity() {
return Ok(());
}
let old_bytes =
retained_hash_storage::<(GeneratedNodeIdentity, usize)>(cache.assigned.capacity());
let old_capacity = cache.assigned.capacity();
let target_capacity = old_capacity.saturating_add(old_capacity.max(4));
let requested_bytes = retained_hash_storage::<(GeneratedNodeIdentity, usize)>(target_capacity);
context.reserve_string_allocation(requested_bytes)?;
let mut replacement = HashMap::new();
if replacement.try_reserve(target_capacity).is_err() {
context.release_temporary_allocation(requested_bytes)?;
return Err(function::Error::Other {
what: "failed to reserve generated-ID cache storage".into(),
});
}
let actual_bytes =
retained_hash_storage::<(GeneratedNodeIdentity, usize)>(replacement.capacity());
if actual_bytes > requested_bytes {
if let Err(error) = context.reserve_string_allocation(actual_bytes - requested_bytes) {
context.release_temporary_allocation(requested_bytes)?;
return Err(error);
}
} else {
context.release_temporary_allocation(requested_bytes - actual_bytes)?;
}
replacement.extend(cache.assigned.drain());
cache.assigned = replacement;
context.release_temporary_allocation(old_bytes)?;
cache.owned_bytes = cache
.owned_bytes
.checked_sub(old_bytes)
.expect("generated-ID hash capacity was previously accounted")
.saturating_add(actual_bytes);
Ok(())
}
enum NodeNameFunction {
Qualified,
Local,
NamespaceUri,
}
fn is_projection_document_wrapper(node: &nodeset::Node<'_>) -> bool {
let Some(name) = node.expanded_name() else {
return false;
};
if name.namespace_uri().is_some() || name.local_part() != DOCUMENT_ELEMENT {
return false;
}
let Some(container) = node.parent().and_then(|parent| parent.element()) else {
return false;
};
let container_node = nodeset::Node::Element(container);
container_node.expanded_name().is_some_and(|name| {
name.namespace_uri().is_none() && name.local_part() == DOCUMENTS_ELEMENT
}) && container
.parent()
.is_some_and(|parent| parent.root().is_some())
}
fn projected_document_root<'d>(node: nodeset::Node<'d>) -> nodeset::Node<'d> {
let mut current = node;
loop {
if is_projection_document_wrapper(¤t) {
return current;
}
let Some(parent) = current.parent() else {
return current.document().root().into();
};
current = parent;
}
}
impl function::Function for NodeNameFunction {
fn evaluate<'c, 'd>(
&self,
context: &sxd_xpath_no_unsafe::context::Evaluation<'c, 'd>,
args: Vec<SxdValue<'d>>,
) -> std::result::Result<SxdValue<'d>, function::Error> {
let node = match args.as_slice() {
[] => Some(context.node.clone()),
[SxdValue::Nodeset(nodes)] => nodes.document_order_first_with_context(context)?,
[_] => {
return Err(function::Error::Other {
what: "node name functions require a node-set argument".into(),
});
}
_ => {
return Err(function::Error::Other {
what: "node name functions accept at most one argument".into(),
});
}
};
let Some(node) = node else {
return Ok(SxdValue::String(String::new()));
};
if is_projection_document_wrapper(&node) {
return Ok(SxdValue::String(String::new()));
}
let value = match self {
Self::Qualified => qualified_node_name(context, &node)?,
Self::Local => node.expanded_name().map_or_else(
|| Ok(String::new()),
|name| materialize_node_name(context, name.local_part()),
)?,
Self::NamespaceUri => node.expanded_name().map_or_else(
|| Ok(String::new()),
|name| materialize_node_name(context, name.namespace_uri().unwrap_or_default()),
)?,
};
Ok(SxdValue::String(value))
}
}
fn qualified_node_name(
context: &sxd_xpath_no_unsafe::context::Evaluation<'_, '_>,
node: &nodeset::Node<'_>,
) -> std::result::Result<String, function::Error> {
match node {
nodeset::Node::Element(element) => {
let prefix = element.preferred_prefix();
let name = element.name();
materialize_qualified_node_name(context, prefix.as_deref(), name.get().local_part())
}
nodeset::Node::Attribute(attribute) => {
let prefix = attribute.preferred_prefix();
let name = attribute.name();
materialize_qualified_node_name(context, prefix.as_deref(), name.get().local_part())
}
nodeset::Node::ProcessingInstruction(instruction) => {
let value = instruction.target();
materialize_node_name(context, &value)
}
nodeset::Node::Namespace(namespace) => {
let value = namespace.prefix();
materialize_node_name(context, value)
}
_ => Ok(String::new()),
}
}
fn materialize_qualified_node_name(
context: &sxd_xpath_no_unsafe::context::Evaluation<'_, '_>,
prefix: Option<&str>,
local: &str,
) -> std::result::Result<String, function::Error> {
let prefix = prefix.filter(|prefix| !prefix.is_empty());
let output_len = prefix.map_or(local.len(), |prefix| prefix.len() + 1 + local.len());
context.charge_work(output_len)?;
context.reserve_string_allocation(output_len)?;
let Some(prefix) = prefix else {
return Ok(local.to_owned());
};
let mut output = String::with_capacity(output_len);
output.push_str(prefix);
output.push(':');
output.push_str(local);
Ok(output)
}
fn materialize_node_name(
context: &sxd_xpath_no_unsafe::context::Evaluation<'_, '_>,
value: &str,
) -> std::result::Result<String, function::Error> {
context.charge_work(value.len())?;
context.reserve_string_allocation(value.len())?;
Ok(value.to_owned())
}
struct LangFunction;
impl function::Function for LangFunction {
fn evaluate<'c, 'd>(
&self,
context: &sxd_xpath_no_unsafe::context::Evaluation<'c, 'd>,
args: Vec<SxdValue<'d>>,
) -> std::result::Result<SxdValue<'d>, function::Error> {
if args.len() != 1 {
return Err(function::Error::Other {
what: "lang() requires one argument".into(),
});
}
let requested = args.into_iter().next().expect("arity checked above");
let requested = function::string_argument_view(&requested, context)?;
context.charge_work(requested.len())?;
let mut current = context
.node
.element()
.or_else(|| context.node.parent().and_then(|node| node.element()));
while let Some(element) = current {
context.charge_work(1)?;
for index in 0..element.attributes_len() {
context.charge_work(1)?;
let attribute = element
.attribute_at(index)
.expect("attribute index is in range");
let name = attribute.name();
let name = name.get();
if name.local_part() == "lang"
&& name.namespace_uri() == Some("http://www.w3.org/XML/1998/namespace")
{
let language = attribute.value();
context.charge_work(requested.len().min(language.len()).saturating_mul(2))?;
return Ok(SxdValue::Boolean(language_tag_matches(
&language, &requested,
)));
}
}
current = element.parent().and_then(|node| node.element());
}
Ok(SxdValue::Boolean(false))
}
}
fn language_tag_matches(language: &str, requested: &str) -> bool {
!language.is_empty()
&& (language.eq_ignore_ascii_case(requested)
|| language
.get(..requested.len())
.is_some_and(|prefix| prefix.eq_ignore_ascii_case(requested))
&& language.as_bytes().get(requested.len()) == Some(&b'-'))
}
type IdIndex = Vec<HashMap<String, NodePath>>;
struct IdFunction {
nodes_by_document: Rc<RefCell<IdIndex>>,
}
impl function::Function for IdFunction {
fn evaluate<'c, 'd>(
&self,
context: &sxd_xpath_no_unsafe::context::Evaluation<'c, 'd>,
args: Vec<SxdValue<'d>>,
) -> std::result::Result<SxdValue<'d>, function::Error> {
if args.len() != 1 {
return Err(function::Error::Other {
what: "id() requires one argument".into(),
});
}
let context_path = typed_path_to_with_context(&context.node, context)?;
let nodes_by_document = self.nodes_by_document.borrow();
let Some(nodes) = context_path
.ordinary()
.get(1)
.and_then(|index| nodes_by_document.get(*index))
else {
return Ok(SxdValue::Nodeset(nodeset::Nodeset::new()));
};
let mut result = nodeset::Nodeset::new();
let mut add_tokens = |value: &str| -> std::result::Result<(), function::Error> {
context.charge_work(value.len())?;
for token in value
.split(super::lexical::is_xml_whitespace)
.filter(|token| !token.is_empty())
{
context.charge_work(token.len())?;
context.charge_work(1)?;
if let Some(path) = nodes.get(token)
&& let Some(node) = resolve_node_path(context, path)?
{
result.add_metered(context, node)?;
}
}
Ok(())
};
match &args[0] {
SxdValue::Nodeset(nodes) => {
for node in nodes.iter() {
let value = node.string_value_with_context(context)?;
add_tokens(&value)?;
}
}
SxdValue::String(value) => {
add_tokens(value)?;
}
SxdValue::ResultTreeFragment(_, value) => {
add_tokens(value)?;
}
value => {
context.reserve_string_allocation(value.string_len())?;
add_tokens(&value.string())?;
}
}
Ok(SxdValue::Nodeset(result))
}
}
fn extend_id_index(
source: &Document,
maps: &NodeMaps,
first_node: usize,
index: &mut IdIndex,
meter: &mut Meter,
) -> Result<()> {
while index.len() < source.logical_roots().len() {
reserve_retained_vec_slot(index, meter)?;
index.push(HashMap::new());
}
for (value, root, owner) in source.ids().filter(|(_, _, owner)| owner.0 >= first_node) {
let Some(root_index) = source.logical_roots().binary_search(&root).ok() else {
continue;
};
let Some(path) = maps.forward.get(&SourceNode::Node(owner)).cloned() else {
continue;
};
let retained_bytes = value
.len()
.saturating_add(path.owned_bytes())
.saturating_add(std::mem::size_of::<(String, NodePath)>().saturating_mul(2));
meter.charge(BudgetKind::OwnedBytes, retained_bytes)?;
index[root_index].insert(value.to_owned(), path);
}
Ok(())
}
fn build_unparsed_entity_index(
source: &Document,
meter: &mut Meter,
) -> Result<Vec<HashMap<String, String>>> {
let root_count = source.logical_roots().len();
let requested_index_bytes =
root_count.saturating_mul(std::mem::size_of::<HashMap<String, String>>());
meter.charge(BudgetKind::OwnedBytes, requested_index_bytes)?;
let mut index = Vec::new();
if let Err(error) = index.try_reserve_exact(root_count) {
meter.release_owned_bytes(requested_index_bytes);
return Err(Error::Dynamic(format!(
"failed to reserve unparsed-entity document index: {error}"
)));
}
let actual_index_bytes = index
.capacity()
.saturating_mul(std::mem::size_of::<HashMap<String, String>>());
if actual_index_bytes < requested_index_bytes {
meter.release_owned_bytes(requested_index_bytes - actual_index_bytes);
} else if actual_index_bytes > requested_index_bytes {
meter.charge(
BudgetKind::OwnedBytes,
actual_index_bytes - requested_index_bytes,
)?;
}
let count_bytes = root_count.saturating_mul(std::mem::size_of::<usize>());
meter.charge(BudgetKind::OwnedBytes, count_bytes)?;
let mut entity_counts = Vec::new();
if let Err(error) = entity_counts.try_reserve_exact(root_count) {
meter.release_owned_bytes(count_bytes);
return Err(Error::Dynamic(format!(
"failed to reserve unparsed-entity document counts: {error}"
)));
}
entity_counts.resize(root_count, 0usize);
for (_, _, root) in source.unparsed_entities() {
if let Ok(root_index) = source.logical_roots().binary_search(&root) {
entity_counts[root_index] = entity_counts[root_index].saturating_add(1);
}
}
for entity_count in entity_counts {
let requested_table_bytes = unparsed_entity_table_bytes(entity_count);
meter.charge(BudgetKind::OwnedBytes, requested_table_bytes)?;
let mut entities = HashMap::new();
if let Err(error) = entities.try_reserve(entity_count) {
meter.release_owned_bytes(requested_table_bytes);
meter.release_owned_bytes(count_bytes);
return Err(Error::Dynamic(format!(
"failed to reserve unparsed-entity lookup table: {error}"
)));
}
reconcile_unparsed_entity_table_reservation(&entities, requested_table_bytes, meter)?;
index.push(entities);
}
meter.release_owned_bytes(count_bytes);
for (name, uri, root) in source.unparsed_entities() {
let Some(entities) = source
.logical_roots()
.binary_search(&root)
.ok()
.and_then(|root_index| index.get_mut(root_index))
else {
continue;
};
meter.charge(BudgetKind::OwnedBytes, name.len().saturating_add(uri.len()))?;
entities.insert(name.to_owned(), uri.to_owned());
}
Ok(index)
}
fn append_unparsed_entity_document(
source: &Document,
root: NodeId,
index: &mut Vec<HashMap<String, String>>,
meter: &mut Meter,
) -> Result<()> {
let entity_count = source
.unparsed_entities()
.filter(|(_, _, candidate)| *candidate == root)
.count();
let requested_table_bytes = unparsed_entity_table_bytes(entity_count);
meter.charge(BudgetKind::OwnedBytes, requested_table_bytes)?;
let mut entities = HashMap::new();
if let Err(error) = entities.try_reserve(entity_count) {
meter.release_owned_bytes(requested_table_bytes);
return Err(Error::Dynamic(format!(
"failed to reserve unparsed-entity lookup table: {error}"
)));
}
reconcile_unparsed_entity_table_reservation(&entities, requested_table_bytes, meter)?;
for (name, uri, _) in source
.unparsed_entities()
.filter(|(_, _, candidate)| *candidate == root)
{
meter.charge(BudgetKind::OwnedBytes, name.len().saturating_add(uri.len()))?;
entities.insert(name.to_owned(), uri.to_owned());
}
reserve_retained_vec_slot(index, meter)?;
index.push(entities);
Ok(())
}
fn reconcile_unparsed_entity_table_reservation(
entities: &HashMap<String, String>,
requested_bytes: usize,
meter: &mut Meter,
) -> Result<()> {
let actual_bytes = unparsed_entity_table_bytes(entities.capacity());
if actual_bytes < requested_bytes {
meter.release_owned_bytes(requested_bytes - actual_bytes);
} else if actual_bytes > requested_bytes {
meter.charge(BudgetKind::OwnedBytes, actual_bytes - requested_bytes)?;
}
Ok(())
}
fn unparsed_entity_table_bytes(capacity: usize) -> usize {
capacity
.saturating_mul(std::mem::size_of::<(String, String)>())
.saturating_mul(2)
}
struct UnparsedEntityUriFunction {
documents: Rc<RefCell<Vec<HashMap<String, String>>>>,
}
impl function::Function for UnparsedEntityUriFunction {
fn evaluate<'c, 'd>(
&self,
context: &sxd_xpath_no_unsafe::context::Evaluation<'c, 'd>,
args: Vec<SxdValue<'d>>,
) -> std::result::Result<SxdValue<'d>, function::Error> {
if args.len() != 1 {
return Err(function::Error::Other {
what: "unparsed-entity-uri() requires one argument".into(),
});
}
let document_index = typed_path_to_with_context(&context.node, context)?
.ordinary()
.get(1)
.copied()
.ok_or_else(|| function::Error::Other {
what: "unparsed-entity-uri() context has no logical document".into(),
})?;
let name = function::string_argument_view(&args[0], context)?;
context.charge_work(name.len())?;
let documents = self.documents.borrow();
let uri = documents
.get(document_index)
.and_then(|entities| entities.get(name.as_ref()));
context.reserve_string_allocation(uri.map_or(0, String::len))?;
Ok(SxdValue::String(uri.cloned().unwrap_or_default()))
}
}
struct DocumentFunction<'a, R: Resolver + ?Sized> {
resolver: &'a R,
access_error: &'a RefCell<Option<Error>>,
roots: Rc<RefCell<HashMap<DocumentRequest, CachedDocumentRoots>>>,
pending: Rc<RefCell<PendingDocumentRequests>>,
node_base_uris: Rc<RefCell<HashMap<Vec<usize>, Option<String>>>>,
static_base_uri: Option<Arc<str>>,
static_document: StylesheetDocumentId,
}
fn authorize_document_request<R: Resolver + ?Sized>(
resolver: &R,
request: &DocumentRequest,
identity: Option<&ResourceIdentity>,
) -> Result<()> {
if request.empty_document.is_some() {
return Ok(());
}
let uri = request
.href
.split_once('#')
.map_or(request.href.as_str(), |(uri, _)| uri);
let request = ResolveRequest::new(uri, request.base_uri.as_deref(), ResolvePurpose::Document);
resolver.authorize(match identity {
Some(identity) => super::ResourceAccess::Retained { request, identity },
None => super::ResourceAccess::Request(request),
})
}
enum DocumentBaseSelection {
Omitted,
Explicit {
base_uri: Option<String>,
logical_document: Option<usize>,
},
}
fn clone_metered_optional_string(
context: &sxd_xpath_no_unsafe::context::Evaluation<'_, '_>,
value: Option<&str>,
) -> std::result::Result<Option<String>, function::Error> {
context.reserve_temporary_allocation(value.map_or(0, str::len))?;
Ok(value.map(str::to_owned))
}
fn register_exslt_functions(
context: &mut sxd_xpath_no_unsafe::context::ScopedContext<'_, '_>,
clock: Option<Arc<dyn Clock>>,
) {
macro_rules! register {
($namespace:expr, $name:expr, $function:expr) => {{
context.set_function(($namespace, $name), $function);
}};
}
super::exslt_date::register(context, clock);
register!(EXSLT_MATH_NS, "max", ExsltMathFunction::Max);
register!(EXSLT_MATH_NS, "min", ExsltMathFunction::Min);
register!(EXSLT_MATH_NS, "highest", ExsltMathFunction::Highest);
register!(EXSLT_MATH_NS, "lowest", ExsltMathFunction::Lowest);
register!(EXSLT_MATH_NS, "power", ExsltMathFunction::Power);
register!(EXSLT_SETS_NS, "difference", ExsltSetFunction::Difference);
register!(
EXSLT_SETS_NS,
"intersection",
ExsltSetFunction::Intersection
);
register!(EXSLT_SETS_NS, "distinct", ExsltSetFunction::Distinct);
register!(
EXSLT_SETS_NS,
"has-same-node",
ExsltSetFunction::HasSameNode
);
register!(EXSLT_SETS_NS, "leading", ExsltSetFunction::Leading);
register!(EXSLT_SETS_NS, "trailing", ExsltSetFunction::Trailing);
register!(EXSLT_STRINGS_NS, "align", ExsltStringFunction::Align);
register!(EXSLT_STRINGS_NS, "padding", ExsltStringFunction::Padding);
register!(
EXSLT_STRINGS_NS,
"encode-uri",
ExsltStringFunction::EncodeUri
);
register!(
EXSLT_STRINGS_NS,
"decode-uri",
ExsltStringFunction::DecodeUri
);
register!(EXSLT_CRYPTO_NS, "md5", ExsltCryptoFunction::Md5);
register!(EXSLT_CRYPTO_NS, "sha1", ExsltCryptoFunction::Sha1);
register!(
EXSLT_CRYPTO_NS,
"rc4_encrypt",
ExsltCryptoFunction::Rc4Encrypt
);
register!(
EXSLT_CRYPTO_NS,
"rc4_decrypt",
ExsltCryptoFunction::Rc4Decrypt
);
register!(LIBXSLT_TEST_NS, "test", IdentityStringFunction);
register!(LIBXSLT_TEST_PLUGIN_NS, "testplugin", IdentityStringFunction);
}
struct IdentityStringFunction;
impl function::Function for IdentityStringFunction {
fn evaluate<'c, 'd>(
&self,
context: &sxd_xpath_no_unsafe::context::Evaluation<'c, 'd>,
args: Vec<SxdValue<'d>>,
) -> std::result::Result<SxdValue<'d>, function::Error> {
let Ok([value]) = <[SxdValue<'d>; 1]>::try_from(args) else {
return Err(function::Error::Other {
what: "libxslt test function requires one argument".into(),
});
};
if let SxdValue::String(text) = value {
context.charge_extension_work(text.len().max(1))?;
return Ok(SxdValue::String(text));
}
let text = extension_string(context, &value)?;
if matches!(text, Cow::Borrowed(_)) {
context.reserve_string_allocation(text.len())?;
}
Ok(SxdValue::String(text.into_owned()))
}
}
enum ExsltCryptoFunction {
Md5,
Sha1,
Rc4Encrypt,
Rc4Decrypt,
}
impl function::Function for ExsltCryptoFunction {
fn evaluate<'c, 'd>(
&self,
context: &sxd_xpath_no_unsafe::context::Evaluation<'c, 'd>,
args: Vec<SxdValue<'d>>,
) -> std::result::Result<SxdValue<'d>, function::Error> {
use sha1::Digest as _;
match self {
Self::Md5 | Self::Sha1 => {
let Ok([value]) = <[SxdValue<'d>; 1]>::try_from(args) else {
return Err(function::Error::Other {
what: "EXSLT hash function requires one argument".into(),
});
};
let value = extension_string(context, &value)?;
if value.is_empty() {
return Ok(SxdValue::String(String::new()));
}
let output_len = match self {
Self::Md5 => 32,
Self::Sha1 => 40,
_ => unreachable!(),
};
context.reserve_string_allocation(output_len)?;
let output = match self {
Self::Md5 => hex_encode(&md5::Md5::digest(value.as_bytes())),
Self::Sha1 => hex_encode(&sha1::Sha1::digest(value.as_bytes())),
_ => unreachable!(),
};
Ok(SxdValue::String(output))
}
Self::Rc4Encrypt | Self::Rc4Decrypt => {
let Ok([key, input]) = <[SxdValue<'d>; 2]>::try_from(args) else {
return Err(function::Error::Other {
what: "EXSLT RC4 function requires key and data".into(),
});
};
let key = extension_string(context, &key)?;
if key.is_empty() {
return Ok(SxdValue::String(String::new()));
}
let input = extension_string(context, &input)?;
let input_len = input.len();
let workspace_bytes = if matches!(self, Self::Rc4Decrypt) {
if !input_len.is_multiple_of(2) {
return Err(function::Error::Other {
what: "EXSLT RC4 ciphertext must contain complete hex octets".into(),
});
}
input_len
} else {
input_len
.checked_mul(3)
.ok_or_else(|| function::Error::Other {
what: "EXSLT RC4 allocation length overflow".into(),
})?
};
context.reserve_string_allocation(workspace_bytes)?;
context.charge_extension_work(input.len().saturating_add(256))?;
let input = if matches!(self, Self::Rc4Decrypt) {
Cow::Owned(hex_decode(&input)?)
} else {
Cow::Borrowed(input.as_bytes())
};
let mut padded_key = [0_u8; 128];
let key_bytes = key.as_bytes();
let copied = key_bytes.len().min(padded_key.len());
padded_key[..copied].copy_from_slice(&key_bytes[..copied]);
let output = rc4(&padded_key, &input);
if matches!(self, Self::Rc4Encrypt) {
Ok(SxdValue::String(hex_encode(&output)))
} else {
String::from_utf8(output)
.map(SxdValue::String)
.map_err(|_| function::Error::Other {
what: "EXSLT RC4 plaintext is not UTF-8".into(),
})
}
}
}
}
}
pub(crate) fn extension_string<'a>(
context: &sxd_xpath_no_unsafe::context::Evaluation<'_, '_>,
value: &'a SxdValue<'_>,
) -> std::result::Result<Cow<'a, str>, function::Error> {
let text = match value {
SxdValue::String(text) => Cow::Borrowed(text.as_str()),
SxdValue::ResultTreeFragment(_, text) => Cow::Borrowed(text.as_str()),
SxdValue::Nodeset(nodes) => {
context.charge_extension_work(nodes.size())?;
match nodes.document_order_first_with_context(context)? {
Some(node) => Cow::Owned(node.string_value_with_extension_context(context)?),
None => Cow::Borrowed(""),
}
}
_ => {
context.reserve_temporary_allocation(value.string_len())?;
Cow::Owned(value.string())
}
};
context.charge_extension_work(text.len().max(1))?;
Ok(text)
}
fn rc4(key: &[u8], input: &[u8]) -> Vec<u8> {
let mut state = [0_u8; 256];
for (index, value) in state.iter_mut().enumerate() {
*value = index as u8;
}
let mut j = 0_u8;
for index in 0..256 {
j = j
.wrapping_add(state[index])
.wrapping_add(key[index % key.len()]);
state.swap(index, usize::from(j));
}
let (mut i, mut j) = (0_u8, 0_u8);
input
.iter()
.map(|byte| {
i = i.wrapping_add(1);
j = j.wrapping_add(state[usize::from(i)]);
state.swap(usize::from(i), usize::from(j));
let stream =
state[usize::from(state[usize::from(i)].wrapping_add(state[usize::from(j)]))];
byte ^ stream
})
.collect()
}
fn hex_encode(bytes: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut output = String::with_capacity(bytes.len() * 2);
for byte in bytes {
output.push(char::from(HEX[usize::from(byte >> 4)]));
output.push(char::from(HEX[usize::from(byte & 0x0f)]));
}
output
}
fn hex_decode(value: &str) -> std::result::Result<Vec<u8>, function::Error> {
let (pairs, remainder) = value.as_bytes().as_chunks::<2>();
if !remainder.is_empty() {
return Err(function::Error::Other {
what: "EXSLT RC4 ciphertext must contain complete hex octets".into(),
});
}
pairs
.iter()
.map(|pair| {
let [high, low] = pair.map(hex_nibble);
high.zip(low)
.map(|(high, low)| high << 4 | low)
.ok_or_else(|| function::Error::Other {
what: "EXSLT RC4 ciphertext contains invalid hex".into(),
})
})
.collect()
}
fn hex_nibble(byte: u8) -> Option<u8> {
match byte {
b'0'..=b'9' => Some(byte - b'0'),
b'a'..=b'f' => Some(byte - b'a' + 10),
b'A'..=b'F' => Some(byte - b'A' + 10),
_ => None,
}
}
enum ExsltMathFunction {
Max,
Min,
Highest,
Lowest,
Power,
}
impl function::Function for ExsltMathFunction {
fn evaluate<'c, 'd>(
&self,
context: &sxd_xpath_no_unsafe::context::Evaluation<'c, 'd>,
args: Vec<SxdValue<'d>>,
) -> std::result::Result<SxdValue<'d>, function::Error> {
if matches!(self, Self::Power) {
if args.len() != 2 {
return extension_argument_error("math:power() requires two arguments");
}
context.charge_extension_work(1)?;
return Ok(SxdValue::Number(
args[0].number(context)?.powf(args[1].number(context)?),
));
}
let [SxdValue::Nodeset(nodes)] = args.as_slice() else {
return extension_argument_error("EXSLT math node functions require one node-set");
};
let node_count = nodes.size();
context.charge_extension_work(node_count)?;
let vector_bytes = node_count.saturating_mul(
std::mem::size_of::<nodeset::Node<'_>>().saturating_add(std::mem::size_of::<f64>()),
);
let selection_bytes = node_count.saturating_mul(2).saturating_mul(
std::mem::size_of::<nodeset::Node<'_>>().saturating_add(std::mem::size_of::<u64>()),
);
context.reserve_string_allocation(vector_bytes.saturating_add(selection_bytes))?;
let ordered = nodes.document_order_with_context(context)?;
let numbers = ordered
.iter()
.map(|node| {
node.string_value_with_extension_context(context)
.and_then(|value| SxdValue::String(value).number(context))
})
.collect::<std::result::Result<Vec<_>, _>>()?;
if numbers.is_empty() || numbers.iter().any(|value| value.is_nan()) {
return Ok(match self {
Self::Max | Self::Min => SxdValue::Number(f64::NAN),
Self::Highest | Self::Lowest => SxdValue::Nodeset(nodeset::Nodeset::new()),
Self::Power => unreachable!(),
});
}
let target = match self {
Self::Max | Self::Highest => numbers.iter().copied().fold(f64::NEG_INFINITY, f64::max),
Self::Min | Self::Lowest => numbers.iter().copied().fold(f64::INFINITY, f64::min),
Self::Power => unreachable!(),
};
Ok(match self {
Self::Max | Self::Min => SxdValue::Number(target),
Self::Highest | Self::Lowest => {
let mut selected = nodeset::Nodeset::new();
for (node, value) in ordered.into_iter().zip(numbers) {
if value == target {
selected.add_metered(context, node)?;
}
}
SxdValue::Nodeset(selected)
}
Self::Power => unreachable!(),
})
}
}
enum ExsltSetFunction {
Difference,
Intersection,
Distinct,
HasSameNode,
Leading,
Trailing,
}
impl function::Function for ExsltSetFunction {
fn evaluate<'c, 'd>(
&self,
context: &sxd_xpath_no_unsafe::context::Evaluation<'c, 'd>,
args: Vec<SxdValue<'d>>,
) -> std::result::Result<SxdValue<'d>, function::Error> {
if matches!(self, Self::Distinct) {
let [SxdValue::Nodeset(nodes)] = args.as_slice() else {
return extension_argument_error("set:distinct() requires one node-set");
};
let ordered = nodes.document_order_with_context(context)?;
let bucket_bytes = ordered.len().saturating_mul(2).saturating_mul(
std::mem::size_of::<String>().saturating_add(std::mem::size_of::<usize>()),
);
context.reserve_string_allocation(bucket_bytes)?;
let mut seen = std::collections::HashSet::with_capacity(ordered.len());
let mut result = nodeset::Nodeset::new();
for node in ordered {
let value = node.string_value_with_extension_context(context)?;
if seen.insert(value) {
result.add_metered(context, node)?;
}
}
return Ok(SxdValue::Nodeset(result));
}
let [SxdValue::Nodeset(left), SxdValue::Nodeset(right)] = args.as_slice() else {
return extension_argument_error("EXSLT set function requires two node-sets");
};
if matches!(self, Self::HasSameNode) {
for node in left.iter_ref() {
context.charge_extension_work(1)?;
if right.contains_ref(node) {
return Ok(SxdValue::Boolean(true));
}
}
return Ok(SxdValue::Boolean(false));
}
let mut result = nodeset::Nodeset::new();
match self {
Self::Difference => {
for node in left.iter_ref() {
context.charge_extension_work(1)?;
if !right.contains_ref(node) {
result.add_metered(context, node.clone_with_context(context)?)?;
}
}
}
Self::Intersection => {
for node in left.iter_ref() {
context.charge_extension_work(1)?;
if right.contains_ref(node) {
result.add_metered(context, node.clone_with_context(context)?)?;
}
}
}
Self::Leading | Self::Trailing => {
let Some(boundary) = right.document_order_first_with_context(context)? else {
return Ok(args.into_iter().next().expect("arity checked above"));
};
context.charge_extension_work(1)?;
if !left.contains_ref(&boundary) {
return Ok(SxdValue::Nodeset(nodeset::Nodeset::new()));
}
let mut after_boundary = false;
for node in left.document_order_with_context(context)? {
context.charge_extension_work(1)?;
if node == boundary {
after_boundary = true;
continue;
}
if matches!(self, Self::Trailing) == after_boundary {
result.add_metered(context, node)?;
}
}
}
Self::Distinct | Self::HasSameNode => unreachable!(),
}
Ok(SxdValue::Nodeset(result))
}
}
fn utf8_prefix_bytes(value: &str, characters: usize) -> usize {
value
.char_indices()
.nth(characters)
.map_or(value.len(), |(offset, _)| offset)
}
enum ExsltStringFunction {
Align,
Padding,
EncodeUri,
DecodeUri,
}
impl function::Function for ExsltStringFunction {
fn evaluate<'c, 'd>(
&self,
context: &sxd_xpath_no_unsafe::context::Evaluation<'c, 'd>,
args: Vec<SxdValue<'d>>,
) -> std::result::Result<SxdValue<'d>, function::Error> {
match self {
Self::Align => {
if !(2..=3).contains(&args.len()) {
return extension_argument_error("str:align() requires two or three arguments");
}
let value = extension_string(context, &args[0])?;
let padding = extension_string(context, &args[1])?;
let alignment = args
.get(2)
.map(|value| extension_string(context, value))
.transpose()?
.unwrap_or(Cow::Borrowed(""));
let width = padding.chars().count();
let value_width = value.chars().count();
if value_width >= width {
context.charge_extension_work(value.len())?;
let output_bytes = utf8_prefix_bytes(&value, width);
context.charge_extension_work(output_bytes)?;
context.reserve_string_allocation(output_bytes)?;
return Ok(SxdValue::String(value[..output_bytes].to_owned()));
}
let missing = width - value_width;
let left = match alignment.as_ref() {
"right" => missing,
"center" => missing / 2,
_ => 0,
};
context.charge_extension_work(padding.len())?;
let left_end = utf8_prefix_bytes(&padding, left);
let right_start = left_end + utf8_prefix_bytes(&padding[left_end..], value_width);
let output_bytes = left_end
.checked_add(value.len())
.and_then(|bytes| bytes.checked_add(padding.len() - right_start))
.unwrap_or(usize::MAX);
context.reserve_string_allocation(output_bytes)?;
context.charge_extension_work(output_bytes)?;
let mut result = String::with_capacity(output_bytes);
result.push_str(&padding[..left_end]);
result.push_str(&value);
result.push_str(&padding[right_start..]);
Ok(SxdValue::String(result))
}
Self::Padding => {
if !(1..=2).contains(&args.len()) {
return extension_argument_error("str:padding() requires one or two arguments");
}
let requested = args[0].number(context)?.floor().max(0.0);
let length = if requested.is_finite() && requested <= usize::MAX as f64 {
requested as usize
} else {
usize::MAX
};
let pattern = match args.get(1) {
Some(value) => extension_string(context, value)?,
None => Cow::Borrowed(" "),
};
context.charge_extension_work(pattern.len())?;
let pattern_characters = pattern.chars().count();
if pattern_characters == 0 || length == 0 {
return Ok(SxdValue::String(String::new()));
}
let complete = length / pattern_characters;
let remainder = length % pattern_characters;
context.charge_extension_work(pattern.len())?;
let suffix_bytes = utf8_prefix_bytes(&pattern, remainder);
let bytes = pattern
.len()
.checked_mul(complete)
.and_then(|bytes| bytes.checked_add(suffix_bytes))
.unwrap_or(usize::MAX);
context.charge_extension_work(bytes)?;
context.reserve_string_allocation(bytes)?;
let mut output = String::with_capacity(bytes);
for _ in 0..complete {
output.push_str(&pattern);
}
output.push_str(&pattern[..suffix_bytes]);
Ok(SxdValue::String(output))
}
Self::EncodeUri => {
if !(2..=3).contains(&args.len()) {
return extension_argument_error(
"str:encode-uri() requires two or three arguments",
);
}
let value = extension_string(context, &args[0])?;
let escape_reserved = args[1].boolean();
let encoding_label = args
.get(2)
.map(|value| extension_string(context, value))
.transpose()?;
context.charge_extension_work(value.len())?;
let Some(encoding) =
uri_encoding(encoding_label.as_deref(), UriEncodingCapability::Encode)
else {
return Ok(SxdValue::String(String::new()));
};
let Some(encoded_len) = percent_encoded_uri_len(&value, escape_reserved, encoding)?
else {
return Ok(SxdValue::String(String::new()));
};
context.reserve_string_allocation(encoded_len)?;
Ok(SxdValue::String(percent_encode_uri(
&value,
escape_reserved,
encoded_len,
encoding,
)?))
}
Self::DecodeUri => {
if !(1..=2).contains(&args.len()) {
return extension_argument_error(
"str:decode-uri() requires one or two arguments",
);
}
let value = extension_string(context, &args[0])?;
let encoding_label = args
.get(1)
.map(|value| extension_string(context, value))
.transpose()?;
context.charge_extension_work(value.len().saturating_mul(2))?;
let Some(encoding) =
uri_encoding(encoding_label.as_deref(), UriEncodingCapability::Decode)
else {
return Ok(SxdValue::String(String::new()));
};
let Some(decoded_len) = percent_decoded_uri_len(&value)? else {
return Ok(SxdValue::String(String::new()));
};
let (transcoded_len, workspace_len) = match encoding {
UriEncoding::Standard(encoding) => {
let transcoded_len = encoding
.new_decoder()
.max_utf8_buffer_length_without_replacement(decoded_len)
.ok_or_else(|| function::Error::Other {
what: "str:decode-uri() result length overflow".into(),
})?;
let workspace_len =
decoded_len.checked_add(transcoded_len).ok_or_else(|| {
function::Error::Other {
what: "str:decode-uri() allocation length overflow".into(),
}
})?;
(transcoded_len, workspace_len)
}
UriEncoding::Registered(encoding) => {
let transcoded_len = registered_uri_decoded_len(&value, encoding)?;
(transcoded_len, transcoded_len)
}
};
context.reserve_string_allocation(workspace_len)?;
Ok(SxdValue::String(percent_decode_uri(
&value,
encoding,
decoded_len,
transcoded_len,
)?))
}
}
}
}
fn extension_argument_error<T>(message: &str) -> std::result::Result<T, function::Error> {
Err(function::Error::Other {
what: message.into(),
})
}
fn dynamic_expression_error_is_recoverable(error: &Error) -> bool {
!matches!(
error.kind(),
ErrorKind::Budget | ErrorKind::Resource | ErrorKind::Resolver
)
}
fn percent_encode_uri(
value: &str,
escape_reserved: bool,
encoded_len: usize,
encoding: UriEncoding,
) -> std::result::Result<String, function::Error> {
const HEX: &[u8; 16] = b"0123456789ABCDEF";
let mut output = String::with_capacity(encoded_len);
let outcome = visit_uri_encoded_bytes(value, encoding, |byte| {
let character = char::from(byte);
if uri_byte_is_unescaped(character, escape_reserved) {
output.push(character);
} else {
output.push('%');
output.push(char::from(HEX[usize::from(byte >> 4)]));
output.push(char::from(HEX[usize::from(byte & 0x0f)]));
}
Ok(())
})?;
debug_assert_eq!(outcome, UriEncodingOutcome::Complete);
Ok(output)
}
fn percent_encoded_uri_len(
value: &str,
escape_reserved: bool,
encoding: UriEncoding,
) -> std::result::Result<Option<usize>, function::Error> {
let mut length = 0usize;
let outcome = visit_uri_encoded_bytes(value, encoding, |byte| {
let character = char::from(byte);
let encoded = !uri_byte_is_unescaped(character, escape_reserved);
length = length
.checked_add(if encoded { 3 } else { 1 })
.ok_or_else(|| function::Error::Other {
what: "str:encode-uri() result length overflow".into(),
})?;
Ok(())
})?;
Ok(outcome.is_complete().then_some(length))
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum UriEncodingOutcome {
Complete,
Unrepresentable,
}
impl UriEncodingOutcome {
fn is_complete(self) -> bool {
self == Self::Complete
}
}
fn visit_uri_encoded_bytes(
value: &str,
encoding: UriEncoding,
mut visit: impl FnMut(u8) -> std::result::Result<(), function::Error>,
) -> std::result::Result<UriEncodingOutcome, function::Error> {
match encoding {
UriEncoding::Registered(encoding) => {
for character in value.chars() {
let Some(byte) = encoding.encode_char(character) else {
return Ok(UriEncodingOutcome::Unrepresentable);
};
visit(byte)?;
}
}
UriEncoding::Standard(encoding) if encoding == encoding_rs::UTF_8 => {
for &byte in value.as_bytes() {
visit(byte)?;
}
}
UriEncoding::Standard(encoding) => {
let mut encoder = encoding.new_encoder();
let mut source = value;
let mut buffer = [0_u8; 1024];
loop {
let (result, read, written) =
encoder.encode_from_utf8_without_replacement(source, &mut buffer, true);
for &byte in &buffer[..written] {
visit(byte)?;
}
source = &source[read..];
match result {
encoding_rs::EncoderResult::InputEmpty => break,
encoding_rs::EncoderResult::OutputFull => {}
encoding_rs::EncoderResult::Unmappable(_) => {
return Ok(UriEncodingOutcome::Unrepresentable);
}
}
}
}
}
Ok(UriEncodingOutcome::Complete)
}
fn uri_byte_is_unescaped(character: char, escape_reserved: bool) -> bool {
const RESERVED: &str = ";/?:@&=+$,";
character.is_ascii_alphanumeric()
|| matches!(
character,
'-' | '_' | '.' | '!' | '~' | '*' | '\'' | '(' | ')'
)
|| (!escape_reserved && RESERVED.contains(character))
}
fn percent_decode_uri(
value: &str,
encoding: UriEncoding,
decoded_len: usize,
transcoded_len: usize,
) -> std::result::Result<String, function::Error> {
if let UriEncoding::Registered(encoding) = encoding {
let mut output = String::with_capacity(transcoded_len);
visit_percent_decoded_bytes(value, |byte| {
let character = encoding
.decode_byte(byte)
.ok_or_else(|| function::Error::Other {
what: format!("str:decode-uri() input is not valid {}", encoding.name()),
})?;
output.push(character);
Ok(())
})?;
return Ok(output);
}
let UriEncoding::Standard(encoding) = encoding else {
unreachable!("registered encodings return above")
};
let mut decoded = Vec::with_capacity(decoded_len);
visit_percent_decoded_bytes(value, |byte| {
decoded.push(byte);
Ok(())
})?;
let (value, _, had_errors) = encoding.decode(&decoded);
if had_errors {
return extension_argument_error(&format!(
"str:decode-uri() input is not valid {}",
encoding.name()
));
}
Ok(value.into_owned())
}
#[derive(Clone, Copy)]
enum UriEncoding {
Standard(&'static encoding_rs::Encoding),
Registered(xml_sec_xml_input::IanaSingleByteEncoding),
}
#[derive(Clone, Copy)]
enum UriEncodingCapability {
Encode,
Decode,
}
fn uri_encoding(label: Option<&str>, capability: UriEncodingCapability) -> Option<UriEncoding> {
label.map_or(Some(UriEncoding::Standard(encoding_rs::UTF_8)), |label| {
if label.is_empty() {
return None;
}
if let Some(encoding) = xml_sec_xml_input::registered_single_byte_encoding(label) {
return Some(UriEncoding::Registered(encoding));
}
let encoding = match capability {
UriEncodingCapability::Encode => {
encoding_rs::Encoding::for_label_no_replacement(label.as_bytes())
}
UriEncodingCapability::Decode => encoding_rs::Encoding::for_label(label.as_bytes()),
};
encoding
.filter(|encoding| xml_sec_xml_input::legacy_label_matches_encoding(label, encoding))
.map(UriEncoding::Standard)
})
}
fn registered_uri_decoded_len(
value: &str,
encoding: xml_sec_xml_input::IanaSingleByteEncoding,
) -> std::result::Result<usize, function::Error> {
let mut length = 0usize;
visit_percent_decoded_bytes(value, |byte| {
let character = encoding
.decode_byte(byte)
.ok_or_else(|| function::Error::Other {
what: format!("str:decode-uri() input is not valid {}", encoding.name()),
})?;
length =
length
.checked_add(character.len_utf8())
.ok_or_else(|| function::Error::Other {
what: "str:decode-uri() result length overflow".into(),
})?;
Ok(())
})?;
Ok(length)
}
fn visit_percent_decoded_bytes(
value: &str,
mut visit: impl FnMut(u8) -> std::result::Result<(), function::Error>,
) -> std::result::Result<(), function::Error> {
let bytes = value.as_bytes();
let mut cursor = 0;
while cursor < bytes.len() {
if bytes[cursor] == b'%' {
let encoded = bytes
.get(cursor + 1..cursor + 3)
.and_then(|encoded| std::str::from_utf8(encoded).ok())
.and_then(|encoded| u8::from_str_radix(encoded, 16).ok())
.ok_or_else(|| function::Error::Other {
what: "str:decode-uri() contains a malformed percent escape".into(),
})?;
visit(encoded)?;
cursor += 3;
} else {
visit(bytes[cursor])?;
cursor += 1;
}
}
Ok(())
}
fn percent_decoded_uri_len(value: &str) -> std::result::Result<Option<usize>, function::Error> {
let bytes = value.as_bytes();
let mut cursor = 0;
let mut decoded_len = 0usize;
while cursor < bytes.len() {
if bytes[cursor] == b'%' {
let Some(encoded) = bytes.get(cursor + 1..cursor + 3) else {
return Ok(None);
};
if !encoded.iter().all(u8::is_ascii_hexdigit) {
return Ok(None);
}
cursor += 3;
} else {
cursor += 1;
}
decoded_len = decoded_len
.checked_add(1)
.ok_or_else(|| function::Error::Other {
what: "str:decode-uri() result length overflow".into(),
})?;
}
Ok(Some(decoded_len))
}
impl<R: Resolver + ?Sized> function::Function for DocumentFunction<'_, R> {
fn evaluate<'c, 'd>(
&self,
context: &sxd_xpath_no_unsafe::context::Evaluation<'c, 'd>,
args: Vec<SxdValue<'d>>,
) -> std::result::Result<SxdValue<'d>, function::Error> {
if !(1..=2).contains(&args.len()) {
return Err(function::Error::Other {
what: "document() requires one or two arguments".into(),
});
}
let base_selection = if let Some(base) = args.get(1) {
let SxdValue::Nodeset(nodes) = base else {
return Err(function::Error::Other {
what: "document() second argument must be a node-set".into(),
});
};
let Some(node) = nodes
.document_order_with_context(context)?
.into_iter()
.next()
else {
return Ok(SxdValue::Nodeset(nodeset::Nodeset::new()));
};
let path = typed_path_to_with_context(&node, context)?;
let base_uris = self.node_base_uris.borrow();
DocumentBaseSelection::Explicit {
base_uri: clone_metered_optional_string(
context,
node_base_uri_at(&base_uris, &path),
)?,
logical_document: path.ordinary().get(1).copied(),
}
} else {
DocumentBaseSelection::Omitted
};
let mut result = nodeset::Nodeset::new();
let roots = self.roots.borrow();
let mut process = |request: DocumentRequest| -> std::result::Result<(), function::Error> {
let Some(cached) = roots.get(&request) else {
let mut pending = self.pending.borrow_mut();
if !pending.items.contains(&request) {
let capacity = pending.items.capacity();
let next_capacity = if pending.items.len() < capacity {
capacity
} else if capacity == 0 {
3
} else {
capacity.saturating_mul(2).saturating_add(1)
};
let index_growth = retained_hash_storage::<DocumentRequest>(next_capacity)
.saturating_sub(retained_hash_storage::<DocumentRequest>(capacity));
context.reserve_temporary_allocation(
index_growth.saturating_add(request.payload_owned_bytes()),
)?;
pending.payload_bytes = pending
.payload_bytes
.saturating_add(request.payload_owned_bytes());
pending.items.insert(request);
}
return Ok(());
};
if let Err(error) =
authorize_document_request(self.resolver, &request, cached.identity.as_deref())
{
*self.access_error.borrow_mut() = Some(error);
return Err(function::Error::Other {
what: "document resource access denied".into(),
});
}
for path in &cached.paths {
let node =
resolve_node_path(context, path)?.ok_or_else(|| function::Error::Other {
what: format!("document resource `{}` is stale", request.href),
})?;
result.add_metered(context, node)?;
}
Ok(())
};
match &args[0] {
SxdValue::Nodeset(nodes) => {
for node in nodes.iter() {
let path = typed_path_to_with_context(&node, context)?;
let (base_uri, logical_document) = match &base_selection {
DocumentBaseSelection::Explicit {
base_uri,
logical_document,
} => (
clone_metered_optional_string(context, base_uri.as_deref())?,
*logical_document,
),
DocumentBaseSelection::Omitted => {
let base_uris = self.node_base_uris.borrow();
(
clone_metered_optional_string(
context,
node_base_uri_at(&base_uris, &path),
)?,
path.ordinary().get(1).copied(),
)
}
};
let href = node.string_value_with_context(context)?;
context.charge_work(href.len())?;
context.reserve_temporary_allocation(std::mem::size_of::<DocumentRequest>())?;
process(DocumentRequest::relative_to(
href,
base_uri,
logical_document.map(EmptyDocumentId::Logical),
))?;
}
}
value => {
let (base_uri, logical_document) = match &base_selection {
DocumentBaseSelection::Explicit {
base_uri,
logical_document,
} => (
clone_metered_optional_string(context, base_uri.as_deref())?,
logical_document.map(EmptyDocumentId::Logical),
),
DocumentBaseSelection::Omitted => (
clone_metered_optional_string(context, self.static_base_uri.as_deref())?,
Some(EmptyDocumentId::Stylesheet(self.static_document)),
),
};
let href_len = value.string_len();
context.charge_work(href_len)?;
context.reserve_temporary_allocation(href_len)?;
let href = value.string();
context.charge_work(href.len())?;
context.reserve_temporary_allocation(std::mem::size_of::<DocumentRequest>())?;
process(DocumentRequest::relative_to(
href,
base_uri,
logical_document,
))?;
}
}
Ok(SxdValue::Nodeset(result))
}
}
struct SystemProperty {
namespaces: Arc<Vec<(String, String)>>,
}
impl function::Function for SystemProperty {
fn evaluate<'c, 'd>(
&self,
context: &sxd_xpath_no_unsafe::context::Evaluation<'c, 'd>,
args: Vec<SxdValue<'d>>,
) -> std::result::Result<SxdValue<'d>, function::Error> {
let name = one_qname_argument(context, args, &self.namespaces, "system-property")?;
if name.namespace.as_deref() == Some(super::compiler::XSLT_NS) && name.local == "version" {
return Ok(SxdValue::Number(1.0));
}
let value = if name.namespace.as_deref() == Some(super::compiler::XSLT_NS) {
match name.local.as_str() {
"vendor" => "structured-world xml-sec-xslt",
"vendor-url" => "https://github.com/structured-world/xml-sec",
_ => "",
}
} else {
""
};
Ok(SxdValue::String(value.into()))
}
}
struct ElementAvailable {
namespaces: Arc<Vec<(String, String)>>,
}
impl function::Function for ElementAvailable {
fn evaluate<'c, 'd>(
&self,
context: &sxd_xpath_no_unsafe::context::Evaluation<'c, 'd>,
args: Vec<SxdValue<'d>>,
) -> std::result::Result<SxdValue<'d>, function::Error> {
let name = one_qname_argument(context, args, &self.namespaces, "element-available")?;
let available = (name.namespace.as_deref() == Some(super::compiler::XSLT_NS)
&& matches!(
name.local.as_str(),
"apply-imports"
| "apply-templates"
| "attribute"
| "call-template"
| "choose"
| "comment"
| "copy"
| "copy-of"
| "element"
| "for-each"
| "fallback"
| "if"
| "message"
| "number"
| "processing-instruction"
| "text"
| "value-of"
| "variable"
))
|| (name.namespace.as_deref() == Some(super::compiler::EXSLT_FUNCTIONS_NS)
&& name.local == "result")
|| matches!(
(name.namespace.as_deref(), name.local.as_str()),
(Some(SAXON_NS), "output")
| (Some(XALAN_REDIRECT_NS), "write")
| (Some(XT_NS), "document")
);
Ok(SxdValue::Boolean(available))
}
}
fn hide_projection_elements_from_axes(source: &str) -> std::borrow::Cow<'_, str> {
const AXES: &[&str] = &[
"ancestor-or-self",
"ancestor",
"following",
"following-sibling",
"preceding",
"preceding-sibling",
"parent",
"self",
];
if !source.contains("..")
&& !AXES
.iter()
.any(|axis| source.split("::").any(|prefix| prefix.ends_with(axis)))
{
return std::borrow::Cow::Borrowed(source);
}
let mut output = String::with_capacity(source.len());
let mut quote = None;
let mut cursor = 0;
while cursor < source.len() {
let character = source[cursor..]
.chars()
.next()
.expect("cursor is in bounds");
if let Some(active) = quote {
output.push(character);
cursor += character.len_utf8();
if character == active {
quote = None;
}
continue;
}
if matches!(character, '\'' | '"') {
quote = Some(character);
output.push(character);
cursor += character.len_utf8();
continue;
}
if source[cursor..].starts_with("..")
&& source[..cursor]
.chars()
.next_back()
.is_none_or(|previous| previous != '.' && !is_xpath_name_character(previous))
&& source[cursor + 2..]
.chars()
.next()
.is_none_or(|next| next != '.' && !next.is_ascii_digit())
{
output
.push_str("..[parent::node()][not(self::__xml_sec_documents and not(parent::*))]");
cursor += 2;
continue;
}
let axis = AXES.iter().find(|axis| {
source[cursor..].starts_with(**axis) && source[cursor + axis.len()..].starts_with("::")
});
if let Some(axis) = axis
&& let Some(end) = xpath_axis_node_test_end(source, cursor + axis.len() + 2)
{
let node_test = trim_xml_whitespace(&source[cursor + axis.len() + 2..end]);
output.push_str(&source[cursor..end]);
output.push_str("[not(self::__xml_sec_documents and not(parent::*))]");
if matches!(*axis, "following-sibling" | "preceding-sibling") {
output.push_str("[not(self::__xml_sec_document and parent::__xml_sec_documents[not(parent::*)])]");
} else if xpath_test_matches_root(node_test) {
output.push_str("[parent::node()]");
} else {
output.push_str("[not(self::__xml_sec_document and parent::__xml_sec_documents[not(parent::*)])]");
}
cursor = end;
continue;
}
output.push(character);
cursor += character.len_utf8();
}
if output == source {
std::borrow::Cow::Borrowed(source)
} else {
std::borrow::Cow::Owned(output)
}
}
fn xpath_axis_node_test_end(source: &str, mut cursor: usize) -> Option<usize> {
while source[cursor..]
.chars()
.next()
.is_some_and(is_xml_whitespace)
{
cursor += source[cursor..].chars().next()?.len_utf8();
}
if source[cursor..].starts_with('*') {
return Some(cursor + 1);
}
let start = cursor;
while let Some(character) = source[cursor..].chars().next() {
if !is_xpath_name_character(character) && character != ':' && character != '*' {
break;
}
cursor += character.len_utf8();
}
if cursor == start {
return None;
}
while source[cursor..]
.chars()
.next()
.is_some_and(is_xml_whitespace)
{
cursor += source[cursor..].chars().next()?.len_utf8();
}
if !source[cursor..].starts_with('(') {
return Some(cursor);
}
let mut depth = 0usize;
let mut quote = None;
for (offset, character) in source[cursor..].char_indices() {
if let Some(active) = quote {
if character == active {
quote = None;
}
continue;
}
if matches!(character, '\'' | '"') {
quote = Some(character);
} else if character == '(' {
depth += 1;
} else if character == ')' {
depth -= 1;
if depth == 0 {
return Some(cursor + offset + 1);
}
}
}
None
}
fn xpath_test_matches_root(node_test: &str) -> bool {
let Some(arguments) = node_test
.strip_prefix("node")
.map(|value| value.trim_start_matches(is_xml_whitespace))
.and_then(|value| value.strip_prefix('('))
.and_then(|value| value.strip_suffix(')'))
else {
return false;
};
trim_xml_whitespace(arguments).is_empty()
}
struct FunctionAvailable {
namespaces: Arc<Vec<(String, String)>>,
stylesheet_functions: Arc<HashSet<ExpandedName>>,
}
impl function::Function for FunctionAvailable {
fn evaluate<'c, 'd>(
&self,
context: &sxd_xpath_no_unsafe::context::Evaluation<'c, 'd>,
args: Vec<SxdValue<'d>>,
) -> std::result::Result<SxdValue<'d>, function::Error> {
let name = one_qname_argument(context, args, &self.namespaces, "function-available")?;
let available = if name.namespace.is_some() {
self.stylesheet_functions.contains(&name) || is_builtin_extension_function(&name)
} else {
matches!(
name.local.as_str(),
"boolean"
| "ceiling"
| "concat"
| "contains"
| "count"
| "current"
| "document"
| "false"
| "floor"
| "format-number"
| "generate-id"
| "id"
| "key"
| "lang"
| "last"
| "local-name"
| "name"
| "namespace-uri"
| "normalize-space"
| "not"
| "number"
| "position"
| "round"
| "starts-with"
| "string"
| "string-length"
| "substring"
| "substring-after"
| "substring-before"
| "sum"
| "system-property"
| "translate"
| "true"
| "unparsed-entity-uri"
| "element-available"
| "function-available"
)
};
Ok(SxdValue::Boolean(available))
}
}
fn is_builtin_extension_function(name: &ExpandedName) -> bool {
match name.namespace.as_deref() {
Some(EXSLT_COMMON_NS) => matches!(name.local.as_str(), "node-set" | "object-type"),
Some(EXSLT_STRINGS_NS) => matches!(
name.local.as_str(),
"split" | "tokenize" | "replace" | "align" | "padding" | "encode-uri" | "decode-uri"
),
Some(EXSLT_DYNAMIC_NS) => matches!(name.local.as_str(), "evaluate" | "map"),
Some(EXSLT_MATH_NS) => matches!(
name.local.as_str(),
"max" | "min" | "highest" | "lowest" | "power"
),
Some(EXSLT_SETS_NS) => matches!(
name.local.as_str(),
"difference" | "intersection" | "distinct" | "has-same-node" | "leading" | "trailing"
),
Some(EXSLT_CRYPTO_NS) => {
matches!(
name.local.as_str(),
"md5" | "sha1" | "rc4_encrypt" | "rc4_decrypt"
)
}
Some(super::exslt_date::NAMESPACE) => {
super::exslt_date::function_names().any(|candidate| candidate == name.local)
}
Some(SAXON_NS) => matches!(
name.local.as_str(),
"expression" | "eval" | "evaluate" | "line-number" | "node-set"
),
Some(XT_NS | LIBXSLT_NS) => name.local == "node-set",
Some(LIBXSLT_TEST_NS) => name.local == "test",
Some(LIBXSLT_TEST_PLUGIN_NS) => name.local == "testplugin",
_ => false,
}
}
fn one_qname_argument(
context: &sxd_xpath_no_unsafe::context::Evaluation<'_, '_>,
args: Vec<SxdValue<'_>>,
namespaces: &[(String, String)],
function: &str,
) -> std::result::Result<ExpandedName, function::Error> {
if args.len() != 1 {
return Err(function::Error::Other {
what: format!("{function}() requires one argument"),
});
}
qname_argument(context, &args[0], namespaces)
}
fn qname_argument(
context: &sxd_xpath_no_unsafe::context::Evaluation<'_, '_>,
value: &SxdValue<'_>,
namespaces: &[(String, String)],
) -> std::result::Result<ExpandedName, function::Error> {
let lexical = function::string_argument_view(value, context)?;
context.charge_work(lexical.len())?;
let retained_bytes = lexical.len().saturating_add(
lexical
.split_once(':')
.and_then(|(prefix, _)| namespaces.iter().find(|(candidate, _)| candidate == prefix))
.map_or(0, |(_, namespace)| namespace.len()),
);
context.reserve_string_allocation(retained_bytes)?;
resolve_lexical_name(&lexical, namespaces)
}
struct CurrentNode {
path: NodePath,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum NodePath {
Ordinary(Vec<usize>),
Attribute {
parent: Vec<usize>,
namespace: Option<String>,
local: String,
},
Namespace {
parent: Vec<usize>,
prefix: String,
uri: String,
},
}
impl NodePath {
fn ordinary(&self) -> &[usize] {
match self {
Self::Ordinary(path)
| Self::Attribute { parent: path, .. }
| Self::Namespace { parent: path, .. } => path,
}
}
fn owned_bytes(&self) -> usize {
let payload = self
.ordinary()
.len()
.saturating_mul(std::mem::size_of::<usize>());
std::mem::size_of::<Self>().saturating_add(match self {
Self::Ordinary(_) => payload,
Self::Attribute {
namespace, local, ..
} => payload
.saturating_add(namespace.as_deref().map_or(0, str::len))
.saturating_add(local.len()),
Self::Namespace { prefix, uri, .. } => payload
.saturating_add(prefix.len())
.saturating_add(uri.len()),
})
}
}
#[cfg(test)]
fn typed_path_to(node: &nodeset::Node<'_>) -> NodePath {
build_node_path(node, &mut |_, _| Ok::<_, std::convert::Infallible>(()))
.expect("unlimited fixture path")
}
fn typed_path_to_metered(node: &nodeset::Node<'_>, meter: &mut Meter) -> Result<(NodePath, usize)> {
let mut reserved = 0usize;
let result = build_node_path(node, &mut |kind, amount| {
meter.charge(kind, amount)?;
if kind == BudgetKind::OwnedBytes {
reserved += amount;
}
Ok(())
});
match result {
Ok(path) => Ok((path, reserved)),
Err(error) => {
meter.release_owned_bytes(reserved);
Err(error)
}
}
}
fn typed_path_to_with_context(
node: &nodeset::Node<'_>,
context: &sxd_xpath_no_unsafe::context::Evaluation<'_, '_>,
) -> std::result::Result<NodePath, function::Error> {
build_node_path(node, &mut |kind, amount| match kind {
BudgetKind::OwnedBytes => context.reserve_temporary_allocation(amount),
_ => context.charge_work(amount),
})
}
fn build_node_path<E>(
node: &nodeset::Node<'_>,
charge: &mut impl FnMut(BudgetKind, usize) -> std::result::Result<(), E>,
) -> std::result::Result<NodePath, E> {
let owner = match node {
nodeset::Node::Attribute(attribute) => attribute.parent().map(nodeset::Node::Element),
nodeset::Node::Namespace(namespace) => Some(nodeset::Node::Element(namespace.parent)),
node => Some(node.clone()),
};
let mut depth = 0;
let mut current = owner.clone();
while let Some(node) = current {
charge(BudgetKind::XPathOperations, 1)?;
current = node.parent();
if current.is_some() {
depth += 1;
}
}
charge(BudgetKind::OwnedBytes, depth * std::mem::size_of::<usize>())?;
let mut path = Vec::with_capacity(depth);
let mut current = owner;
while let Some(node) = current {
let Some(parent) = node.parent() else {
break;
};
let mut index = 0;
loop {
charge(BudgetKind::XPathOperations, 1)?;
if parent.child_at(index).as_ref() == Some(&node) {
break;
}
index += 1;
}
path.push(index);
current = Some(parent);
}
path.reverse();
Ok(match node {
nodeset::Node::Attribute(attribute) => {
let name = attribute.name();
let name = name.get();
charge(
BudgetKind::OwnedBytes,
name.namespace_uri().map_or(0, str::len) + name.local_part().len(),
)?;
NodePath::Attribute {
parent: path,
namespace: name.namespace_uri().map(str::to_owned),
local: name.local_part().to_owned(),
}
}
nodeset::Node::Namespace(namespace) => {
charge(
BudgetKind::OwnedBytes,
namespace.prefix().len() + namespace.uri().len(),
)?;
NodePath::Namespace {
parent: path,
prefix: namespace.prefix().to_owned(),
uri: namespace.uri().to_owned(),
}
}
_ => NodePath::Ordinary(path),
})
}
type KeyIndex = HashMap<(usize, usize), HashMap<String, Vec<NodePath>>>;
struct KeyFunction {
index: Rc<RefCell<KeyIndex>>,
namespaces: Arc<Vec<(String, String)>>,
declarations: Arc<[KeyDeclaration]>,
name_indices: Arc<[usize]>,
ready: Rc<RefCell<HashSet<(usize, usize)>>>,
pending: Rc<RefCell<Option<(usize, usize)>>>,
}
impl function::Function for KeyFunction {
fn evaluate<'c, 'd>(
&self,
context: &sxd_xpath_no_unsafe::context::Evaluation<'c, 'd>,
args: Vec<SxdValue<'d>>,
) -> std::result::Result<SxdValue<'d>, function::Error> {
if args.len() != 2 {
return Err(function::Error::Other {
what: "key() requires exactly two arguments".into(),
});
}
let name = qname_argument(context, &args[0], &self.namespaces)?;
let document_index = typed_path_to_with_context(&context.node, context)?
.ordinary()
.get(1)
.copied()
.ok_or_else(|| function::Error::Other {
what: "key() context has no logical document".into(),
})?;
let key_slot = self
.name_indices
.iter()
.position(|index| self.declarations[*index].name == name);
if let Some(key_slot) = key_slot
&& !self.ready.borrow().contains(&(key_slot, document_index))
{
let mut pending = self.pending.borrow_mut();
if pending.is_none() {
*pending = Some((key_slot, document_index));
}
return Ok(SxdValue::Nodeset(nodeset::Nodeset::new()));
}
let mut result = nodeset::Nodeset::new();
let index = self.index.borrow();
let values = key_slot.and_then(|slot| index.get(&(slot, document_index)));
let mut lookup = |value: &str| -> std::result::Result<(), function::Error> {
context.charge_work(value.len())?;
if let Some(paths) = values.and_then(|values| values.get(value)) {
for path in paths {
if let Some(node) = resolve_node_path(context, path)? {
result.add_metered(context, node)?;
}
}
}
Ok(())
};
match &args[1] {
SxdValue::Nodeset(nodes) => {
for node in nodes.iter() {
lookup(&node.string_value_with_context(context)?)?;
}
}
value => {
lookup(&function::string_argument_view(value, context)?)?;
}
}
Ok(SxdValue::Nodeset(result))
}
}
struct FormatNumberFunction {
formats: Arc<[DecimalFormat]>,
namespaces: Arc<Vec<(String, String)>>,
}
impl function::Function for FormatNumberFunction {
fn evaluate<'c, 'd>(
&self,
context: &sxd_xpath_no_unsafe::context::Evaluation<'c, 'd>,
args: Vec<SxdValue<'d>>,
) -> std::result::Result<SxdValue<'d>, function::Error> {
if !(2..=3).contains(&args.len()) {
return Err(function::Error::Other {
what: "format-number() requires two or three arguments".into(),
});
}
let has_name = args.len() == 3;
let mut args = function::Args::new(args);
let name = if has_name {
let lexical = args.pop_string(context)?;
Some(resolve_lexical_name(&lexical, &self.namespaces)?)
} else {
None
};
let default_format;
let format =
if let Some(format) = self.formats.iter().rev().find(|format| format.name == name) {
format
} else if name.is_none() {
default_format = default_decimal_format();
&default_format
} else {
return Err(function::Error::Other {
what: "unknown decimal-format".into(),
});
};
let pattern = args.pop_string(context)?;
context.charge_work(pattern.len().saturating_mul(32).saturating_add(2_048))?;
context.reserve_string_allocation(decimal_render_workspace_bytes(&pattern))?;
Ok(SxdValue::String(render_decimal(
args.pop_number(context)?,
&pattern,
format,
context,
)?))
}
}
fn decimal_render_workspace_bytes(pattern: &str) -> usize {
let characters = pattern.chars().count();
let token_storage = characters.saturating_mul(
std::mem::size_of::<DecimalPatternToken>()
.saturating_add(std::mem::size_of::<Vec<DecimalPatternToken>>()),
);
token_storage
.saturating_add(pattern.len().saturating_mul(8))
.saturating_add(2_048)
}
fn resolve_lexical_name(
lexical: &str,
namespaces: &[(String, String)],
) -> std::result::Result<ExpandedName, function::Error> {
if let Some((prefix, local)) = lexical.split_once(':') {
if !is_ncname(prefix) || !is_ncname(local) || local.contains(':') {
return Err(function::Error::Other {
what: format!("invalid QName {lexical}"),
});
}
let namespace = namespaces
.iter()
.find(|(candidate, _)| candidate == prefix)
.map(|(_, uri)| uri.clone())
.ok_or_else(|| function::Error::Other {
what: format!("unbound QName prefix {prefix}"),
})?;
Ok(ExpandedName::new(Some(namespace), local))
} else if is_ncname(lexical) {
Ok(ExpandedName::new(None::<String>, lexical))
} else {
Err(function::Error::Other {
what: format!("invalid QName {lexical}"),
})
}
}
fn follow_path<'d>(mut node: nodeset::Node<'d>, path: &[usize]) -> Option<nodeset::Node<'d>> {
for index in path {
node = node.child_at(*index)?;
}
Some(node)
}
fn resolve_node_path<'d>(
context: &sxd_xpath_no_unsafe::context::Evaluation<'_, 'd>,
path: &NodePath,
) -> std::result::Result<Option<nodeset::Node<'d>>, function::Error> {
context.charge_work(path.ordinary().len().max(1))?;
let Some(node) = follow_path(context.node.document().root().into(), path.ordinary()) else {
return Ok(None);
};
match path {
NodePath::Ordinary(_) => Ok(Some(node)),
NodePath::Attribute {
namespace, local, ..
} => {
let Some(element) = node.element() else {
return Ok(None);
};
for index in 0..element.attributes_len() {
context.charge_work(1)?;
let attribute = element
.attribute_at(index)
.expect("attribute index is in range");
let name = attribute.name();
let name = name.get();
context.charge_work(
local
.len()
.saturating_add(namespace.as_deref().map_or(0, str::len)),
)?;
if name.local_part() == local && name.namespace_uri() == namespace.as_deref() {
return Ok(Some(nodeset::Node::Attribute(attribute)));
}
}
Ok(None)
}
NodePath::Namespace { prefix, uri, .. } => {
let Some(element) = node.element() else {
return Ok(None);
};
context.charge_work(
path.ordinary()
.len()
.saturating_add(1)
.saturating_mul(prefix.len().max(1))
.saturating_add(uri.len()),
)?;
let matches = if prefix == "xml" {
uri == "http://www.w3.org/XML/1998/namespace"
} else if prefix.is_empty() {
element.recursive_default_namespace_uri().as_deref() == Some(uri.as_str())
} else {
element.namespace_uri_for_prefix(prefix).as_deref() == Some(uri.as_str())
};
if !matches {
return Ok(None);
}
context.reserve_temporary_allocation(prefix.len().saturating_add(uri.len()))?;
Ok(Some(nodeset::Node::Namespace(nodeset::Namespace {
parent: element,
prefix: sxd_document_no_unsafe::to_ns_str!(prefix.as_str()),
uri: sxd_document_no_unsafe::to_ns_str!(uri.as_str()),
})))
}
}
}
fn resolve_attribute_node<'d>(
node: nodeset::Node<'d>,
namespace: Option<&str>,
local: &str,
) -> Option<nodeset::Node<'d>> {
let element = node.element()?;
for index in 0..element.attributes_len() {
let attribute = element.attribute_at(index)?;
let name = attribute.name();
let name = name.get();
if name.local_part() == local && name.namespace_uri() == namespace {
return Some(nodeset::Node::Attribute(attribute));
}
}
None
}
fn resolve_namespace_node<'d>(
node: nodeset::Node<'d>,
prefix: &str,
uri: &str,
) -> Option<nodeset::Node<'d>> {
let element = node.element()?;
let matches = if prefix == "xml" {
uri == "http://www.w3.org/XML/1998/namespace"
} else {
(prefix.is_empty() && element.recursive_default_namespace_uri().as_deref() == Some(uri))
|| element.namespace_uri_for_prefix(prefix).as_deref() == Some(uri)
};
if !matches {
return None;
}
Some(nodeset::Node::Namespace(nodeset::Namespace {
parent: element,
prefix: sxd_document_no_unsafe::to_ns_str!(prefix),
uri: sxd_document_no_unsafe::to_ns_str!(uri),
}))
}
fn default_decimal_format() -> DecimalFormat {
DecimalFormat {
name: None,
decimal_separator: '.',
grouping_separator: ',',
infinity: "Infinity".into(),
minus_sign: '-',
nan: "NaN".into(),
percent: '%',
per_mille: '‰',
zero_digit: '0',
digit: '#',
pattern_separator: ';',
}
}
fn render_decimal(
value: f64,
pattern: &str,
format: &DecimalFormat,
context: &sxd_xpath_no_unsafe::context::Evaluation<'_, '_>,
) -> std::result::Result<String, function::Error> {
if value.is_nan() {
context.charge_work(format.nan.len())?;
context.reserve_string_allocation(format.nan.len())?;
return Ok(format.nan.clone());
}
let alternatives = tokenize_decimal_pattern(pattern, format)?;
let negative = value.is_sign_negative();
let negative_subpattern = negative && alternatives.len() > 1;
let affix_pattern = if negative_subpattern {
&alternatives[1]
} else {
&alternatives[0]
};
let number_pattern = &alternatives[0];
let multiplier = if number_pattern
.iter()
.any(|token| token.syntax && token.value == format.percent)
{
100.0
} else if number_pattern
.iter()
.any(|token| token.syntax && token.value == format.per_mille)
{
1000.0
} else {
1.0
};
let (number_first, number_last) = decimal_pattern_bounds(number_pattern, format)?;
let (affix_first, affix_last) = decimal_pattern_bounds(affix_pattern, format)?;
let scaled = value.abs() * multiplier;
if scaled.is_infinite() {
context.charge_work(format.infinity.len())?;
context.reserve_string_allocation(format.infinity.len())?;
let mut output = String::with_capacity(
format
.infinity
.len()
.checked_add(pattern.len())
.and_then(|length| length.checked_add(format.minus_sign.len_utf8()))
.ok_or_else(|| function::Error::Other {
what: "decimal output size overflow".into(),
})?,
);
if negative && !negative_subpattern {
output.push(format.minus_sign);
}
output.extend(affix_pattern[..affix_first].iter().map(|token| token.value));
output.push_str(&format.infinity);
output.extend(affix_pattern[affix_last..].iter().map(|token| token.value));
return Ok(output);
}
let number = &number_pattern[number_first..number_last];
let decimal = number
.iter()
.position(|token| token.syntax && token.value == format.decimal_separator);
let (integer_pattern, fraction_pattern) = decimal.map_or((number, &[][..]), |index| {
(&number[..index], &number[index + 1..])
});
let minimum_integer = integer_pattern
.iter()
.filter(|token| token.syntax && token.value == format.zero_digit)
.count();
let minimum_fraction = fraction_pattern
.iter()
.filter(|token| token.syntax && token.value == format.zero_digit)
.count();
let mut maximum_fraction = fraction_pattern
.iter()
.filter(|token| {
token.syntax && matches!(token.value, value if value == format.zero_digit || value == format.digit)
})
.count();
let mut minimum_fraction = minimum_fraction;
if integer_pattern.is_empty()
&& number
.first()
.is_some_and(|token| token.syntax && token.value == format.decimal_separator)
&& maximum_fraction > 0
{
minimum_fraction = minimum_fraction.max(1);
maximum_fraction = maximum_fraction.max(1);
}
let factor = 10_f64.powi(i32::try_from(maximum_fraction).unwrap_or(i32::MAX));
let shifted = scaled * factor;
let rounded = if factor.is_finite() && shifted.is_finite() {
(shifted + 0.5).floor() / factor
} else {
scaled
};
let mut rendered = format!("{rounded:.maximum_fraction$}");
if maximum_fraction > minimum_fraction
&& let Some(decimal) = rendered.find('.')
{
let minimum_len = decimal + 1 + minimum_fraction;
while rendered.len() > minimum_len && rendered.ends_with('0') {
rendered.pop();
}
if rendered.ends_with('.') {
rendered.pop();
}
}
let (integer, fraction) = rendered.split_once('.').unwrap_or((&rendered, ""));
let mut integer = format!("{integer:0>minimum_integer$}");
if minimum_integer == 0 && minimum_fraction > 0 && integer == "0" {
integer.clear();
}
if format.zero_digit != '0' {
integer = localize_generated_decimal_digits(&integer, format.zero_digit);
}
let localized_fraction = (format.zero_digit != '0')
.then(|| localize_generated_decimal_digits(fraction, format.zero_digit));
let fraction = localized_fraction.as_deref().unwrap_or(fraction);
if let Some(group) = integer_pattern
.iter()
.rposition(|token| token.syntax && token.value == format.grouping_separator)
{
let size = integer_pattern[group + 1..]
.iter()
.filter(|token| {
token.syntax
&& matches!(token.value, value if value == format.zero_digit || value == format.digit)
})
.count();
if let Some(size) = std::num::NonZeroUsize::new(size) {
let digits = integer.chars().count();
let separators = digits.saturating_sub(1) / size;
let mut grouped = String::with_capacity(
integer.len() + separators * format.grouping_separator.len_utf8(),
);
for (index, digit) in integer.chars().enumerate() {
if index != 0 && (digits - index).is_multiple_of(size.get()) {
grouped.push(format.grouping_separator);
}
grouped.push(digit);
}
integer = grouped;
}
}
let mut output = String::new();
if negative && !negative_subpattern {
output.push(format.minus_sign);
}
output.extend(affix_pattern[..affix_first].iter().map(|token| token.value));
output.push_str(&integer);
if !fraction.is_empty()
|| number
.last()
.is_some_and(|token| token.syntax && token.value == format.decimal_separator)
{
output.push(format.decimal_separator);
if !fraction.is_empty() {
output.push_str(fraction);
}
}
output.extend(affix_pattern[affix_last..].iter().map(|token| token.value));
Ok(output)
}
fn localize_generated_decimal_digits(value: &str, zero_digit: char) -> String {
value
.chars()
.map(|character| {
character
.to_digit(10)
.filter(|_| character.is_ascii_digit())
.and_then(|digit| char::from_u32(u32::from(zero_digit) + digit))
.unwrap_or(character)
})
.collect()
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct DecimalPatternToken {
value: char,
syntax: bool,
}
fn tokenize_decimal_pattern(
pattern: &str,
format: &DecimalFormat,
) -> std::result::Result<Vec<Vec<DecimalPatternToken>>, function::Error> {
let mut alternatives = vec![Vec::new()];
let mut quoted = false;
let mut characters = pattern.chars().peekable();
while let Some(character) = characters.next() {
if character == '\'' {
if characters.peek() == Some(&'\'') {
characters.next();
alternatives
.last_mut()
.expect("decimal pattern has one subpattern")
.push(DecimalPatternToken {
value: '\'',
syntax: false,
});
} else {
quoted = !quoted;
}
continue;
}
if !quoted && character == format.pattern_separator {
alternatives.push(Vec::new());
continue;
}
alternatives
.last_mut()
.expect("decimal pattern has one subpattern")
.push(DecimalPatternToken {
value: character,
syntax: !quoted,
});
}
if quoted {
return Err(function::Error::Other {
what: "format-number pattern has an unterminated quoted literal".into(),
});
}
if alternatives.len() > 2 {
return Err(invalid_decimal_pattern("more than two subpatterns"));
}
for alternative in &alternatives {
validate_decimal_subpattern(alternative, format)?;
}
Ok(alternatives)
}
fn validate_decimal_subpattern(
pattern: &[DecimalPatternToken],
format: &DecimalFormat,
) -> std::result::Result<(), function::Error> {
let mut first = None;
let mut last = None;
let mut scaling_symbols = 0usize;
for (index, token) in pattern.iter().enumerate().filter(|(_, token)| token.syntax) {
if is_decimal_number_marker(token.value, format) {
first.get_or_insert(index);
last = Some(index);
}
if token.value == format.percent || token.value == format.per_mille {
scaling_symbols = scaling_symbols.saturating_add(1);
}
}
if scaling_symbols > 1 {
return Err(invalid_decimal_pattern(
"subpattern has more than one percent or per-mille symbol",
));
}
let (first, last) = first
.zip(last)
.ok_or_else(|| invalid_decimal_pattern("subpattern has no digit"))?;
let number = &pattern[first..=last];
let mut has_digit = false;
let mut saw_decimal = false;
let mut saw_required_integer = false;
let mut saw_optional_fraction = false;
for token in number {
if !token.syntax {
return Err(invalid_decimal_pattern(
"numeric portion contains a quoted literal",
));
}
if token.value == format.zero_digit {
has_digit = true;
if saw_decimal {
if saw_optional_fraction {
return Err(invalid_decimal_pattern(
"required fractional digit follows an optional digit",
));
}
} else {
saw_required_integer = true;
}
} else if token.value == format.digit {
has_digit = true;
if saw_decimal {
saw_optional_fraction = true;
} else if saw_required_integer {
return Err(invalid_decimal_pattern(
"optional integer digit follows a required digit",
));
}
} else if token.value == format.decimal_separator {
if saw_decimal {
return Err(invalid_decimal_pattern(
"subpattern has more than one decimal separator",
));
}
saw_decimal = true;
} else if token.value == format.grouping_separator {
if saw_decimal {
return Err(invalid_decimal_pattern(
"grouping separator appears in the fractional portion",
));
}
} else {
return Err(invalid_decimal_pattern(
"numeric portion contains an affix symbol",
));
}
}
if !(has_digit || number.len() == 1 && saw_decimal) {
return Err(invalid_decimal_pattern("subpattern has no digit"));
}
Ok(())
}
fn is_decimal_digit_symbol(value: char, format: &DecimalFormat) -> bool {
value == format.zero_digit || value == format.digit
}
fn is_decimal_number_marker(value: char, format: &DecimalFormat) -> bool {
is_decimal_digit_symbol(value, format) || value == format.decimal_separator
}
fn invalid_decimal_pattern(reason: &str) -> function::Error {
function::Error::Other {
what: format!("invalid format-number pattern: {reason}"),
}
}
fn decimal_pattern_bounds(
pattern: &[DecimalPatternToken],
format: &DecimalFormat,
) -> std::result::Result<(usize, usize), function::Error> {
let first = pattern.iter().position(|token| {
token.syntax
&& matches!(token.value, value if value == format.digit || value == format.zero_digit || value == format.decimal_separator)
});
let last = pattern.iter().rposition(|token| {
token.syntax
&& matches!(token.value, value if value == format.digit || value == format.zero_digit || value == format.decimal_separator)
});
first
.zip(last)
.map(|(first, last)| (first, last + 1))
.ok_or_else(|| invalid_decimal_pattern("subpattern has no digit"))
}
impl function::Function for CurrentNode {
fn evaluate<'c, 'd>(
&self,
context: &sxd_xpath_no_unsafe::context::Evaluation<'c, 'd>,
args: Vec<SxdValue<'d>>,
) -> std::result::Result<SxdValue<'d>, function::Error> {
if !args.is_empty() {
return Err(function::Error::TooManyArguments {
expected: 0,
actual: args.len(),
});
}
let node =
resolve_node_path(context, &self.path)?.ok_or_else(|| function::Error::Other {
what: "current() context identity is stale".into(),
})?;
let mut set = nodeset::Nodeset::new();
set.add_metered(context, node)?;
Ok(SxdValue::Nodeset(set))
}
}
#[cfg(all(test, not(feature = "embedded")))]
mod tests;