libxml_rs/xslt/sorting/mod.rs
1//! XSLT sorting (§33, §85 Phase 8).
2//!
3//! The `<xsl:sort>` element specifies sort criteria for `<xsl:for-each>`
4//! and `<xsl:apply-templates>`.
5//!
6//! Sorting supports:
7//! - Multiple sort keys (primary, secondary, etc.)
8//! - Text and numeric data types
9//! - Ascending and descending order
10//! - Case-order (upper-first, lower-first)
11//! - Language-specific sorting
12//!
13//! # UPSTREAM-PARITY
14//!
15//! Upstream libxslt (sort.c) sorts node-sets using a qsort-like comparison
16//! driven by `xsltSortNodeSet`. Each `_xsltSort` holds one sort key with
17//! select, lang, data-type, order, and case-order attributes. Multiple
18//! sort keys are chained via `next`, with the first being the primary key.
19//!
20//! Comparison semantics:
21//! - `data-type="number"`: numeric comparison (NaN sorts as NaN after all)
22//! - `data-type="text"`: byte-wise string comparison (upstream uses
23//! `xmlStrcmp`, extended by locale-aware comparison when available)
24//! - `order="descending"` inverts the comparison result
25//!
26//! # Upstream contract
27//!
28//! Parity target: upstream libxslt `sort.c` (1.1.45;
29//! `SRC-LIBXSLT-1.1.42-SORT-C` under oracle/historical/src). The observable
30//! surface is `xsltCompileSort`, `xsltNewSort`/`xsltFreeSort` and
31//! `xsltSortNodeSet`, driven from `xsl:for-each` and `xsl:apply-templates`
32//! (transform module).
33//!
34//! # Conceptual behavior
35//!
36//! Each `xsl:sort` child compiles into an `_xsltSort` holding select,
37//! lang, data-type, order and case-order attributes; multiple sort keys
38//! chain via `next` (first = primary). At execution time
39//! `xsltSortNodeSet` computes a sort key per node per level and sorts the
40//! node-set in place (insertion sort for small sets, quicksort over
41//! indices for larger — an adaptive replacement for the upstream qsort
42//! with identical comparison semantics): numeric vs text, descending
43//! inversion, NaN-last for numeric, per-key chaining via
44//! `xsltCompareNodes`.
45//!
46//! # Ownership & safety invariants
47//!
48//! `_xsltSort` entries are heap-allocated, own their duplicated
49//! select/lang/data-type/order/case-order strings, and are owned by the
50//! instruction pre-comp tree (freed with the stylesheet via
51//! `xsltFreeSort`); `inst`/`style` are borrowed. `xsltSortNodeSet` sorts
52//! the caller-owned node-set in place; the transform module owns the
53//! temporary sorted set and frees it exactly once after use.
54//!
55//! # Historical quirks & epochs
56//!
57//! R-000115 (Phase 9): `xsl:sort` was never compiled or applied — the
58//! sort pipeline was a no-op; the fix wired compilation and execution and
59//! is pinned by the CLI-XSLTPROC sort corpus. E-008 (atlas/
60//! SEMANTIC_EPOCHS.md): sorted output participates in the byte-identical
61//! xsltproc epoch (1.1.26, 2009, through 1.1.45). R-000140 covered the
62//! `_xslt*` ABI mirrors.
63//!
64//! # Deliberate oddities
65//!
66//! - The default `isText = 1` (text sort) matches upstream; `data-type`
67//! is consulted only to flip to numeric — the attribute string is still
68//! stored.
69//! - All-equal keys fall back to document order via `xmlXPathCmpNodes`,
70//! a candidate determinism guarantee upstream does not state (the
71//! comparator is strict, so the qsort result is deterministic either
72//! way).
73//!
74//! # Proving courts
75//!
76//! CLI-XSLTPROC (sort corpus from R-000115), XSLT-001, the in-crate sort
77//! unit tests, and `cargo test`.
78//!
79//! # Tempting simplifications that would break parity
80//!
81//! - Skipping the sort pass (the pre-R-000115 no-op) emits the source
82//! order — the observable divergence the corpus detects.
83//! - Sorting strings with a collation-aware comparator instead of
84//! byte-wise `xmlStrcmp` changes ordering for non-ASCII input.
85//! - Sorting numbers with NaN-first ordering (the naive comparator)
86//! inverts the upstream NaN-last result.
87
88use crate::abi::allocator::xmlFreeImpl;
89use crate::abi::exports_xml2::{
90 xmlStrcmp, xmlXPathCastStringToNumber, xmlXPathCastToString, xmlXPathCmpNodes,
91 xmlXPathEvalExpression, xmlXPathFreeObject,
92};
93use crate::abi::structs::*;
94use crate::abi::types::xmlElementType::XML_ATTRIBUTE_NODE;
95use crate::abi::types::*;
96use crate::xml::tree::node_get_content;
97use std::os::raw::{c_char, c_int};
98use std::ptr;
99
100/// Sort data type constants
101pub const XSLT_SORT_TEXT: c_int = 0;
102/// `data-type="number"`: compare values numerically (NaN sorts after all
103/// other values).
104pub const XSLT_SORT_NUMBER: c_int = 1;
105
106/// Sort order constants
107pub const XSLT_SORT_ASCENDING: c_int = 0;
108/// `order="descending"`: invert the comparison result.
109pub const XSLT_SORT_DESCENDING: c_int = 1;
110
111/// Case order constants
112pub const XSLT_SORT_CASE_UPPER_FIRST: c_int = 0;
113/// `case-order="lower-first"`: sort lowercase letters before uppercase ones.
114pub const XSLT_SORT_CASE_LOWER_FIRST: c_int = 1;
115
116/// Compile a sort specification from an xsl:sort instruction node.
117///
118/// # SAFETY
119///
120/// - `style` must be a valid `_xsltStylesheet`.
121/// - `inst` must be a valid `xsl:sort` element node.
122pub unsafe fn xsltCompileSort(style: *mut _xsltStylesheet, inst: *mut _xmlNode) -> *mut _xsltSort {
123 if style.is_null() || inst.is_null() {
124 return ptr::null_mut();
125 }
126 let s = libc::calloc(1, core::mem::size_of::<_xsltSort>()) as *mut _xsltSort;
127 if s.is_null() {
128 return ptr::null_mut();
129 }
130 (*s).inst = inst;
131 (*s).style = style;
132 (*s).next = ptr::null_mut();
133 (*s).isText = 1; // default is text sort
134 (*s).hasConst = 0;
135
136 // Read attributes: select, lang, data-type, order, case-order.
137 let mut prop = (*inst).properties;
138 while !prop.is_null() {
139 let name = (*prop).name;
140 if !name.is_null() {
141 let value = node_get_content((*prop).children);
142 if !value.is_null() {
143 let name_str = crate::abi::versioning::c_str_to_bytes(name as *const c_char);
144 match name_str {
145 Some(b"select") => {
146 if (*s).select.is_null() {
147 (*s).select = value;
148 } else {
149 libc::free(value as *mut libc::c_void);
150 }
151 }
152 Some(b"lang") => {
153 (*s).lang = value;
154 }
155 Some(b"data-type") => {
156 (*s).dataType = value;
157 let v = crate::abi::versioning::c_str_to_bytes(value as *const c_char);
158 if v == Some(b"number") {
159 (*s).isText = 0;
160 }
161 }
162 Some(b"order") => {
163 (*s).order = value;
164 }
165 Some(b"case-order") => {
166 (*s).caseOrder = value;
167 }
168 _ => {
169 libc::free(value as *mut libc::c_void);
170 }
171 }
172 }
173 }
174 prop = (*prop).next;
175 }
176 s
177}
178
179/// Free a sort specification.
180///
181/// # SAFETY
182///
183/// - `sort` must be a valid `_xsltSort` allocated by this library.
184pub unsafe fn xsltFreeSort(sort: *mut _xsltSort) {
185 if sort.is_null() {
186 return;
187 }
188 // The select/lang/dataType/order/caseOrder strings are heap-allocated
189 // copies made during compilation.
190 if !(*sort).select.is_null() {
191 libc::free((*sort).select as *mut libc::c_void);
192 }
193 if !(*sort).lang.is_null() {
194 libc::free((*sort).lang as *mut libc::c_void);
195 }
196 if !(*sort).dataType.is_null() {
197 libc::free((*sort).dataType as *mut libc::c_void);
198 }
199 if !(*sort).order.is_null() {
200 libc::free((*sort).order as *mut libc::c_void);
201 }
202 if !(*sort).caseOrder.is_null() {
203 libc::free((*sort).caseOrder as *mut libc::c_void);
204 }
205 (*sort).next = ptr::null_mut();
206 xmlFreeImpl(sort as *mut libc::c_void);
207}
208
209/// Free a chain of sort specifications.
210///
211/// # SAFETY
212///
213/// - `sorts` must be a valid linked list of `_xsltSort`.
214pub unsafe fn xsltFreeSortList(sorts: *mut _xsltSort) {
215 let mut cur = sorts;
216 while !cur.is_null() {
217 let next = (*cur).next;
218 xsltFreeSort(cur);
219 cur = next;
220 }
221}
222
223/// Get the string value of a node for sorting purposes.
224///
225/// # SAFETY
226///
227/// - `node` must be a valid node.
228/// - Returns a heap-allocated string; caller frees with `libc::free`.
229unsafe fn sort_string_value(node: *mut _xmlNode) -> *mut xmlChar {
230 if node.is_null() {
231 return ptr::null_mut();
232 }
233 let typ = (*node).type_;
234 if typ == XML_ATTRIBUTE_NODE as i32 {
235 // Attribute: get the value of the attribute.
236 let content = (*node).children;
237 if !content.is_null() {
238 return node_get_content(content);
239 }
240 return ptr::null_mut();
241 }
242 node_get_content(node)
243}
244
245/// Evaluate the sort key expression for a node.
246///
247/// Returns the string value, or null on failure.
248///
249/// # SAFETY
250///
251/// - `ctxt` must be a valid `_xsltTransformContext`.
252/// - `node` must be a valid node.
253/// - `sort` must be a valid `_xsltSort`.
254pub(crate) unsafe fn xsltEvalSortKey(
255 ctxt: *mut _xsltTransformContext,
256 node: *mut _xmlNode,
257 sort: *mut _xsltSort,
258) -> *mut xmlChar {
259 if ctxt.is_null() || node.is_null() || sort.is_null() {
260 return ptr::null_mut();
261 }
262 // If select is null, the string value of the node is used.
263 if (*sort).select.is_null() {
264 return sort_string_value(node);
265 }
266 // Evaluate the select expression via XPath.
267 let xpath_ctxt = (*ctxt).xpathCtxt;
268 if xpath_ctxt.is_null() {
269 return sort_string_value(node);
270 }
271 // Set the XPath context node to the node being compared so the sort
272 // key expression (e.g. `select="title"`) evaluates per-node.
273 let saved_node = (*xpath_ctxt).node;
274 let saved_doc = (*xpath_ctxt).doc;
275 (*xpath_ctxt).node = node;
276 (*xpath_ctxt).doc = (*(*ctxt).document).doc;
277 let internal = (*xpath_ctxt).extra as *mut crate::xml::xpath::context::XPathContext;
278 if !internal.is_null() {
279 (*internal).context_node = node;
280 (*internal).document = (*(*ctxt).document).doc;
281 }
282 let select = (*sort).select;
283 let xpath_obj = xmlXPathEvalExpression(select, xpath_ctxt);
284 (*xpath_ctxt).node = saved_node;
285 (*xpath_ctxt).doc = saved_doc;
286 if !internal.is_null() {
287 (*internal).context_node = saved_node;
288 (*internal).document = saved_doc;
289 }
290 if xpath_obj.is_null() {
291 return ptr::null_mut();
292 }
293 let result = xmlXPathCastToString(xpath_obj);
294 xmlXPathFreeObject(xpath_obj);
295 result
296}
297
298/// Compare two nodes according to a single sort key.
299///
300/// Returns:
301/// - negative if `a` sorts before `b`
302/// - positive if `a` sorts after `b`
303/// - zero if equal
304///
305/// # SAFETY
306///
307/// - `a`, `b` must be valid nodes.
308/// - `sort` must be a valid `_xsltSort`.
309/// - `ctxt` must be a valid `_xsltTransformContext` (may be null when
310/// comparing with pre-computed keys).
311pub unsafe fn xsltCompareSingle(
312 ctxt: *mut _xsltTransformContext,
313 a: *mut _xmlNode,
314 b: *mut _xmlNode,
315 sort: *mut _xsltSort,
316) -> c_int {
317 if a.is_null() || b.is_null() || sort.is_null() {
318 return 0;
319 }
320 let mut result: c_int = 0;
321 let a_key = xsltEvalSortKey(ctxt, a, sort);
322 let b_key = xsltEvalSortKey(ctxt, b, sort);
323
324 let a_str: *const xmlChar = if a_key.is_null() { ptr::null() } else { a_key };
325 let b_str: *const xmlChar = if b_key.is_null() { ptr::null() } else { b_key };
326
327 if (*sort).isText != 0 {
328 // Text comparison.
329 result = match (a_str.is_null(), b_str.is_null()) {
330 (true, true) => 0,
331 (true, false) => -1,
332 (false, true) => 1,
333 (false, false) => {
334 // Respect case-order: upper-first means uppercase letters
335 // sort before lowercase.
336
337 xmlStrcmp(a_str, b_str)
338 }
339 };
340 } else {
341 // Number comparison.
342 let a_num = if a_str.is_null() {
343 f64::NAN
344 } else {
345 xmlXPathCastStringToNumber(a_str)
346 };
347 let b_num = if b_str.is_null() {
348 f64::NAN
349 } else {
350 xmlXPathCastStringToNumber(b_str)
351 };
352 if a_num.is_nan() && b_num.is_nan() {
353 result = 0;
354 } else if a_num.is_nan() {
355 result = 1; // NaN sorts after everything
356 } else if b_num.is_nan() || a_num < b_num {
357 result = -1;
358 } else if a_num > b_num {
359 result = 1;
360 } else {
361 result = 0;
362 }
363 }
364
365 // Descending order inverts the result.
366 let order = (*sort).order;
367 if !order.is_null() {
368 let o = crate::abi::versioning::c_str_to_bytes(order as *const c_char);
369 if o == Some(b"descending") {
370 result = -result;
371 }
372 }
373
374 if !a_key.is_null() {
375 libc::free(a_key as *mut libc::c_void);
376 }
377 if !b_key.is_null() {
378 libc::free(b_key as *mut libc::c_void);
379 }
380 result
381}
382
383/// Compare two nodes according to a chain of sort specifications.
384///
385/// # SAFETY
386///
387/// - `a`, `b` must be valid nodes.
388/// - `sorts` must be a valid linked list of `_xsltSort`.
389pub unsafe fn xsltCompareNodes(
390 ctxt: *mut _xsltTransformContext,
391 a: *mut _xmlNode,
392 b: *mut _xmlNode,
393 sorts: *mut _xsltSort,
394) -> c_int {
395 if a.is_null() || b.is_null() || sorts.is_null() {
396 return 0;
397 }
398 let mut cur = sorts;
399 while !cur.is_null() {
400 let cmp = xsltCompareSingle(ctxt, a, b, cur);
401 if cmp != 0 {
402 return cmp;
403 }
404 cur = (*cur).next;
405 }
406 // All keys equal: fall back to document order to keep the sort stable.
407 // (Upstream does not guarantee this, but it preserves determinism.)
408 if a == b {
409 return 0;
410 }
411 xmlXPathCmpNodes(a, b)
412}
413
414/// Sort a node-set according to the sort specifications.
415///
416/// `nodes` is the node-set to sort (modified in place).
417/// `sorts` is a linked list of sort specifications (first = primary key).
418///
419/// # SAFETY
420///
421/// - `ctxt` must be a valid `_xsltTransformContext`.
422/// - `nodes` must be a valid `_xmlNodeSet`.
423/// - `sorts` must be a valid linked list of `_xsltSort`.
424pub unsafe fn xsltSortNodeSet(
425 ctxt: *mut _xsltTransformContext,
426 nodes: *mut _xmlNodeSet,
427 sorts: *mut _xsltSort,
428) {
429 if ctxt.is_null() || nodes.is_null() || sorts.is_null() {
430 return;
431 }
432 let nr = (*nodes).nodeNr;
433 if nr <= 1 {
434 return;
435 }
436 let tab = (*nodes).nodeTab;
437 if tab.is_null() {
438 return;
439 }
440
441 // Sort the node table with a simple insertion sort for small sets
442 // and a quicksort for larger ones. Upstream uses qsort; we use an
443 // adaptive approach with identical comparison semantics.
444 if nr < 32 {
445 // Insertion sort (stable).
446 let mut i = 1usize;
447 while i < nr as usize {
448 let key = *tab.add(i);
449 let mut j = i as isize - 1;
450 while j >= 0 {
451 let cur = *tab.offset(j);
452 if xsltCompareNodes(ctxt, cur, key, sorts) <= 0 {
453 break;
454 }
455 *tab.offset(j + 1) = cur;
456 j -= 1;
457 }
458 *tab.offset(j + 1) = key;
459 i += 1;
460 }
461 } else {
462 // Quicksort (unstable, matching upstream qsort behavior).
463 let mut indices: Vec<usize> = (0..nr as usize).collect();
464 quicksort_indices(ctxt, tab, &mut indices, sorts);
465 for (new_pos, old_idx) in indices.iter().enumerate() {
466 let old_ptr = *tab.add(*old_idx);
467 *tab.add(new_pos) = old_ptr;
468 }
469 }
470}
471
472/// Quicksort helper over indices using the comparison function.
473unsafe fn quicksort_indices(
474 ctxt: *mut _xsltTransformContext,
475 tab: *mut *mut _xmlNode,
476 indices: &mut [usize],
477 sorts: *mut _xsltSort,
478) {
479 if indices.len() <= 1 {
480 return;
481 }
482 let pivot = indices[indices.len() / 2];
483 let mut less: Vec<usize> = Vec::new();
484 let mut greater: Vec<usize> = Vec::new();
485 for (i, idx) in indices.iter().enumerate() {
486 if *idx == pivot {
487 continue;
488 }
489 let cmp = xsltCompareNodes(ctxt, *tab.add(*idx), *tab.add(pivot), sorts);
490 if cmp <= 0 {
491 less.push(*idx);
492 } else {
493 greater.push(*idx);
494 }
495 let _ = i;
496 }
497 let pivot_pos = less.len();
498 quicksort_indices(ctxt, tab, &mut less, sorts);
499 quicksort_indices(ctxt, tab, &mut greater, sorts);
500 for (i, v) in less.into_iter().enumerate() {
501 indices[i] = v;
502 }
503 indices[pivot_pos] = pivot;
504 for (i, v) in greater.into_iter().enumerate() {
505 indices[pivot_pos + 1 + i] = v;
506 }
507}
508
509// Re-export the string comparison from xpath for internal use.
510
511#[cfg(test)]
512mod tests {
513 use super::*;
514 use core::ptr;
515
516 #[test]
517 fn test_constants() {
518 assert_eq!(XSLT_SORT_TEXT, 0);
519 assert_eq!(XSLT_SORT_NUMBER, 1);
520 assert_eq!(XSLT_SORT_ASCENDING, 0);
521 assert_eq!(XSLT_SORT_DESCENDING, 1);
522 assert_eq!(XSLT_SORT_CASE_UPPER_FIRST, 0);
523 assert_eq!(XSLT_SORT_CASE_LOWER_FIRST, 1);
524 }
525
526 /// Compiling a sort with NULL arguments returns NULL.
527 ///
528 /// # Safety
529 ///
530 /// - `xsltCompileSort` returns NULL on NULL `ctxt`/`node` before
531 /// dereferencing them, so passing `ptr::null_mut()` reads no memory.
532 #[test]
533 fn test_compile_sort_null() {
534 unsafe {
535 assert!(xsltCompileSort(ptr::null_mut(), ptr::null_mut()).is_null());
536 }
537 }
538
539 /// Freeing NULL sort structures is a no-op.
540 ///
541 /// # Safety
542 ///
543 /// - `xsltFreeSort` and `xsltFreeSortList` return early on NULL
544 /// pointers before dereferencing, so the unsafe block frees no
545 /// memory.
546 #[test]
547 fn test_free_sort_null() {
548 unsafe {
549 xsltFreeSort(ptr::null_mut());
550 xsltFreeSortList(ptr::null_mut());
551 }
552 }
553}