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