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) || (0xd800..0xe000).contains(&c) {
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 !(0x10000..0x110000).contains(&c) {
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.
156const unsafe 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`).
224const fn 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 /* whitespace is converted to space (normalize == 0) */
629 if chunk != str {
630 out.extend_from_slice(slice::from_raw_parts(
631 chunk,
632 str.offset_from(chunk) as usize,
633 ));
634 }
635 out.push(b' ');
636 chunk = str.add(1);
637 }
638 /* c == 0x20 is kept inside the chunk */
639 str = str.add(1);
640 } else if *str.add(1) == b'#' {
641 /* numeric character reference */
642 if chunk != str {
643 out.extend_from_slice(slice::from_raw_parts(
644 chunk,
645 str.offset_from(chunk) as usize,
646 ));
647 }
648 let val = parse_string_char_ref(&mut str);
649 if val == 0 {
650 /* upstream: invalid reference -> stop, return the prefix */
651 chunk = str;
652 break 'scan;
653 }
654 if val == b' ' as u32 {
655 out.push(b' ');
656 } else {
657 utf8_encode_char(out, val);
658 }
659 chunk = str;
660 } else {
661 /* named entity reference */
662 if chunk != str {
663 out.extend_from_slice(slice::from_raw_parts(
664 chunk,
665 str.offset_from(chunk) as usize,
666 ));
667 }
668 str = str.add(1);
669 let name_start = str;
670 while *str != 0 && *str != b';' {
671 str = str.add(1);
672 }
673 if *str != b';' {
674 /* upstream: XML_ERR_ENTITYREF_SEMICOL_MISSING -> stop */
675 chunk = str;
676 break 'scan;
677 }
678 let name = xml_strndup(name_start, str.offset_from(name_start) as usize);
679 if name.is_null() {
680 chunk = str;
681 break 'scan;
682 }
683 if let Some(content) = predefined_entity_content(name) {
684 out.extend_from_slice(content);
685 } else {
686 let ent = get_entity(doc, name);
687 if !ent.is_null() && (*ent).etype == XML_INTERNAL_PREDEFINED_ENTITY as c_int {
688 if (*ent).content.is_null() {
689 /* upstream: fatal "predefined entity has no content" */
690 xmlFreeImpl(name as *mut c_void);
691 chunk = str;
692 break 'scan;
693 }
694 let content = (*ent).content;
695 let clen = xml_strlen(content);
696 out.extend_from_slice(slice::from_raw_parts(content, clen));
697 } else if !ent.is_null() && !(*ent).content.is_null() {
698 if !pent.is_null() {
699 (*pent).flags |= XML_ENT_EXPANDING;
700 }
701 expand_entity_into(doc, out, (*ent).content, depth, ent);
702 if !pent.is_null() {
703 (*pent).flags &= !XML_ENT_EXPANDING;
704 }
705 }
706 /* ent == NULL (undeclared): the reference is dropped */
707 }
708 xmlFreeImpl(name as *mut c_void);
709 str = str.add(1); /* skip ';' */
710 chunk = str;
711 }
712 }
713 if chunk != str {
714 out.extend_from_slice(slice::from_raw_parts(
715 chunk,
716 str.offset_from(chunk) as usize,
717 ));
718 }
719}
720
721/// Upstream `xmlExpandEntitiesInAttValue` (parser.c) with `normalize = 0`:
722/// expand entity references in a NUL-terminated string into a freshly
723/// allocated `xmlChar*` (caller frees with `xmlFree`).
724///
725/// # SAFETY
726///
727/// - `str` must be a valid NUL-terminated string.
728unsafe fn expand_entities_in_att_value(doc: *mut _xmlDoc, str: *const xmlChar) -> *mut xmlChar {
729 let mut out: Vec<u8> = Vec::new();
730 expand_entity_into(doc, &mut out, str, 0, ptr::null_mut());
731 let p = xmlMallocImpl(out.len() + 1) as *mut xmlChar;
732 if p.is_null() {
733 return ptr::null_mut();
734 }
735 if !out.is_empty() {
736 ptr::copy_nonoverlapping(out.as_ptr(), p, out.len());
737 }
738 *p.add(out.len()) = 0;
739 p
740}
741
742/// Expand general entity references in a string with a known length
743/// (upstream parser.c `xmlStringLenDecodeEntities`).
744///
745/// # UPSTREAM-PARITY
746///
747/// ```c
748/// xmlChar *xmlStringLenDecodeEntities(xmlParserCtxt *ctxt,
749/// const xmlChar *str, int len,
750/// int what, xmlChar end,
751/// xmlChar end2, xmlChar end3);
752/// ```
753///
754/// Returns NULL when `ctxt`/`str` is NULL, `len < 0`, `str[len] != 0`, or
755/// any end marker is non-zero (the git-version contract). `what` is
756/// ignored, matching upstream where it is marked `ATTRIBUTE_UNUSED`.
757/// Otherwise returns a freshly allocated string with references expanded
758/// (numeric references and predefined/general entities; see
759/// `expand_entity_into` for the simplifications).
760///
761/// # SAFETY
762///
763/// - `ctxt` must be a valid `xmlParserCtxt*` or NULL.
764/// - `str` must point to a buffer of at least `len + 1` readable bytes
765/// with `str[len] == 0` (upstream reads `str[len]` unconditionally).
766#[no_mangle]
767pub unsafe extern "C" fn xmlStringLenDecodeEntities(
768 ctxt: *mut _xmlParserCtxt,
769 str: *const xmlChar,
770 len: c_int,
771 what: c_int,
772 end: xmlChar,
773 end2: xmlChar,
774 end3: xmlChar,
775) -> *mut xmlChar {
776 if ctxt.is_null() || str.is_null() || len < 0 {
777 return ptr::null_mut();
778 }
779 if unsafe { *str.add(len as usize) } != 0 || end != 0 || end2 != 0 || end3 != 0 {
780 return ptr::null_mut();
781 }
782 unsafe { expand_entities_in_att_value((*ctxt).myDoc, str) }
783}
784
785/// Expand general entity references in a NUL-terminated string (upstream
786/// parser.c `xmlStringDecodeEntities`, the macro-less variant).
787///
788/// # UPSTREAM-PARITY
789///
790/// ```c
791/// xmlChar *xmlStringDecodeEntities(xmlParserCtxt *ctxt,
792/// const xmlChar *str, int what,
793/// xmlChar end, xmlChar end2,
794/// xmlChar end3);
795/// ```
796///
797/// Returns NULL when `ctxt`/`str` is NULL or any end marker is non-zero
798/// (the git-version contract). `what` is ignored, matching upstream where
799/// it is marked `ATTRIBUTE_UNUSED`.
800///
801/// # SAFETY
802///
803/// - `ctxt` must be a valid `xmlParserCtxt*` or NULL.
804/// - `str` must be a valid NUL-terminated string.
805#[no_mangle]
806pub unsafe extern "C" fn xmlStringDecodeEntities(
807 ctxt: *mut _xmlParserCtxt,
808 str: *const xmlChar,
809 what: c_int,
810 end: xmlChar,
811 end2: xmlChar,
812 end3: xmlChar,
813) -> *mut xmlChar {
814 if ctxt.is_null() || str.is_null() {
815 return ptr::null_mut();
816 }
817 if end != 0 || end2 != 0 || end3 != 0 {
818 return ptr::null_mut();
819 }
820 unsafe { expand_entities_in_att_value((*ctxt).myDoc, str) }
821}
822
823// ═══════════════════════════════════════════════════════════════════════════════
824// xmlStringLenGetNodeList (tree.h)
825// ═══════════════════════════════════════════════════════════════════════════════
826
827/// Entity flags (include/private/entities.h).
828const XML_ENT_PARSED: c_int = 1 << 0;
829const XML_ENT_EXPANDING: c_int = 1 << 3;
830
831/// Upstream `xmlNewDocText` (tree.c): a text node associated with `doc`
832/// (NULL allowed). The dictionary lookup of the name is skipped — names
833/// are heap-allocated copies throughout this crate.
834///
835/// # SAFETY
836///
837/// - `doc` must be a valid `xmlDoc*` or NULL.
838/// - `content` must be a valid NUL-terminated string or NULL.
839unsafe fn new_doc_text(doc: *const _xmlDoc, content: *const xmlChar) -> *mut _xmlNode {
840 if !doc.is_null() {
841 let t = (*doc).type_;
842 if t != XML_DOCUMENT_NODE as c_int && t != XML_HTML_DOCUMENT_NODE as c_int {
843 return ptr::null_mut();
844 }
845 }
846 let node = new_text(content);
847 if node.is_null() {
848 return ptr::null_mut();
849 }
850 if !doc.is_null() {
851 (*node).doc = doc as *mut _xmlDoc;
852 }
853 node
854}
855
856/// Upstream `xmlNewEntityReference` (tree.c): an `XML_ENTITY_REF_NODE`
857/// carrying the entity's name.
858///
859/// # SAFETY
860///
861/// - `doc` must be a valid `xmlDoc*` or NULL.
862/// - `name` must be a valid NUL-terminated string.
863unsafe fn new_entity_ref(doc: *const _xmlDoc, name: *const xmlChar) -> *mut _xmlNode {
864 if name.is_null() {
865 return ptr::null_mut();
866 }
867 if !doc.is_null() {
868 let t = (*doc).type_;
869 if t != XML_DOCUMENT_NODE as c_int && t != XML_HTML_DOCUMENT_NODE as c_int {
870 return ptr::null_mut();
871 }
872 }
873 let node = xmlMallocZero(size_of::<_xmlNode>()) as *mut _xmlNode;
874 if node.is_null() {
875 return ptr::null_mut();
876 }
877 let name_copy = xml_strdup(name);
878 if name_copy.is_null() {
879 xmlFreeImpl(node as *mut c_void);
880 return ptr::null_mut();
881 }
882 unsafe {
883 (*node).type_ = XML_ENTITY_REF_NODE as c_int;
884 (*node).name = name_copy;
885 if !doc.is_null() {
886 (*node).doc = doc as *mut _xmlDoc;
887 }
888 }
889 node
890}
891
892/// Port of upstream `xmlNodeParseAttValue` (tree.c) for the
893/// `xmlStringLenGetNodeList` path: parse an attribute value into a list of
894/// text nodes and entity reference nodes. `attr` is the entity whose
895/// `children`/`last` receive the parsed list during recursive entity
896/// content parsing (NULL for the top-level call). The node list is
897/// returned through `list_ptr` (may be NULL); returns 0 on success, -1 on
898/// allocation failure.
899///
900/// # SAFETY
901///
902/// - `doc` must be a valid `xmlDoc*` or NULL.
903/// - `value` must be a valid NUL-terminated string of at least `len` bytes
904/// or NULL.
905/// - `list_ptr` must be a valid `xmlNode**` or NULL.
906unsafe fn node_parse_att_value(
907 doc: *const _xmlDoc,
908 attr: *mut _xmlNode,
909 value: *const xmlChar,
910 len: usize,
911 list_ptr: *mut *mut _xmlNode,
912) -> c_int {
913 let mut head: *mut _xmlNode = ptr::null_mut();
914 let mut last: *mut _xmlNode = ptr::null_mut();
915
916 if !list_ptr.is_null() {
917 *list_ptr = ptr::null_mut();
918 }
919
920 if value.is_null() || unsafe { *value } == 0 {
921 return 0;
922 }
923
924 let mut buf: Vec<u8> = Vec::new();
925 let mut cur = value;
926 let mut q = cur;
927 let mut remaining = len;
928
929 'scan: loop {
930 if remaining == 0 || unsafe { *cur } == 0 {
931 break 'scan;
932 }
933 if unsafe { *cur } == b'&' {
934 let mut charval: u32 = 0;
935
936 /* Save the current text. */
937 if cur != q {
938 unsafe {
939 buf.extend_from_slice(slice::from_raw_parts(q, cur.offset_from(q) as usize));
940 }
941 // `q` is re-established by each reference branch below.
942 }
943
944 if remaining > 2 && unsafe { *cur.add(1) } == b'#' && unsafe { *cur.add(2) } == b'x' {
945 /* hex character reference */
946 let mut tmp: u8 = 0;
947 unsafe {
948 cur = cur.add(3);
949 }
950 remaining -= 3;
951 loop {
952 if remaining == 0 {
953 break;
954 }
955 tmp = unsafe { *cur };
956 if tmp == b';' {
957 break;
958 }
959 let digit: u32 = match tmp {
960 b'0'..=b'9' => (tmp - b'0') as u32,
961 b'a'..=b'f' => (tmp - b'a' + 10) as u32,
962 b'A'..=b'F' => (tmp - b'A' + 10) as u32,
963 _ => {
964 charval = 0;
965 break;
966 }
967 };
968 charval = charval.wrapping_mul(16).wrapping_add(digit);
969 if charval > 0x110000 {
970 charval = 0x110000;
971 }
972 unsafe {
973 cur = cur.add(1);
974 }
975 remaining -= 1;
976 }
977 if tmp == b';' {
978 unsafe {
979 cur = cur.add(1);
980 }
981 remaining -= 1;
982 }
983 q = cur;
984 } else if remaining > 1 && unsafe { *cur.add(1) } == b'#' {
985 /* decimal character reference */
986 let mut tmp: u8 = 0;
987 unsafe {
988 cur = cur.add(2);
989 }
990 remaining -= 2;
991 loop {
992 if remaining == 0 {
993 break;
994 }
995 tmp = unsafe { *cur };
996 if tmp == b';' {
997 break;
998 }
999 if !tmp.is_ascii_digit() {
1000 charval = 0;
1001 break;
1002 }
1003 charval = charval.wrapping_mul(10).wrapping_add((tmp - b'0') as u32);
1004 if charval > 0x110000 {
1005 charval = 0x110000;
1006 }
1007 unsafe {
1008 cur = cur.add(1);
1009 }
1010 remaining -= 1;
1011 }
1012 if tmp == b';' {
1013 unsafe {
1014 cur = cur.add(1);
1015 }
1016 remaining -= 1;
1017 }
1018 q = cur;
1019 } else {
1020 /* read the entity name */
1021 unsafe {
1022 cur = cur.add(1);
1023 }
1024 remaining -= 1;
1025 q = cur;
1026 while remaining > 0 && unsafe { *cur } != 0 && unsafe { *cur } != b';' {
1027 unsafe {
1028 cur = cur.add(1);
1029 }
1030 remaining -= 1;
1031 }
1032 if remaining == 0 || unsafe { *cur } == 0 {
1033 break 'scan;
1034 }
1035 if cur != q {
1036 let name = unsafe { xml_strndup(q, cur.offset_from(q) as usize) };
1037 if name.is_null() {
1038 free_node_list(head);
1039 return -1;
1040 }
1041 let ent = get_doc_entity(doc, name);
1042 if !ent.is_null() && (*ent).etype == XML_INTERNAL_PREDEFINED_ENTITY as c_int {
1043 /* predefined entities don't generate nodes */
1044 let content = (*ent).content;
1045 let clen = xml_strlen(content);
1046 unsafe {
1047 buf.extend_from_slice(slice::from_raw_parts(content, clen));
1048 }
1049 } else if ent.is_null() || ((*ent).flags & XML_ENT_EXPANDING) == 0 {
1050 /* flush the buffer so far */
1051 if !buf.is_empty() {
1052 buf.push(0); /* NUL-terminate for the text-node dup */
1053 let node = new_doc_text(doc, buf.as_ptr() as *const xmlChar);
1054 buf.pop();
1055 if node.is_null() {
1056 xmlFreeImpl(name as *mut c_void);
1057 free_node_list(head);
1058 return -1;
1059 }
1060 (*node).parent = attr;
1061 if last.is_null() {
1062 head = node;
1063 } else {
1064 (*last).next = node;
1065 (*node).prev = last;
1066 }
1067 last = node;
1068 buf.clear();
1069 }
1070
1071 /* parse the entity content if not parsed yet */
1072 if !ent.is_null()
1073 && ((*ent).flags & XML_ENT_PARSED) == 0
1074 && !(*ent).content.is_null()
1075 {
1076 (*ent).flags |= XML_ENT_EXPANDING;
1077 let res = node_parse_att_value(
1078 doc,
1079 ent as *mut _xmlNode,
1080 (*ent).content,
1081 usize::MAX,
1082 ptr::null_mut(),
1083 );
1084 (*ent).flags &= !XML_ENT_EXPANDING;
1085 if res < 0 {
1086 xmlFreeImpl(name as *mut c_void);
1087 free_node_list(head);
1088 return -1;
1089 }
1090 (*ent).flags |= XML_ENT_PARSED;
1091 }
1092
1093 /* create a new REFERENCE_REF node */
1094 let node = new_entity_ref(doc, name);
1095 if node.is_null() {
1096 xmlFreeImpl(name as *mut c_void);
1097 free_node_list(head);
1098 return -1;
1099 }
1100 (*node).parent = attr;
1101 (*node).last = ent as *mut _xmlNode;
1102 if !ent.is_null() {
1103 (*node).children = ent as *mut _xmlNode;
1104 (*node).content = (*ent).content;
1105 }
1106 if last.is_null() {
1107 head = node;
1108 } else {
1109 (*last).next = node;
1110 (*node).prev = last;
1111 }
1112 last = node;
1113 }
1114 xmlFreeImpl(name as *mut c_void);
1115 }
1116 unsafe {
1117 cur = cur.add(1);
1118 }
1119 remaining -= 1;
1120 q = cur;
1121 }
1122 if charval != 0 {
1123 let charval = if charval >= 0x110000 { 0xFFFD } else { charval };
1124 utf8_encode_char(&mut buf, charval);
1125 }
1126 } else {
1127 unsafe {
1128 cur = cur.add(1);
1129 }
1130 remaining -= 1;
1131 }
1132 }
1133
1134 /* handle the last piece of text */
1135 if cur != q {
1136 unsafe {
1137 buf.extend_from_slice(slice::from_raw_parts(q, cur.offset_from(q) as usize));
1138 }
1139 }
1140
1141 if !buf.is_empty() {
1142 buf.push(0); /* NUL-terminate for the text-node dup */
1143 let node = new_doc_text(doc, buf.as_ptr() as *const xmlChar);
1144 buf.pop();
1145 if node.is_null() {
1146 free_node_list(head);
1147 return -1;
1148 }
1149 (*node).parent = attr;
1150 if last.is_null() {
1151 head = node;
1152 } else {
1153 (*last).next = node;
1154 (*node).prev = last;
1155 }
1156 last = node;
1157 } else if head.is_null() {
1158 head = new_doc_text(doc, b"" as *const u8 as *const xmlChar);
1159 if head.is_null() {
1160 return -1;
1161 }
1162 (*head).parent = attr;
1163 last = head;
1164 }
1165
1166 if !attr.is_null() {
1167 (*attr).children = head;
1168 (*attr).last = last;
1169 }
1170 if !list_ptr.is_null() {
1171 *list_ptr = head;
1172 }
1173 0
1174}
1175
1176/// Build a node list (text and entity reference nodes) from an attribute
1177/// value (upstream tree.c `xmlStringLenGetNodeList`).
1178///
1179/// # UPSTREAM-PARITY
1180///
1181/// ```c
1182/// xmlNode *xmlStringLenGetNodeList(const xmlDoc *doc,
1183/// const xmlChar *value, int len);
1184/// ```
1185///
1186/// Returns the head of a linked list of `XML_TEXT_NODE` /
1187/// `XML_ENTITY_REF_NODE` nodes, or NULL for a NULL/empty `value` or on
1188/// allocation failure. A negative `len` means the value is NUL-terminated.
1189/// Predefined entity references are expanded into text; other declared
1190/// entities produce entity reference nodes (whose content is parsed into
1191/// the entity declaration's children); undeclared references produce
1192/// entity reference nodes without content, as upstream.
1193///
1194/// # SAFETY
1195///
1196/// - `doc` must be a valid `xmlDoc*` or NULL.
1197/// - `value` must be a valid NUL-terminated string of at least `len` bytes
1198/// or NULL.
1199#[no_mangle]
1200pub unsafe extern "C" fn xmlStringLenGetNodeList(
1201 doc: *const _xmlDoc,
1202 value: *const xmlChar,
1203 len: c_int,
1204) -> *mut _xmlNode {
1205 let max_size: usize = if len < 0 { usize::MAX } else { len as usize };
1206 let mut ret: *mut _xmlNode = ptr::null_mut();
1207 unsafe {
1208 node_parse_att_value(doc, ptr::null_mut(), value, max_size, &mut ret);
1209 }
1210 ret
1211}
1212
1213// ═══════════════════════════════════════════════════════════════════════════════
1214// xmlUTF8* family (xmlstring.h)
1215// ═══════════════════════════════════════════════════════════════════════════════
1216
1217/// Compare two UTF-8 characters (upstream xmlstring.c `xmlUTF8Charcmp`).
1218///
1219/// # UPSTREAM-PARITY
1220///
1221/// ```c
1222/// int xmlUTF8Charcmp(const xmlChar *utf1, const xmlChar *utf2);
1223/// ```
1224///
1225/// Returns the result of comparing the first `xmlUTF8Size(utf1)` bytes
1226/// (like `xmlStrncmp`); NULL `utf1` sorts before non-NULL, both NULL are
1227/// equal.
1228///
1229/// # SAFETY
1230///
1231/// - `utf1` must be a valid pointer into a UTF-8 string or NULL.
1232/// - `utf2` must be a valid pointer or NULL.
1233#[no_mangle]
1234pub unsafe extern "C" fn xmlUTF8Charcmp(utf1: *const xmlChar, utf2: *const xmlChar) -> c_int {
1235 if utf1.is_null() {
1236 return if utf2.is_null() { 0 } else { -1 };
1237 }
1238 unsafe { xml_strncmp(utf1, utf2, utf8_size(utf1)) }
1239}
1240
1241/// Byte size of the first `len` UTF-8 characters (upstream xmlstring.c
1242/// `xmlUTF8Strsize`).
1243///
1244/// # UPSTREAM-PARITY
1245///
1246/// ```c
1247/// int xmlUTF8Strsize(const xmlChar *utf, int len);
1248/// ```
1249///
1250/// Returns 0 for NULL input, `len <= 0` or at the end of the string.
1251/// The behaviour is not guaranteed for invalid UTF-8 (as upstream).
1252///
1253/// # SAFETY
1254///
1255/// - `utf` must be a valid NUL-terminated byte string or NULL.
1256#[no_mangle]
1257pub const unsafe extern "C" fn xmlUTF8Strsize(utf: *const xmlChar, len: c_int) -> c_int {
1258 unsafe { utf8_strsize(utf, len) }
1259}
1260
1261/// Duplicate the first `len` UTF-8 characters of `utf` (upstream
1262/// xmlstring.c `xmlUTF8Strndup`).
1263///
1264/// # UPSTREAM-PARITY
1265///
1266/// ```c
1267/// xmlChar *xmlUTF8Strndup(const xmlChar *utf, int len);
1268/// ```
1269///
1270/// Returns a freshly allocated NUL-terminated string (caller frees with
1271/// `xmlFree`), or NULL when `utf` is NULL, `len < 0` or allocation fails.
1272///
1273/// # SAFETY
1274///
1275/// - `utf` must be a valid NUL-terminated byte string or NULL.
1276#[no_mangle]
1277pub unsafe extern "C" fn xmlUTF8Strndup(utf: *const xmlChar, len: c_int) -> *mut xmlChar {
1278 if utf.is_null() || len < 0 {
1279 return ptr::null_mut();
1280 }
1281 let i = unsafe { utf8_strsize(utf, len) };
1282 let ret = unsafe { xmlMallocImpl(i as usize + 1) as *mut xmlChar };
1283 if ret.is_null() {
1284 return ptr::null_mut();
1285 }
1286 unsafe {
1287 ptr::copy_nonoverlapping(utf, ret, i as usize);
1288 *ret.add(i as usize) = 0;
1289 }
1290 ret
1291}
1292
1293/// Pointer to the UTF-8 character at character position `pos` (upstream
1294/// xmlstring.c `xmlUTF8Strpos`).
1295///
1296/// # UPSTREAM-PARITY
1297///
1298/// ```c
1299/// const xmlChar *xmlUTF8Strpos(const xmlChar *utf, int pos);
1300/// ```
1301///
1302/// Returns NULL when `utf` is NULL, `pos < 0`, the position is past the
1303/// end, or the input is not well-formed UTF-8.
1304///
1305/// # SAFETY
1306///
1307/// - `utf` must be a valid NUL-terminated byte string or NULL.
1308#[no_mangle]
1309pub const unsafe extern "C" fn xmlUTF8Strpos(utf: *const xmlChar, pos: c_int) -> *const xmlChar {
1310 if utf.is_null() || pos < 0 {
1311 return ptr::null();
1312 }
1313 unsafe {
1314 let mut p = utf;
1315 let mut n = pos;
1316 while n > 0 {
1317 let ch = *p;
1318 p = p.add(1);
1319 if ch == 0 {
1320 return ptr::null();
1321 }
1322 if (ch & 0x80) != 0 {
1323 /* if not simple ascii, verify proper format */
1324 if (ch & 0xc0) != 0xc0 {
1325 return ptr::null();
1326 }
1327 /* skip over the remaining bytes for this char */
1328 let mut m = ch;
1329 loop {
1330 m <<= 1;
1331 if (m & 0x80) == 0 {
1332 break;
1333 }
1334 let cont = *p;
1335 p = p.add(1);
1336 if (cont & 0xc0) != 0x80 {
1337 return ptr::null();
1338 }
1339 }
1340 }
1341 n -= 1;
1342 }
1343 p
1344 }
1345}
1346
1347/// Relative character position of the UTF-8 character `utfchar` within
1348/// `utf` (upstream xmlstring.c `xmlUTF8Strloc`).
1349///
1350/// # UPSTREAM-PARITY
1351///
1352/// ```c
1353/// int xmlUTF8Strloc(const xmlChar *utf, const xmlChar *utfchar);
1354/// ```
1355///
1356/// Returns the character offset (0-based) of the first occurrence, or -1
1357/// when not found / arguments are NULL / the input is not well-formed
1358/// UTF-8.
1359///
1360/// # SAFETY
1361///
1362/// - `utf` and `utfchar` must be valid NUL-terminated byte strings or NULL.
1363#[no_mangle]
1364pub unsafe extern "C" fn xmlUTF8Strloc(utf: *const xmlChar, utfchar: *const xmlChar) -> c_int {
1365 if utf.is_null() || utfchar.is_null() {
1366 return -1;
1367 }
1368 unsafe {
1369 let size = utf8_strsize(utfchar, 1);
1370 let mut p = utf;
1371 let mut i: usize = 0;
1372 loop {
1373 let ch = *p;
1374 if ch == 0 {
1375 break;
1376 }
1377 if xml_strncmp(p, utfchar, size) == 0 {
1378 return if i > c_int::MAX as usize {
1379 0
1380 } else {
1381 i as c_int
1382 };
1383 }
1384 p = p.add(1);
1385 if (ch & 0x80) != 0 {
1386 /* if not simple ascii, verify proper format */
1387 if (ch & 0xc0) != 0xc0 {
1388 return -1;
1389 }
1390 /* skip over the remaining bytes for this char */
1391 let mut m = ch;
1392 loop {
1393 m <<= 1;
1394 if (m & 0x80) == 0 {
1395 break;
1396 }
1397 if (*p & 0xc0) != 0x80 {
1398 return -1;
1399 }
1400 p = p.add(1);
1401 }
1402 }
1403 i += 1;
1404 }
1405 }
1406 -1
1407}
1408
1409/// Extract a substring by UTF-8 character positions (upstream xmlstring.c
1410/// `xmlUTF8Strsub`).
1411///
1412/// # UPSTREAM-PARITY
1413///
1414/// ```c
1415/// xmlChar *xmlUTF8Strsub(const xmlChar *utf, int start, int len);
1416/// ```
1417///
1418/// Returns a freshly allocated NUL-terminated string (caller frees with
1419/// `xmlFree`), or NULL when `utf` is NULL, `start < 0`, `len < 0`, the
1420/// start index is past the end, or allocation fails. If `len` is too
1421/// large, the result is truncated.
1422///
1423/// # SAFETY
1424///
1425/// - `utf` must be a valid NUL-terminated byte string or NULL.
1426#[no_mangle]
1427pub unsafe extern "C" fn xmlUTF8Strsub(
1428 utf: *const xmlChar,
1429 start: c_int,
1430 len: c_int,
1431) -> *mut xmlChar {
1432 if utf.is_null() || start < 0 || len < 0 {
1433 return ptr::null_mut();
1434 }
1435 unsafe {
1436 let mut p = utf;
1437 for _ in 0..start {
1438 let mut ch = *p;
1439 p = p.add(1);
1440 if ch == 0 {
1441 return ptr::null_mut();
1442 }
1443 /* skip over the remaining bytes for this char */
1444 if (ch & 0x80) != 0 {
1445 ch <<= 1;
1446 while (ch & 0x80) != 0 {
1447 if *p == 0 {
1448 return ptr::null_mut();
1449 }
1450 p = p.add(1);
1451 ch <<= 1;
1452 }
1453 }
1454 }
1455 xmlUTF8Strndup(p, len)
1456 }
1457}