Skip to main content

libxml_rs/xml/
string.rs

1//! String utility functions for libxml-rs.
2//!
3//! Provides operations on `xmlChar*` (i.e. `*mut u8`) strings compatible
4//! with upstream libxml2 string handling.
5//!
6//! # Upstream contract
7//!
8//! Mirrors upstream xmlstring.c (SRC-LIBXML2-2.15.0-XMLSTRING-C): xmlStrlen,
9//! xmlStrdup, xmlStrndup, xmlStrchr, xmlStrstr, xmlStrcmp, xmlStrEqual,
10//! xmlStrsub, the UTF-8 helpers and the xmlChar* memory functions. Parity
11//! target: the system libxml2 2.15.3 oracle.
12//!
13//! # Conceptual behavior
14//!
15//! Provides operations on xmlChar* (u8) NUL-terminated strings compatible
16//! with upstream semantics: length scan, duplication, comparison, UTF-8
17//! iteration and substring extraction. String values are owned per the
18//! upstream contract — the caller frees xmlStrdup results with xmlFree.
19//!
20//! # Ownership & safety invariants
21//!
22//! SAFETY: functions require NUL-terminated inputs (or NULL); callers own
23//! returned copies (freed with xmlFree). The R-000169 lesson applies here:
24//! xml_strndup must be used when the source is a Rust String with an exact
25//! length — xml_strdup on a non-NUL-terminated as_ptr() scans past the
26//! allocation (heap-buffer-overflow, caught by ASan).
27//!
28//! # Historical quirks & epochs
29//!
30//! The 11.1-X fix (R-000169) switched the parser filename duplication from
31//! xml_strdup to xml_strndup(fname.as_ptr(), fname.len()) after ASan pinned
32//! the overflow. Historical quirk: the upstream limit macro XML_MAX_TEXT_
33//! LENGHT was misspelled for years (QUIRK-0004, commit 1fb2e0df) — the
34//! spelling is part of the observable header surface.
35//!
36//! # Deliberate oddities
37//!
38//! Deliberate oddity: xml_strdup returns NULL on NULL input and on OOM
39//! (matching upstream xmlStrdup); the module deliberately never assumes
40//! Rust-length semantics — every operation is NUL-terminated-centric.
41//!
42//! # Proving courts
43//!
44//! Exercised indirectly by TREE-001 (URL/base fingerprints), ERROR-001
45//! (str1/str2/str3 copies), the data-ABI family probes, and `cargo test
46//! --lib` under ASan (which caught the R-000169 overflow).
47//!
48//! # Tempting simplifications that would break parity
49//!
50//! The tempting simplification is using Rust String/slices everywhere and
51//! dropping the NUL-terminated xmlChar* model — it would break the C ABI
52//! (xmlChar* parameters) and the ownership contract. Do not fix xml_strdup
53//! callers to assume NUL-termination of Rust Strings: that was the exact
54//! heap-buffer-overflow R-000169 fixed.
55
56use crate::abi::allocator::{xmlFreeImpl, xmlMallocImpl};
57use crate::abi::types::xmlChar;
58use core::ffi::c_void;
59use core::ptr;
60use std::os::raw::{c_char, c_int};
61use std::slice;
62
63/// Compute the length of a null-terminated `xmlChar` string.
64///
65/// # UPSTREAM-PARITY
66///
67/// Equivalent to `strlen((const char *)str)` in C.
68///
69/// # Safety
70///
71/// `str` must point to a null-terminated sequence of bytes.
72#[inline]
73pub(crate) const unsafe fn xml_strlen(str: *const xmlChar) -> usize {
74    if str.is_null() {
75        return 0;
76    }
77    let mut len: usize = 0;
78    while *str.add(len) != 0 {
79        len += 1;
80    }
81    len
82}
83
84/// Duplicate a null-terminated `xmlChar` string using `xmlMalloc`.
85///
86/// # UPSTREAM-PARITY
87///
88/// Equivalent to `xmlStrdup` in upstream libxml2.
89/// Returns a newly allocated copy. Caller must free with `xmlFree`.
90///
91/// # Safety
92///
93/// `str` must point to a null-terminated sequence of bytes, or be NULL.
94#[inline]
95pub(crate) unsafe fn xml_strdup(str: *const xmlChar) -> *mut xmlChar {
96    if str.is_null() {
97        return ptr::null_mut();
98    }
99    let len = xml_strlen(str);
100    let copy = xmlMallocImpl(len + 1) as *mut xmlChar;
101    if copy.is_null() {
102        return ptr::null_mut();
103    }
104    ptr::copy_nonoverlapping(str, copy, len + 1);
105    copy
106}
107
108/// Duplicate a C `char*` string using `xmlMalloc`.
109///
110/// # Safety
111///
112/// `str` must point to a null-terminated C string, or be NULL.
113#[inline]
114pub(crate) unsafe fn c_strdup(str: *const c_char) -> *mut c_char {
115    if str.is_null() {
116        return ptr::null_mut();
117    }
118    let len = libc::strlen(str);
119    let copy = xmlMallocImpl(len + 1) as *mut c_char;
120    if copy.is_null() {
121        return ptr::null_mut();
122    }
123    ptr::copy_nonoverlapping(str as *const u8, copy as *mut u8, len + 1);
124    copy
125}
126
127/// Convert a Rust byte slice to a null-terminated `xmlChar*` allocated via `xmlMalloc`.
128///
129/// # Safety
130///
131/// The caller must free the returned pointer with `xmlFree`.
132pub(crate) unsafe fn bytes_to_xmlstr(bytes: &[u8]) -> *mut xmlChar {
133    let len = bytes.len();
134    let ptr = xmlMallocImpl(len + 1) as *mut xmlChar;
135    if ptr.is_null() {
136        return ptr::null_mut();
137    }
138    ptr::copy_nonoverlapping(bytes.as_ptr(), ptr, len);
139    *ptr.add(len) = 0; // null-terminate
140    ptr
141}
142
143/// Convert a `*const xmlChar` to a byte slice.
144///
145/// # Safety
146///
147/// `str` must be NULL or point to a null-terminated sequence of bytes.
148/// The returned slice borrows from the original memory.
149#[inline]
150pub(crate) const unsafe fn xmlstr_to_bytes(str: *const xmlChar) -> &'static [u8] {
151    if str.is_null() {
152        return &[];
153    }
154    let len = xml_strlen(str);
155    slice::from_raw_parts(str, len)
156}
157
158/// Compare two null-terminated `xmlChar` strings.
159///
160/// Returns 0 if equal, <0 if str1 < str2, >0 if str1 > str2.
161///
162/// # Safety
163///
164/// Both strings must be null-terminated or NULL.
165#[inline]
166pub(crate) unsafe fn xml_strcmp(str1: *const xmlChar, str2: *const xmlChar) -> i32 {
167    if str1 == str2 {
168        return 0;
169    }
170    if str1.is_null() {
171        return -1;
172    }
173    if str2.is_null() {
174        return 1;
175    }
176    let mut i: usize = 0;
177    loop {
178        let a = *str1.add(i);
179        let b = *str2.add(i);
180        if a != b {
181            return a as i32 - b as i32;
182        }
183        if a == 0 {
184            return 0;
185        }
186        i += 1;
187    }
188}
189
190/// Concatenate two null-terminated `xmlChar` strings.
191///
192/// Returns a newly allocated string. Caller must free with `xmlFree`.
193///
194/// # Safety
195///
196/// Both strings must be null-terminated or NULL.
197#[inline]
198#[allow(dead_code)]
199pub(crate) unsafe fn xml_strcat(str1: *const xmlChar, str2: *const xmlChar) -> *mut xmlChar {
200    let len1 = xml_strlen(str1);
201    let len2 = xml_strlen(str2);
202    let result = xmlMallocImpl(len1 + len2 + 1) as *mut xmlChar;
203    if result.is_null() {
204        return ptr::null_mut();
205    }
206    if !str1.is_null() {
207        ptr::copy_nonoverlapping(str1, result, len1);
208    }
209    if !str2.is_null() {
210        ptr::copy_nonoverlapping(str2, result.add(len1), len2);
211    }
212    *result.add(len1 + len2) = 0;
213    result
214}
215
216/// Convert a `*const xmlChar` to a Rust `String`.
217///
218/// Returns an empty string for NULL pointers.
219///
220/// # Safety
221///
222/// `str` must be NULL or point to a null-terminated sequence of bytes.
223#[inline]
224pub(crate) unsafe fn xml_strndup(str: *const xmlChar, len: usize) -> *mut xmlChar {
225    if str.is_null() {
226        return ptr::null_mut();
227    }
228    let p = unsafe { xmlMallocImpl(len + 1) as *mut xmlChar };
229    if p.is_null() {
230        return ptr::null_mut();
231    }
232    unsafe {
233        ptr::copy_nonoverlapping(str, p, len);
234        *p.add(len) = 0;
235    }
236    p
237}
238
239/// Convert a null-terminated `xmlChar` string into a Rust `String`
240/// (lossy UTF-8 conversion; empty string for NULL).
241///
242/// # Safety
243///
244/// `str` must be NULL or point to a null-terminated byte sequence.
245pub(crate) unsafe fn xmlstr_to_string(str: *const xmlChar) -> String {
246    if str.is_null() {
247        return String::new();
248    }
249    let bytes = unsafe { xmlstr_to_bytes(str) };
250    String::from_utf8_lossy(bytes).to_string()
251}
252
253/// Build a QName `prefix:local` (upstream tree.c `xmlBuildQName`):
254/// writes into `memory` when it is large enough, otherwise allocates.
255/// Returns the resulting string (allocator-owned when not `memory`), or
256/// NULL on error. A NULL prefix returns `ncname` unchanged.
257///
258/// # Safety
259///
260/// - `ncname`, `prefix` must be valid null-terminated strings or NULL.
261/// - `memory` must be a valid buffer of `len` bytes or NULL.
262pub unsafe fn build_qname(
263    ncname: *const xmlChar,
264    prefix: *const xmlChar,
265    memory: *mut xmlChar,
266    len: c_int,
267) -> *mut xmlChar {
268    if ncname.is_null() {
269        return ptr::null_mut();
270    }
271    if prefix.is_null() {
272        return ncname as *mut xmlChar;
273    }
274    unsafe {
275        let lenn = xml_strlen(ncname);
276        let lenp = xml_strlen(prefix);
277        let ret = if memory.is_null() || (len as usize) < lenn + lenp + 2 {
278            let p = xmlMallocImpl(lenn + lenp + 2) as *mut xmlChar;
279            if p.is_null() {
280                return ptr::null_mut();
281            }
282            p
283        } else {
284            memory
285        };
286        ptr::copy_nonoverlapping(prefix, ret, lenp);
287        *ret.add(lenp) = b':' as xmlChar;
288        ptr::copy_nonoverlapping(ncname, ret.add(lenp + 1), lenn);
289        *ret.add(lenn + lenp + 1) = 0;
290        ret
291    }
292}
293
294/// Split a QName into prefix and local part (upstream tree.c
295/// `xmlSplitQName2`): returns NULL when the name has no prefix (or starts
296/// with ':'), otherwise allocates `*prefix` with the prefix and returns the
297/// local part.
298///
299/// # Safety
300///
301/// - `name` must be a valid null-terminated string or NULL.
302/// - `prefix` must be a valid `xmlChar**`.
303pub unsafe fn split_qname2(name: *const xmlChar, prefix: *mut *mut xmlChar) -> *mut xmlChar {
304    if prefix.is_null() {
305        return ptr::null_mut();
306    }
307    unsafe {
308        *prefix = ptr::null_mut();
309    }
310    if name.is_null() {
311        return ptr::null_mut();
312    }
313    unsafe {
314        // "nasty but valid" (upstream): leading ':' has no prefix
315        if *name == b':' as xmlChar {
316            return ptr::null_mut();
317        }
318        let mut len: usize = 0;
319        while *name.add(len) != 0 && *name.add(len) != b':' as xmlChar {
320            len += 1;
321        }
322        if *name.add(len) == 0 || *name.add(len + 1) == 0 {
323            return ptr::null_mut();
324        }
325        let p = xml_strndup(name, len);
326        if p.is_null() {
327            return ptr::null_mut();
328        }
329        *prefix = p;
330        let ret = xml_strdup(name.add(len + 1));
331        if ret.is_null() {
332            xmlFreeImpl(*prefix as *mut c_void);
333            *prefix = ptr::null_mut();
334            return ptr::null_mut();
335        }
336        ret
337    }
338}
339
340/// Split a QName returning the local-name pointer (upstream tree.c
341/// `xmlSplitQName3`): returns a pointer to the local part after the ':' and
342/// fills `*len` with the prefix length, or NULL when the name has no prefix
343/// (or NULL arguments). R-000176: the candidate previously returned the
344/// prefix length as an int.
345///
346/// # Safety
347///
348/// - `name` must be a valid null-terminated string or NULL.
349pub unsafe fn split_qname3(name: *const xmlChar, len: *mut c_int) -> *mut xmlChar {
350    if name.is_null() || len.is_null() {
351        return ptr::null_mut();
352    }
353    unsafe {
354        if *name == b':' as xmlChar {
355            return ptr::null_mut();
356        }
357        let mut l: usize = 0;
358        while *name.add(l) != 0 && *name.add(l) != b':' as xmlChar {
359            l += 1;
360        }
361        if *name.add(l) == 0 {
362            return ptr::null_mut();
363        }
364        *len = l as c_int;
365        name.add(l + 1) as *mut xmlChar
366    }
367}
368
369/// Return the number of UTF-8 characters in a string (upstream
370/// `xmlUTF8Strlen`).
371///
372/// # Safety
373///
374/// - `utf` must be a valid null-terminated UTF-8 string or NULL (NULL
375///   returns -1, matching upstream xmlstring.c `xmlUTF8Strlen`).
376pub const unsafe fn utf8_strlen(utf: *const xmlChar) -> c_int {
377    if utf.is_null() {
378        return -1;
379    }
380    unsafe {
381        let mut n: c_int = 0;
382        let mut cur = utf;
383        while *cur != 0 {
384            let c = *cur;
385            if c & 0x80 == 0 {
386                cur = cur.add(1);
387            } else if c & 0xe0 == 0xc0 {
388                cur = cur.add(2);
389            } else if c & 0xf0 == 0xe0 {
390                cur = cur.add(3);
391            } else if c & 0xf8 == 0xf0 {
392                cur = cur.add(4);
393            } else {
394                // invalid sequence: stop counting
395                return n;
396            }
397            n += 1;
398        }
399        n
400    }
401}
402
403/// Size in bytes of the UTF-8 sequence starting at `utf` (upstream
404/// `xmlUTF8Size`): returns the sequence length, or -1 on invalid leading
405/// byte, 0 on NUL.
406///
407/// # Safety
408///
409/// - `utf` must be a valid pointer into a UTF-8 string.
410pub const unsafe fn utf8_size(utf: *const xmlChar) -> c_int {
411    if utf.is_null() {
412        return -1;
413    }
414    unsafe {
415        let c = *utf;
416        if c == 0 {
417            return 0;
418        }
419        if c & 0x80 == 0 {
420            1
421        } else if c & 0xe0 == 0xc0 {
422            2
423        } else if c & 0xf0 == 0xe0 {
424            3
425        } else if c & 0xf8 == 0xf0 {
426            4
427        } else {
428            -1
429        }
430    }
431}
432
433/// Check that a byte string is valid UTF-8 (upstream `xmlCheckUTF8`):
434/// returns 1 when valid, 0 otherwise.
435///
436/// # Safety
437///
438/// - `utf` must be a valid null-terminated byte string.
439pub const unsafe fn check_utf8(utf: *const xmlChar) -> c_int {
440    if utf.is_null() {
441        return 0;
442    }
443    unsafe {
444        let mut cur = utf;
445        while *cur != 0 {
446            let c = *cur;
447            if c & 0x80 == 0 {
448                cur = cur.add(1);
449            } else if c & 0xe0 == 0xc0 {
450                // 2-byte: 110xxxxx 10xxxxxx
451                let c1 = *cur.add(1);
452                if c1 & 0xc0 != 0x80 {
453                    return 0;
454                }
455                cur = cur.add(2);
456            } else if c & 0xf0 == 0xe0 {
457                let c1 = *cur.add(1);
458                let c2 = *cur.add(2);
459                if c1 & 0xc0 != 0x80 || c2 & 0xc0 != 0x80 {
460                    return 0;
461                }
462                cur = cur.add(3);
463            } else if c & 0xf8 == 0xf0 {
464                let c1 = *cur.add(1);
465                let c2 = *cur.add(2);
466                let c3 = *cur.add(3);
467                if c1 & 0xc0 != 0x80 || c2 & 0xc0 != 0x80 || c3 & 0xc0 != 0x80 {
468                    return 0;
469                }
470                cur = cur.add(4);
471            } else {
472                return 0;
473            }
474        }
475        1
476    }
477}
478#[inline]
479pub(crate) const unsafe fn xml_str_starts_with(
480    str: *const xmlChar,
481    prefix: *const xmlChar,
482) -> bool {
483    if str.is_null() || prefix.is_null() {
484        return false;
485    }
486    let mut i: usize = 0;
487    loop {
488        let p = *prefix.add(i);
489        if p == 0 {
490            return true; // reached end of prefix without mismatch
491        }
492        if *str.add(i) != p {
493            return false;
494        }
495        i += 1;
496    }
497}
498
499#[cfg(test)]
500mod tests {
501    use super::*;
502    use crate::abi::allocator::xmlFreeImpl;
503
504    /// Measure C-string lengths, including the NULL and empty cases.
505    ///
506    /// # Safety
507    ///
508    /// - The tested pointers are NULL or static NUL-terminated strings
509    ///   valid for the duration of `xml_strlen`'s scan.
510    #[test]
511    fn test_xml_strlen() {
512        unsafe {
513            assert_eq!(xml_strlen(ptr::null()), 0);
514            let s = b"hello\0" as *const u8 as *const xmlChar;
515            assert_eq!(xml_strlen(s), 5);
516            let empty = b"\0" as *const u8 as *const xmlChar;
517            assert_eq!(xml_strlen(empty), 0);
518        }
519    }
520
521    /// Duplicate a C string and verify the copy's contents and terminator.
522    ///
523    /// # Safety
524    ///
525    /// - `s` is NULL or a static NUL-terminated string; `dup` is
526    ///   allocator-owned, valid for `len + 1` bytes, and freed with
527    ///   `xmlFreeImpl` exactly once; `dup` must not be read afterwards.
528    #[test]
529    fn test_xml_strdup() {
530        unsafe {
531            assert!(xml_strdup(ptr::null()).is_null());
532            let s = b"hello\0" as *const u8 as *const xmlChar;
533            let dup = xml_strdup(s);
534            assert!(!dup.is_null());
535            assert_eq!(xml_strlen(dup), 5);
536            assert_eq!(*dup.add(0), b'h');
537            assert_eq!(*dup.add(4), b'o');
538            assert_eq!(*dup.add(5), 0);
539            xmlFreeImpl(dup as *mut c_void);
540        }
541    }
542
543    /// Compare C strings, including NULL arguments.
544    ///
545    /// # Safety
546    ///
547    /// - Every tested pointer is NULL or a static NUL-terminated string
548    ///   valid for the duration of `xml_strcmp`'s scan.
549    #[test]
550    fn test_xml_strcmp() {
551        unsafe {
552            assert_eq!(xml_strcmp(ptr::null(), ptr::null()), 0);
553            assert!(xml_strcmp(b"a\0" as *const u8 as *const xmlChar, ptr::null()) > 0);
554            let a = b"abc\0" as *const u8 as *const xmlChar;
555            let b = b"abc\0" as *const u8 as *const xmlChar;
556            assert_eq!(xml_strcmp(a, b), 0);
557            let c = b"abd\0" as *const u8 as *const xmlChar;
558            assert!(xml_strcmp(a, c) < 0);
559            assert!(xml_strcmp(c, a) > 0);
560        }
561    }
562
563    /// Concatenate two C strings and verify the result.
564    ///
565    /// # Safety
566    ///
567    /// - `a` and `b` are static NUL-terminated strings valid for the call;
568    ///   `result` is allocator-owned, valid while read, and freed with
569    ///   `xmlFreeImpl` exactly once.
570    #[test]
571    fn test_xml_strcat() {
572        unsafe {
573            let a = b"hello \0" as *const u8 as *const xmlChar;
574            let b = b"world\0" as *const u8 as *const xmlChar;
575            let result = xml_strcat(a, b);
576            assert!(!result.is_null());
577            assert_eq!(xml_strlen(result), 11);
578            let expected = b"hello world\0";
579            let mut i = 0;
580            while expected[i] != 0 {
581                assert_eq!(*result.add(i), expected[i]);
582                i += 1;
583            }
584            xmlFreeImpl(result as *mut c_void);
585        }
586    }
587
588    /// Convert a byte slice to a NUL-terminated xmlChar buffer.
589    ///
590    /// # Safety
591    ///
592    /// - `bytes` is a static slice; `ptr` is allocator-owned, valid for
593    ///   `len + 1` bytes, and freed with `xmlFreeImpl` exactly once.
594    #[test]
595    fn test_bytes_to_xmlstr() {
596        unsafe {
597            let bytes = b"hello";
598            let ptr = bytes_to_xmlstr(bytes);
599            assert!(!ptr.is_null());
600            assert_eq!(xml_strlen(ptr), 5);
601            assert_eq!(*ptr.add(5), 0);
602            xmlFreeImpl(ptr as *mut c_void);
603        }
604    }
605
606    /// Check prefix matching, including NULL arguments.
607    ///
608    /// # Safety
609    ///
610    /// - The tested pointers are NULL or static NUL-terminated strings
611    ///   valid for the duration of `xml_str_starts_with`'s scan.
612    #[test]
613    fn test_xml_str_starts_with() {
614        unsafe {
615            let s = b"hello world\0" as *const u8 as *const xmlChar;
616            let prefix = b"hello\0" as *const u8 as *const xmlChar;
617            let not_prefix = b"world\0" as *const u8 as *const xmlChar;
618            assert!(xml_str_starts_with(s, prefix));
619            assert!(!xml_str_starts_with(s, not_prefix));
620            assert!(!xml_str_starts_with(ptr::null(), prefix));
621            assert!(!xml_str_starts_with(s, ptr::null()));
622        }
623    }
624}