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, xmlReallocImpl};
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) 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) 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]
148pub(crate) unsafe fn xml_strcat(str1: *const xmlChar, str2: *const xmlChar) -> *mut xmlChar {
149    let len1 = xml_strlen(str1);
150    let len2 = xml_strlen(str2);
151    let result = xmlMallocImpl(len1 + len2 + 1) as *mut xmlChar;
152    if result.is_null() {
153        return ptr::null_mut();
154    }
155    if !str1.is_null() {
156        ptr::copy_nonoverlapping(str1, result, len1);
157    }
158    if !str2.is_null() {
159        ptr::copy_nonoverlapping(str2, result.add(len1), len2);
160    }
161    *result.add(len1 + len2) = 0;
162    result
163}
164
165/// Convert a `*const xmlChar` to a Rust `String`.
166///
167/// Returns an empty string for NULL pointers.
168///
169/// # Safety
170///
171/// `str` must be NULL or point to a null-terminated sequence of bytes.
172#[inline]
173pub(crate) unsafe fn xml_strndup(str: *const xmlChar, len: usize) -> *mut xmlChar {
174    if str.is_null() {
175        return ptr::null_mut();
176    }
177    let p = unsafe { xmlMallocImpl(len + 1) as *mut xmlChar };
178    if p.is_null() {
179        return ptr::null_mut();
180    }
181    unsafe {
182        ptr::copy_nonoverlapping(str, p, len);
183        *p.add(len) = 0;
184    }
185    p
186}
187
188/// Convert a null-terminated `xmlChar` string into a Rust `String`
189/// (lossy UTF-8 conversion; empty string for NULL).
190///
191/// # Safety
192///
193/// `str` must be NULL or point to a null-terminated byte sequence.
194pub(crate) unsafe fn xmlstr_to_string(str: *const xmlChar) -> String {
195    if str.is_null() {
196        return String::new();
197    }
198    let bytes = unsafe { xmlstr_to_bytes(str) };
199    String::from_utf8_lossy(bytes).to_string()
200}
201
202/// Build a QName `prefix:local` (upstream tree.c `xmlBuildQName`):
203/// writes into `memory` when it is large enough, otherwise allocates.
204/// Returns the resulting string (allocator-owned when not `memory`), or
205/// NULL on error. A NULL prefix returns `ncname` unchanged.
206///
207/// # Safety
208///
209/// - `ncname`, `prefix` must be valid null-terminated strings or NULL.
210/// - `memory` must be a valid buffer of `len` bytes or NULL.
211pub unsafe fn build_qname(
212    ncname: *const xmlChar,
213    prefix: *const xmlChar,
214    memory: *mut xmlChar,
215    len: c_int,
216) -> *mut xmlChar {
217    if ncname.is_null() {
218        return ptr::null_mut();
219    }
220    if prefix.is_null() {
221        return ncname as *mut xmlChar;
222    }
223    unsafe {
224        let lenn = xml_strlen(ncname);
225        let lenp = xml_strlen(prefix);
226        let ret = if memory.is_null() || (len as usize) < lenn + lenp + 2 {
227            let p = xmlMallocImpl(lenn + lenp + 2) as *mut xmlChar;
228            if p.is_null() {
229                return ptr::null_mut();
230            }
231            p
232        } else {
233            memory
234        };
235        ptr::copy_nonoverlapping(prefix, ret, lenp);
236        *ret.add(lenp) = b':' as xmlChar;
237        ptr::copy_nonoverlapping(ncname, ret.add(lenp + 1), lenn);
238        *ret.add(lenn + lenp + 1) = 0;
239        ret
240    }
241}
242
243/// Split a QName into prefix and local part (upstream tree.c
244/// `xmlSplitQName2`): returns NULL when the name has no prefix (or starts
245/// with ':'), otherwise allocates `*prefix` with the prefix and returns the
246/// local part.
247///
248/// # Safety
249///
250/// - `name` must be a valid null-terminated string or NULL.
251/// - `prefix` must be a valid `xmlChar**`.
252pub unsafe fn split_qname2(name: *const xmlChar, prefix: *mut *mut xmlChar) -> *mut xmlChar {
253    if prefix.is_null() {
254        return ptr::null_mut();
255    }
256    unsafe {
257        *prefix = ptr::null_mut();
258    }
259    if name.is_null() {
260        return ptr::null_mut();
261    }
262    unsafe {
263        // "nasty but valid" (upstream): leading ':' has no prefix
264        if *name == b':' as xmlChar {
265            return ptr::null_mut();
266        }
267        let mut len: usize = 0;
268        while *name.add(len) != 0 && *name.add(len) != b':' as xmlChar {
269            len += 1;
270        }
271        if *name.add(len) == 0 || *name.add(len + 1) == 0 {
272            return ptr::null_mut();
273        }
274        let p = xml_strndup(name, len);
275        if p.is_null() {
276            return ptr::null_mut();
277        }
278        *prefix = p;
279        let ret = xml_strdup(name.add(len + 1));
280        if ret.is_null() {
281            xmlFreeImpl(*prefix as *mut c_void);
282            *prefix = ptr::null_mut();
283            return ptr::null_mut();
284        }
285        ret
286    }
287}
288
289/// Split a QName returning the prefix length (upstream tree.c
290/// `xmlSplitQName3`): returns the length of the prefix (before ':'), or 0
291/// when there is no prefix. `name` must not start with ':'.
292///
293/// # Safety
294///
295/// - `name` must be a valid null-terminated string or NULL.
296pub unsafe fn split_qname3(name: *const xmlChar, len: *mut c_int) -> c_int {
297    if name.is_null() || len.is_null() {
298        return 0;
299    }
300    unsafe {
301        if *name == b':' as xmlChar {
302            return 0;
303        }
304        let mut l: usize = 0;
305        while *name.add(l) != 0 && *name.add(l) != b':' as xmlChar {
306            l += 1;
307        }
308        if *name.add(l) == 0 {
309            return 0;
310        }
311        *len = l as c_int;
312        l as c_int
313    }
314}
315
316/// Return the number of UTF-8 characters in a string (upstream
317/// `xmlUTF8Strlen`).
318///
319/// # Safety
320///
321/// - `utf` must be a valid null-terminated UTF-8 string.
322pub unsafe fn utf8_strlen(utf: *const xmlChar) -> c_int {
323    if utf.is_null() {
324        return 0;
325    }
326    unsafe {
327        let mut n: c_int = 0;
328        let mut cur = utf;
329        while *cur != 0 {
330            let c = *cur;
331            if c & 0x80 == 0 {
332                cur = cur.add(1);
333            } else if c & 0xe0 == 0xc0 {
334                cur = cur.add(2);
335            } else if c & 0xf0 == 0xe0 {
336                cur = cur.add(3);
337            } else if c & 0xf8 == 0xf0 {
338                cur = cur.add(4);
339            } else {
340                // invalid sequence: stop counting
341                return n;
342            }
343            n += 1;
344        }
345        n
346    }
347}
348
349/// Size in bytes of the UTF-8 sequence starting at `utf` (upstream
350/// `xmlUTF8Size`): returns the sequence length, or -1 on invalid leading
351/// byte, 0 on NUL.
352///
353/// # Safety
354///
355/// - `utf` must be a valid pointer into a UTF-8 string.
356pub unsafe fn utf8_size(utf: *const xmlChar) -> c_int {
357    if utf.is_null() {
358        return -1;
359    }
360    unsafe {
361        let c = *utf;
362        if c == 0 {
363            return 0;
364        }
365        if c & 0x80 == 0 {
366            1
367        } else if c & 0xe0 == 0xc0 {
368            2
369        } else if c & 0xf0 == 0xe0 {
370            3
371        } else if c & 0xf8 == 0xf0 {
372            4
373        } else {
374            -1
375        }
376    }
377}
378
379/// Check that a byte string is valid UTF-8 (upstream `xmlCheckUTF8`):
380/// returns 1 when valid, 0 otherwise.
381///
382/// # Safety
383///
384/// - `utf` must be a valid null-terminated byte string.
385pub unsafe fn check_utf8(utf: *const xmlChar) -> c_int {
386    if utf.is_null() {
387        return 0;
388    }
389    unsafe {
390        let mut cur = utf;
391        while *cur != 0 {
392            let c = *cur;
393            if c & 0x80 == 0 {
394                cur = cur.add(1);
395            } else if c & 0xe0 == 0xc0 {
396                // 2-byte: 110xxxxx 10xxxxxx
397                let c1 = *cur.add(1);
398                if c1 & 0xc0 != 0x80 {
399                    return 0;
400                }
401                cur = cur.add(2);
402            } else if c & 0xf0 == 0xe0 {
403                let c1 = *cur.add(1);
404                let c2 = *cur.add(2);
405                if c1 & 0xc0 != 0x80 || c2 & 0xc0 != 0x80 {
406                    return 0;
407                }
408                cur = cur.add(3);
409            } else if c & 0xf8 == 0xf0 {
410                let c1 = *cur.add(1);
411                let c2 = *cur.add(2);
412                let c3 = *cur.add(3);
413                if c1 & 0xc0 != 0x80 || c2 & 0xc0 != 0x80 || c3 & 0xc0 != 0x80 {
414                    return 0;
415                }
416                cur = cur.add(4);
417            } else {
418                return 0;
419            }
420        }
421        1
422    }
423}
424#[inline]
425pub(crate) unsafe fn xml_str_starts_with(str: *const xmlChar, prefix: *const xmlChar) -> bool {
426    if str.is_null() || prefix.is_null() {
427        return false;
428    }
429    let mut i: usize = 0;
430    loop {
431        let p = *prefix.add(i);
432        if p == 0 {
433            return true; // reached end of prefix without mismatch
434        }
435        if *str.add(i) != p {
436            return false;
437        }
438        i += 1;
439    }
440}
441
442#[cfg(test)]
443mod tests {
444    use super::*;
445    use crate::abi::allocator::xmlFreeImpl;
446
447    #[test]
448    fn test_xml_strlen() {
449        unsafe {
450            assert_eq!(xml_strlen(ptr::null()), 0);
451            let s = b"hello\0" as *const u8 as *const xmlChar;
452            assert_eq!(xml_strlen(s), 5);
453            let empty = b"\0" as *const u8 as *const xmlChar;
454            assert_eq!(xml_strlen(empty), 0);
455        }
456    }
457
458    #[test]
459    fn test_xml_strdup() {
460        unsafe {
461            assert!(xml_strdup(ptr::null()).is_null());
462            let s = b"hello\0" as *const u8 as *const xmlChar;
463            let dup = xml_strdup(s);
464            assert!(!dup.is_null());
465            assert_eq!(xml_strlen(dup), 5);
466            assert_eq!(*dup.add(0), b'h');
467            assert_eq!(*dup.add(4), b'o');
468            assert_eq!(*dup.add(5), 0);
469            xmlFreeImpl(dup as *mut c_void);
470        }
471    }
472
473    #[test]
474    fn test_xml_strcmp() {
475        unsafe {
476            assert_eq!(xml_strcmp(ptr::null(), ptr::null()), 0);
477            assert!(xml_strcmp(b"a\0" as *const u8 as *const xmlChar, ptr::null()) > 0);
478            let a = b"abc\0" as *const u8 as *const xmlChar;
479            let b = b"abc\0" as *const u8 as *const xmlChar;
480            assert_eq!(xml_strcmp(a, b), 0);
481            let c = b"abd\0" as *const u8 as *const xmlChar;
482            assert!(xml_strcmp(a, c) < 0);
483            assert!(xml_strcmp(c, a) > 0);
484        }
485    }
486
487    #[test]
488    fn test_xml_strcat() {
489        unsafe {
490            let a = b"hello \0" as *const u8 as *const xmlChar;
491            let b = b"world\0" as *const u8 as *const xmlChar;
492            let result = xml_strcat(a, b);
493            assert!(!result.is_null());
494            assert_eq!(xml_strlen(result), 11);
495            let expected = b"hello world\0";
496            let mut i = 0;
497            while expected[i] != 0 {
498                assert_eq!(*result.add(i), expected[i]);
499                i += 1;
500            }
501            xmlFreeImpl(result as *mut c_void);
502        }
503    }
504
505    #[test]
506    fn test_bytes_to_xmlstr() {
507        unsafe {
508            let bytes = b"hello";
509            let ptr = bytes_to_xmlstr(bytes);
510            assert!(!ptr.is_null());
511            assert_eq!(xml_strlen(ptr), 5);
512            assert_eq!(*ptr.add(5), 0);
513            xmlFreeImpl(ptr as *mut c_void);
514        }
515    }
516
517    #[test]
518    fn test_xml_str_starts_with() {
519        unsafe {
520            let s = b"hello world\0" as *const u8 as *const xmlChar;
521            let prefix = b"hello\0" as *const u8 as *const xmlChar;
522            let not_prefix = b"world\0" as *const u8 as *const xmlChar;
523            assert!(xml_str_starts_with(s, prefix));
524            assert!(!xml_str_starts_with(s, not_prefix));
525            assert!(!xml_str_starts_with(ptr::null(), prefix));
526            assert!(!xml_str_starts_with(s, ptr::null()));
527        }
528    }
529}