Skip to main content

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}