Skip to main content

libxml_rs/abi/
exports_string.rs

1//! exports_string — xmlStr*/xmlUTF8*/xmlString* C ABI family (§11.1-I).
2//!
3//! Completes the string family that `exports_xml2.rs` does not already
4//! provide, with exact upstream signatures:
5//!
6//! - xmlstring.h: `xmlStrPrintf`, `xmlStrVPrintf`, `xmlStrcasestr`,
7//!   `xmlStrstr`, `xmlUTF8Charcmp`, `xmlUTF8Strloc`, `xmlUTF8Strndup`,
8//!   `xmlUTF8Strpos`, `xmlUTF8Strsize`, `xmlUTF8Strsub`
9//! - parserInternals.h: `xmlStringCurrentChar`, `xmlStringDecodeEntities`,
10//!   `xmlStringLenDecodeEntities`
11//! - tree.h: `xmlStringLenGetNodeList`
12//!
13//! Semantics follow archaeology/libxml2-git (xmlstring.c,
14//! parserInternals.c, parser.c, tree.c). `xmlStrPrintf` is variadic in C,
15//! which stable Rust cannot express (`c_variadic` is unstable); it is
16//! provided through the same inline-assembly forwarder used by the writer
17//! module's `xmlTextWriterWriteFormat*` exports (see
18//! `src/xml/writer/mod.rs`, `format_shims`).
19//!
20//! # Upstream contract
21//!
22//! Parity target is upstream `xmlstring.c` (libxml2 2.15.3,
23//! SRC-LIBXML2-2.15.0-XMLSTRING-C) plus the `xmlstring.h`/`parserInternals.h`/
24//! `tree.h` signatures; R-000165 closed the string export gaps (the string
25//! family here completes the rest).
26//!
27//! # Conceptual behavior
28//!
29//! This module implements the string-family ABI: printf-style construction
30//! (`xmlStrPrintf`/`xmlStrVPrintf` — the former variadic, provided through the
31//! same inline-assembly forwarder as the writers `xmlTextWriterWriteFormat*`,
32//! R-000155 pattern), substring and case search, UTF-8 position/size/char
33//! helpers, and the entity-decoding string entry points (`xmlStringCurrentChar`,
34//! `xmlStringDecodeEntities`, `xmlStringLenDecodeEntities`).
35//!
36//! # Ownership & safety invariants
37//!
38//! All returned strings are xml-allocator allocations the caller frees with
39//! `xmlFree` (OWNERSHIP_ATLAS section 3); `xmlStrVPrintf` output is
40//! caller-freed. The decode-entities entry points take a live `xmlParserCtxt*`
41//! (or NULL) and return fresh strings; input buffers must be readable per the
42//! documented SAFETY sections.
43//!
44//! # Historical quirks & epochs
45//!
46//! The decode-entities API is the 2.0-era entity path that the modern parser
47//! no longer uses for its main flow; SECURITY_HISTORY section 5.3 records that
48//! `xmlStringDecodeEntities`/`xmlStringLenDecodeEntities` are a documented
49//! simplified port (the depth-20/XML_ENT_EXPANDING guards exist but errors are
50//! silent — the main parser path carries the full semantics). R-000165
51//! (11.1-O) added the string gaps.
52//!
53//! # Deliberate oddities
54//!
55//! The silent-error simplification of the decode-entities port is the
56//! deliberate oddity here (fidelity note in SECURITY_HISTORY 5.3); the
57//! git-version contract (NULL on `str[len] != 0` or any non-zero end marker)
58//! is reproduced verbatim.
59//!
60//! # Proving courts
61//!
62//! The DSO-LOADER (25/25) and HEADER-COMPILE (595/595) courts plus the string
63//! unit tests under cargo test cover this module; the ENCODING-001 probe
64//! exercises the UTF-8 helpers against the oracle.
65//!
66//! # Tempting simplifications that would break parity
67//!
68//! A tempting simplification is to make the decode-entities entry points raise
69//! errors through the full parser path — the upstream API here is silent
70//! (fidelity note), so adding errors would change observable behavior;
71//! conversely, dropping the depth/expansion guards entirely would reintroduce
72//! the entity-expansion class that SEC-0006 (CVE-2014-3660) bounded. Both
73//! simplifications must not be applied.
74
75#![allow(
76    missing_docs,
77    non_snake_case,
78    non_camel_case_types,
79    non_upper_case_globals
80)]
81#![allow(unused_variables)]
82#![allow(private_interfaces)]
83#![allow(clippy::missing_safety_doc)]
84#![allow(clippy::not_unsafe_ptr_arg_deref)]
85
86use core::ffi::c_void;
87use core::ptr;
88use std::mem::size_of;
89use std::os::raw::{c_char, c_int, c_uint};
90use std::slice;
91
92use crate::abi::allocator::{xmlFreeImpl, xmlMallocImpl, xmlMallocZero};
93use crate::abi::structs::{_xmlDoc, _xmlEntity, _xmlNode, _xmlParserCtxt};
94use crate::abi::types::xmlChar;
95use crate::abi::types::xmlElementType::*;
96use crate::abi::types::xmlEntityType::*;
97use crate::xml::entities::get_entity;
98use crate::xml::string::{utf8_size, xml_strdup, xml_strlen, xml_strndup};
99use crate::xml::tree::{free_node_list, get_doc_entity, new_text};
100
101// ═══════════════════════════════════════════════════════════════════════════════
102// Shared internal helpers
103// ═══════════════════════════════════════════════════════════════════════════════
104
105/// Upstream `xmlStrncmp` (xmlstring.c): length-limited byte comparison,
106/// NULL-aware (NULL sorts before any non-NULL string; equal pointers are
107/// equal).
108///
109/// # SAFETY
110///
111/// - `str1`/`str2` must be valid pointers or NULL; only `len` bytes are
112///   read from each.
113unsafe fn xml_strncmp(str1: *const xmlChar, str2: *const xmlChar, len: c_int) -> c_int {
114    if len <= 0 {
115        return 0;
116    }
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    unsafe { libc::strncmp(str1 as *const c_char, str2 as *const c_char, len as usize) as c_int }
127}
128
129/// Upstream `xmlGetUTF8Char` (xmlstring.c): decode the UTF-8 character
130/// starting at `utf`; sets `*len` to the number of bytes consumed and
131/// returns the code point, or -1 (with `*len = 0`) on error.
132///
133/// # SAFETY
134///
135/// - `utf` must point to at least `*len` readable bytes (NUL-terminated
136///   buffers pass `*len = 4`).
137/// - `len` must be a valid `int*`.
138unsafe fn get_utf8_char(utf: *const xmlChar, len: *mut c_int) -> c_int {
139    if utf.is_null() || len.is_null() {
140        if !len.is_null() {
141            *len = 0;
142        }
143        return -1;
144    }
145    unsafe {
146        let mut c: u32 = *utf as u32;
147        if c < 0x80 {
148            if *len < 1 {
149                *len = 0;
150                return -1;
151            }
152            *len = 1;
153        } else {
154            if (*len < 2) || ((*utf.add(1) & 0xc0) != 0x80) {
155                *len = 0;
156                return -1;
157            }
158            if c < 0xe0 {
159                if c < 0xc2 {
160                    *len = 0;
161                    return -1;
162                }
163                /* 2-byte code */
164                *len = 2;
165                c = (c & 0x1f) << 6;
166                c |= (*utf.add(1) & 0x3f) as u32;
167            } else {
168                if (*len < 3) || ((*utf.add(2) & 0xc0) != 0x80) {
169                    *len = 0;
170                    return -1;
171                }
172                if c < 0xf0 {
173                    /* 3-byte code */
174                    *len = 3;
175                    c = (c & 0xf) << 12;
176                    c |= ((*utf.add(1) & 0x3f) as u32) << 6;
177                    c |= (*utf.add(2) & 0x3f) as u32;
178                    if (c < 0x800) || (0xd800..0xe000).contains(&c) {
179                        *len = 0;
180                        return -1;
181                    }
182                } else {
183                    if (*len < 4) || ((*utf.add(3) & 0xc0) != 0x80) {
184                        *len = 0;
185                        return -1;
186                    }
187                    /* 4-byte code */
188                    *len = 4;
189                    c = (c & 0x7) << 18;
190                    c |= ((*utf.add(1) & 0x3f) as u32) << 12;
191                    c |= ((*utf.add(2) & 0x3f) as u32) << 6;
192                    c |= (*utf.add(3) & 0x3f) as u32;
193                    if !(0x10000..0x110000).contains(&c) {
194                        *len = 0;
195                        return -1;
196                    }
197                }
198            }
199        }
200        c as c_int
201    }
202}
203
204/// Upstream `xmlUTF8Strsize` (xmlstring.c): byte size of the first `len`
205/// UTF-8 characters of `utf`; returns 0 for NULL/`len <= 0` and stops at
206/// the end of the string.
207///
208/// # SAFETY
209///
210/// - `utf` must be a valid null-terminated byte string or NULL.
211const unsafe fn utf8_strsize(utf: *const xmlChar, len: c_int) -> c_int {
212    if utf.is_null() || len <= 0 {
213        return 0;
214    }
215    unsafe {
216        let mut ptr = utf;
217        let mut n = len;
218        while n > 0 {
219            if *ptr == 0 {
220                break;
221            }
222            let mut ch = *ptr;
223            ptr = ptr.add(1);
224            if (ch & 0x80) != 0 {
225                loop {
226                    ch <<= 1;
227                    if (ch & 0x80) == 0 {
228                        break;
229                    }
230                    if *ptr == 0 {
231                        break;
232                    }
233                    ptr = ptr.add(1);
234                }
235            }
236            n -= 1;
237        }
238        let ret = ptr.offset_from(utf) as usize;
239        if ret > c_int::MAX as usize {
240            0
241        } else {
242            ret as c_int
243        }
244    }
245}
246
247/// Encode a Unicode code point as UTF-8 and append it to `out` (upstream
248/// `xmlCopyCharMultiByte`).
249fn utf8_encode_char(out: &mut Vec<u8>, val: u32) {
250    if val < 0x80 {
251        out.push(val as u8);
252    } else if val < 0x800 {
253        out.push(0xC0 | ((val >> 6) as u8));
254        out.push(0x80 | ((val & 0x3F) as u8));
255    } else if val < 0x10000 {
256        out.push(0xE0 | ((val >> 12) as u8));
257        out.push(0x80 | (((val >> 6) & 0x3F) as u8));
258        out.push(0x80 | ((val & 0x3F) as u8));
259    } else if val < 0x110000 {
260        out.push(0xF0 | ((val >> 18) as u8));
261        out.push(0x80 | (((val >> 12) & 0x3F) as u8));
262        out.push(0x80 | (((val >> 6) & 0x3F) as u8));
263        out.push(0x80 | ((val & 0x3F) as u8));
264    }
265}
266
267/// `IS_CHAR` (chvalid.h): XML [2] Char production.
268#[inline]
269fn is_xml_char(c: u32) -> bool {
270    c == 0x9
271        || c == 0xA
272        || c == 0xD
273        || (0x20..=0xD7FF).contains(&c)
274        || (0xE000..=0xFFFD).contains(&c)
275        || (0x10000..=0x10FFFF).contains(&c)
276}
277
278/// Content of the five predefined entities (upstream `xmlGetPredefinedEntity`).
279const fn predefined_entity_content(name: *const xmlChar) -> Option<&'static [u8]> {
280    if name.is_null() {
281        return None;
282    }
283    // SAFETY: the caller passes a NUL-terminated name.
284    let bytes = unsafe { slice::from_raw_parts(name, xml_strlen(name)) };
285    match bytes {
286        b"lt" => Some(b"<"),
287        b"gt" => Some(b">"),
288        b"amp" => Some(b"&"),
289        b"quot" => Some(b"\""),
290        b"apos" => Some(b"'"),
291        _ => None,
292    }
293}
294
295/// Upstream `xmlParseStringCharRef` (parser.c): parse `&#NN;` / `&#xHH;`
296/// at `*str`, advancing `*str` past the reference. Returns the code point,
297/// or 0 on error (upstream reports the error through the parser context
298/// and returns 0; the error callback is not replicated here).
299///
300/// # SAFETY
301///
302/// - `str` must point to a valid `*const xmlChar` into a NUL-terminated
303///   string.
304unsafe fn parse_string_char_ref(str: &mut *const xmlChar) -> u32 {
305    unsafe {
306        let ptr = *str;
307        if ptr.is_null() || *ptr != b'&' {
308            return 0;
309        }
310        if *ptr.add(1) != b'#' {
311            return 0;
312        }
313        if *ptr.add(2) == b'x' {
314            /* hex: &#xHH; */
315            let mut p = ptr.add(3);
316            let mut cur = *p;
317            let mut val: u32 = 0;
318            while cur != b';' {
319                let digit = match cur {
320                    b'0'..=b'9' => (cur - b'0') as u32,
321                    b'a'..=b'f' => (cur - b'a' + 10) as u32,
322                    b'A'..=b'F' => (cur - b'A' + 10) as u32,
323                    _ => {
324                        val = 0;
325                        break;
326                    }
327                };
328                val = val.wrapping_mul(16).wrapping_add(digit);
329                if val > 0x110000 {
330                    val = 0x110000;
331                }
332                p = p.add(1);
333                cur = *p;
334            }
335            if cur == b';' {
336                p = p.add(1);
337            }
338            *str = p;
339            if val >= 0x110000 || !is_xml_char(val) {
340                return 0;
341            }
342            val
343        } else {
344            /* decimal: &#NN; */
345            let mut p = ptr.add(2);
346            let mut cur = *p;
347            let mut val: u32 = 0;
348            while cur != b';' {
349                if !cur.is_ascii_digit() {
350                    val = 0;
351                    break;
352                }
353                val = val.wrapping_mul(10).wrapping_add((cur - b'0') as u32);
354                if val > 0x110000 {
355                    val = 0x110000;
356                }
357                p = p.add(1);
358                cur = *p;
359            }
360            if cur == b';' {
361                p = p.add(1);
362            }
363            *str = p;
364            if val >= 0x110000 || !is_xml_char(val) {
365                return 0;
366            }
367            val
368        }
369    }
370}
371
372// ═══════════════════════════════════════════════════════════════════════════════
373// xmlStrPrintf / xmlStrVPrintf (xmlstring.h)
374// ═══════════════════════════════════════════════════════════════════════════════
375
376/// The System V AMD64 `__va_list_tag` (24 bytes): gp_offset, fp_offset,
377/// overflow_arg_area, reg_save_area. A C `va_list` parameter decays to a
378/// pointer to this structure, which is exactly what the VFormat exports and
379/// the Format shims exchange (same layout as src/xml/writer/mod.rs).
380#[repr(C)]
381#[derive(Clone, Copy)]
382struct VaListTag {
383    gp_offset: c_uint,
384    fp_offset: c_uint,
385    overflow_arg_area: *mut c_void,
386    reg_save_area: *mut c_void,
387}
388
389// The platform `vsnprintf` (system libc — not an oracle dependency).
390unsafe extern "C" {
391    fn vsnprintf(s: *mut c_char, n: usize, format: *const c_char, ap: *mut VaListTag) -> c_int;
392}
393
394/// Format `msg` and place the result into `buf` (upstream xmlstring.c
395/// `xmlStrVPrintf`).
396///
397/// # UPSTREAM-PARITY
398///
399/// ```c
400/// int xmlStrVPrintf(xmlChar *buf, int len, const char *msg, va_list ap);
401/// ```
402///
403/// The C `va_list` (SysV AMD64 `__va_list_tag[1]`) decays to a pointer to
404/// the tag struct, hence the `*mut VaListTag` parameter. Returns the number
405/// of characters that would have been written had `buf` been large enough,
406/// or -1 when `buf`/`msg` is NULL or `len <= 0`. As upstream, `buf[len-1]`
407/// is always zeroed ("be safe !").
408///
409/// # SAFETY
410///
411/// - `buf` must point to a writable buffer of at least `len` bytes.
412/// - `msg` must be a valid printf format string.
413/// - `ap` must point to a valid `va_list` matching `msg`'s specifiers.
414#[no_mangle]
415pub unsafe extern "C" fn xmlStrVPrintf(
416    buf: *mut xmlChar,
417    len: c_int,
418    msg: *const c_char,
419    ap: *mut VaListTag,
420) -> c_int {
421    if buf.is_null() || msg.is_null() || len <= 0 {
422        return -1;
423    }
424    let ret = unsafe { vsnprintf(buf as *mut c_char, len as usize, msg, ap) };
425    unsafe {
426        *buf.add(len as usize - 1) = 0; /* be safe ! */
427    }
428    ret
429}
430
431/// Assembly shim for the variadic `xmlStrPrintf` export.
432///
433/// Stable Rust cannot define variadic `extern "C"` functions (c_variadic is
434/// unstable), so this #[no_mangle] export is a `noreturn` inline-asm block
435/// that captures the SysV x86-64 register save area exactly like `va_start`,
436/// builds a `va_list` and forwards it to `xmlStrVPrintf`, then restores the
437/// stack and returns directly. Same technique as the writer module's
438/// `vfmt_shim!` (see `src/xml/writer/mod.rs`).
439///
440/// # UPSTREAM-PARITY
441///
442/// ```c
443/// int xmlStrPrintf(xmlChar *buf, int len, const char *msg, ...);
444/// ```
445///
446/// Three fixed arguments (buf, len, msg) precede the varargs, so the
447/// `va_list` is built with `gp_offset = 24` and passed as the fourth
448/// argument (register `rcx`).
449///
450/// Layout: reg_save_area = rsp+0 (6 GP + 8 SSE slots, 176 bytes); the
451/// va_list struct lives at rsp+176 (gp_offset, fp_offset,
452/// overflow_arg_area, reg_save_area); overflow varargs are above the
453/// return address. LLVM emits an 8-byte alignment `push` before the block,
454/// so a 240-byte frame (≡ 0 mod 16) keeps the `call` 16-aligned, the
455/// overflow area points at rsp+256 (= entry_rsp + 8) and the alignment
456/// push is popped before `ret`.
457///
458/// # SAFETY
459///
460/// - Must only be called from C with `(xmlChar*, int, const char*, ...)`
461///   arguments matching the format string.
462#[cfg(target_arch = "x86_64")]
463#[no_mangle]
464pub unsafe extern "C" fn xmlStrPrintf() -> c_int {
465    unsafe {
466        core::arch::asm!(
467            "sub rsp, 240",
468            "mov [rsp+0], rdi",
469            "mov [rsp+8], rsi",
470            "mov [rsp+16], rdx",
471            "mov [rsp+24], rcx",
472            "mov [rsp+32], r8",
473            "mov [rsp+40], r9",
474            "movaps [rsp+48], xmm0",
475            "movaps [rsp+64], xmm1",
476            "movaps [rsp+80], xmm2",
477            "movaps [rsp+96], xmm3",
478            "movaps [rsp+112], xmm4",
479            "movaps [rsp+128], xmm5",
480            "movaps [rsp+144], xmm6",
481            "movaps [rsp+160], xmm7",
482            "mov dword ptr [rsp+176], 24",
483            "mov dword ptr [rsp+180], 48",
484            "lea rax, [rsp+256]",
485            "mov [rsp+184], rax",
486            "lea rax, [rsp]",
487            "mov [rsp+192], rax",
488            "lea rcx, [rsp+176]",
489            "call xmlStrVPrintf",
490            "add rsp, 240",
491            "add rsp, 8",
492            "ret",
493            options(noreturn),
494        );
495    }
496}
497
498// ═══════════════════════════════════════════════════════════════════════════════
499// xmlStrstr / xmlStrcasestr (xmlstring.h)
500// ═══════════════════════════════════════════════════════════════════════════════
501
502/// Find the first occurrence of `val` in `str` (upstream xmlstring.c
503/// `xmlStrstr`).
504///
505/// # UPSTREAM-PARITY
506///
507/// ```c
508/// const xmlChar *xmlStrstr(const xmlChar *str, const xmlChar *val);
509/// ```
510///
511/// Returns a pointer to the first occurrence, `str` itself when `val` is
512/// empty, or NULL when not found / either argument is NULL.
513///
514/// # SAFETY
515///
516/// - `str` and `val` must be valid null-terminated byte strings or NULL.
517#[no_mangle]
518pub unsafe extern "C" fn xmlStrstr(str: *const xmlChar, val: *const xmlChar) -> *const xmlChar {
519    if str.is_null() || val.is_null() {
520        return ptr::null();
521    }
522    let n = unsafe { xml_strlen(val) };
523    if n == 0 {
524        return str;
525    }
526    unsafe {
527        let mut cur = str;
528        while *cur != 0 {
529            if *cur == *val && libc::strncmp(cur as *const c_char, val as *const c_char, n) == 0 {
530                return cur;
531            }
532            cur = cur.add(1);
533        }
534    }
535    ptr::null()
536}
537
538/// Case-insensitive variant of `xmlStrstr` (upstream xmlstring.c
539/// `xmlStrcasestr`).
540///
541/// # UPSTREAM-PARITY
542///
543/// ```c
544/// const xmlChar *xmlStrcasestr(const xmlChar *str, const xmlChar *val);
545/// ```
546///
547/// Returns a pointer to the first case-insensitive occurrence, `str` itself
548/// when `val` is empty, or NULL when not found / either argument is NULL.
549/// The upstream `casemap[]` ASCII fold is matched by `tolower`/`strncasecmp`
550/// in the C locale.
551///
552/// # SAFETY
553///
554/// - `str` and `val` must be valid null-terminated byte strings or NULL.
555#[no_mangle]
556pub unsafe extern "C" fn xmlStrcasestr(str: *const xmlChar, val: *const xmlChar) -> *const xmlChar {
557    if str.is_null() || val.is_null() {
558        return ptr::null();
559    }
560    let n = unsafe { xml_strlen(val) };
561    if n == 0 {
562        return str;
563    }
564    unsafe {
565        let mut cur = str;
566        while *cur != 0 {
567            if libc::tolower(*cur as c_int) == libc::tolower(*val as c_int)
568                && libc::strncasecmp(cur as *const c_char, val as *const c_char, n) == 0
569            {
570                return cur;
571            }
572            cur = cur.add(1);
573        }
574    }
575    ptr::null()
576}
577
578// ═══════════════════════════════════════════════════════════════════════════════
579// xmlStringCurrentChar (parserInternals.h)
580// ═══════════════════════════════════════════════════════════════════════════════
581
582/// Decode the current character starting at `cur` (upstream
583/// parserInternals.c `xmlStringCurrentChar`).
584///
585/// # UPSTREAM-PARITY
586///
587/// ```c
588/// int xmlStringCurrentChar(xmlParserCtxt *ctxt, const xmlChar *cur, int *len);
589/// ```
590///
591/// Returns the character value (as a UCS-4 code point) and sets `*len` to
592/// the number of bytes consumed. Returns 0 (with `*len = 0`) on error or
593/// NULL arguments. The upstream implementation ignores `ctxt` (it only
594/// influences encoding detection, and the candidate is UTF-8 only), so it
595/// is unused here as well.
596///
597/// # SAFETY
598///
599/// - `cur` must be a valid pointer into a NUL-terminated byte string (a
600///   single NUL-terminated buffer suffices; the byte length is probed
601///   through `*len`, initialized to 4 as upstream).
602/// - `len` must be a valid `int*`.
603#[no_mangle]
604pub unsafe extern "C" fn xmlStringCurrentChar(
605    ctxt: *mut _xmlParserCtxt,
606    cur: *const xmlChar,
607    len: *mut c_int,
608) -> c_int {
609    if cur.is_null() || len.is_null() {
610        return 0;
611    }
612    unsafe {
613        /* cur is zero-terminated, so we can lie about its length. */
614        *len = 4;
615        let c = get_utf8_char(cur, len);
616        if c < 0 {
617            0
618        } else {
619            c
620        }
621    }
622}
623
624// ═══════════════════════════════════════════════════════════════════════════════
625// xmlStringDecodeEntities / xmlStringLenDecodeEntities (parserInternals.h)
626// ═══════════════════════════════════════════════════════════════════════════════
627
628/// Port of upstream `xmlExpandEntityInAttValue` (parser.c) restricted to
629/// the `normalize == 0` path taken by the two decode-entities exports.
630///
631/// This is a faithful simplified port: numeric character references
632/// (`&#NN;` / `&#xHH;`), the five predefined entities and general entities
633/// declared in `doc`'s DTD are expanded (recursively, with the upstream
634/// depth limit and `XML_ENT_EXPANDING` loop detection). Deviations from the
635/// full upstream machinery:
636///
637/// - errors that upstream reports through the parser context are handled
638///   silently (undeclared entity references are dropped, malformed
639///   references stop decoding, exactly like upstream, but no error
640///   callback fires);
641/// - entity resolution uses `doc` (the caller's `ctxt->myDoc`) directly
642///   instead of the SAX `getEntity` hook chain.
643///
644/// # SAFETY
645///
646/// - `str` must be a valid NUL-terminated string (which the exported
647///   entry points guarantee for the `len`-bounded variant).
648unsafe fn expand_entity_into(
649    doc: *mut _xmlDoc,
650    out: &mut Vec<u8>,
651    mut str: *const xmlChar,
652    depth: c_int,
653    pent: *mut _xmlEntity,
654) {
655    let depth = depth + 1;
656    if depth > 20 {
657        /* upstream: XML_ERR_RESOURCE_LIMIT "Maximum entity nesting depth exceeded" */
658        return;
659    }
660    if !pent.is_null() && ((*pent).flags & XML_ENT_EXPANDING) != 0 {
661        /* upstream: XML_ERR_ENTITY_LOOP */
662        return;
663    }
664
665    let mut chunk: *const xmlChar = str;
666    'scan: loop {
667        if *str == 0 {
668            break 'scan;
669        }
670        let c = *str;
671        if c != b'&' {
672            /*
673             * If this function is called without an entity, it is used to
674             * expand entities in attribute content where '<' was already
675             * unescaped and is allowed; inside entity content it is not.
676             */
677            if !pent.is_null() && c == b'<' {
678                /* upstream: fatal error + break; the chunk accumulated
679                 * before '<' is still flushed by the tail below. */
680                break 'scan;
681            }
682            if c < 0x20 {
683                /* whitespace is converted to space (normalize == 0) */
684                if chunk != str {
685                    out.extend_from_slice(slice::from_raw_parts(
686                        chunk,
687                        str.offset_from(chunk) as usize,
688                    ));
689                }
690                out.push(b' ');
691                chunk = str.add(1);
692            }
693            /* c == 0x20 is kept inside the chunk */
694            str = str.add(1);
695        } else if *str.add(1) == b'#' {
696            /* numeric character reference */
697            if chunk != str {
698                out.extend_from_slice(slice::from_raw_parts(
699                    chunk,
700                    str.offset_from(chunk) as usize,
701                ));
702            }
703            let val = parse_string_char_ref(&mut str);
704            if val == 0 {
705                /* upstream: invalid reference -> stop, return the prefix */
706                chunk = str;
707                break 'scan;
708            }
709            if val == b' ' as u32 {
710                out.push(b' ');
711            } else {
712                utf8_encode_char(out, val);
713            }
714            chunk = str;
715        } else {
716            /* named entity reference */
717            if chunk != str {
718                out.extend_from_slice(slice::from_raw_parts(
719                    chunk,
720                    str.offset_from(chunk) as usize,
721                ));
722            }
723            str = str.add(1);
724            let name_start = str;
725            while *str != 0 && *str != b';' {
726                str = str.add(1);
727            }
728            if *str != b';' {
729                /* upstream: XML_ERR_ENTITYREF_SEMICOL_MISSING -> stop */
730                chunk = str;
731                break 'scan;
732            }
733            let name = xml_strndup(name_start, str.offset_from(name_start) as usize);
734            if name.is_null() {
735                chunk = str;
736                break 'scan;
737            }
738            if let Some(content) = predefined_entity_content(name) {
739                out.extend_from_slice(content);
740            } else {
741                let ent = get_entity(doc, name);
742                if !ent.is_null() && (*ent).etype == XML_INTERNAL_PREDEFINED_ENTITY as c_int {
743                    if (*ent).content.is_null() {
744                        /* upstream: fatal "predefined entity has no content" */
745                        xmlFreeImpl(name as *mut c_void);
746                        chunk = str;
747                        break 'scan;
748                    }
749                    let content = (*ent).content;
750                    let clen = xml_strlen(content);
751                    out.extend_from_slice(slice::from_raw_parts(content, clen));
752                } else if !ent.is_null() && !(*ent).content.is_null() {
753                    if !pent.is_null() {
754                        (*pent).flags |= XML_ENT_EXPANDING;
755                    }
756                    expand_entity_into(doc, out, (*ent).content, depth, ent);
757                    if !pent.is_null() {
758                        (*pent).flags &= !XML_ENT_EXPANDING;
759                    }
760                }
761                /* ent == NULL (undeclared): the reference is dropped */
762            }
763            xmlFreeImpl(name as *mut c_void);
764            str = str.add(1); /* skip ';' */
765            chunk = str;
766        }
767    }
768    if chunk != str {
769        out.extend_from_slice(slice::from_raw_parts(
770            chunk,
771            str.offset_from(chunk) as usize,
772        ));
773    }
774}
775
776/// Upstream `xmlExpandEntitiesInAttValue` (parser.c) with `normalize = 0`:
777/// expand entity references in a NUL-terminated string into a freshly
778/// allocated `xmlChar*` (caller frees with `xmlFree`).
779///
780/// # SAFETY
781///
782/// - `str` must be a valid NUL-terminated string.
783unsafe fn expand_entities_in_att_value(doc: *mut _xmlDoc, str: *const xmlChar) -> *mut xmlChar {
784    let mut out: Vec<u8> = Vec::new();
785    expand_entity_into(doc, &mut out, str, 0, ptr::null_mut());
786    let p = xmlMallocImpl(out.len() + 1) as *mut xmlChar;
787    if p.is_null() {
788        return ptr::null_mut();
789    }
790    if !out.is_empty() {
791        ptr::copy_nonoverlapping(out.as_ptr(), p, out.len());
792    }
793    *p.add(out.len()) = 0;
794    p
795}
796
797/// Expand general entity references in a string with a known length
798/// (upstream parser.c `xmlStringLenDecodeEntities`).
799///
800/// # UPSTREAM-PARITY
801///
802/// ```c
803/// xmlChar *xmlStringLenDecodeEntities(xmlParserCtxt *ctxt,
804///                                     const xmlChar *str, int len,
805///                                     int what, xmlChar end,
806///                                     xmlChar end2, xmlChar end3);
807/// ```
808///
809/// Returns NULL when `ctxt`/`str` is NULL, `len < 0`, `str[len] != 0`, or
810/// any end marker is non-zero (the git-version contract). `what` is
811/// ignored, matching upstream where it is marked `ATTRIBUTE_UNUSED`.
812/// Otherwise returns a freshly allocated string with references expanded
813/// (numeric references and predefined/general entities; see
814/// `expand_entity_into` for the simplifications).
815///
816/// # SAFETY
817///
818/// - `ctxt` must be a valid `xmlParserCtxt*` or NULL.
819/// - `str` must point to a buffer of at least `len + 1` readable bytes
820///   with `str[len] == 0` (upstream reads `str[len]` unconditionally).
821#[no_mangle]
822pub unsafe extern "C" fn xmlStringLenDecodeEntities(
823    ctxt: *mut _xmlParserCtxt,
824    str: *const xmlChar,
825    len: c_int,
826    what: c_int,
827    end: xmlChar,
828    end2: xmlChar,
829    end3: xmlChar,
830) -> *mut xmlChar {
831    if ctxt.is_null() || str.is_null() || len < 0 {
832        return ptr::null_mut();
833    }
834    if unsafe { *str.add(len as usize) } != 0 || end != 0 || end2 != 0 || end3 != 0 {
835        return ptr::null_mut();
836    }
837    unsafe { expand_entities_in_att_value((*ctxt).myDoc, str) }
838}
839
840/// Expand general entity references in a NUL-terminated string (upstream
841/// parser.c `xmlStringDecodeEntities`, the macro-less variant).
842///
843/// # UPSTREAM-PARITY
844///
845/// ```c
846/// xmlChar *xmlStringDecodeEntities(xmlParserCtxt *ctxt,
847///                                  const xmlChar *str, int what,
848///                                  xmlChar end, xmlChar end2,
849///                                  xmlChar end3);
850/// ```
851///
852/// Returns NULL when `ctxt`/`str` is NULL or any end marker is non-zero
853/// (the git-version contract). `what` is ignored, matching upstream where
854/// it is marked `ATTRIBUTE_UNUSED`.
855///
856/// # SAFETY
857///
858/// - `ctxt` must be a valid `xmlParserCtxt*` or NULL.
859/// - `str` must be a valid NUL-terminated string.
860#[no_mangle]
861pub unsafe extern "C" fn xmlStringDecodeEntities(
862    ctxt: *mut _xmlParserCtxt,
863    str: *const xmlChar,
864    what: c_int,
865    end: xmlChar,
866    end2: xmlChar,
867    end3: xmlChar,
868) -> *mut xmlChar {
869    // SECURITY_HISTORY 5.3 fidelity note: this port keeps the depth-20 /
870    // XML_ENT_EXPANDING guards but raises errors silently — deliberate; the
871    // main parser path carries the full error semantics.
872    if ctxt.is_null() || str.is_null() {
873        return ptr::null_mut();
874    }
875    if end != 0 || end2 != 0 || end3 != 0 {
876        return ptr::null_mut();
877    }
878    unsafe { expand_entities_in_att_value((*ctxt).myDoc, str) }
879}
880
881// ═══════════════════════════════════════════════════════════════════════════════
882// xmlStringLenGetNodeList (tree.h)
883// ═══════════════════════════════════════════════════════════════════════════════
884
885/// Entity flags (include/private/entities.h).
886const XML_ENT_PARSED: c_int = 1 << 0;
887const XML_ENT_EXPANDING: c_int = 1 << 3;
888
889/// Upstream `xmlNewDocText` (tree.c): a text node associated with `doc`
890/// (NULL allowed). The dictionary lookup of the name is skipped — names
891/// are heap-allocated copies throughout this crate.
892///
893/// # SAFETY
894///
895/// - `doc` must be a valid `xmlDoc*` or NULL.
896/// - `content` must be a valid NUL-terminated string or NULL.
897unsafe fn new_doc_text(doc: *const _xmlDoc, content: *const xmlChar) -> *mut _xmlNode {
898    if !doc.is_null() {
899        let t = (*doc).type_;
900        if t != XML_DOCUMENT_NODE as c_int && t != XML_HTML_DOCUMENT_NODE as c_int {
901            return ptr::null_mut();
902        }
903    }
904    let node = new_text(content);
905    if node.is_null() {
906        return ptr::null_mut();
907    }
908    if !doc.is_null() {
909        (*node).doc = doc as *mut _xmlDoc;
910    }
911    node
912}
913
914/// Upstream `xmlNewEntityReference` (tree.c): an `XML_ENTITY_REF_NODE`
915/// carrying the entity's name.
916///
917/// # SAFETY
918///
919/// - `doc` must be a valid `xmlDoc*` or NULL.
920/// - `name` must be a valid NUL-terminated string.
921unsafe fn new_entity_ref(doc: *const _xmlDoc, name: *const xmlChar) -> *mut _xmlNode {
922    if name.is_null() {
923        return ptr::null_mut();
924    }
925    if !doc.is_null() {
926        let t = (*doc).type_;
927        if t != XML_DOCUMENT_NODE as c_int && t != XML_HTML_DOCUMENT_NODE as c_int {
928            return ptr::null_mut();
929        }
930    }
931    let node = xmlMallocZero(size_of::<_xmlNode>()) as *mut _xmlNode;
932    if node.is_null() {
933        return ptr::null_mut();
934    }
935    let name_copy = xml_strdup(name);
936    if name_copy.is_null() {
937        xmlFreeImpl(node as *mut c_void);
938        return ptr::null_mut();
939    }
940    unsafe {
941        (*node).type_ = XML_ENTITY_REF_NODE as c_int;
942        (*node).name = name_copy;
943        if !doc.is_null() {
944            (*node).doc = doc as *mut _xmlDoc;
945        }
946    }
947    node
948}
949
950/// Port of upstream `xmlNodeParseAttValue` (tree.c) for the
951/// `xmlStringLenGetNodeList` path: parse an attribute value into a list of
952/// text nodes and entity reference nodes. `attr` is the entity whose
953/// `children`/`last` receive the parsed list during recursive entity
954/// content parsing (NULL for the top-level call). The node list is
955/// returned through `list_ptr` (may be NULL); returns 0 on success, -1 on
956/// allocation failure.
957///
958/// # SAFETY
959///
960/// - `doc` must be a valid `xmlDoc*` or NULL.
961/// - `value` must be a valid NUL-terminated string of at least `len` bytes
962///   or NULL.
963/// - `list_ptr` must be a valid `xmlNode**` or NULL.
964unsafe fn node_parse_att_value(
965    doc: *const _xmlDoc,
966    attr: *mut _xmlNode,
967    value: *const xmlChar,
968    len: usize,
969    list_ptr: *mut *mut _xmlNode,
970) -> c_int {
971    let mut head: *mut _xmlNode = ptr::null_mut();
972    let mut last: *mut _xmlNode = ptr::null_mut();
973
974    if !list_ptr.is_null() {
975        *list_ptr = ptr::null_mut();
976    }
977
978    if value.is_null() || unsafe { *value } == 0 {
979        return 0;
980    }
981
982    let mut buf: Vec<u8> = Vec::new();
983    let mut cur = value;
984    let mut q = cur;
985    let mut remaining = len;
986
987    'scan: loop {
988        if remaining == 0 || unsafe { *cur } == 0 {
989            break 'scan;
990        }
991        if unsafe { *cur } == b'&' {
992            let mut charval: u32 = 0;
993
994            /* Save the current text. */
995            if cur != q {
996                unsafe {
997                    buf.extend_from_slice(slice::from_raw_parts(q, cur.offset_from(q) as usize));
998                }
999                // `q` is re-established by each reference branch below.
1000            }
1001
1002            if remaining > 2 && unsafe { *cur.add(1) } == b'#' && unsafe { *cur.add(2) } == b'x' {
1003                /* hex character reference */
1004                let mut tmp: u8 = 0;
1005                unsafe {
1006                    cur = cur.add(3);
1007                }
1008                remaining -= 3;
1009                loop {
1010                    if remaining == 0 {
1011                        break;
1012                    }
1013                    tmp = unsafe { *cur };
1014                    if tmp == b';' {
1015                        break;
1016                    }
1017                    let digit: u32 = match tmp {
1018                        b'0'..=b'9' => (tmp - b'0') as u32,
1019                        b'a'..=b'f' => (tmp - b'a' + 10) as u32,
1020                        b'A'..=b'F' => (tmp - b'A' + 10) as u32,
1021                        _ => {
1022                            charval = 0;
1023                            break;
1024                        }
1025                    };
1026                    charval = charval.wrapping_mul(16).wrapping_add(digit);
1027                    if charval > 0x110000 {
1028                        charval = 0x110000;
1029                    }
1030                    unsafe {
1031                        cur = cur.add(1);
1032                    }
1033                    remaining -= 1;
1034                }
1035                if tmp == b';' {
1036                    unsafe {
1037                        cur = cur.add(1);
1038                    }
1039                    remaining -= 1;
1040                }
1041                q = cur;
1042            } else if remaining > 1 && unsafe { *cur.add(1) } == b'#' {
1043                /* decimal character reference */
1044                let mut tmp: u8 = 0;
1045                unsafe {
1046                    cur = cur.add(2);
1047                }
1048                remaining -= 2;
1049                loop {
1050                    if remaining == 0 {
1051                        break;
1052                    }
1053                    tmp = unsafe { *cur };
1054                    if tmp == b';' {
1055                        break;
1056                    }
1057                    if !tmp.is_ascii_digit() {
1058                        charval = 0;
1059                        break;
1060                    }
1061                    charval = charval.wrapping_mul(10).wrapping_add((tmp - b'0') as u32);
1062                    if charval > 0x110000 {
1063                        charval = 0x110000;
1064                    }
1065                    unsafe {
1066                        cur = cur.add(1);
1067                    }
1068                    remaining -= 1;
1069                }
1070                if tmp == b';' {
1071                    unsafe {
1072                        cur = cur.add(1);
1073                    }
1074                    remaining -= 1;
1075                }
1076                q = cur;
1077            } else {
1078                /* read the entity name */
1079                unsafe {
1080                    cur = cur.add(1);
1081                }
1082                remaining -= 1;
1083                q = cur;
1084                while remaining > 0 && unsafe { *cur } != 0 && unsafe { *cur } != b';' {
1085                    unsafe {
1086                        cur = cur.add(1);
1087                    }
1088                    remaining -= 1;
1089                }
1090                if remaining == 0 || unsafe { *cur } == 0 {
1091                    break 'scan;
1092                }
1093                if cur != q {
1094                    let name = unsafe { xml_strndup(q, cur.offset_from(q) as usize) };
1095                    if name.is_null() {
1096                        free_node_list(head);
1097                        return -1;
1098                    }
1099                    let ent = get_doc_entity(doc, name);
1100                    if !ent.is_null() && (*ent).etype == XML_INTERNAL_PREDEFINED_ENTITY as c_int {
1101                        /* predefined entities don't generate nodes */
1102                        let content = (*ent).content;
1103                        let clen = xml_strlen(content);
1104                        unsafe {
1105                            buf.extend_from_slice(slice::from_raw_parts(content, clen));
1106                        }
1107                    } else if ent.is_null() || ((*ent).flags & XML_ENT_EXPANDING) == 0 {
1108                        /* flush the buffer so far */
1109                        if !buf.is_empty() {
1110                            buf.push(0); /* NUL-terminate for the text-node dup */
1111                            let node = new_doc_text(doc, buf.as_ptr() as *const xmlChar);
1112                            buf.pop();
1113                            if node.is_null() {
1114                                xmlFreeImpl(name as *mut c_void);
1115                                free_node_list(head);
1116                                return -1;
1117                            }
1118                            (*node).parent = attr;
1119                            if last.is_null() {
1120                                head = node;
1121                            } else {
1122                                (*last).next = node;
1123                                (*node).prev = last;
1124                            }
1125                            last = node;
1126                            buf.clear();
1127                        }
1128
1129                        /* parse the entity content if not parsed yet */
1130                        if !ent.is_null()
1131                            && ((*ent).flags & XML_ENT_PARSED) == 0
1132                            && !(*ent).content.is_null()
1133                        {
1134                            (*ent).flags |= XML_ENT_EXPANDING;
1135                            let res = node_parse_att_value(
1136                                doc,
1137                                ent as *mut _xmlNode,
1138                                (*ent).content,
1139                                usize::MAX,
1140                                ptr::null_mut(),
1141                            );
1142                            (*ent).flags &= !XML_ENT_EXPANDING;
1143                            if res < 0 {
1144                                xmlFreeImpl(name as *mut c_void);
1145                                free_node_list(head);
1146                                return -1;
1147                            }
1148                            (*ent).flags |= XML_ENT_PARSED;
1149                        }
1150
1151                        /* create a new REFERENCE_REF node */
1152                        let node = new_entity_ref(doc, name);
1153                        if node.is_null() {
1154                            xmlFreeImpl(name as *mut c_void);
1155                            free_node_list(head);
1156                            return -1;
1157                        }
1158                        (*node).parent = attr;
1159                        (*node).last = ent as *mut _xmlNode;
1160                        if !ent.is_null() {
1161                            (*node).children = ent as *mut _xmlNode;
1162                            (*node).content = (*ent).content;
1163                        }
1164                        if last.is_null() {
1165                            head = node;
1166                        } else {
1167                            (*last).next = node;
1168                            (*node).prev = last;
1169                        }
1170                        last = node;
1171                    }
1172                    xmlFreeImpl(name as *mut c_void);
1173                }
1174                unsafe {
1175                    cur = cur.add(1);
1176                }
1177                remaining -= 1;
1178                q = cur;
1179            }
1180            if charval != 0 {
1181                let charval = if charval >= 0x110000 { 0xFFFD } else { charval };
1182                utf8_encode_char(&mut buf, charval);
1183            }
1184        } else {
1185            unsafe {
1186                cur = cur.add(1);
1187            }
1188            remaining -= 1;
1189        }
1190    }
1191
1192    /* handle the last piece of text */
1193    if cur != q {
1194        unsafe {
1195            buf.extend_from_slice(slice::from_raw_parts(q, cur.offset_from(q) as usize));
1196        }
1197    }
1198
1199    if !buf.is_empty() {
1200        buf.push(0); /* NUL-terminate for the text-node dup */
1201        let node = new_doc_text(doc, buf.as_ptr() as *const xmlChar);
1202        buf.pop();
1203        if node.is_null() {
1204            free_node_list(head);
1205            return -1;
1206        }
1207        (*node).parent = attr;
1208        if last.is_null() {
1209            head = node;
1210        } else {
1211            (*last).next = node;
1212            (*node).prev = last;
1213        }
1214        last = node;
1215    } else if head.is_null() {
1216        head = new_doc_text(doc, b"" as *const u8 as *const xmlChar);
1217        if head.is_null() {
1218            return -1;
1219        }
1220        (*head).parent = attr;
1221        last = head;
1222    }
1223
1224    if !attr.is_null() {
1225        (*attr).children = head;
1226        (*attr).last = last;
1227    }
1228    if !list_ptr.is_null() {
1229        *list_ptr = head;
1230    }
1231    0
1232}
1233
1234/// Build a node list (text and entity reference nodes) from an attribute
1235/// value (upstream tree.c `xmlStringLenGetNodeList`).
1236///
1237/// # UPSTREAM-PARITY
1238///
1239/// ```c
1240/// xmlNode *xmlStringLenGetNodeList(const xmlDoc *doc,
1241///                                  const xmlChar *value, int len);
1242/// ```
1243///
1244/// Returns the head of a linked list of `XML_TEXT_NODE` /
1245/// `XML_ENTITY_REF_NODE` nodes, or NULL for a NULL/empty `value` or on
1246/// allocation failure. A negative `len` means the value is NUL-terminated.
1247/// Predefined entity references are expanded into text; other declared
1248/// entities produce entity reference nodes (whose content is parsed into
1249/// the entity declaration's children); undeclared references produce
1250/// entity reference nodes without content, as upstream.
1251///
1252/// # SAFETY
1253///
1254/// - `doc` must be a valid `xmlDoc*` or NULL.
1255/// - `value` must be a valid NUL-terminated string of at least `len` bytes
1256///   or NULL.
1257#[no_mangle]
1258pub unsafe extern "C" fn xmlStringLenGetNodeList(
1259    doc: *const _xmlDoc,
1260    value: *const xmlChar,
1261    len: c_int,
1262) -> *mut _xmlNode {
1263    let max_size: usize = if len < 0 { usize::MAX } else { len as usize };
1264    let mut ret: *mut _xmlNode = ptr::null_mut();
1265    unsafe {
1266        node_parse_att_value(doc, ptr::null_mut(), value, max_size, &mut ret);
1267    }
1268    ret
1269}
1270
1271// ═══════════════════════════════════════════════════════════════════════════════
1272// xmlUTF8* family (xmlstring.h)
1273// ═══════════════════════════════════════════════════════════════════════════════
1274
1275/// Compare two UTF-8 characters (upstream xmlstring.c `xmlUTF8Charcmp`).
1276///
1277/// # UPSTREAM-PARITY
1278///
1279/// ```c
1280/// int xmlUTF8Charcmp(const xmlChar *utf1, const xmlChar *utf2);
1281/// ```
1282///
1283/// Returns the result of comparing the first `xmlUTF8Size(utf1)` bytes
1284/// (like `xmlStrncmp`); NULL `utf1` sorts before non-NULL, both NULL are
1285/// equal.
1286///
1287/// # SAFETY
1288///
1289/// - `utf1` must be a valid pointer into a UTF-8 string or NULL.
1290/// - `utf2` must be a valid pointer or NULL.
1291#[no_mangle]
1292pub unsafe extern "C" fn xmlUTF8Charcmp(utf1: *const xmlChar, utf2: *const xmlChar) -> c_int {
1293    if utf1.is_null() {
1294        return if utf2.is_null() { 0 } else { -1 };
1295    }
1296    unsafe { xml_strncmp(utf1, utf2, utf8_size(utf1)) }
1297}
1298
1299/// Byte size of the first `len` UTF-8 characters (upstream xmlstring.c
1300/// `xmlUTF8Strsize`).
1301///
1302/// # UPSTREAM-PARITY
1303///
1304/// ```c
1305/// int xmlUTF8Strsize(const xmlChar *utf, int len);
1306/// ```
1307///
1308/// Returns 0 for NULL input, `len <= 0` or at the end of the string.
1309/// The behaviour is not guaranteed for invalid UTF-8 (as upstream).
1310///
1311/// # SAFETY
1312///
1313/// - `utf` must be a valid NUL-terminated byte string or NULL.
1314#[no_mangle]
1315pub const unsafe extern "C" fn xmlUTF8Strsize(utf: *const xmlChar, len: c_int) -> c_int {
1316    unsafe { utf8_strsize(utf, len) }
1317}
1318
1319/// Duplicate the first `len` UTF-8 characters of `utf` (upstream
1320/// xmlstring.c `xmlUTF8Strndup`).
1321///
1322/// # UPSTREAM-PARITY
1323///
1324/// ```c
1325/// xmlChar *xmlUTF8Strndup(const xmlChar *utf, int len);
1326/// ```
1327///
1328/// Returns a freshly allocated NUL-terminated string (caller frees with
1329/// `xmlFree`), or NULL when `utf` is NULL, `len < 0` or allocation fails.
1330///
1331/// # SAFETY
1332///
1333/// - `utf` must be a valid NUL-terminated byte string or NULL.
1334#[no_mangle]
1335pub unsafe extern "C" fn xmlUTF8Strndup(utf: *const xmlChar, len: c_int) -> *mut xmlChar {
1336    if utf.is_null() || len < 0 {
1337        return ptr::null_mut();
1338    }
1339    let i = unsafe { utf8_strsize(utf, len) };
1340    let ret = unsafe { xmlMallocImpl(i as usize + 1) as *mut xmlChar };
1341    if ret.is_null() {
1342        return ptr::null_mut();
1343    }
1344    unsafe {
1345        ptr::copy_nonoverlapping(utf, ret, i as usize);
1346        *ret.add(i as usize) = 0;
1347    }
1348    ret
1349}
1350
1351/// Pointer to the UTF-8 character at character position `pos` (upstream
1352/// xmlstring.c `xmlUTF8Strpos`).
1353///
1354/// # UPSTREAM-PARITY
1355///
1356/// ```c
1357/// const xmlChar *xmlUTF8Strpos(const xmlChar *utf, int pos);
1358/// ```
1359///
1360/// Returns NULL when `utf` is NULL, `pos < 0`, the position is past the
1361/// end, or the input is not well-formed UTF-8.
1362///
1363/// # SAFETY
1364///
1365/// - `utf` must be a valid NUL-terminated byte string or NULL.
1366#[no_mangle]
1367pub const unsafe extern "C" fn xmlUTF8Strpos(utf: *const xmlChar, pos: c_int) -> *const xmlChar {
1368    if utf.is_null() || pos < 0 {
1369        return ptr::null();
1370    }
1371    unsafe {
1372        let mut p = utf;
1373        let mut n = pos;
1374        while n > 0 {
1375            let ch = *p;
1376            p = p.add(1);
1377            if ch == 0 {
1378                return ptr::null();
1379            }
1380            if (ch & 0x80) != 0 {
1381                /* if not simple ascii, verify proper format */
1382                if (ch & 0xc0) != 0xc0 {
1383                    return ptr::null();
1384                }
1385                /* skip over the remaining bytes for this char */
1386                let mut m = ch;
1387                loop {
1388                    m <<= 1;
1389                    if (m & 0x80) == 0 {
1390                        break;
1391                    }
1392                    let cont = *p;
1393                    p = p.add(1);
1394                    if (cont & 0xc0) != 0x80 {
1395                        return ptr::null();
1396                    }
1397                }
1398            }
1399            n -= 1;
1400        }
1401        p
1402    }
1403}
1404
1405/// Relative character position of the UTF-8 character `utfchar` within
1406/// `utf` (upstream xmlstring.c `xmlUTF8Strloc`).
1407///
1408/// # UPSTREAM-PARITY
1409///
1410/// ```c
1411/// int xmlUTF8Strloc(const xmlChar *utf, const xmlChar *utfchar);
1412/// ```
1413///
1414/// Returns the character offset (0-based) of the first occurrence, or -1
1415/// when not found / arguments are NULL / the input is not well-formed
1416/// UTF-8.
1417///
1418/// # SAFETY
1419///
1420/// - `utf` and `utfchar` must be valid NUL-terminated byte strings or NULL.
1421#[no_mangle]
1422pub unsafe extern "C" fn xmlUTF8Strloc(utf: *const xmlChar, utfchar: *const xmlChar) -> c_int {
1423    if utf.is_null() || utfchar.is_null() {
1424        return -1;
1425    }
1426    unsafe {
1427        let size = utf8_strsize(utfchar, 1);
1428        let mut p = utf;
1429        let mut i: usize = 0;
1430        loop {
1431            let ch = *p;
1432            if ch == 0 {
1433                break;
1434            }
1435            if xml_strncmp(p, utfchar, size) == 0 {
1436                return if i > c_int::MAX as usize {
1437                    0
1438                } else {
1439                    i as c_int
1440                };
1441            }
1442            p = p.add(1);
1443            if (ch & 0x80) != 0 {
1444                /* if not simple ascii, verify proper format */
1445                if (ch & 0xc0) != 0xc0 {
1446                    return -1;
1447                }
1448                /* skip over the remaining bytes for this char */
1449                let mut m = ch;
1450                loop {
1451                    m <<= 1;
1452                    if (m & 0x80) == 0 {
1453                        break;
1454                    }
1455                    if (*p & 0xc0) != 0x80 {
1456                        return -1;
1457                    }
1458                    p = p.add(1);
1459                }
1460            }
1461            i += 1;
1462        }
1463    }
1464    -1
1465}
1466
1467/// Extract a substring by UTF-8 character positions (upstream xmlstring.c
1468/// `xmlUTF8Strsub`).
1469///
1470/// # UPSTREAM-PARITY
1471///
1472/// ```c
1473/// xmlChar *xmlUTF8Strsub(const xmlChar *utf, int start, int len);
1474/// ```
1475///
1476/// Returns a freshly allocated NUL-terminated string (caller frees with
1477/// `xmlFree`), or NULL when `utf` is NULL, `start < 0`, `len < 0`, the
1478/// start index is past the end, or allocation fails. If `len` is too
1479/// large, the result is truncated.
1480///
1481/// # SAFETY
1482///
1483/// - `utf` must be a valid NUL-terminated byte string or NULL.
1484#[no_mangle]
1485pub unsafe extern "C" fn xmlUTF8Strsub(
1486    utf: *const xmlChar,
1487    start: c_int,
1488    len: c_int,
1489) -> *mut xmlChar {
1490    if utf.is_null() || start < 0 || len < 0 {
1491        return ptr::null_mut();
1492    }
1493    unsafe {
1494        let mut p = utf;
1495        for _ in 0..start {
1496            let mut ch = *p;
1497            p = p.add(1);
1498            if ch == 0 {
1499                return ptr::null_mut();
1500            }
1501            /* skip over the remaining bytes for this char */
1502            if (ch & 0x80) != 0 {
1503                ch <<= 1;
1504                while (ch & 0x80) != 0 {
1505                    if *p == 0 {
1506                        return ptr::null_mut();
1507                    }
1508                    p = p.add(1);
1509                    ch <<= 1;
1510                }
1511            }
1512        }
1513        xmlUTF8Strndup(p, len)
1514    }
1515}