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's string handling.
5
6use crate::abi::allocator::{xmlFreeImpl, xmlMallocImpl};
7use crate::abi::types::xmlChar;
8use core::ffi::c_void;
9use core::ptr;
10use std::os::raw::{c_char, c_int};
11use std::slice;
12
13/// Compute the length of a null-terminated `xmlChar` string.
14///
15/// # UPSTREAM-PARITY
16///
17/// Equivalent to `strlen((const char *)str)` in C.
18///
19/// # Safety
20///
21/// `str` must point to a null-terminated sequence of bytes.
22#[inline]
23pub(crate) const unsafe fn xml_strlen(str: *const xmlChar) -> usize {
24    if str.is_null() {
25        return 0;
26    }
27    let mut len: usize = 0;
28    while *str.add(len) != 0 {
29        len += 1;
30    }
31    len
32}
33
34/// Duplicate a null-terminated `xmlChar` string using `xmlMalloc`.
35///
36/// # UPSTREAM-PARITY
37///
38/// Equivalent to `xmlStrdup` in upstream libxml2.
39/// Returns a newly allocated copy. Caller must free with `xmlFree`.
40///
41/// # Safety
42///
43/// `str` must point to a null-terminated sequence of bytes, or be NULL.
44#[inline]
45pub(crate) unsafe fn xml_strdup(str: *const xmlChar) -> *mut xmlChar {
46    if str.is_null() {
47        return ptr::null_mut();
48    }
49    let len = xml_strlen(str);
50    let copy = xmlMallocImpl(len + 1) as *mut xmlChar;
51    if copy.is_null() {
52        return ptr::null_mut();
53    }
54    ptr::copy_nonoverlapping(str, copy, len + 1);
55    copy
56}
57
58/// Duplicate a C `char*` string using `xmlMalloc`.
59///
60/// # Safety
61///
62/// `str` must point to a null-terminated C string, or be NULL.
63#[inline]
64pub(crate) unsafe fn c_strdup(str: *const c_char) -> *mut c_char {
65    if str.is_null() {
66        return ptr::null_mut();
67    }
68    let len = libc::strlen(str);
69    let copy = xmlMallocImpl(len + 1) as *mut c_char;
70    if copy.is_null() {
71        return ptr::null_mut();
72    }
73    ptr::copy_nonoverlapping(str as *const u8, copy as *mut u8, len + 1);
74    copy
75}
76
77/// Convert a Rust byte slice to a null-terminated `xmlChar*` allocated via `xmlMalloc`.
78///
79/// # Safety
80///
81/// The caller must free the returned pointer with `xmlFree`.
82pub(crate) unsafe fn bytes_to_xmlstr(bytes: &[u8]) -> *mut xmlChar {
83    let len = bytes.len();
84    let ptr = xmlMallocImpl(len + 1) as *mut xmlChar;
85    if ptr.is_null() {
86        return ptr::null_mut();
87    }
88    ptr::copy_nonoverlapping(bytes.as_ptr(), ptr, len);
89    *ptr.add(len) = 0; // null-terminate
90    ptr
91}
92
93/// Convert a `*const xmlChar` to a byte slice.
94///
95/// # Safety
96///
97/// `str` must be NULL or point to a null-terminated sequence of bytes.
98/// The returned slice borrows from the original memory.
99#[inline]
100pub(crate) const unsafe fn xmlstr_to_bytes(str: *const xmlChar) -> &'static [u8] {
101    if str.is_null() {
102        return &[];
103    }
104    let len = xml_strlen(str);
105    slice::from_raw_parts(str, len)
106}
107
108/// Compare two null-terminated `xmlChar` strings.
109///
110/// Returns 0 if equal, <0 if str1 < str2, >0 if str1 > str2.
111///
112/// # Safety
113///
114/// Both strings must be null-terminated or NULL.
115#[inline]
116pub(crate) unsafe fn xml_strcmp(str1: *const xmlChar, str2: *const xmlChar) -> i32 {
117    if str1 == str2 {
118        return 0;
119    }
120    if str1.is_null() {
121        return -1;
122    }
123    if str2.is_null() {
124        return 1;
125    }
126    let mut i: usize = 0;
127    loop {
128        let a = *str1.add(i);
129        let b = *str2.add(i);
130        if a != b {
131            return a as i32 - b as i32;
132        }
133        if a == 0 {
134            return 0;
135        }
136        i += 1;
137    }
138}
139
140/// Concatenate two null-terminated `xmlChar` strings.
141///
142/// Returns a newly allocated string. Caller must free with `xmlFree`.
143///
144/// # Safety
145///
146/// Both strings must be null-terminated or NULL.
147#[inline]
148#[allow(dead_code)]
149pub(crate) unsafe fn xml_strcat(str1: *const xmlChar, str2: *const xmlChar) -> *mut xmlChar {
150    let len1 = xml_strlen(str1);
151    let len2 = xml_strlen(str2);
152    let result = xmlMallocImpl(len1 + len2 + 1) as *mut xmlChar;
153    if result.is_null() {
154        return ptr::null_mut();
155    }
156    if !str1.is_null() {
157        ptr::copy_nonoverlapping(str1, result, len1);
158    }
159    if !str2.is_null() {
160        ptr::copy_nonoverlapping(str2, result.add(len1), len2);
161    }
162    *result.add(len1 + len2) = 0;
163    result
164}
165
166/// Convert a `*const xmlChar` to a Rust `String`.
167///
168/// Returns an empty string for NULL pointers.
169///
170/// # Safety
171///
172/// `str` must be NULL or point to a null-terminated sequence of bytes.
173#[inline]
174pub(crate) unsafe fn xml_strndup(str: *const xmlChar, len: usize) -> *mut xmlChar {
175    if str.is_null() {
176        return ptr::null_mut();
177    }
178    let p = unsafe { xmlMallocImpl(len + 1) as *mut xmlChar };
179    if p.is_null() {
180        return ptr::null_mut();
181    }
182    unsafe {
183        ptr::copy_nonoverlapping(str, p, len);
184        *p.add(len) = 0;
185    }
186    p
187}
188
189/// Convert a null-terminated `xmlChar` string into a Rust `String`
190/// (lossy UTF-8 conversion; empty string for NULL).
191///
192/// # Safety
193///
194/// `str` must be NULL or point to a null-terminated byte sequence.
195pub(crate) unsafe fn xmlstr_to_string(str: *const xmlChar) -> String {
196    if str.is_null() {
197        return String::new();
198    }
199    let bytes = unsafe { xmlstr_to_bytes(str) };
200    String::from_utf8_lossy(bytes).to_string()
201}
202
203/// Build a QName `prefix:local` (upstream tree.c `xmlBuildQName`):
204/// writes into `memory` when it is large enough, otherwise allocates.
205/// Returns the resulting string (allocator-owned when not `memory`), or
206/// NULL on error. A NULL prefix returns `ncname` unchanged.
207///
208/// # Safety
209///
210/// - `ncname`, `prefix` must be valid null-terminated strings or NULL.
211/// - `memory` must be a valid buffer of `len` bytes or NULL.
212pub unsafe fn build_qname(
213    ncname: *const xmlChar,
214    prefix: *const xmlChar,
215    memory: *mut xmlChar,
216    len: c_int,
217) -> *mut xmlChar {
218    if ncname.is_null() {
219        return ptr::null_mut();
220    }
221    if prefix.is_null() {
222        return ncname as *mut xmlChar;
223    }
224    unsafe {
225        let lenn = xml_strlen(ncname);
226        let lenp = xml_strlen(prefix);
227        let ret = if memory.is_null() || (len as usize) < lenn + lenp + 2 {
228            let p = xmlMallocImpl(lenn + lenp + 2) as *mut xmlChar;
229            if p.is_null() {
230                return ptr::null_mut();
231            }
232            p
233        } else {
234            memory
235        };
236        ptr::copy_nonoverlapping(prefix, ret, lenp);
237        *ret.add(lenp) = b':' as xmlChar;
238        ptr::copy_nonoverlapping(ncname, ret.add(lenp + 1), lenn);
239        *ret.add(lenn + lenp + 1) = 0;
240        ret
241    }
242}
243
244/// Split a QName into prefix and local part (upstream tree.c
245/// `xmlSplitQName2`): returns NULL when the name has no prefix (or starts
246/// with ':'), otherwise allocates `*prefix` with the prefix and returns the
247/// local part.
248///
249/// # Safety
250///
251/// - `name` must be a valid null-terminated string or NULL.
252/// - `prefix` must be a valid `xmlChar**`.
253pub unsafe fn split_qname2(name: *const xmlChar, prefix: *mut *mut xmlChar) -> *mut xmlChar {
254    if prefix.is_null() {
255        return ptr::null_mut();
256    }
257    unsafe {
258        *prefix = ptr::null_mut();
259    }
260    if name.is_null() {
261        return ptr::null_mut();
262    }
263    unsafe {
264        // "nasty but valid" (upstream): leading ':' has no prefix
265        if *name == b':' as xmlChar {
266            return ptr::null_mut();
267        }
268        let mut len: usize = 0;
269        while *name.add(len) != 0 && *name.add(len) != b':' as xmlChar {
270            len += 1;
271        }
272        if *name.add(len) == 0 || *name.add(len + 1) == 0 {
273            return ptr::null_mut();
274        }
275        let p = xml_strndup(name, len);
276        if p.is_null() {
277            return ptr::null_mut();
278        }
279        *prefix = p;
280        let ret = xml_strdup(name.add(len + 1));
281        if ret.is_null() {
282            xmlFreeImpl(*prefix as *mut c_void);
283            *prefix = ptr::null_mut();
284            return ptr::null_mut();
285        }
286        ret
287    }
288}
289
290/// Split a QName returning the prefix length (upstream tree.c
291/// `xmlSplitQName3`): returns the length of the prefix (before ':'), or 0
292/// when there is no prefix. `name` must not start with ':'.
293///
294/// # Safety
295///
296/// - `name` must be a valid null-terminated string or NULL.
297pub unsafe fn split_qname3(name: *const xmlChar, len: *mut c_int) -> c_int {
298    if name.is_null() || len.is_null() {
299        return 0;
300    }
301    unsafe {
302        if *name == b':' as xmlChar {
303            return 0;
304        }
305        let mut l: usize = 0;
306        while *name.add(l) != 0 && *name.add(l) != b':' as xmlChar {
307            l += 1;
308        }
309        if *name.add(l) == 0 {
310            return 0;
311        }
312        *len = l as c_int;
313        l as c_int
314    }
315}
316
317/// Return the number of UTF-8 characters in a string (upstream
318/// `xmlUTF8Strlen`).
319///
320/// # Safety
321///
322/// - `utf` must be a valid null-terminated UTF-8 string.
323pub const unsafe fn utf8_strlen(utf: *const xmlChar) -> c_int {
324    if utf.is_null() {
325        return 0;
326    }
327    unsafe {
328        let mut n: c_int = 0;
329        let mut cur = utf;
330        while *cur != 0 {
331            let c = *cur;
332            if c & 0x80 == 0 {
333                cur = cur.add(1);
334            } else if c & 0xe0 == 0xc0 {
335                cur = cur.add(2);
336            } else if c & 0xf0 == 0xe0 {
337                cur = cur.add(3);
338            } else if c & 0xf8 == 0xf0 {
339                cur = cur.add(4);
340            } else {
341                // invalid sequence: stop counting
342                return n;
343            }
344            n += 1;
345        }
346        n
347    }
348}
349
350/// Size in bytes of the UTF-8 sequence starting at `utf` (upstream
351/// `xmlUTF8Size`): returns the sequence length, or -1 on invalid leading
352/// byte, 0 on NUL.
353///
354/// # Safety
355///
356/// - `utf` must be a valid pointer into a UTF-8 string.
357pub const unsafe fn utf8_size(utf: *const xmlChar) -> c_int {
358    if utf.is_null() {
359        return -1;
360    }
361    unsafe {
362        let c = *utf;
363        if c == 0 {
364            return 0;
365        }
366        if c & 0x80 == 0 {
367            1
368        } else if c & 0xe0 == 0xc0 {
369            2
370        } else if c & 0xf0 == 0xe0 {
371            3
372        } else if c & 0xf8 == 0xf0 {
373            4
374        } else {
375            -1
376        }
377    }
378}
379
380/// Check that a byte string is valid UTF-8 (upstream `xmlCheckUTF8`):
381/// returns 1 when valid, 0 otherwise.
382///
383/// # Safety
384///
385/// - `utf` must be a valid null-terminated byte string.
386pub const unsafe fn check_utf8(utf: *const xmlChar) -> c_int {
387    if utf.is_null() {
388        return 0;
389    }
390    unsafe {
391        let mut cur = utf;
392        while *cur != 0 {
393            let c = *cur;
394            if c & 0x80 == 0 {
395                cur = cur.add(1);
396            } else if c & 0xe0 == 0xc0 {
397                // 2-byte: 110xxxxx 10xxxxxx
398                let c1 = *cur.add(1);
399                if c1 & 0xc0 != 0x80 {
400                    return 0;
401                }
402                cur = cur.add(2);
403            } else if c & 0xf0 == 0xe0 {
404                let c1 = *cur.add(1);
405                let c2 = *cur.add(2);
406                if c1 & 0xc0 != 0x80 || c2 & 0xc0 != 0x80 {
407                    return 0;
408                }
409                cur = cur.add(3);
410            } else if c & 0xf8 == 0xf0 {
411                let c1 = *cur.add(1);
412                let c2 = *cur.add(2);
413                let c3 = *cur.add(3);
414                if c1 & 0xc0 != 0x80 || c2 & 0xc0 != 0x80 || c3 & 0xc0 != 0x80 {
415                    return 0;
416                }
417                cur = cur.add(4);
418            } else {
419                return 0;
420            }
421        }
422        1
423    }
424}
425#[inline]
426pub(crate) const unsafe fn xml_str_starts_with(
427    str: *const xmlChar,
428    prefix: *const xmlChar,
429) -> bool {
430    if str.is_null() || prefix.is_null() {
431        return false;
432    }
433    let mut i: usize = 0;
434    loop {
435        let p = *prefix.add(i);
436        if p == 0 {
437            return true; // reached end of prefix without mismatch
438        }
439        if *str.add(i) != p {
440            return false;
441        }
442        i += 1;
443    }
444}
445
446#[cfg(test)]
447mod tests {
448    use super::*;
449    use crate::abi::allocator::xmlFreeImpl;
450
451    #[test]
452    fn test_xml_strlen() {
453        unsafe {
454            assert_eq!(xml_strlen(ptr::null()), 0);
455            let s = b"hello\0" as *const u8 as *const xmlChar;
456            assert_eq!(xml_strlen(s), 5);
457            let empty = b"\0" as *const u8 as *const xmlChar;
458            assert_eq!(xml_strlen(empty), 0);
459        }
460    }
461
462    #[test]
463    fn test_xml_strdup() {
464        unsafe {
465            assert!(xml_strdup(ptr::null()).is_null());
466            let s = b"hello\0" as *const u8 as *const xmlChar;
467            let dup = xml_strdup(s);
468            assert!(!dup.is_null());
469            assert_eq!(xml_strlen(dup), 5);
470            assert_eq!(*dup.add(0), b'h');
471            assert_eq!(*dup.add(4), b'o');
472            assert_eq!(*dup.add(5), 0);
473            xmlFreeImpl(dup as *mut c_void);
474        }
475    }
476
477    #[test]
478    fn test_xml_strcmp() {
479        unsafe {
480            assert_eq!(xml_strcmp(ptr::null(), ptr::null()), 0);
481            assert!(xml_strcmp(b"a\0" as *const u8 as *const xmlChar, ptr::null()) > 0);
482            let a = b"abc\0" as *const u8 as *const xmlChar;
483            let b = b"abc\0" as *const u8 as *const xmlChar;
484            assert_eq!(xml_strcmp(a, b), 0);
485            let c = b"abd\0" as *const u8 as *const xmlChar;
486            assert!(xml_strcmp(a, c) < 0);
487            assert!(xml_strcmp(c, a) > 0);
488        }
489    }
490
491    #[test]
492    fn test_xml_strcat() {
493        unsafe {
494            let a = b"hello \0" as *const u8 as *const xmlChar;
495            let b = b"world\0" as *const u8 as *const xmlChar;
496            let result = xml_strcat(a, b);
497            assert!(!result.is_null());
498            assert_eq!(xml_strlen(result), 11);
499            let expected = b"hello world\0";
500            let mut i = 0;
501            while expected[i] != 0 {
502                assert_eq!(*result.add(i), expected[i]);
503                i += 1;
504            }
505            xmlFreeImpl(result as *mut c_void);
506        }
507    }
508
509    #[test]
510    fn test_bytes_to_xmlstr() {
511        unsafe {
512            let bytes = b"hello";
513            let ptr = bytes_to_xmlstr(bytes);
514            assert!(!ptr.is_null());
515            assert_eq!(xml_strlen(ptr), 5);
516            assert_eq!(*ptr.add(5), 0);
517            xmlFreeImpl(ptr as *mut c_void);
518        }
519    }
520
521    #[test]
522    fn test_xml_str_starts_with() {
523        unsafe {
524            let s = b"hello world\0" as *const u8 as *const xmlChar;
525            let prefix = b"hello\0" as *const u8 as *const xmlChar;
526            let not_prefix = b"world\0" as *const u8 as *const xmlChar;
527            assert!(xml_str_starts_with(s, prefix));
528            assert!(!xml_str_starts_with(s, not_prefix));
529            assert!(!xml_str_starts_with(ptr::null(), prefix));
530            assert!(!xml_str_starts_with(s, ptr::null()));
531        }
532    }
533}