Skip to main content

libxml_rs/xml/
chvalid.rs

1//! XML character-class validation (upstream chvalid.c / parserInternals.c).
2//!
3//! The exported `xmlIs*` family and `xmlCharInRange` use the generated
4//! character-class tables from `unicode_tables.rs` (extracted verbatim from
5//! upstream `codegen/ranges.inc`; see
6//! `tools/archaeology/gen_chvalid_tables.py`).
7//!
8//! # UPSTREAM-PARITY
9//!
10//! The semantics mirror upstream chvalid.h macros exactly:
11//!
12//! - `xmlIsBaseCharQ`: `(c < 0x100) ? xmlIsBaseChar_ch(c) : xmlCharInRange(c, &xmlIsBaseCharGroup)`
13//! - `xmlIsBlankQ`: `(c < 0x100) ? (c==0x20 || 0x9<=c<=0xa || c==0xd) : 0`
14//! - `xmlIsCharQ`: `(c < 0x100) ? (0x9<=c<=0xa || c==0xd || 0x20<=c) : (0x100<=c<=0xd7ff || 0xe000<=c<=0xfffd || 0x10000<=c<=0x10ffff)`
15//! - `xmlIsCombiningQ`: `(c < 0x100) ? 0 : xmlCharInRange(c, &xmlIsCombiningGroup)`
16//! - `xmlIsDigitQ`: `(c < 0x100) ? (0x30<=c<=0x39) : xmlCharInRange(c, &xmlIsDigitGroup)`
17//! - `xmlIsExtenderQ`: `(c < 0x100) ? (c==0xb7) : xmlCharInRange(c, &xmlIsExtenderGroup)`
18//! - `xmlIsIdeographicQ`: `(c < 0x100) ? 0 : (0x4e00<=c<=0x9fa5 || c==0x3007 || 0x3021<=c<=0x3029)`
19//! - `xmlIsPubidCharQ`: `(c < 0x100) ? xmlIsPubidChar_tab[c] : 0`
20//! - `xmlIsLetter`: `xmlIsBaseCharQ(c) || xmlIsIdeographicQ(c)` (parserInternals.c)
21//! - `xmlIsBlankNode`: tree.c — text/CDATA node whose content is empty or
22//!   all blank.
23//!
24//! # Courts
25//!
26//! CHVALID-* differential tests compare against the oracle DSO for the whole
27//! BMP + representative supplementary-plane code points.
28
29use crate::abi::structs::{_xmlNode, xmlChRangeGroup};
30use crate::xml::unicode_tables::*;
31use std::os::raw::{c_int, c_uint, c_ushort};
32
33/// Binary search over the short/long range tables (upstream `xmlCharInRange`,
34/// chvalid.c — the tables are sorted, so the search is exact).
35///
36/// # SAFETY
37///
38/// - `group` must be NULL or point to a valid `xmlChRangeGroup` whose range
39///   arrays cover `nbShortRange`/`nbLongRange` entries.
40#[no_mangle]
41pub unsafe extern "C" fn xmlCharInRange(val: c_uint, group: *const xmlChRangeGroup) -> c_int {
42    if group.is_null() {
43        return 0;
44    }
45    let g = unsafe { &*group };
46    if val < 0x10000 {
47        // Short (16-bit) ranges.
48        if g.nbShortRange == 0 {
49            return 0;
50        }
51        let mut low = 0;
52        let mut high = g.nbShortRange - 1;
53        let sptr = g.shortRange;
54        if sptr.is_null() {
55            return 0;
56        }
57        while low <= high {
58            let mid = (low + high) / 2;
59            let s = unsafe { &*sptr.add(mid as usize) };
60            if (val as c_ushort) < s.low {
61                high = mid - 1;
62            } else if (val as c_ushort) > s.high {
63                low = mid + 1;
64            } else {
65                return 1;
66            }
67        }
68        0
69    } else {
70        // Long (32-bit) ranges.
71        if g.nbLongRange == 0 {
72            return 0;
73        }
74        let mut low = 0;
75        let mut high = g.nbLongRange - 1;
76        let lptr = g.longRange;
77        if lptr.is_null() {
78            return 0;
79        }
80        while low <= high {
81            let mid = (low + high) / 2;
82            let l = unsafe { &*lptr.add(mid as usize) };
83            if val < l.low {
84                high = mid - 1;
85            } else if val > l.high {
86                low = mid + 1;
87            } else {
88                return 1;
89            }
90        }
91        0
92    }
93}
94
95#[inline]
96fn is_base_char_ch(c: c_uint) -> bool {
97    // upstream xmlIsBaseChar_ch (genChRanges.py): ASCII letters plus the
98    // Latin-1 letters that do not fall in the group's short ranges.
99    (0x41..=0x5a).contains(&c)
100        || (0x61..=0x7a).contains(&c)
101        || (0xc0..=0xd6).contains(&c)
102        || (0xd8..=0xf6).contains(&c)
103        || c >= 0xf8
104}
105
106/// `xmlIsBaseChar(unsigned int ch)` — XML 1.0 BaseChar production.
107#[no_mangle]
108pub unsafe extern "C" fn xmlIsBaseChar(ch: c_uint) -> c_int {
109    if ch < 0x100 {
110        is_base_char_ch(ch) as c_int
111    } else {
112        unsafe { xmlCharInRange(ch, &xmlIsBaseCharGroup) }
113    }
114}
115
116/// `xmlIsBlank(unsigned int ch)` — space, tab, LF, CR.
117#[no_mangle]
118pub unsafe extern "C" fn xmlIsBlank(ch: c_uint) -> c_int {
119    if ch < 0x100 {
120        (ch == 0x20 || (0x9..=0xa).contains(&ch) || ch == 0xd) as c_int
121    } else {
122        0
123    }
124}
125
126/// `xmlIsChar(unsigned int ch)` — XML 1.0 Char production.
127#[no_mangle]
128pub unsafe extern "C" fn xmlIsChar(ch: c_uint) -> c_int {
129    if ch < 0x100 {
130        ((0x9..=0xa).contains(&ch) || ch == 0xd || ch >= 0x20) as c_int
131    } else {
132        ((0x100..=0xd7ff).contains(&ch)
133            || (0xe000..=0xfffd).contains(&ch)
134            || (0x10000..=0x10ffff).contains(&ch)) as c_int
135    }
136}
137
138/// `xmlIsCombining(unsigned int ch)` — XML 1.0 CombiningChar production.
139#[no_mangle]
140pub unsafe extern "C" fn xmlIsCombining(ch: c_uint) -> c_int {
141    if ch < 0x100 {
142        0
143    } else {
144        unsafe { xmlCharInRange(ch, &xmlIsCombiningGroup) }
145    }
146}
147
148/// `xmlIsDigit(unsigned int ch)` — XML 1.0 Digit production.
149#[no_mangle]
150pub unsafe extern "C" fn xmlIsDigit(ch: c_uint) -> c_int {
151    if ch < 0x100 {
152        (0x30..=0x39).contains(&ch) as c_int
153    } else {
154        unsafe { xmlCharInRange(ch, &xmlIsDigitGroup) }
155    }
156}
157
158/// `xmlIsExtender(unsigned int ch)` — XML 1.0 Extender production.
159#[no_mangle]
160pub unsafe extern "C" fn xmlIsExtender(ch: c_uint) -> c_int {
161    if ch < 0x100 {
162        (ch == 0xb7) as c_int
163    } else {
164        unsafe { xmlCharInRange(ch, &xmlIsExtenderGroup) }
165    }
166}
167
168/// `xmlIsIdeographic(unsigned int ch)` — XML 1.0 Ideographic production.
169#[no_mangle]
170pub unsafe extern "C" fn xmlIsIdeographic(ch: c_uint) -> c_int {
171    if ch < 0x100 {
172        0
173    } else {
174        ((0x4e00..=0x9fa5).contains(&ch) || ch == 0x3007 || (0x3021..=0x3029).contains(&ch))
175            as c_int
176    }
177}
178
179/// `xmlIsPubidChar(unsigned int ch)` — PubidChar production (ASCII table).
180#[no_mangle]
181pub unsafe extern "C" fn xmlIsPubidChar(ch: c_uint) -> c_int {
182    if ch >= 0x100 {
183        0
184    } else {
185        xmlIsPubidChar_tab[ch as usize] as c_int
186    }
187}
188
189/// `xmlIsLetter(int c)` — BaseChar or Ideographic (parserInternals.c).
190#[no_mangle]
191pub unsafe extern "C" fn xmlIsLetter(c: c_int) -> c_int {
192    let ch = c as c_uint;
193    if ch < 0x100 {
194        is_base_char_ch(ch) as c_int
195    } else {
196        unsafe { xmlIsBaseChar(ch) | xmlIsIdeographic(ch) }
197    }
198}
199
200/// `xmlIsBlankNode(const xmlNode *node)` — text/CDATA node with empty or
201/// whitespace-only content (tree.c 2.15).
202///
203/// # SAFETY
204///
205/// - `node` must be NULL or a valid node pointer.
206#[no_mangle]
207pub unsafe extern "C" fn xmlIsBlankNode(node: *const _xmlNode) -> c_int {
208    if node.is_null() {
209        return 0;
210    }
211    let n = unsafe { &*node };
212    if n.type_ != crate::abi::types::xmlElementType::XML_TEXT_NODE as c_int
213        && n.type_ != crate::abi::types::xmlElementType::XML_CDATA_SECTION_NODE as c_int
214    {
215        return 0;
216    }
217    if n.content.is_null() {
218        return 1;
219    }
220    let mut cur = n.content;
221    while !cur.is_null() && *cur != 0 {
222        let ch = *cur as c_uint;
223        if ch != 0x20 && !(0x9..=0xa).contains(&ch) && ch != 0xd {
224            return 0;
225        }
226        cur = cur.add(1);
227    }
228    1
229}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234    use crate::abi::allocator;
235    use crate::abi::types::xmlChar;
236    use std::os::raw::c_uint;
237
238    /// Differential-oracle spot checks (values verified against the system
239    /// libxml2 2.15.3 DSO via tools/abi/data_globals_probe.py).
240    fn oracle_is_base_char(ch: c_uint) -> c_int {
241        unsafe { xmlIsBaseChar(ch) }
242    }
243
244    #[test]
245    fn test_xml_is_char_basic() {
246        unsafe {
247            // XML 1.0 Char production.
248            assert_eq!(xmlIsChar(0x9), 1); // tab
249            assert_eq!(xmlIsChar(0xa), 1); // lf
250            assert_eq!(xmlIsChar(0xd), 1); // cr
251            assert_eq!(xmlIsChar(0x20), 1); // space
252            assert_eq!(xmlIsChar(0x1f), 0); // below space
253            assert_eq!(xmlIsChar(0xd7ff), 1);
254            assert_eq!(xmlIsChar(0xd800), 0); // surrogate
255            assert_eq!(xmlIsChar(0xe000), 1);
256            assert_eq!(xmlIsChar(0xfffe), 0);
257            assert_eq!(xmlIsChar(0x10000), 1);
258            assert_eq!(xmlIsChar(0x10ffff), 1);
259            assert_eq!(xmlIsChar(0x110000), 0);
260        }
261    }
262
263    #[test]
264    fn test_xml_is_blank() {
265        unsafe {
266            assert_eq!(xmlIsBlank(0x20), 1);
267            assert_eq!(xmlIsBlank(0x9), 1);
268            assert_eq!(xmlIsBlank(0xa), 1);
269            assert_eq!(xmlIsBlank(0xd), 1);
270            assert_eq!(xmlIsBlank(b'x' as c_uint), 0);
271            assert_eq!(xmlIsBlank(0x100), 0);
272            assert_eq!(xmlIsBlank(0x3000), 0); // ideographic space NOT blank upstream
273        }
274    }
275
276    #[test]
277    fn test_xml_is_base_char_ascii_and_ranges() {
278        unsafe {
279            assert_eq!(xmlIsBaseChar(b'A' as c_uint), 1);
280            assert_eq!(xmlIsBaseChar(b'z' as c_uint), 1);
281            assert_eq!(xmlIsBaseChar(b'0' as c_uint), 0);
282            assert_eq!(xmlIsBaseChar(0xc0), 1); // À
283            assert_eq!(xmlIsBaseChar(0xd7), 0);
284            assert_eq!(xmlIsBaseChar(0x100), 1); // Ā (short range)
285            assert_eq!(xmlIsBaseChar(0x132), 0); // between ranges
286            assert_eq!(xmlIsBaseChar(0x386), 1); // Greek
287            assert_eq!(xmlIsBaseChar(0x5d0), 1); // Hebrew
288            assert_eq!(xmlIsBaseChar(0xac00), 1); // Hangul
289            assert_eq!(xmlIsBaseChar(0xac00), oracle_is_base_char(0xac00));
290            assert_eq!(xmlIsBaseChar(0x2a8), 1);
291            assert_eq!(xmlIsBaseChar(0x2a9), 0);
292        }
293    }
294
295    #[test]
296    fn test_xml_is_digit() {
297        unsafe {
298            assert_eq!(xmlIsDigit(b'0' as c_uint), 1);
299            assert_eq!(xmlIsDigit(b'9' as c_uint), 1);
300            assert_eq!(xmlIsDigit(b'a' as c_uint), 0);
301            assert_eq!(xmlIsDigit(0x660), 1); // Arabic-Indic zero
302            assert_eq!(xmlIsDigit(0x6f9), 1);
303            assert_eq!(xmlIsDigit(0x670), 0);
304        }
305    }
306
307    #[test]
308    fn test_xml_is_combining_extender_ideographic() {
309        unsafe {
310            assert_eq!(xmlIsCombining(0x300), 1); // combining grave
311            assert_eq!(xmlIsCombining(0x20,), 0);
312            assert_eq!(xmlIsExtender(0xb7), 1); // middle dot
313            assert_eq!(xmlIsExtender(0x2d0), 1);
314            assert_eq!(xmlIsExtender(0x3005), 1);
315            assert_eq!(xmlIsExtender(0x20), 0);
316            assert_eq!(xmlIsIdeographic(0x4e00), 1); // CJK
317            assert_eq!(xmlIsIdeographic(0x3007), 1);
318            assert_eq!(xmlIsIdeographic(0x3029), 1);
319            assert_eq!(xmlIsIdeographic(0x302a), 0);
320            assert_eq!(xmlIsIdeographic(0x9fa5), 1);
321            assert_eq!(xmlIsIdeographic(b'A' as c_uint), 0);
322        }
323    }
324
325    #[test]
326    fn test_xml_is_pubid_char() {
327        unsafe {
328            assert_eq!(xmlIsPubidChar(b'a' as c_uint), 1);
329            assert_eq!(xmlIsPubidChar(b' ' as c_uint), 1);
330            assert_eq!(xmlIsPubidChar(b'!' as c_uint), 1);
331            // @ IS a PubidChar ([-'()+,./:=?;!*#@$_%]).
332            assert_eq!(xmlIsPubidChar(b'@' as c_uint), 1);
333            // ^ and ~ are not.
334            assert_eq!(xmlIsPubidChar(b'^' as c_uint), 0);
335            assert_eq!(xmlIsPubidChar(b'~' as c_uint), 0);
336            assert_eq!(xmlIsPubidChar(0x80), 0);
337            assert_eq!(xmlIsPubidChar(0x100), 0);
338            // tab is not a pubid char upstream.
339            assert_eq!(xmlIsPubidChar(0x9), 0);
340        }
341    }
342
343    #[test]
344    fn test_xml_is_letter() {
345        unsafe {
346            assert_eq!(xmlIsLetter(b'A' as c_int), 1);
347            assert_eq!(xmlIsLetter(0x4e00), 1); // ideographic counts
348            assert_eq!(xmlIsLetter(b'0' as c_int), 0);
349            assert_eq!(xmlIsLetter(0x386), 1);
350        }
351    }
352
353    #[test]
354    fn test_xml_char_in_range_null_group() {
355        unsafe {
356            assert_eq!(xmlCharInRange(0x41, core::ptr::null()), 0);
357        }
358    }
359
360    #[test]
361    fn test_xml_is_blank_node() {
362        unsafe {
363            use crate::abi::types::xmlElementType::*;
364            // Null node -> 0.
365            assert_eq!(xmlIsBlankNode(core::ptr::null()), 0);
366            // Text node with NULL content -> 1.
367            let node = allocator::xmlMalloc(core::mem::size_of::<_xmlNode>()) as *mut _xmlNode;
368            assert!(!node.is_null());
369            core::ptr::write(
370                node,
371                _xmlNode {
372                    type_: XML_TEXT_NODE as c_int,
373                    content: core::ptr::null_mut(),
374                    ..core::mem::zeroed()
375                },
376            );
377            assert_eq!(xmlIsBlankNode(node), 1);
378            // Whitespace-only -> 1.
379            let ws = b" \t\n\r\0" as *const u8 as *mut xmlChar;
380            (*node).content = ws;
381            assert_eq!(xmlIsBlankNode(node), 1);
382            // Non-whitespace -> 0.
383            let nw = b" x\0" as *const u8 as *mut xmlChar;
384            (*node).content = nw;
385            assert_eq!(xmlIsBlankNode(node), 0);
386            // Non-text node -> 0.
387            (*node).type_ = XML_ELEMENT_NODE as c_int;
388            assert_eq!(xmlIsBlankNode(node), 0);
389            allocator::xmlFree(node as *mut libc::c_void);
390        }
391    }
392}