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::xmlMalloc;
7use crate::abi::types::xmlChar;
8use core::ffi::c_void;
9use core::ptr;
10use std::os::raw::c_char;
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 = xmlMalloc(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 = xmlMalloc(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 = xmlMalloc(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 = xmlMalloc(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 xmlstr_to_string(str: *const xmlChar) -> String {
174    if str.is_null() {
175        return String::new();
176    }
177    let bytes = xmlstr_to_bytes(str);
178    String::from_utf8_lossy(bytes).to_string()
179}
180
181/// Check if `str` starts with `prefix`.
182///
183/// # Safety
184///
185/// Both pointers must be null-terminated or NULL.
186#[inline]
187pub(crate) unsafe fn xml_str_starts_with(str: *const xmlChar, prefix: *const xmlChar) -> bool {
188    if str.is_null() || prefix.is_null() {
189        return false;
190    }
191    let mut i: usize = 0;
192    loop {
193        let p = *prefix.add(i);
194        if p == 0 {
195            return true; // reached end of prefix without mismatch
196        }
197        if *str.add(i) != p {
198            return false;
199        }
200        i += 1;
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207    use crate::abi::allocator::xmlFree;
208
209    #[test]
210    fn test_xml_strlen() {
211        unsafe {
212            assert_eq!(xml_strlen(ptr::null()), 0);
213            let s = b"hello\0" as *const u8 as *const xmlChar;
214            assert_eq!(xml_strlen(s), 5);
215            let empty = b"\0" as *const u8 as *const xmlChar;
216            assert_eq!(xml_strlen(empty), 0);
217        }
218    }
219
220    #[test]
221    fn test_xml_strdup() {
222        unsafe {
223            assert!(xml_strdup(ptr::null()).is_null());
224            let s = b"hello\0" as *const u8 as *const xmlChar;
225            let dup = xml_strdup(s);
226            assert!(!dup.is_null());
227            assert_eq!(xml_strlen(dup), 5);
228            assert_eq!(*dup.add(0), b'h');
229            assert_eq!(*dup.add(4), b'o');
230            assert_eq!(*dup.add(5), 0);
231            xmlFree(dup as *mut c_void);
232        }
233    }
234
235    #[test]
236    fn test_xml_strcmp() {
237        unsafe {
238            assert_eq!(xml_strcmp(ptr::null(), ptr::null()), 0);
239            assert!(xml_strcmp(b"a\0" as *const u8 as *const xmlChar, ptr::null()) > 0);
240            let a = b"abc\0" as *const u8 as *const xmlChar;
241            let b = b"abc\0" as *const u8 as *const xmlChar;
242            assert_eq!(xml_strcmp(a, b), 0);
243            let c = b"abd\0" as *const u8 as *const xmlChar;
244            assert!(xml_strcmp(a, c) < 0);
245            assert!(xml_strcmp(c, a) > 0);
246        }
247    }
248
249    #[test]
250    fn test_xml_strcat() {
251        unsafe {
252            let a = b"hello \0" as *const u8 as *const xmlChar;
253            let b = b"world\0" as *const u8 as *const xmlChar;
254            let result = xml_strcat(a, b);
255            assert!(!result.is_null());
256            assert_eq!(xml_strlen(result), 11);
257            let expected = b"hello world\0";
258            let mut i = 0;
259            while expected[i] != 0 {
260                assert_eq!(*result.add(i), expected[i]);
261                i += 1;
262            }
263            xmlFree(result as *mut c_void);
264        }
265    }
266
267    #[test]
268    fn test_bytes_to_xmlstr() {
269        unsafe {
270            let bytes = b"hello";
271            let ptr = bytes_to_xmlstr(bytes);
272            assert!(!ptr.is_null());
273            assert_eq!(xml_strlen(ptr), 5);
274            assert_eq!(*ptr.add(5), 0);
275            xmlFree(ptr as *mut c_void);
276        }
277    }
278
279    #[test]
280    fn test_xml_str_starts_with() {
281        unsafe {
282            let s = b"hello world\0" as *const u8 as *const xmlChar;
283            let prefix = b"hello\0" as *const u8 as *const xmlChar;
284            let not_prefix = b"world\0" as *const u8 as *const xmlChar;
285            assert!(xml_str_starts_with(s, prefix));
286            assert!(!xml_str_starts_with(s, not_prefix));
287            assert!(!xml_str_starts_with(ptr::null(), prefix));
288            assert!(!xml_str_starts_with(s, ptr::null()));
289        }
290    }
291}