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