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