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