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}