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 const 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///
108/// # SAFETY
109///
110/// The function touches crate-global state only; it is safe
111/// as long as the caller respects the library's global
112/// initialization/cleanup ordering (xmlInitParser before use,
113/// xmlCleanupParser only after all users are done).
114///
115/// Violating the global lifecycle ordering, or calling this after
116/// teardown or from a signal handler, is undefined behavior.
117#[no_mangle]
118pub unsafe extern "C" fn xmlIsBaseChar(ch: c_uint) -> c_int {
119    if ch < 0x100 {
120        is_base_char_ch(ch) as c_int
121    } else {
122        unsafe { xmlCharInRange(ch, &xmlIsBaseCharGroup) }
123    }
124}
125
126/// `xmlIsBlank(unsigned int ch)` — space, tab, LF, CR.
127///
128/// # SAFETY
129///
130/// The function touches crate-global state only; it is safe
131/// as long as the caller respects the library's global
132/// initialization/cleanup ordering (xmlInitParser before use,
133/// xmlCleanupParser only after all users are done).
134///
135/// Violating the global lifecycle ordering, or calling this after
136/// teardown or from a signal handler, is undefined behavior.
137#[no_mangle]
138pub unsafe extern "C" fn xmlIsBlank(ch: c_uint) -> c_int {
139    if ch < 0x100 {
140        (ch == 0x20 || (0x9..=0xa).contains(&ch) || ch == 0xd) as c_int
141    } else {
142        0
143    }
144}
145
146/// `xmlIsChar(unsigned int ch)` — XML 1.0 Char production.
147///
148/// # SAFETY
149///
150/// The function touches crate-global state only; it is safe
151/// as long as the caller respects the library's global
152/// initialization/cleanup ordering (xmlInitParser before use,
153/// xmlCleanupParser only after all users are done).
154///
155/// Violating the global lifecycle ordering, or calling this after
156/// teardown or from a signal handler, is undefined behavior.
157#[no_mangle]
158pub unsafe extern "C" fn xmlIsChar(ch: c_uint) -> c_int {
159    if ch < 0x100 {
160        ((0x9..=0xa).contains(&ch) || ch == 0xd || ch >= 0x20) as c_int
161    } else {
162        ((0x100..=0xd7ff).contains(&ch)
163            || (0xe000..=0xfffd).contains(&ch)
164            || (0x10000..=0x10ffff).contains(&ch)) as c_int
165    }
166}
167
168/// `xmlIsCombining(unsigned int ch)` — XML 1.0 CombiningChar production.
169///
170/// # SAFETY
171///
172/// The function touches crate-global state only; it is safe
173/// as long as the caller respects the library's global
174/// initialization/cleanup ordering (xmlInitParser before use,
175/// xmlCleanupParser only after all users are done).
176///
177/// Violating the global lifecycle ordering, or calling this after
178/// teardown or from a signal handler, is undefined behavior.
179#[no_mangle]
180pub unsafe extern "C" fn xmlIsCombining(ch: c_uint) -> c_int {
181    if ch < 0x100 {
182        0
183    } else {
184        unsafe { xmlCharInRange(ch, &xmlIsCombiningGroup) }
185    }
186}
187
188/// `xmlIsDigit(unsigned int ch)` — XML 1.0 Digit production.
189///
190/// # SAFETY
191///
192/// The function touches crate-global state only; it is safe
193/// as long as the caller respects the library's global
194/// initialization/cleanup ordering (xmlInitParser before use,
195/// xmlCleanupParser only after all users are done).
196///
197/// Violating the global lifecycle ordering, or calling this after
198/// teardown or from a signal handler, is undefined behavior.
199#[no_mangle]
200pub unsafe extern "C" fn xmlIsDigit(ch: c_uint) -> c_int {
201    if ch < 0x100 {
202        (0x30..=0x39).contains(&ch) as c_int
203    } else {
204        unsafe { xmlCharInRange(ch, &xmlIsDigitGroup) }
205    }
206}
207
208/// `xmlIsExtender(unsigned int ch)` — XML 1.0 Extender production.
209///
210/// # SAFETY
211///
212/// The function touches crate-global state only; it is safe
213/// as long as the caller respects the library's global
214/// initialization/cleanup ordering (xmlInitParser before use,
215/// xmlCleanupParser only after all users are done).
216///
217/// Violating the global lifecycle ordering, or calling this after
218/// teardown or from a signal handler, is undefined behavior.
219#[no_mangle]
220pub unsafe extern "C" fn xmlIsExtender(ch: c_uint) -> c_int {
221    if ch < 0x100 {
222        (ch == 0xb7) as c_int
223    } else {
224        unsafe { xmlCharInRange(ch, &xmlIsExtenderGroup) }
225    }
226}
227
228/// `xmlIsIdeographic(unsigned int ch)` — XML 1.0 Ideographic production.
229///
230/// # SAFETY
231///
232/// The function touches crate-global state only; it is safe
233/// as long as the caller respects the library's global
234/// initialization/cleanup ordering (xmlInitParser before use,
235/// xmlCleanupParser only after all users are done).
236///
237/// Violating the global lifecycle ordering, or calling this after
238/// teardown or from a signal handler, is undefined behavior.
239#[no_mangle]
240pub unsafe extern "C" fn xmlIsIdeographic(ch: c_uint) -> c_int {
241    if ch < 0x100 {
242        0
243    } else {
244        ((0x4e00..=0x9fa5).contains(&ch) || ch == 0x3007 || (0x3021..=0x3029).contains(&ch))
245            as c_int
246    }
247}
248
249/// `xmlIsPubidChar(unsigned int ch)` — PubidChar production (ASCII table).
250///
251/// # SAFETY
252///
253/// The function touches crate-global state only; it is safe
254/// as long as the caller respects the library's global
255/// initialization/cleanup ordering (xmlInitParser before use,
256/// xmlCleanupParser only after all users are done).
257///
258/// Violating the global lifecycle ordering, or calling this after
259/// teardown or from a signal handler, is undefined behavior.
260#[no_mangle]
261pub unsafe extern "C" fn xmlIsPubidChar(ch: c_uint) -> c_int {
262    if ch >= 0x100 {
263        0
264    } else {
265        xmlIsPubidChar_tab[ch as usize] as c_int
266    }
267}
268
269/// `xmlIsLetter(int c)` — BaseChar or Ideographic (parserInternals.c).
270///
271/// # SAFETY
272///
273/// The function touches crate-global state only; it is safe
274/// as long as the caller respects the library's global
275/// initialization/cleanup ordering (xmlInitParser before use,
276/// xmlCleanupParser only after all users are done).
277///
278/// Violating the global lifecycle ordering, or calling this after
279/// teardown or from a signal handler, is undefined behavior.
280#[no_mangle]
281pub unsafe extern "C" fn xmlIsLetter(c: c_int) -> c_int {
282    let ch = c as c_uint;
283    if ch < 0x100 {
284        is_base_char_ch(ch) as c_int
285    } else {
286        unsafe { xmlIsBaseChar(ch) | xmlIsIdeographic(ch) }
287    }
288}
289
290/// `xmlIsBlankNode(const xmlNode *node)` — text/CDATA node with empty or
291/// whitespace-only content (tree.c 2.15).
292///
293/// # SAFETY
294///
295/// - `node` must be NULL or a valid node pointer.
296#[no_mangle]
297pub unsafe extern "C" fn xmlIsBlankNode(node: *const _xmlNode) -> c_int {
298    if node.is_null() {
299        return 0;
300    }
301    let n = unsafe { &*node };
302    if n.type_ != crate::abi::types::xmlElementType::XML_TEXT_NODE as c_int
303        && n.type_ != crate::abi::types::xmlElementType::XML_CDATA_SECTION_NODE as c_int
304    {
305        return 0;
306    }
307    if n.content.is_null() {
308        return 1;
309    }
310    let mut cur = n.content;
311    while !cur.is_null() && *cur != 0 {
312        let ch = *cur as c_uint;
313        if ch != 0x20 && !(0x9..=0xa).contains(&ch) && ch != 0xd {
314            return 0;
315        }
316        cur = cur.add(1);
317    }
318    1
319}
320
321#[cfg(test)]
322mod tests {
323    use super::*;
324    use crate::abi::allocator;
325    use crate::abi::types::xmlChar;
326    use std::os::raw::c_uint;
327
328    /// Differential-oracle spot checks (values verified against the system
329    /// libxml2 2.15.3 DSO via tools/abi/data_globals_probe.py).
330    fn oracle_is_base_char(ch: c_uint) -> c_int {
331        unsafe { xmlIsBaseChar(ch) }
332    }
333
334    #[test]
335    fn test_xml_is_char_basic() {
336        unsafe {
337            // XML 1.0 Char production.
338            assert_eq!(xmlIsChar(0x9), 1); // tab
339            assert_eq!(xmlIsChar(0xa), 1); // lf
340            assert_eq!(xmlIsChar(0xd), 1); // cr
341            assert_eq!(xmlIsChar(0x20), 1); // space
342            assert_eq!(xmlIsChar(0x1f), 0); // below space
343            assert_eq!(xmlIsChar(0xd7ff), 1);
344            assert_eq!(xmlIsChar(0xd800), 0); // surrogate
345            assert_eq!(xmlIsChar(0xe000), 1);
346            assert_eq!(xmlIsChar(0xfffe), 0);
347            assert_eq!(xmlIsChar(0x10000), 1);
348            assert_eq!(xmlIsChar(0x10ffff), 1);
349            assert_eq!(xmlIsChar(0x110000), 0);
350        }
351    }
352
353    #[test]
354    fn test_xml_is_blank() {
355        unsafe {
356            assert_eq!(xmlIsBlank(0x20), 1);
357            assert_eq!(xmlIsBlank(0x9), 1);
358            assert_eq!(xmlIsBlank(0xa), 1);
359            assert_eq!(xmlIsBlank(0xd), 1);
360            assert_eq!(xmlIsBlank(b'x' as c_uint), 0);
361            assert_eq!(xmlIsBlank(0x100), 0);
362            assert_eq!(xmlIsBlank(0x3000), 0); // ideographic space NOT blank upstream
363        }
364    }
365
366    #[test]
367    fn test_xml_is_base_char_ascii_and_ranges() {
368        unsafe {
369            assert_eq!(xmlIsBaseChar(b'A' as c_uint), 1);
370            assert_eq!(xmlIsBaseChar(b'z' as c_uint), 1);
371            assert_eq!(xmlIsBaseChar(b'0' as c_uint), 0);
372            assert_eq!(xmlIsBaseChar(0xc0), 1); // À
373            assert_eq!(xmlIsBaseChar(0xd7), 0);
374            assert_eq!(xmlIsBaseChar(0x100), 1); // Ā (short range)
375            assert_eq!(xmlIsBaseChar(0x132), 0); // between ranges
376            assert_eq!(xmlIsBaseChar(0x386), 1); // Greek
377            assert_eq!(xmlIsBaseChar(0x5d0), 1); // Hebrew
378            assert_eq!(xmlIsBaseChar(0xac00), 1); // Hangul
379            assert_eq!(xmlIsBaseChar(0xac00), oracle_is_base_char(0xac00));
380            assert_eq!(xmlIsBaseChar(0x2a8), 1);
381            assert_eq!(xmlIsBaseChar(0x2a9), 0);
382        }
383    }
384
385    #[test]
386    fn test_xml_is_digit() {
387        unsafe {
388            assert_eq!(xmlIsDigit(b'0' as c_uint), 1);
389            assert_eq!(xmlIsDigit(b'9' as c_uint), 1);
390            assert_eq!(xmlIsDigit(b'a' as c_uint), 0);
391            assert_eq!(xmlIsDigit(0x660), 1); // Arabic-Indic zero
392            assert_eq!(xmlIsDigit(0x6f9), 1);
393            assert_eq!(xmlIsDigit(0x670), 0);
394        }
395    }
396
397    #[test]
398    fn test_xml_is_combining_extender_ideographic() {
399        unsafe {
400            assert_eq!(xmlIsCombining(0x300), 1); // combining grave
401            assert_eq!(xmlIsCombining(0x20,), 0);
402            assert_eq!(xmlIsExtender(0xb7), 1); // middle dot
403            assert_eq!(xmlIsExtender(0x2d0), 1);
404            assert_eq!(xmlIsExtender(0x3005), 1);
405            assert_eq!(xmlIsExtender(0x20), 0);
406            assert_eq!(xmlIsIdeographic(0x4e00), 1); // CJK
407            assert_eq!(xmlIsIdeographic(0x3007), 1);
408            assert_eq!(xmlIsIdeographic(0x3029), 1);
409            assert_eq!(xmlIsIdeographic(0x302a), 0);
410            assert_eq!(xmlIsIdeographic(0x9fa5), 1);
411            assert_eq!(xmlIsIdeographic(b'A' as c_uint), 0);
412        }
413    }
414
415    #[test]
416    fn test_xml_is_pubid_char() {
417        unsafe {
418            assert_eq!(xmlIsPubidChar(b'a' as c_uint), 1);
419            assert_eq!(xmlIsPubidChar(b' ' as c_uint), 1);
420            assert_eq!(xmlIsPubidChar(b'!' as c_uint), 1);
421            // @ IS a PubidChar ([-'()+,./:=?;!*#@$_%]).
422            assert_eq!(xmlIsPubidChar(b'@' as c_uint), 1);
423            // ^ and ~ are not.
424            assert_eq!(xmlIsPubidChar(b'^' as c_uint), 0);
425            assert_eq!(xmlIsPubidChar(b'~' as c_uint), 0);
426            assert_eq!(xmlIsPubidChar(0x80), 0);
427            assert_eq!(xmlIsPubidChar(0x100), 0);
428            // tab is not a pubid char upstream.
429            assert_eq!(xmlIsPubidChar(0x9), 0);
430        }
431    }
432
433    #[test]
434    fn test_xml_is_letter() {
435        unsafe {
436            assert_eq!(xmlIsLetter(b'A' as c_int), 1);
437            assert_eq!(xmlIsLetter(0x4e00), 1); // ideographic counts
438            assert_eq!(xmlIsLetter(b'0' as c_int), 0);
439            assert_eq!(xmlIsLetter(0x386), 1);
440        }
441    }
442
443    #[test]
444    fn test_xml_char_in_range_null_group() {
445        unsafe {
446            assert_eq!(xmlCharInRange(0x41, core::ptr::null()), 0);
447        }
448    }
449
450    #[test]
451    fn test_xml_is_blank_node() {
452        unsafe {
453            use crate::abi::types::xmlElementType::*;
454            // Null node -> 0.
455            assert_eq!(xmlIsBlankNode(core::ptr::null()), 0);
456            // Text node with NULL content -> 1.
457            let node = allocator::xmlMallocImpl(core::mem::size_of::<_xmlNode>()) as *mut _xmlNode;
458            assert!(!node.is_null());
459            core::ptr::write(
460                node,
461                _xmlNode {
462                    type_: XML_TEXT_NODE as c_int,
463                    content: core::ptr::null_mut(),
464                    ..core::mem::zeroed()
465                },
466            );
467            assert_eq!(xmlIsBlankNode(node), 1);
468            // Whitespace-only -> 1.
469            let ws = b" \t\n\r\0" as *const u8 as *mut xmlChar;
470            (*node).content = ws;
471            assert_eq!(xmlIsBlankNode(node), 1);
472            // Non-whitespace -> 0.
473            let nw = b" x\0" as *const u8 as *mut xmlChar;
474            (*node).content = nw;
475            assert_eq!(xmlIsBlankNode(node), 0);
476            // Non-text node -> 0.
477            (*node).type_ = XML_ELEMENT_NODE as c_int;
478            assert_eq!(xmlIsBlankNode(node), 0);
479            allocator::xmlFreeImpl(node as *mut libc::c_void);
480        }
481    }
482}