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
26use crate::abi::allocator::xmlFree;
27use crate::abi::exports_xml2::{
28    xmlStrcmp, xmlXPathCastStringToNumber, xmlXPathCastToString, xmlXPathCmpNodes,
29    xmlXPathEvalExpression, xmlXPathFreeObject,
30};
31use crate::abi::structs::*;
32use crate::abi::types::xmlElementType::XML_ATTRIBUTE_NODE;
33use crate::abi::types::*;
34use crate::xml::tree::node_get_content;
35use std::os::raw::{c_char, c_int};
36use std::ptr;
37
38/// Sort data type constants
39pub const XSLT_SORT_TEXT: c_int = 0;
40pub const XSLT_SORT_NUMBER: c_int = 1;
41
42/// Sort order constants
43pub const XSLT_SORT_ASCENDING: c_int = 0;
44pub const XSLT_SORT_DESCENDING: c_int = 1;
45
46/// Case order constants
47pub const XSLT_SORT_CASE_UPPER_FIRST: c_int = 0;
48pub const XSLT_SORT_CASE_LOWER_FIRST: c_int = 1;
49
50/// Compile a sort specification from an xsl:sort instruction node.
51///
52/// # SAFETY
53///
54/// - `style` must be a valid `_xsltStylesheet`.
55/// - `inst` must be a valid `xsl:sort` element node.
56pub unsafe fn xsltCompileSort(style: *mut _xsltStylesheet, inst: *mut _xmlNode) -> *mut _xsltSort {
57    if style.is_null() || inst.is_null() {
58        return ptr::null_mut();
59    }
60    let s = libc::calloc(1, core::mem::size_of::<_xsltSort>()) as *mut _xsltSort;
61    if s.is_null() {
62        return ptr::null_mut();
63    }
64    (*s).inst = inst;
65    (*s).style = style;
66    (*s).next = ptr::null_mut();
67    (*s).isText = 1; // default is text sort
68    (*s).hasConst = 0;
69
70    // Read attributes: select, lang, data-type, order, case-order.
71    let mut prop = (*inst).properties;
72    while !prop.is_null() {
73        let name = (*prop).name;
74        if !name.is_null() {
75            let value = node_get_content((*prop).children);
76            if !value.is_null() {
77                let name_str = crate::abi::versioning::c_str_to_bytes(name as *const c_char);
78                match name_str {
79                    Some(b"select") => {
80                        if (*s).select.is_null() {
81                            (*s).select = value;
82                        } else {
83                            libc::free(value as *mut libc::c_void);
84                        }
85                    }
86                    Some(b"lang") => {
87                        (*s).lang = value;
88                    }
89                    Some(b"data-type") => {
90                        (*s).dataType = value;
91                        let v = crate::abi::versioning::c_str_to_bytes(value as *const c_char);
92                        if v == Some(b"number") {
93                            (*s).isText = 0;
94                        }
95                    }
96                    Some(b"order") => {
97                        (*s).order = value;
98                    }
99                    Some(b"case-order") => {
100                        (*s).caseOrder = value;
101                    }
102                    _ => {
103                        libc::free(value as *mut libc::c_void);
104                    }
105                }
106            }
107        }
108        prop = (*prop).next;
109    }
110    s
111}
112
113/// Free a sort specification.
114///
115/// # SAFETY
116///
117/// - `sort` must be a valid `_xsltSort` allocated by this library.
118pub unsafe fn xsltFreeSort(sort: *mut _xsltSort) {
119    if sort.is_null() {
120        return;
121    }
122    // The select/lang/dataType/order/caseOrder strings are heap-allocated
123    // copies made during compilation.
124    if !(*sort).select.is_null() {
125        libc::free((*sort).select as *mut libc::c_void);
126    }
127    if !(*sort).lang.is_null() {
128        libc::free((*sort).lang as *mut libc::c_void);
129    }
130    if !(*sort).dataType.is_null() {
131        libc::free((*sort).dataType as *mut libc::c_void);
132    }
133    if !(*sort).order.is_null() {
134        libc::free((*sort).order as *mut libc::c_void);
135    }
136    if !(*sort).caseOrder.is_null() {
137        libc::free((*sort).caseOrder as *mut libc::c_void);
138    }
139    (*sort).next = ptr::null_mut();
140    xmlFree(sort as *mut libc::c_void);
141}
142
143/// Free a chain of sort specifications.
144///
145/// # SAFETY
146///
147/// - `sorts` must be a valid linked list of `_xsltSort`.
148pub unsafe fn xsltFreeSortList(sorts: *mut _xsltSort) {
149    let mut cur = sorts;
150    while !cur.is_null() {
151        let next = (*cur).next;
152        xsltFreeSort(cur);
153        cur = next;
154    }
155}
156
157/// Get the string value of a node for sorting purposes.
158///
159/// # SAFETY
160///
161/// - `node` must be a valid node.
162/// - Returns a heap-allocated string; caller frees with `libc::free`.
163unsafe fn sort_string_value(node: *mut _xmlNode) -> *mut xmlChar {
164    if node.is_null() {
165        return ptr::null_mut();
166    }
167    let typ = (*node).type_;
168    if typ == XML_ATTRIBUTE_NODE as i32 {
169        // Attribute: get the value of the attribute.
170        let content = (*node).children;
171        if !content.is_null() {
172            return node_get_content(content);
173        }
174        return ptr::null_mut();
175    }
176    node_get_content(node)
177}
178
179/// Evaluate the sort key expression for a node.
180///
181/// Returns the string value, or null on failure.
182///
183/// # SAFETY
184///
185/// - `ctxt` must be a valid `_xsltTransformContext`.
186/// - `node` must be a valid node.
187/// - `sort` must be a valid `_xsltSort`.
188unsafe fn xsltEvalSortKey(
189    ctxt: *mut _xsltTransformContext,
190    node: *mut _xmlNode,
191    sort: *mut _xsltSort,
192) -> *mut xmlChar {
193    if ctxt.is_null() || node.is_null() || sort.is_null() {
194        return ptr::null_mut();
195    }
196    // If select is null, the string value of the node is used.
197    if (*sort).select.is_null() {
198        return sort_string_value(node);
199    }
200    // Evaluate the select expression via XPath.
201    let xpath_ctxt = (*ctxt).xpathCtxt;
202    if xpath_ctxt.is_null() {
203        return sort_string_value(node);
204    }
205    let select = (*sort).select;
206    let xpath_obj = xmlXPathEvalExpression(select, xpath_ctxt);
207    if xpath_obj.is_null() {
208        return ptr::null_mut();
209    }
210    let result = xmlXPathCastToString(xpath_obj);
211    xmlXPathFreeObject(xpath_obj);
212    result
213}
214
215/// Compare two nodes according to a single sort key.
216///
217/// Returns:
218/// - negative if `a` sorts before `b`
219/// - positive if `a` sorts after `b`
220/// - zero if equal
221///
222/// # SAFETY
223///
224/// - `a`, `b` must be valid nodes.
225/// - `sort` must be a valid `_xsltSort`.
226/// - `ctxt` must be a valid `_xsltTransformContext` (may be null when
227///   comparing with pre-computed keys).
228pub unsafe fn xsltCompareSingle(
229    ctxt: *mut _xsltTransformContext,
230    a: *mut _xmlNode,
231    b: *mut _xmlNode,
232    sort: *mut _xsltSort,
233) -> c_int {
234    if a.is_null() || b.is_null() || sort.is_null() {
235        return 0;
236    }
237    let mut result: c_int = 0;
238    let a_key = xsltEvalSortKey(ctxt, a, sort);
239    let b_key = xsltEvalSortKey(ctxt, b, sort);
240
241    let a_str: *const xmlChar = if a_key.is_null() { ptr::null() } else { a_key };
242    let b_str: *const xmlChar = if b_key.is_null() { ptr::null() } else { b_key };
243
244    if (*sort).isText != 0 {
245        // Text comparison.
246        result = match (a_str.is_null(), b_str.is_null()) {
247            (true, true) => 0,
248            (true, false) => -1,
249            (false, true) => 1,
250            (false, false) => {
251                // Respect case-order: upper-first means uppercase letters
252                // sort before lowercase.
253                let cmp = xmlStrcmp(a_str, b_str);
254                cmp
255            }
256        };
257    } else {
258        // Number comparison.
259        let a_num = if a_str.is_null() {
260            f64::NAN
261        } else {
262            xmlXPathCastStringToNumber(a_str)
263        };
264        let b_num = if b_str.is_null() {
265            f64::NAN
266        } else {
267            xmlXPathCastStringToNumber(b_str)
268        };
269        if a_num.is_nan() && b_num.is_nan() {
270            result = 0;
271        } else if a_num.is_nan() {
272            result = 1; // NaN sorts after everything
273        } else if b_num.is_nan() {
274            result = -1;
275        } else if a_num < b_num {
276            result = -1;
277        } else if a_num > b_num {
278            result = 1;
279        } else {
280            result = 0;
281        }
282    }
283
284    // Descending order inverts the result.
285    let order = (*sort).order;
286    if !order.is_null() {
287        let o = crate::abi::versioning::c_str_to_bytes(order as *const c_char);
288        if o == Some(b"descending") {
289            result = -result;
290        }
291    }
292
293    if !a_key.is_null() {
294        libc::free(a_key as *mut libc::c_void);
295    }
296    if !b_key.is_null() {
297        libc::free(b_key as *mut libc::c_void);
298    }
299    result
300}
301
302/// Compare two nodes according to a chain of sort specifications.
303///
304/// # SAFETY
305///
306/// - `a`, `b` must be valid nodes.
307/// - `sorts` must be a valid linked list of `_xsltSort`.
308pub unsafe fn xsltCompareNodes(
309    ctxt: *mut _xsltTransformContext,
310    a: *mut _xmlNode,
311    b: *mut _xmlNode,
312    sorts: *mut _xsltSort,
313) -> c_int {
314    if a.is_null() || b.is_null() || sorts.is_null() {
315        return 0;
316    }
317    let mut cur = sorts;
318    while !cur.is_null() {
319        let cmp = xsltCompareSingle(ctxt, a, b, cur);
320        if cmp != 0 {
321            return cmp;
322        }
323        cur = (*cur).next;
324    }
325    // All keys equal: fall back to document order to keep the sort stable.
326    // (Upstream does not guarantee this, but it preserves determinism.)
327    if a == b {
328        return 0;
329    }
330    xmlXPathCmpNodes(a, b)
331}
332
333/// Sort a node-set according to the sort specifications.
334///
335/// `nodes` is the node-set to sort (modified in place).
336/// `sorts` is a linked list of sort specifications (first = primary key).
337///
338/// # SAFETY
339///
340/// - `ctxt` must be a valid `_xsltTransformContext`.
341/// - `nodes` must be a valid `_xmlNodeSet`.
342/// - `sorts` must be a valid linked list of `_xsltSort`.
343pub unsafe fn xsltSortNodeSet(
344    ctxt: *mut _xsltTransformContext,
345    nodes: *mut _xmlNodeSet,
346    sorts: *mut _xsltSort,
347) {
348    if ctxt.is_null() || nodes.is_null() || sorts.is_null() {
349        return;
350    }
351    let nr = (*nodes).nodeNr;
352    if nr <= 1 {
353        return;
354    }
355    let tab = (*nodes).nodeTab;
356    if tab.is_null() {
357        return;
358    }
359
360    // Sort the node table with a simple insertion sort for small sets
361    // and a quicksort for larger ones. Upstream uses qsort; we use an
362    // adaptive approach with identical comparison semantics.
363    if nr < 32 {
364        // Insertion sort (stable).
365        let mut i = 1usize;
366        while i < nr as usize {
367            let key = *tab.offset(i as isize);
368            let mut j = i as isize - 1;
369            while j >= 0 {
370                let cur = *tab.offset(j);
371                if xsltCompareNodes(ctxt, cur, key, sorts) <= 0 {
372                    break;
373                }
374                *tab.offset((j + 1) as isize) = cur;
375                j -= 1;
376            }
377            *tab.offset((j + 1) as isize) = key;
378            i += 1;
379        }
380    } else {
381        // Quicksort (unstable, matching upstream qsort behavior).
382        let mut indices: Vec<usize> = (0..nr as usize).collect();
383        quicksort_indices(ctxt, tab, &mut indices, sorts);
384        for (new_pos, old_idx) in indices.iter().enumerate() {
385            let old_ptr = *tab.offset(*old_idx as isize);
386            *tab.offset(new_pos as isize) = old_ptr;
387        }
388    }
389}
390
391/// Quicksort helper over indices using the comparison function.
392unsafe fn quicksort_indices(
393    ctxt: *mut _xsltTransformContext,
394    tab: *mut *mut _xmlNode,
395    indices: &mut [usize],
396    sorts: *mut _xsltSort,
397) {
398    if indices.len() <= 1 {
399        return;
400    }
401    let pivot = indices[indices.len() / 2];
402    let mut less: Vec<usize> = Vec::new();
403    let mut greater: Vec<usize> = Vec::new();
404    for (i, idx) in indices.iter().enumerate() {
405        if *idx == pivot {
406            continue;
407        }
408        let cmp = xsltCompareNodes(
409            ctxt,
410            *tab.offset(*idx as isize),
411            *tab.offset(pivot as isize),
412            sorts,
413        );
414        if cmp <= 0 {
415            less.push(*idx);
416        } else {
417            greater.push(*idx);
418        }
419        let _ = i;
420    }
421    let pivot_pos = less.len();
422    quicksort_indices(ctxt, tab, &mut less, sorts);
423    quicksort_indices(ctxt, tab, &mut greater, sorts);
424    for (i, v) in less.into_iter().enumerate() {
425        indices[i] = v;
426    }
427    indices[pivot_pos] = pivot;
428    for (i, v) in greater.into_iter().enumerate() {
429        indices[pivot_pos + 1 + i] = v;
430    }
431}
432
433// Re-export the string comparison from xpath for internal use.
434
435#[cfg(test)]
436mod tests {
437    use super::*;
438    use core::ptr;
439
440    #[test]
441    fn test_constants() {
442        assert_eq!(XSLT_SORT_TEXT, 0);
443        assert_eq!(XSLT_SORT_NUMBER, 1);
444        assert_eq!(XSLT_SORT_ASCENDING, 0);
445        assert_eq!(XSLT_SORT_DESCENDING, 1);
446        assert_eq!(XSLT_SORT_CASE_UPPER_FIRST, 0);
447        assert_eq!(XSLT_SORT_CASE_LOWER_FIRST, 1);
448    }
449
450    #[test]
451    fn test_compile_sort_null() {
452        unsafe {
453            assert!(xsltCompileSort(ptr::null_mut(), ptr::null_mut()).is_null());
454        }
455    }
456
457    #[test]
458    fn test_free_sort_null() {
459        unsafe {
460            xsltFreeSort(ptr::null_mut());
461            xsltFreeSortList(ptr::null_mut());
462        }
463    }
464}