libxml_rs/abi/exports_parser.rs
1//! exports_parser — C ABI exports for the XML parser family (§11.1-I).
2//!
3//! Implements the parser/parserInternals/xmlIO/encoding/tree export surface:
4//! parser-context creation and lifecycle, the `xmlCtxtRead*` family, parser
5//! input buffers and streams, encoding switches, the deprecated node-info
6//! sequence, global I/O callback registration, external-entity loaders, the
7//! `xmlFile*` I/O callbacks, low-level character scanning helpers and the
8//! SAX/DTD parse front-ends.
9//!
10//! Where an internal engine entry point exists (e.g. `crate::xml::parser::helpers`),
11//! the export wraps it; otherwise the function is ported from upstream
12//! `parser.c` / `parserInternals.c` / `xmlIO.c` / `error.c` / `encoding.c`
13//! (see `archaeology/libxml2-git`).
14//!
15//! # Upstream contract
16//!
17//! Parity target is upstream `parser.c`, `parserInternals.c`, `xmlIO.c`,
18//! `error.c` and `encoding.c` (libxml2 2.15.3) with the `parser.h`/
19//! `parserInternals.h`/`xmlIO.h` signatures. Residuals R-000164 (parser/tree
20//! structural parity), R-000165 (parser-context accessors and input
21//! constructors) and R-000169 (input filename ownership) all land here.
22//!
23//! # Conceptual behavior
24//!
25//! This module implements the parser export surface: parser-context
26//! creation/lifecycle, the `xmlCtxtRead*`/`xmlRead*` families, parser input
27//! buffers and streams, encoding switches, the deprecated node-info sequence,
28//! global I/O callback registration, external-entity loaders, the `xmlFile*`
29//! I/O callbacks and the SAX/DTD parse front-ends. Internal engine entry
30//! points are wrapped; the rest are ported from the upstream sources.
31//!
32//! # Ownership & safety invariants
33//!
34//! Parser contexts are caller-owned (freed with `xmlFreeParserCtxt`); docs
35//! returned by `xmlRead*` are caller-owned (freed with `xmlFreeDoc`); inputs
36//! created by `xmlNewInputFrom*` are owned by the context once pushed.
37//! Filenames stored in `_xmlParserInput.filename` and `doc->URL` are owned
38//! copies — R-000169 fixed xml_strndup on non-NUL-terminated Rust Strings
39//! (heap-buffer-overflow) and borrowed filename pointers.
40//!
41//! # Historical quirks & epochs
42//!
43//! QUIRK-0001/LORE-0001: since 2.9.0 (commit `52d8ade7`, 2012-07-30) default
44//! parser limits apply unless `XML_PARSE_HUGE` is set. E-002: parse-error
45//! diagnostics changed across 2.9.10 (non-recursive parser refactor) and
46//! 2.12.x (error-handling rework); E-005: exit codes reworked in 2.13.0.
47//! R-000164 (11.1-N) aligned the parse-time DOM construction with upstream
48//! (TREE-001 byte-identical).
49//!
50//! # Deliberate oddities
51//!
52//! The deprecated `xmlParse*` no-ops and the `xmlFileMatch`/
53//! `xmlParserInputRead` trivial bodies are deliberate (R-000138 set:
54//! upstreams own bodies are empty/trivial). `xmlReadMemory` accepts size-0
55//! input (R-000163) and applies options/URL on both success and recovery
56//! paths (R-000164).
57//!
58//! # Proving courts
59//!
60//! The PARSER court family, the ERROR-001 probe (error-family-probe.c, 48/48
61//! byte-identical), the TREE-001 structural probe and the DSO-LOADER/
62//! HEADER-COMPILE courts cover this module; the parser unit suite runs under
63//! cargo test.
64//!
65//! # Tempting simplifications that would break parity
66//!
67//! A tempting simplification is to store the input filename as a borrowed
68//! pointer into a Rust String — R-000169 proved that produces dangling
69//! `filename`/`doc->URL` pointers and heap-reuse garbage on the second parse;
70//! every construction path must own its filename copy. Another shortcut,
71//! skipping the `XML_PARSE_HUGE`/limits logic, would diverge from the 2.9.0+
72//! oracle on large documents (PARSER-LIMIT courts).
73
74#![allow(missing_docs)]
75#![allow(non_snake_case)]
76#![allow(non_camel_case_types)]
77#![allow(non_upper_case_globals)]
78
79// SAFETY-SCOPE: EXPORT-PARSER-MECHANICAL-001
80// (11.1-Z.3 proof scope, classified-generated) — this module is the
81// mechanical extern-"C" export surface: every `unsafe` block in it is
82// the documented indirection/registry-access pattern whose validity
83// rests on the upstream C contract, and the exported signatures are
84// machine-measured by the ABI-FUNCTION-SIGNATURE and DSO-LOADER
85// courts and the C-API differential probes. The safety contract of
86// each export is stated in its own doc comment; this scope covers the
87// mechanical wrappers' unsafe blocks.
88
89use core::ffi::CStr;
90use core::ptr;
91use std::os::raw::{c_char, c_int, c_long, c_uchar, c_uint, c_ulong, c_void};
92
93use parking_lot::Mutex;
94
95use crate::abi::allocator::{
96 xmlFreeImpl, xmlMallocImpl, xmlMallocZero, xmlMemStrdupImpl, xmlReallocImpl,
97};
98use crate::abi::callbacks::{
99 xmlGenericErrorFunc, xmlInputCloseCallback, xmlInputReadCallback, xmlOutputCloseCallback,
100 xmlOutputWriteCallback, xmlStructuredErrorFunc,
101};
102use crate::abi::structs::*;
103use crate::abi::types::*;
104use crate::xml::parser::helpers;
105use crate::xml::parser::input::InputBuffer;
106use crate::xml::{dtd, encoding, entities, errors, globals, io, string, tree};
107
108// ═══════════════════════════════════════════════════════════════════════════════
109// Local ABI types (upstream xmlIO.h / parser.h, not present in callbacks.rs)
110// ═══════════════════════════════════════════════════════════════════════════════
111
112/// `xmlInputMatchCallback` — decide whether a filename is handled by the
113/// registered input callback pair.
114type xmlInputMatchCallback = unsafe extern "C" fn(filename: *const c_char) -> c_int;
115
116/// `xmlInputOpenCallback` — open a resource and return an I/O context.
117type xmlInputOpenCallback = unsafe extern "C" fn(filename: *const c_char) -> *mut c_void;
118
119/// `xmlOutputMatchCallback` — decide whether a filename is handled by the
120/// registered output callback pair.
121type xmlOutputMatchCallback = unsafe extern "C" fn(filename: *const c_char) -> c_int;
122
123/// `xmlOutputOpenCallback` — open a resource for writing and return a context.
124type xmlOutputOpenCallback = unsafe extern "C" fn(filename: *const c_char) -> *mut c_void;
125
126/// `xmlExternalEntityLoader` — resolve an external entity to a parser input.
127type xmlExternalEntityLoader = unsafe extern "C" fn(
128 URL: *const c_char,
129 ID: *const c_char,
130 ctxt: *mut _xmlParserCtxt,
131) -> *mut _xmlParserInput;
132
133#[derive(Clone, Copy)]
134struct InputCallbackEntry {
135 matchcb: Option<xmlInputMatchCallback>,
136 opencb: Option<xmlInputOpenCallback>,
137 readcb: Option<xmlInputReadCallback>,
138 closecb: Option<xmlInputCloseCallback>,
139}
140
141#[allow(dead_code)]
142#[derive(Clone, Copy)]
143struct OutputCallbackEntry {
144 matchcb: Option<xmlOutputMatchCallback>,
145 opencb: Option<xmlOutputOpenCallback>,
146 writecb: Option<xmlOutputWriteCallback>,
147 closecb: Option<xmlOutputCloseCallback>,
148}
149
150static INPUT_CALLBACKS: Mutex<Vec<InputCallbackEntry>> = Mutex::new(Vec::new());
151static OUTPUT_CALLBACKS: Mutex<Vec<OutputCallbackEntry>> = Mutex::new(Vec::new());
152
153static EXTERNAL_ENTITY_LOADER: Mutex<Option<xmlExternalEntityLoader>> =
154 Mutex::new(Some(default_external_entity_loader));
155
156// Deprecated legacy function codes not present in types.rs (upstream xmlerror.h).
157const XML_ERR_USER_STOP: c_int = 111;
158#[allow(dead_code)]
159const XML_ERR_RESOURCE_LIMIT: c_int = 114;
160
161// XML_SCAN_* flags (upstream include/private/parser.h).
162const XML_SCAN_NC: c_int = 1;
163const XML_SCAN_NMTOKEN: c_int = 2;
164const XML_SCAN_OLD10: c_int = 4;
165
166// xmlParserLoadSubset bits (upstream parser.h, 2.15.3):
167// XML_DETECT_IDS = 2, XML_COMPLETE_ATTRS = 4, XML_SKIP_IDS = 8.
168// These are internal bits of `ctxt->loadsubset`, NOT parse-option flags — they
169// must match the header values exactly or consumers that inspect `loadsubset`
170// directly (lxml's SAX target bridge gates default-attribute delivery on
171// `loadsubset & XML_COMPLETE_ATTRS`) drop DTD default attributes.
172#[allow(dead_code)]
173const XML_DETECT_IDS: c_int = 2;
174const XML_COMPLETE_ATTRS: c_int = 4;
175const XML_SKIP_IDS: c_int = 8;
176
177/// Keep enough input around to show errors in context (parserInternals.c).
178const LINE_LEN: usize = 80;
179
180/// Minimal amount of data the parser expects in the buffer (parserInternals.c).
181#[allow(dead_code)]
182const INPUT_CHUNK: usize = 100;
183
184const XML_INVALID_CHAR: c_int = -1;
185
186// ═══════════════════════════════════════════════════════════════════════════════
187// Internal helpers
188// ═══════════════════════════════════════════════════════════════════════════════
189
190/// Shared context initialisation: zeroes `ctxt`, installs the SAX handler and
191/// sets the initial parser state (upstream `xmlInitSAXParserCtxt`).
192///
193/// # Safety
194///
195/// `ctxt` must be a valid, writable, freshly allocated parser context.
196unsafe fn init_sax_parser_ctxt(
197 ctxt: *mut _xmlParserCtxt,
198 sax: *const _xmlSAXHandler,
199 userData: *mut c_void,
200) -> c_int {
201 unsafe {
202 ptr::write_bytes(ctxt as *mut u8, 0, core::mem::size_of::<_xmlParserCtxt>());
203
204 let c = &mut *ctxt;
205
206 // SAX handler.
207 if c.sax.is_null() {
208 let new_sax =
209 xmlMallocZero(core::mem::size_of::<_xmlSAXHandler>()) as *mut _xmlSAXHandler;
210 if new_sax.is_null() {
211 return -1;
212 }
213 c.sax = new_sax;
214 }
215 if sax.is_null() {
216 crate::xml::sax::xmlSAX2InitDefaultSAXHandler(c.sax);
217 c.userData = ctxt as *mut c_void;
218 } else if (*sax).initialized == XML_SAX2_MAGIC as c_uint {
219 // Full SAX2 handler copy.
220 ptr::copy_nonoverlapping(sax, c.sax, 1);
221 c.userData = if userData.is_null() {
222 ctxt as *mut c_void
223 } else {
224 userData
225 };
226 } else {
227 // SAX1 handler: only the V1 prefix is meaningful.
228 ptr::write_bytes(c.sax as *mut u8, 0, core::mem::size_of::<_xmlSAXHandler>());
229 ptr::copy_nonoverlapping(
230 sax as *const u8,
231 c.sax as *mut u8,
232 core::mem::size_of::<_xmlSAXHandlerV1>(),
233 );
234 c.userData = if userData.is_null() {
235 ctxt as *mut c_void
236 } else {
237 userData
238 };
239 }
240
241 c.wellFormed = 1;
242 c.standalone = -1;
243 c.errNo = XML_ERR_OK;
244 c.valid = 1;
245 c.nsWellFormed = 1;
246 c.instate = xmlParserInputState::XML_PARSER_START as c_int;
247 c.keepBlanks = globals::get_keep_blanks_default();
248 c.replaceEntities = globals::get_substitute_entities_default();
249 c.linenumbers = 1;
250 c.charset = xmlCharEncoding::XML_CHAR_ENCODING_UTF8 as c_int;
251 c.pedantic = globals::get_pedantic_parser_default();
252 c.loadsubset = globals::get_load_ext_dtd_default();
253 c.docdict = 1;
254 c.options = 0;
255
256 c.vctxt.userData = ctxt as *mut c_void;
257 c.vctxt.valid = 1;
258 }
259 0
260}
261
262/// Mirror `options` into the parser context's historical struct members
263/// (upstream `xmlCtxtSetOptionsInternal`).
264///
265/// `keepBlanks` is special: the executed 2.15.3 oracle seeds it from the
266/// deprecated `xmlKeepBlanksDefaultValue` at context creation and only ever
267/// LOWERS it (XML_PARSE_NOBLANKS) — option application never re-raises it.
268/// Empirical: a context created while `xmlKeepBlanksDefault(0)` drops
269/// whitespace-only text for ALL its reads, even reused ones, and
270/// `xmlCtxtUseOptions`/read options without NOBLANKS do not restore it.
271///
272/// # Safety
273///
274/// `ctxt` must be a valid, writable parser context.
275pub(crate) unsafe fn apply_options(ctxt: *mut _xmlParserCtxt, options: c_int) {
276 unsafe {
277 let c = &mut *ctxt;
278 c.options = options;
279 c.recovery = (options & XML_PARSE_RECOVER != 0) as c_int;
280 c.replaceEntities = (options & XML_PARSE_NOENT != 0) as c_int;
281 // UPSTREAM-PARITY (parser.c xmlCtxtUseOptions):
282 // ctxt->loadsubset = (options & XML_PARSE_DTDLOAD) ? XML_DETECT_IDS : 0;
283 // ctxt->loadsubset |= (options & XML_PARSE_DTDATTR) ? XML_COMPLETE_ATTRS : 0;
284 // ctxt->loadsubset |= (options & XML_PARSE_SKIP_IDS) ? XML_SKIP_IDS : 0;
285 // `loadsubset` is an ABI-visible field; lxml's SAX target bridge reads
286 // `loadsubset & XML_COMPLETE_ATTRS` to decide whether to deliver DTD
287 // default attributes, so the bit values must be the upstream ones
288 // (XML_DETECT_IDS=2, XML_COMPLETE_ATTRS=4, XML_SKIP_IDS=8).
289 c.loadsubset = if options & XML_PARSE_DTDLOAD != 0 {
290 XML_DETECT_IDS
291 } else {
292 0
293 };
294 if options & XML_PARSE_DTDATTR != 0 {
295 c.loadsubset |= XML_COMPLETE_ATTRS;
296 }
297 if options & XML_PARSE_SKIP_IDS != 0 {
298 c.loadsubset |= XML_SKIP_IDS;
299 }
300 c.validate = (options & XML_PARSE_DTDVALID != 0) as c_int;
301 c.pedantic = (options & XML_PARSE_PEDANTIC != 0) as c_int;
302 if options & XML_PARSE_NOBLANKS != 0 {
303 c.keepBlanks = 0;
304 }
305 c.dictNames = if options & XML_PARSE_NODICT != 0 {
306 0
307 } else {
308 1
309 };
310 }
311}
312
313/// Find the registered encoding handler for an `xmlCharEncoding` value, or NULL.
314unsafe fn encoding_handler_for(enc: c_int) -> *mut _xmlCharEncodingHandler {
315 let e: xmlCharEncoding = unsafe { core::mem::transmute(enc) };
316 match encoding::encoding_name(e) {
317 Some(name) => {
318 let mut nul = name.to_vec();
319 nul.push(0);
320 encoding::find_encoding_handler(nul.as_ptr() as *const xmlChar)
321 }
322 None => ptr::null_mut(),
323 }
324}
325
326/// Build a `_xmlParserInput` that references the data owned by `buf` (an input
327/// buffer previously created by the xmlIO layer). The buffer keeps the data
328/// alive; the returned input must be freed with `helpers::free_parser_input`.
329///
330/// # Safety
331///
332/// `buf` must be a valid input buffer or NULL, and must outlive the returned
333/// input.
334unsafe fn parser_input_from_buf(buf: *mut _xmlParserInputBuffer) -> *mut _xmlParserInput {
335 let input =
336 unsafe { xmlMallocZero(core::mem::size_of::<_xmlParserInput>()) } as *mut _xmlParserInput;
337 if input.is_null() {
338 return ptr::null_mut();
339 }
340 unsafe {
341 (*input).buf = buf;
342 (*input).line = 1;
343 (*input).col = 1;
344 if !buf.is_null() {
345 let b = &*buf;
346 if !b.buffer.is_null() {
347 let xbuf = &*(b.buffer as *mut _xmlBuffer);
348 if !xbuf.content.is_null() {
349 (*input).base = xbuf.content;
350 (*input).cur = xbuf.content;
351 (*input).end = xbuf.content.add(xbuf.use_ as usize);
352 (*input).length = xbuf.use_ as c_int;
353 }
354 }
355 }
356 }
357 input
358}
359
360/// `pub(crate)` wrapper of [`parser_input_from_buf`] for the sibling
361/// xmlNewInputFrom* family (11.1-X R-000165 closure).
362pub(crate) unsafe fn parser_input_from_buf_pub(
363 buf: *mut _xmlParserInputBuffer,
364) -> *mut _xmlParserInput {
365 unsafe { parser_input_from_buf(buf) }
366}
367
368/// Materialise an `InputBuffer` (owned copy) from a raw `_xmlParserInput`,
369/// so the data survives the caller's input lifetime.
370///
371/// # Safety
372///
373/// `input` must be a valid pointer to a `_xmlParserInput`.
374unsafe fn input_buffer_from_parser_input(input: *mut _xmlParserInput) -> InputBuffer {
375 unsafe {
376 let pi = &*input;
377 if !pi.buf.is_null() {
378 let b = &*pi.buf;
379 if let Some(read) = b.readcallback {
380 return helpers::input_from_io(Some(read), b.closecallback, b.context);
381 }
382 }
383 if !pi.base.is_null() && !pi.end.is_null() && pi.end >= pi.base {
384 let len = (pi.end as usize).saturating_sub(pi.base as usize);
385 let slice = core::slice::from_raw_parts(pi.base, len);
386 return InputBuffer::from_memory(slice, None);
387 }
388 InputBuffer::from_memory(&[], None)
389 }
390}
391
392/// Core of `xmlCtxtRead*`: reset the context, wire an input buffer, parse,
393/// and return the resulting document (freed on hard error unless recovery).
394///
395/// # Safety
396///
397/// `ctxt` must be a valid parser context; `input` is consumed.
398unsafe fn ctxt_read_doc(
399 ctxt: *mut _xmlParserCtxt,
400 input: InputBuffer,
401 url: *const c_char,
402 options: c_int,
403) -> *mut _xmlDoc {
404 unsafe {
405 xmlCtxtReset(ctxt);
406 apply_options(ctxt, options);
407 helpers::setup_parser_input(ctxt, input);
408 if helpers::parse_document(ctxt) != 0 {
409 let doc = (*ctxt).myDoc;
410 (*ctxt).myDoc = ptr::null_mut();
411 if options & XML_PARSE_RECOVER != 0 {
412 return doc;
413 }
414 if !doc.is_null() {
415 tree::free_doc(doc);
416 }
417 return ptr::null_mut();
418 }
419 let doc = (*ctxt).myDoc;
420 if !doc.is_null() && !url.is_null() {
421 (*doc).URL = string::xml_strdup(url as *const xmlChar);
422 }
423 doc
424 }
425}
426
427/// Parse DTD declaration text using the internal engine by wrapping it in a
428/// synthetic document (`<!DOCTYPE none [ ... ]><none/>`) when the text is a
429/// bare DTD subset, or parsing it directly when it is already a document.
430///
431/// Returns a detached DTD (never owned by a document), or NULL.
432///
433/// # Safety
434///
435/// `ctxt` must be a valid parser context; `data` must be readable for `len`
436/// bytes.
437unsafe fn parse_dtd_text(
438 ctxt: *mut _xmlParserCtxt,
439 data: &[u8],
440 public_id: *const xmlChar,
441 system_id: *const xmlChar,
442) -> *mut _xmlDtd {
443 unsafe {
444 // If the content is already a full document (contains a DOCTYPE),
445 // parse it directly; otherwise wrap the declarations.
446 let has_doctype = data
447 .windows(9)
448 .any(|w| w.eq_ignore_ascii_case(b"<!DOCTYPE"));
449 let mut wrapped: Vec<u8>;
450 let parse_data: &[u8] = if has_doctype {
451 data
452 } else {
453 wrapped = Vec::with_capacity(data.len() + 32);
454 wrapped.extend_from_slice(b"<!DOCTYPE none [");
455 wrapped.extend_from_slice(data);
456 wrapped.extend_from_slice(b"]><none/>");
457 &wrapped
458 };
459
460 let input = InputBuffer::from_memory(parse_data, None);
461 helpers::setup_parser_input(ctxt, input);
462 let rc = helpers::parse_document(ctxt);
463 let doc = (*ctxt).myDoc;
464 (*ctxt).myDoc = ptr::null_mut();
465
466 if rc == 0 && !doc.is_null() && !(*doc).intSubset.is_null() {
467 let dtd = (*doc).intSubset;
468 (*doc).intSubset = ptr::null_mut();
469 (*dtd).parent = ptr::null_mut();
470 (*dtd).doc = ptr::null_mut();
471 if !public_id.is_null() {
472 (*dtd).ExternalID = string::xml_strdup(public_id);
473 }
474 if !system_id.is_null() {
475 (*dtd).SystemID = string::xml_strdup(system_id);
476 }
477 tree::free_doc(doc);
478 return dtd;
479 }
480
481 if !doc.is_null() {
482 tree::free_doc(doc);
483 }
484 // Fallback: an empty DTD carrying the identifiers.
485
486 dtd::new_dtd(
487 ptr::null_mut(),
488 c"none".as_ptr() as *const xmlChar,
489 public_id,
490 system_id,
491 )
492 }
493}
494
495// ═══════════════════════════════════════════════════════════════════════════════
496// Context creation / lifecycle
497// ═══════════════════════════════════════════════════════════════════════════════
498
499/// Create a new parser context with a default SAX2 handler.
500///
501/// # UPSTREAM-PARITY
502///
503/// ```c
504/// xmlParserCtxtPtr xmlNewParserCtxt(void);
505/// ```
506///
507/// # SAFETY
508///
509/// The function touches crate-global state only; it is safe
510/// as long as the caller respects the library's global
511/// initialization/cleanup ordering (xmlInitParser before use,
512/// xmlCleanupParser only after all users are done).
513///
514/// Violating the global lifecycle ordering, or calling this after
515/// teardown or from a signal handler, is undefined behavior.
516#[no_mangle]
517pub unsafe extern "C" fn xmlNewParserCtxt() -> *mut _xmlParserCtxt {
518 unsafe {
519 globals::init_parser();
520 let ctxt = xmlMallocZero(core::mem::size_of::<_xmlParserCtxt>()) as *mut _xmlParserCtxt;
521 if ctxt.is_null() {
522 return ptr::null_mut();
523 }
524 if init_sax_parser_ctxt(ctxt, ptr::null(), ptr::null_mut()) < 0 {
525 helpers::free_parser_ctxt(ctxt);
526 return ptr::null_mut();
527 }
528 ctxt
529 }
530}
531
532/// Create a new parser context using the given SAX handler (or the default
533/// SAX2 handler when `sax` is NULL).
534///
535/// # UPSTREAM-PARITY
536///
537/// ```c
538/// xmlParserCtxtPtr xmlNewSAXParserCtxt(const xmlSAXHandler *sax, void *userData);
539/// ```
540///
541/// # SAFETY
542///
543/// - `sax`, `userData` must be valid pointers (or NULL
544/// where the upstream C contract allows), obtained from the
545/// matching constructor/owner and not yet freed; the callee may
546/// take or keep ownership exactly as the C API specifies.
547///
548/// The caller must not race this call with concurrent mutation of the
549/// same objects from other threads (per-object state is not internally
550/// synchronized). Violating any of the above is undefined behavior.
551///
552/// Exercised by the C-API differential courts
553/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
554/// courts; those pass byte-for-byte against the upstream oracle.
555#[no_mangle]
556pub unsafe extern "C" fn xmlNewSAXParserCtxt(
557 sax: *const _xmlSAXHandler,
558 userData: *mut c_void,
559) -> *mut _xmlParserCtxt {
560 unsafe {
561 globals::init_parser();
562 let ctxt = xmlMallocZero(core::mem::size_of::<_xmlParserCtxt>()) as *mut _xmlParserCtxt;
563 if ctxt.is_null() {
564 return ptr::null_mut();
565 }
566 if init_sax_parser_ctxt(ctxt, sax, userData) < 0 {
567 helpers::free_parser_ctxt(ctxt);
568 return ptr::null_mut();
569 }
570 ctxt
571 }
572}
573
574/// Initialise a parser context (legacy API): zeroes the context, installs a
575/// default SAX2 handler and sets the initial parser state.
576///
577/// # UPSTREAM-PARITY
578///
579/// ```c
580/// int xmlInitParserCtxt(xmlParserCtxtPtr ctxt);
581/// ```
582///
583/// # SAFETY
584///
585/// - `ctxt` must be valid pointers (or NULL
586/// where the upstream C contract allows), obtained from the
587/// matching constructor/owner and not yet freed; the callee may
588/// take or keep ownership exactly as the C API specifies.
589///
590/// The caller must not race this call with concurrent mutation of the
591/// same objects from other threads (per-object state is not internally
592/// synchronized). Violating any of the above is undefined behavior.
593///
594/// Exercised by the C-API differential courts
595/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
596/// courts; those pass byte-for-byte against the upstream oracle.
597#[no_mangle]
598pub unsafe extern "C" fn xmlInitParserCtxt(ctxt: *mut _xmlParserCtxt) -> c_int {
599 unsafe { init_sax_parser_ctxt(ctxt, ptr::null(), ptr::null_mut()) }
600}
601
602/// Clear (reset) a parser context.
603///
604/// # UPSTREAM-PARITY
605///
606/// ```c
607/// void xmlClearParserCtxt(xmlParserCtxtPtr ctxt);
608/// ```
609///
610/// # SAFETY
611///
612/// - `ctxt` must be valid pointers (or NULL
613/// where the upstream C contract allows), obtained from the
614/// matching constructor/owner and not yet freed; the callee may
615/// take or keep ownership exactly as the C API specifies.
616///
617/// The caller must not race this call with concurrent mutation of the
618/// same objects from other threads (per-object state is not internally
619/// synchronized). Violating any of the above is undefined behavior.
620///
621/// Exercised by the C-API differential courts
622/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
623/// courts; those pass byte-for-byte against the upstream oracle.
624#[no_mangle]
625pub unsafe extern "C" fn xmlClearParserCtxt(ctxt: *mut _xmlParserCtxt) {
626 unsafe { xmlCtxtReset(ctxt) }
627}
628
629/// Reset a parser context: drop the input stack, node/name stacks, strings,
630/// document and error state so the context can be reused.
631///
632/// # UPSTREAM-PARITY
633///
634/// ```c
635/// void xmlCtxtReset(xmlParserCtxtPtr ctxt);
636/// ```
637///
638/// # SAFETY
639///
640/// - `ctxt` must be valid pointers (or NULL
641/// where the upstream C contract allows), obtained from the
642/// matching constructor/owner and not yet freed; the callee may
643/// take or keep ownership exactly as the C API specifies.
644///
645/// The caller must not race this call with concurrent mutation of the
646/// same objects from other threads (per-object state is not internally
647/// synchronized). Violating any of the above is undefined behavior.
648///
649/// Exercised by the C-API differential courts
650/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
651/// courts; those pass byte-for-byte against the upstream oracle.
652#[no_mangle]
653pub unsafe extern "C" fn xmlCtxtReset(ctxt: *mut _xmlParserCtxt) {
654 if ctxt.is_null() {
655 return;
656 }
657 unsafe {
658 let c = &mut *ctxt;
659
660 // Free all inputs on the stack.
661 let input_nr = c.inputNr;
662 let input_tab = c.inputTab;
663 if !input_tab.is_null() {
664 for i in 0..input_nr {
665 let input = *input_tab.add(i as usize);
666 if !input.is_null() {
667 helpers::free_parser_input(input);
668 }
669 }
670 xmlFreeImpl(input_tab as *mut c_void);
671 }
672 c.inputTab = ptr::null_mut();
673 c.inputMax = 0;
674 c.inputNr = 0;
675 c.input = ptr::null_mut();
676
677 // Free the stored InputBuffer (stashed by setup_parser_input; the
678 // side table keeps ctxt._private application data — 11.1-X).
679 helpers::free_stashed_input_buffer(ctxt);
680 // Drop any incremental-push state (SP-14.3.1-3).
681 helpers::free_push_state(ctxt);
682
683 // Node stack (array only; nodes are owned by the doc).
684 if !c.nodeTab.is_null() {
685 xmlFreeImpl(c.nodeTab as *mut c_void);
686 }
687 c.nodeTab = ptr::null_mut();
688 c.nodeMax = 0;
689 c.nodeNr = 0;
690 c.node = ptr::null_mut();
691
692 // Name stack.
693 if !c.nameTab.is_null() {
694 xmlFreeImpl(c.nameTab as *mut c_void);
695 }
696 c.nameTab = ptr::null_mut();
697 c.nameMax = 0;
698 c.nameNr = 0;
699 c.name = ptr::null();
700
701 // Space stack: keep the allocation, reset the counter.
702 c.spaceNr = 0;
703 c.space = ptr::null_mut();
704
705 // Namespaces.
706 c.nsNr = 0;
707
708 // Strings owned by the context.
709 if !c.version.is_null() {
710 xmlFreeImpl(c.version as *mut c_void);
711 c.version = ptr::null_mut();
712 }
713 if !c.encoding.is_null() {
714 xmlFreeImpl(c.encoding as *mut c_void);
715 c.encoding = ptr::null_mut();
716 }
717 if !c.extSubURI.is_null() {
718 xmlFreeImpl(c.extSubURI as *mut c_void);
719 c.extSubURI = ptr::null_mut();
720 }
721 if !c.extSubSystem.is_null() {
722 xmlFreeImpl(c.extSubSystem as *mut c_void);
723 c.extSubSystem = ptr::null_mut();
724 }
725 if !c.directory.is_null() {
726 xmlFreeImpl(c.directory as *mut c_void);
727 c.directory = ptr::null_mut();
728 }
729
730 // Document: the context owns it until reset/free.
731 if !c.myDoc.is_null() {
732 tree::free_doc(c.myDoc);
733 }
734 c.myDoc = ptr::null_mut();
735
736 // Parser state.
737 c.standalone = -1;
738 c.hasExternalSubset = 0;
739 c.hasPErefs = 0;
740 c.instate = xmlParserInputState::XML_PARSER_START as c_int;
741 c.wellFormed = 1;
742 c.nsWellFormed = 1;
743 c.disableSAX = 0;
744 c.valid = 1;
745 c.record_info = 0;
746 c.checkIndex = 0;
747 c.inSubset = 0;
748 c.errNo = XML_ERR_OK;
749 c.depth = 0;
750 c.nbentities = 0;
751 c.sizeentities = 0;
752 c.nbErrors = 0;
753 c.nbWarnings = 0;
754
755 xmlInitNodeInfoSeq(&mut c.node_seq);
756
757 if c.lastError.code != XML_ERR_OK {
758 errors::reset_error(&mut c.lastError);
759 }
760 }
761}
762
763/// Reset a push-parser context and set up a fresh input chunk.
764///
765/// # UPSTREAM-PARITY
766///
767/// ```c
768/// int xmlCtxtResetPush(xmlParserCtxtPtr ctxt, const char *chunk, int size,
769/// const char *filename, const char *encoding);
770/// ```
771///
772/// # SAFETY
773///
774/// - `ctxt` must be valid pointers (or NULL
775/// where the upstream C contract allows), obtained from the
776/// matching constructor/owner and not yet freed; the callee may
777/// take or keep ownership exactly as the C API specifies.
778///
779/// - `chunk`, `filename`, `encoding` must point to valid NUL-terminated
780/// strings (or NULL where the C contract allows) for the lifetime
781/// of the call.
782///
783/// The caller must not race this call with concurrent mutation of the
784/// same objects from other threads (per-object state is not internally
785/// synchronized). Violating any of the above is undefined behavior.
786///
787/// Exercised by the C-API differential courts
788/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
789/// courts; those pass byte-for-byte against the upstream oracle.
790#[no_mangle]
791pub unsafe extern "C" fn xmlCtxtResetPush(
792 ctxt: *mut _xmlParserCtxt,
793 chunk: *const c_char,
794 size: c_int,
795 filename: *const c_char,
796 encoding: *const c_char,
797) -> c_int {
798 if ctxt.is_null() {
799 return 1;
800 }
801 unsafe {
802 xmlCtxtReset(ctxt);
803
804 let slice = if size > 0 && !chunk.is_null() {
805 core::slice::from_raw_parts(chunk as *const u8, size as usize)
806 } else {
807 &[]
808 };
809 let uri = if filename.is_null() {
810 None
811 } else {
812 CStr::from_ptr(filename).to_str().ok()
813 };
814 let input = InputBuffer::from_memory(slice, uri);
815 helpers::setup_parser_input(ctxt, input);
816
817 if !encoding.is_null() {
818 let handler = encoding::find_encoding_handler(encoding as *const xmlChar);
819 if !handler.is_null() {
820 xmlSwitchToEncoding(ctxt, handler);
821 }
822 }
823 }
824 0
825}
826
827/// Apply a full set of parser options, clearing options not present.
828///
829/// # UPSTREAM-PARITY
830///
831/// ```c
832/// int xmlCtxtSetOptions(xmlParserCtxtPtr ctxt, int options);
833/// ```
834///
835/// # SAFETY
836///
837/// - `ctxt` must be valid pointers (or NULL
838/// where the upstream C contract allows), obtained from the
839/// matching constructor/owner and not yet freed; the callee may
840/// take or keep ownership exactly as the C API specifies.
841///
842/// The caller must not race this call with concurrent mutation of the
843/// same objects from other threads (per-object state is not internally
844/// synchronized). Violating any of the above is undefined behavior.
845///
846/// Exercised by the C-API differential courts
847/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
848/// courts; those pass byte-for-byte against the upstream oracle.
849#[no_mangle]
850pub unsafe extern "C" fn xmlCtxtSetOptions(ctxt: *mut _xmlParserCtxt, options: c_int) -> c_int {
851 if ctxt.is_null() {
852 return -1;
853 }
854 const ALL_MASK: c_int = XML_PARSE_RECOVER
855 | XML_PARSE_NOENT
856 | XML_PARSE_DTDLOAD
857 | XML_PARSE_DTDATTR
858 | XML_PARSE_DTDVALID
859 | XML_PARSE_NOERROR
860 | XML_PARSE_NOWARNING
861 | XML_PARSE_PEDANTIC
862 | XML_PARSE_NOBLANKS
863 | XML_PARSE_SAX1
864 | XML_PARSE_NONET
865 | XML_PARSE_NODICT
866 | XML_PARSE_NSCLEAN
867 | XML_PARSE_NOCDATA
868 | XML_PARSE_COMPACT
869 | XML_PARSE_OLD10
870 | XML_PARSE_HUGE
871 | XML_PARSE_OLDSAX
872 | XML_PARSE_IGNORE_ENC
873 | XML_PARSE_BIG_LINES;
874
875 unsafe {
876 apply_options(ctxt, options & ALL_MASK);
877 }
878 options & !ALL_MASK
879}
880
881/// Install a per-context structured error handler.
882///
883/// # UPSTREAM-PARITY
884///
885/// ```c
886/// void xmlCtxtSetErrorHandler(xmlParserCtxtPtr ctxt,
887/// xmlStructuredErrorFunc handler, void *data);
888/// ```
889///
890/// # SAFETY
891///
892/// - `ctxt`, `data` must be valid pointers (or NULL
893/// where the upstream C contract allows), obtained from the
894/// matching constructor/owner and not yet freed; the callee may
895/// take or keep ownership exactly as the C API specifies.
896///
897/// - `handler` must be a valid callback (or None);
898/// the callback is invoked with the documented context pointer and
899/// must itself uphold the same pointer invariants.
900///
901/// The caller must not race this call with concurrent mutation of the
902/// same objects from other threads (per-object state is not internally
903/// synchronized). Violating any of the above is undefined behavior.
904///
905/// Exercised by the C-API differential courts
906/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
907/// courts; those pass byte-for-byte against the upstream oracle.
908#[no_mangle]
909pub unsafe extern "C" fn xmlCtxtSetErrorHandler(
910 ctxt: *mut _xmlParserCtxt,
911 handler: Option<xmlStructuredErrorFunc>,
912 data: *mut c_void,
913) {
914 if ctxt.is_null() {
915 return;
916 }
917 unsafe {
918 (*ctxt).errorHandler = handler;
919 (*ctxt).errorCtxt = data;
920 }
921}
922
923/// Set the maximum entity expansion amplification factor.
924///
925/// # UPSTREAM-PARITY
926///
927/// ```c
928/// void xmlCtxtSetMaxAmplification(xmlParserCtxtPtr ctxt, unsigned maxAmpl);
929/// ```
930///
931/// # SAFETY
932///
933/// - `ctxt` must be valid pointers (or NULL
934/// where the upstream C contract allows), obtained from the
935/// matching constructor/owner and not yet freed; the callee may
936/// take or keep ownership exactly as the C API specifies.
937///
938/// The caller must not race this call with concurrent mutation of the
939/// same objects from other threads (per-object state is not internally
940/// synchronized). Violating any of the above is undefined behavior.
941///
942/// Exercised by the C-API differential courts
943/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
944/// courts; those pass byte-for-byte against the upstream oracle.
945#[no_mangle]
946pub unsafe extern "C" fn xmlCtxtSetMaxAmplification(ctxt: *mut _xmlParserCtxt, maxAmpl: c_uint) {
947 if ctxt.is_null() || maxAmpl == 0 {
948 return;
949 }
950 unsafe {
951 (*ctxt).maxAmpl = maxAmpl;
952 }
953}
954
955/// Get the last error raised on the context, or NULL.
956///
957/// # UPSTREAM-PARITY
958///
959/// ```c
960/// const xmlError *xmlCtxtGetLastError(void *ctx);
961/// ```
962///
963/// # SAFETY
964///
965/// - `ctx` must be valid pointers (or NULL
966/// where the upstream C contract allows), obtained from the
967/// matching constructor/owner and not yet freed; the callee may
968/// take or keep ownership exactly as the C API specifies.
969///
970/// The caller must not race this call with concurrent mutation of the
971/// same objects from other threads (per-object state is not internally
972/// synchronized). Violating any of the above is undefined behavior.
973///
974/// Exercised by the C-API differential courts
975/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
976/// courts; those pass byte-for-byte against the upstream oracle.
977#[no_mangle]
978pub unsafe extern "C" fn xmlCtxtGetLastError(ctx: *mut c_void) -> *const _xmlError {
979 if ctx.is_null() {
980 return ptr::null();
981 }
982 let ctxt = ctx as *mut _xmlParserCtxt;
983 unsafe {
984 if (*ctxt).lastError.code == XML_ERR_OK {
985 return ptr::null();
986 }
987 &(*ctxt).lastError
988 }
989}
990
991/// Reset the context's last-error state.
992///
993/// # UPSTREAM-PARITY
994///
995/// ```c
996/// void xmlCtxtResetLastError(void *ctx);
997/// ```
998///
999/// # SAFETY
1000///
1001/// - `ctx` must be valid pointers (or NULL
1002/// where the upstream C contract allows), obtained from the
1003/// matching constructor/owner and not yet freed; the callee may
1004/// take or keep ownership exactly as the C API specifies.
1005///
1006/// The caller must not race this call with concurrent mutation of the
1007/// same objects from other threads (per-object state is not internally
1008/// synchronized). Violating any of the above is undefined behavior.
1009///
1010/// Exercised by the C-API differential courts
1011/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1012/// courts; those pass byte-for-byte against the upstream oracle.
1013#[no_mangle]
1014pub unsafe extern "C" fn xmlCtxtResetLastError(ctx: *mut c_void) {
1015 if ctx.is_null() {
1016 return;
1017 }
1018 let ctxt = ctx as *mut _xmlParserCtxt;
1019 unsafe {
1020 (*ctxt).errNo = XML_ERR_OK;
1021 if (*ctxt).lastError.code != XML_ERR_OK {
1022 // Upstream xmlResetError frees the owned strings.
1023 crate::xml::globals::free_error_strings(&(*ctxt).lastError);
1024 errors::reset_error(&mut (*ctxt).lastError);
1025 }
1026 }
1027}
1028
1029/// Handle an out-of-memory error on a parser context.
1030///
1031/// # UPSTREAM-PARITY
1032///
1033/// ```c
1034/// void xmlCtxtErrMemory(xmlParserCtxtPtr ctxt);
1035/// ```
1036///
1037/// # SAFETY
1038///
1039/// - `ctxt` must be valid pointers (or NULL
1040/// where the upstream C contract allows), obtained from the
1041/// matching constructor/owner and not yet freed; the callee may
1042/// take or keep ownership exactly as the C API specifies.
1043///
1044/// The caller must not race this call with concurrent mutation of the
1045/// same objects from other threads (per-object state is not internally
1046/// synchronized). Violating any of the above is undefined behavior.
1047///
1048/// Exercised by the C-API differential courts
1049/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1050/// courts; those pass byte-for-byte against the upstream oracle.
1051#[no_mangle]
1052pub unsafe extern "C" fn xmlCtxtErrMemory(ctxt: *mut _xmlParserCtxt) {
1053 if ctxt.is_null() {
1054 return;
1055 }
1056 unsafe {
1057 let c = &mut *ctxt;
1058 c.errNo = XML_ERR_NO_MEMORY;
1059 c.instate = xmlParserInputState::XML_PARSER_EOF as c_int;
1060 c.wellFormed = 0;
1061 c.disableSAX = 2;
1062
1063 c.lastError.domain = XML_FROM_PARSER;
1064 c.lastError.code = XML_ERR_NO_MEMORY;
1065 c.lastError.level = xmlErrorLevel::XML_ERR_FATAL as c_int;
1066 // Owned copy (upstream xmlRaiseMemoryError): the per-context last
1067 // error strings are freed on reset/free, so static literals would be
1068 // a double-free/UB hazard.
1069 c.lastError.message =
1070 crate::abi::allocator::xmlMemStrdupImpl(c"out of memory\n".as_ptr()) as *mut c_char;
1071
1072 if let Some(handler) = c.errorHandler {
1073 handler(c.errorCtxt, &c.lastError);
1074 } else if !c.sax.is_null() {
1075 if let Some(serror) = (*c.sax).serror {
1076 serror(c.userData, &c.lastError);
1077 }
1078 }
1079 }
1080}
1081
1082/// Stop the parser: no further processing will happen.
1083///
1084/// # UPSTREAM-PARITY
1085///
1086/// ```c
1087/// void xmlStopParser(xmlParserCtxtPtr ctxt);
1088/// ```
1089///
1090/// # SAFETY
1091///
1092/// - `ctxt` must be valid pointers (or NULL
1093/// where the upstream C contract allows), obtained from the
1094/// matching constructor/owner and not yet freed; the callee may
1095/// take or keep ownership exactly as the C API specifies.
1096///
1097/// The caller must not race this call with concurrent mutation of the
1098/// same objects from other threads (per-object state is not internally
1099/// synchronized). Violating any of the above is undefined behavior.
1100///
1101/// Exercised by the C-API differential courts
1102/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1103/// courts; those pass byte-for-byte against the upstream oracle.
1104#[no_mangle]
1105pub unsafe extern "C" fn xmlStopParser(ctxt: *mut _xmlParserCtxt) {
1106 if ctxt.is_null() {
1107 return;
1108 }
1109 unsafe {
1110 (*ctxt).disableSAX = 2;
1111 if (*ctxt).errNo == XML_ERR_OK {
1112 (*ctxt).errNo = XML_ERR_USER_STOP;
1113 (*ctxt).lastError.code = XML_ERR_USER_STOP;
1114 (*ctxt).wellFormed = 0;
1115 }
1116 }
1117}
1118
1119/// Return the byte offset of the current parse position within the current
1120/// entity, or -1 when it cannot be computed.
1121///
1122/// # UPSTREAM-PARITY
1123///
1124/// ```c
1125/// long xmlByteConsumed(xmlParserCtxtPtr ctxt);
1126/// ```
1127///
1128/// # SAFETY
1129///
1130/// - `ctxt` must be valid pointers (or NULL
1131/// where the upstream C contract allows), obtained from the
1132/// matching constructor/owner and not yet freed; the callee may
1133/// take or keep ownership exactly as the C API specifies.
1134///
1135/// The caller must not race this call with concurrent mutation of the
1136/// same objects from other threads (per-object state is not internally
1137/// synchronized). Violating any of the above is undefined behavior.
1138///
1139/// Exercised by the C-API differential courts
1140/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1141/// courts; those pass byte-for-byte against the upstream oracle.
1142#[no_mangle]
1143pub unsafe extern "C" fn xmlByteConsumed(ctxt: *mut _xmlParserCtxt) -> c_long {
1144 if ctxt.is_null() {
1145 return -1;
1146 }
1147 unsafe {
1148 let input = (*ctxt).input;
1149 if input.is_null() {
1150 return -1;
1151 }
1152 if !(*input).buf.is_null() && !(*(*input).buf).encoder.is_null() {
1153 // With an encoder we cannot cheaply compute the original byte
1154 // position; report the raw consumed count.
1155 return (*(*input).buf).rawconsumed as c_long;
1156 }
1157 let consumed = (*input).consumed;
1158 if (*input).base.is_null() {
1159 return consumed as c_long;
1160 }
1161 (consumed + ((*input).cur as usize).saturating_sub((*input).base as usize) as c_ulong)
1162 as c_long
1163 }
1164}
1165
1166/// Extract the directory part of a filename (newly allocated).
1167///
1168/// # UPSTREAM-PARITY
1169///
1170/// ```c
1171/// char *xmlParserGetDirectory(const char *filename);
1172/// ```
1173///
1174/// # SAFETY
1175///
1176///
1177/// - `filename` must point to valid NUL-terminated
1178/// strings (or NULL where the C contract allows) for the lifetime
1179/// of the call.
1180///
1181/// The caller must not race this call with concurrent mutation of the
1182/// same objects from other threads (per-object state is not internally
1183/// synchronized). Violating any of the above is undefined behavior.
1184///
1185/// Exercised by the C-API differential courts
1186/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1187/// courts; those pass byte-for-byte against the upstream oracle.
1188#[no_mangle]
1189pub unsafe extern "C" fn xmlParserGetDirectory(filename: *const c_char) -> *mut c_char {
1190 if filename.is_null() {
1191 return ptr::null_mut();
1192 }
1193 unsafe {
1194 let len = libc::strlen(filename);
1195 let mut last_sep: Option<usize> = None;
1196 for i in 0..len {
1197 if *filename.add(i) == b'/' as c_char {
1198 last_sep = Some(i);
1199 }
1200 }
1201 match last_sep {
1202 Some(0) => xmlMemStrdupImpl(c"/".as_ptr() as *const c_char) as *mut c_char,
1203 Some(pos) => {
1204 let slice = core::slice::from_raw_parts(filename as *const u8, pos);
1205 let mut v = slice.to_vec();
1206 v.push(0);
1207 xmlMemStrdupImpl(v.as_ptr() as *const c_char) as *mut c_char
1208 }
1209 None => xmlMemStrdupImpl(c".".as_ptr() as *const c_char) as *mut c_char,
1210 }
1211 }
1212}
1213
1214/// Check whether a file exists: 0 if stat fails, 2 if it is a directory,
1215/// 1 otherwise.
1216///
1217/// # UPSTREAM-PARITY
1218///
1219/// ```c
1220/// int xmlCheckFilename(const char *path);
1221/// ```
1222///
1223/// # SAFETY
1224///
1225///
1226/// - `path` must point to valid NUL-terminated
1227/// strings (or NULL where the C contract allows) for the lifetime
1228/// of the call.
1229///
1230/// The caller must not race this call with concurrent mutation of the
1231/// same objects from other threads (per-object state is not internally
1232/// synchronized). Violating any of the above is undefined behavior.
1233///
1234/// Exercised by the C-API differential courts
1235/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1236/// courts; those pass byte-for-byte against the upstream oracle.
1237#[no_mangle]
1238pub unsafe extern "C" fn xmlCheckFilename(path: *const c_char) -> c_int {
1239 if path.is_null() {
1240 return 0;
1241 }
1242 unsafe {
1243 let mut st: libc::stat = core::mem::zeroed();
1244 if libc::stat(path, &mut st) != 0 {
1245 return 0;
1246 }
1247 if st.st_mode & libc::S_IFMT == libc::S_IFDIR {
1248 2
1249 } else {
1250 1
1251 }
1252 }
1253}
1254
1255/// Test whether a public/system ID pair is one of the XHTML DTDs.
1256///
1257/// # UPSTREAM-PARITY
1258///
1259/// ```c
1260/// int xmlIsXHTML(const xmlChar *systemID, const xmlChar *publicID);
1261/// ```
1262///
1263/// # SAFETY
1264///
1265///
1266/// - `systemID`, `publicID` must point to valid NUL-terminated
1267/// strings (or NULL where the C contract allows) for the lifetime
1268/// of the call.
1269///
1270/// The caller must not race this call with concurrent mutation of the
1271/// same objects from other threads (per-object state is not internally
1272/// synchronized). Violating any of the above is undefined behavior.
1273///
1274/// Exercised by the C-API differential courts
1275/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1276/// courts; those pass byte-for-byte against the upstream oracle.
1277#[no_mangle]
1278pub unsafe extern "C" fn xmlIsXHTML(systemID: *const xmlChar, publicID: *const xmlChar) -> c_int {
1279 const XHTML_STRICT_PUBLIC_ID: &[u8] = b"-//W3C//DTD XHTML 1.0 Strict//EN\0";
1280 const XHTML_STRICT_SYSTEM_ID: &[u8] = b"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\0";
1281 const XHTML_FRAME_PUBLIC_ID: &[u8] = b"-//W3C//DTD XHTML 1.0 Frameset//EN\0";
1282 const XHTML_FRAME_SYSTEM_ID: &[u8] = b"http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd\0";
1283 const XHTML_TRANS_PUBLIC_ID: &[u8] = b"-//W3C//DTD XHTML 1.0 Transitional//EN\0";
1284 const XHTML_TRANS_SYSTEM_ID: &[u8] =
1285 b"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\0";
1286
1287 if systemID.is_null() && publicID.is_null() {
1288 return -1;
1289 }
1290 unsafe {
1291 if !publicID.is_null()
1292 && (string::xml_strcmp(publicID, XHTML_STRICT_PUBLIC_ID.as_ptr() as *const xmlChar)
1293 == 0
1294 || string::xml_strcmp(publicID, XHTML_FRAME_PUBLIC_ID.as_ptr() as *const xmlChar)
1295 == 0
1296 || string::xml_strcmp(publicID, XHTML_TRANS_PUBLIC_ID.as_ptr() as *const xmlChar)
1297 == 0)
1298 {
1299 return 1;
1300 }
1301 if !systemID.is_null()
1302 && (string::xml_strcmp(systemID, XHTML_STRICT_SYSTEM_ID.as_ptr() as *const xmlChar)
1303 == 0
1304 || string::xml_strcmp(systemID, XHTML_FRAME_SYSTEM_ID.as_ptr() as *const xmlChar)
1305 == 0
1306 || string::xml_strcmp(systemID, XHTML_TRANS_SYSTEM_ID.as_ptr() as *const xmlChar)
1307 == 0)
1308 {
1309 return 1;
1310 }
1311 }
1312 0
1313}
1314
1315// ═══════════════════════════════════════════════════════════════════════════════
1316// Context creation from sources
1317// ═══════════════════════════════════════════════════════════════════════════════
1318
1319/// Create a parser context for an in-memory document.
1320///
1321/// # UPSTREAM-PARITY
1322///
1323/// ```c
1324/// xmlParserCtxtPtr xmlCreateMemoryParserCtxt(const char *buffer, int size);
1325/// ```
1326///
1327/// # SAFETY
1328///
1329///
1330/// - `buffer` must point to valid NUL-terminated
1331/// strings (or NULL where the C contract allows) for the lifetime
1332/// of the call.
1333///
1334/// The caller must not race this call with concurrent mutation of the
1335/// same objects from other threads (per-object state is not internally
1336/// synchronized). Violating any of the above is undefined behavior.
1337///
1338/// Exercised by the C-API differential courts
1339/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1340/// courts; those pass byte-for-byte against the upstream oracle.
1341#[no_mangle]
1342pub unsafe extern "C" fn xmlCreateMemoryParserCtxt(
1343 buffer: *const c_char,
1344 size: c_int,
1345) -> *mut _xmlParserCtxt {
1346 if buffer.is_null() || size < 0 {
1347 return ptr::null_mut();
1348 }
1349 unsafe {
1350 let ctxt = xmlNewParserCtxt();
1351 if ctxt.is_null() {
1352 return ptr::null_mut();
1353 }
1354 let input = helpers::input_from_memory(buffer, size);
1355 helpers::setup_parser_input(ctxt, input);
1356 ctxt
1357 }
1358}
1359
1360/// Create a parser context for push parsing.
1361///
1362/// # UPSTREAM-PARITY
1363///
1364/// ```c
1365/// xmlParserCtxtPtr xmlCreatePushParserCtxt(xmlSAXHandler *sax, void *user_data,
1366/// const char *chunk, int size,
1367/// const char *filename);
1368/// ```
1369///
1370/// # SAFETY
1371///
1372/// - `sax`, `user_data` must be valid pointers (or NULL
1373/// where the upstream C contract allows), obtained from the
1374/// matching constructor/owner and not yet freed; the callee may
1375/// take or keep ownership exactly as the C API specifies.
1376///
1377/// - `chunk`, `filename` must point to valid NUL-terminated
1378/// strings (or NULL where the C contract allows) for the lifetime
1379/// of the call.
1380///
1381/// The caller must not race this call with concurrent mutation of the
1382/// same objects from other threads (per-object state is not internally
1383/// synchronized). Violating any of the above is undefined behavior.
1384///
1385/// Exercised by the C-API differential courts
1386/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1387/// courts; those pass byte-for-byte against the upstream oracle.
1388#[no_mangle]
1389pub unsafe extern "C" fn xmlCreatePushParserCtxt(
1390 sax: *mut _xmlSAXHandler,
1391 user_data: *mut c_void,
1392 chunk: *const c_char,
1393 size: c_int,
1394 filename: *const c_char,
1395) -> *mut _xmlParserCtxt {
1396 unsafe {
1397 let ctxt = xmlNewSAXParserCtxt(sax, user_data);
1398 if ctxt.is_null() {
1399 return ptr::null_mut();
1400 }
1401 // UPSTREAM-PARITY (parser.c xmlCreatePushParserCtxt): the push
1402 // context forces dictNames on (and clears XML_PARSE_NODICT), so
1403 // element/attribute names are interned in the document dictionary
1404 // and pointer-identical to xmlDictLookup results — lxml's
1405 // _MultiTagMatcher (iterparse tag=...) compares name pointers.
1406 (*ctxt).options &= !crate::abi::types::XML_PARSE_NODICT;
1407 (*ctxt).dictNames = 1;
1408 let slice = if size > 0 && !chunk.is_null() {
1409 core::slice::from_raw_parts(chunk as *const u8, size as usize)
1410 } else {
1411 &[]
1412 };
1413 let uri = if filename.is_null() {
1414 None
1415 } else {
1416 CStr::from_ptr(filename).to_str().ok()
1417 };
1418 let input = InputBuffer::from_memory(slice, uri);
1419 helpers::setup_parser_input(ctxt, input);
1420 ctxt
1421 }
1422}
1423
1424/// Create a parser context for an I/O stream.
1425///
1426/// # UPSTREAM-PARITY
1427///
1428/// ```c
1429/// xmlParserCtxtPtr xmlCreateIOParserCtxt(xmlSAXHandler *sax, void *user_data,
1430/// xmlInputReadCallback ioread,
1431/// xmlInputCloseCallback ioclose,
1432/// void *ioctx, xmlCharEncoding enc);
1433/// ```
1434///
1435/// # SAFETY
1436///
1437/// - `sax`, `user_data`, `ioctx` must be valid pointers (or NULL
1438/// where the upstream C contract allows), obtained from the
1439/// matching constructor/owner and not yet freed; the callee may
1440/// take or keep ownership exactly as the C API specifies.
1441///
1442/// - `ioread`, `ioclose` must be a valid callback (or None);
1443/// the callback is invoked with the documented context pointer and
1444/// must itself uphold the same pointer invariants.
1445///
1446/// The caller must not race this call with concurrent mutation of the
1447/// same objects from other threads (per-object state is not internally
1448/// synchronized). Violating any of the above is undefined behavior.
1449///
1450/// Exercised by the C-API differential courts
1451/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1452/// courts; those pass byte-for-byte against the upstream oracle.
1453#[no_mangle]
1454pub unsafe extern "C" fn xmlCreateIOParserCtxt(
1455 sax: *mut _xmlSAXHandler,
1456 user_data: *mut c_void,
1457 ioread: Option<xmlInputReadCallback>,
1458 ioclose: Option<xmlInputCloseCallback>,
1459 ioctx: *mut c_void,
1460 enc: c_int,
1461) -> *mut _xmlParserCtxt {
1462 unsafe {
1463 let ctxt = xmlNewSAXParserCtxt(sax, user_data);
1464 if ctxt.is_null() {
1465 return ptr::null_mut();
1466 }
1467 let input = helpers::input_from_io(ioread, ioclose, ioctx);
1468 helpers::setup_parser_input(ctxt, input);
1469 if enc != xmlCharEncoding::XML_CHAR_ENCODING_NONE as c_int {
1470 xmlSwitchEncoding(ctxt, enc);
1471 }
1472 ctxt
1473 }
1474}
1475
1476/// Create a parser context for a file or URL.
1477///
1478/// # UPSTREAM-PARITY
1479///
1480/// ```c
1481/// xmlParserCtxtPtr xmlCreateURLParserCtxt(const char *filename, int options);
1482/// ```
1483///
1484/// # SAFETY
1485///
1486///
1487/// - `filename` must point to valid NUL-terminated
1488/// strings (or NULL where the C contract allows) for the lifetime
1489/// of the call.
1490///
1491/// The caller must not race this call with concurrent mutation of the
1492/// same objects from other threads (per-object state is not internally
1493/// synchronized). Violating any of the above is undefined behavior.
1494///
1495/// Exercised by the C-API differential courts
1496/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1497/// courts; those pass byte-for-byte against the upstream oracle.
1498#[no_mangle]
1499pub unsafe extern "C" fn xmlCreateURLParserCtxt(
1500 filename: *const c_char,
1501 options: c_int,
1502) -> *mut _xmlParserCtxt {
1503 if filename.is_null() {
1504 return ptr::null_mut();
1505 }
1506 unsafe {
1507 let ctxt = xmlNewParserCtxt();
1508 if ctxt.is_null() {
1509 return ptr::null_mut();
1510 }
1511 apply_options(ctxt, options);
1512 let input = match open_filename_routed(filename, ctxt) {
1513 RoutedFileOpen::Loaded(i) => i,
1514 RoutedFileOpen::Failed => {
1515 // UPSTREAM-PARITY (parserInternals.c xmlNewInputFromFile via
1516 // the registered loader): NULL loader result is XML_IO_ENOENT
1517 // — raise xmlCtxtErrIO, no built-in fallback.
1518 emit_io_warning(ctxt, io_load_failure_message(filename));
1519 helpers::free_parser_ctxt(ctxt);
1520 return ptr::null_mut();
1521 }
1522 RoutedFileOpen::EntityLoaderFailed => {
1523 // UPSTREAM-PARITY (parser.c xmlCreateURLParserCtxt ->
1524 // xmlLoadResource): a custom entity loader returning NULL
1525 // fails the open SILENTLY.
1526 helpers::free_parser_ctxt(ctxt);
1527 return ptr::null_mut();
1528 }
1529 RoutedFileOpen::Builtin => match helpers::input_from_file(filename) {
1530 Ok(i) => i,
1531 Err(_) => {
1532 helpers::free_parser_ctxt(ctxt);
1533 return ptr::null_mut();
1534 }
1535 },
1536 };
1537 helpers::setup_parser_input(ctxt, input);
1538 ctxt
1539 }
1540}
1541
1542/// Create a parser context for an external entity.
1543///
1544/// # UPSTREAM-PARITY
1545///
1546/// ```c
1547/// xmlParserCtxtPtr xmlCreateEntityParserCtxt(const xmlChar *URL,
1548/// const xmlChar *ID,
1549/// const xmlChar *base);
1550/// ```
1551///
1552/// # SAFETY
1553///
1554///
1555/// - `URL`, `ID`, `base` must point to valid NUL-terminated
1556/// strings (or NULL where the C contract allows) for the lifetime
1557/// of the call.
1558///
1559/// The caller must not race this call with concurrent mutation of the
1560/// same objects from other threads (per-object state is not internally
1561/// synchronized). Violating any of the above is undefined behavior.
1562///
1563/// Exercised by the C-API differential courts
1564/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1565/// courts; those pass byte-for-byte against the upstream oracle.
1566#[no_mangle]
1567pub unsafe extern "C" fn xmlCreateEntityParserCtxt(
1568 URL: *const xmlChar,
1569 ID: *const xmlChar,
1570 base: *const xmlChar,
1571) -> *mut _xmlParserCtxt {
1572 let _ = base; // base URI resolution is a no-op here
1573 unsafe {
1574 let ctxt = xmlNewParserCtxt();
1575 if ctxt.is_null() {
1576 return ptr::null_mut();
1577 }
1578 let input = xmlLoadExternalEntity(URL as *const c_char, ID as *const c_char, ctxt);
1579 if input.is_null() {
1580 helpers::free_parser_ctxt(ctxt);
1581 return ptr::null_mut();
1582 }
1583 if xmlPushInput(ctxt, input) < 0 {
1584 helpers::free_parser_input(input);
1585 helpers::free_parser_ctxt(ctxt);
1586 return ptr::null_mut();
1587 }
1588 ctxt
1589 }
1590}
1591
1592// ═══════════════════════════════════════════════════════════════════════════════
1593// CtxtRead family
1594// ═══════════════════════════════════════════════════════════════════════════════
1595
1596/// Parse an XML in-memory document with a given context.
1597///
1598/// # UPSTREAM-PARITY
1599///
1600/// ```c
1601/// xmlDocPtr xmlCtxtReadDoc(xmlParserCtxtPtr ctxt, const xmlChar *cur,
1602/// const char *URL, const char *encoding, int options);
1603/// ```
1604///
1605/// # SAFETY
1606///
1607/// - `ctxt` must be valid pointers (or NULL
1608/// where the upstream C contract allows), obtained from the
1609/// matching constructor/owner and not yet freed; the callee may
1610/// take or keep ownership exactly as the C API specifies.
1611///
1612/// - `cur`, `URL`, `_encoding` must point to valid NUL-terminated
1613/// strings (or NULL where the C contract allows) for the lifetime
1614/// of the call.
1615///
1616/// The caller must not race this call with concurrent mutation of the
1617/// same objects from other threads (per-object state is not internally
1618/// synchronized). Violating any of the above is undefined behavior.
1619///
1620/// Exercised by the C-API differential courts
1621/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1622/// courts; those pass byte-for-byte against the upstream oracle.
1623#[no_mangle]
1624pub unsafe extern "C" fn xmlCtxtReadDoc(
1625 ctxt: *mut _xmlParserCtxt,
1626 cur: *const xmlChar,
1627 URL: *const c_char,
1628 encoding: *const c_char,
1629 options: c_int,
1630) -> *mut _xmlDoc {
1631 if ctxt.is_null() || cur.is_null() {
1632 return ptr::null_mut();
1633 }
1634 unsafe {
1635 let len = string::xml_strlen(cur);
1636 let mut input = helpers::input_from_memory(cur as *const c_char, len as c_int);
1637 if !encoding.is_null() {
1638 let name = core::ffi::CStr::from_ptr(encoding).to_bytes();
1639 input.apply_explicit_input_encoding(name);
1640 }
1641 ctxt_read_doc(ctxt, input, URL, options)
1642 }
1643}
1644
1645/// Parse an XML file with a given context.
1646///
1647/// # UPSTREAM-PARITY
1648///
1649/// ```c
1650/// xmlDocPtr xmlCtxtReadFile(xmlParserCtxtPtr ctxt, const char *filename,
1651/// const char *encoding, int options);
1652/// ```
1653///
1654/// # SAFETY
1655///
1656/// - `ctxt` must be valid pointers (or NULL
1657/// where the upstream C contract allows), obtained from the
1658/// matching constructor/owner and not yet freed; the callee may
1659/// take or keep ownership exactly as the C API specifies.
1660///
1661/// - `filename`, `_encoding` must point to valid NUL-terminated
1662/// strings (or NULL where the C contract allows) for the lifetime
1663/// of the call.
1664///
1665/// The caller must not race this call with concurrent mutation of the
1666/// same objects from other threads (per-object state is not internally
1667/// synchronized). Violating any of the above is undefined behavior.
1668///
1669/// Exercised by the C-API differential courts
1670/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1671/// courts; those pass byte-for-byte against the upstream oracle.
1672#[no_mangle]
1673pub unsafe extern "C" fn xmlCtxtReadFile(
1674 ctxt: *mut _xmlParserCtxt,
1675 filename: *const c_char,
1676 _encoding: *const c_char,
1677 options: c_int,
1678) -> *mut _xmlDoc {
1679 if ctxt.is_null() || filename.is_null() {
1680 return ptr::null_mut();
1681 }
1682 unsafe {
1683 // UPSTREAM-PARITY (parser.c xmlCtxtReadFile -> xmlCtxtNewInputFromUrl
1684 // -> xmlLoadResource): a registered external entity loader governs the
1685 // open; below it the xmlParserInputBufferCreateFilenameDefault (php
1686 // streams loader) is consulted; a NULL loader result raises
1687 // xmlCtxtErrIO(XML_IO_ENOENT, filename) — "I/O warning : failed to
1688 // load \"%s\": %s\n" — and parsing fails.
1689 match open_filename_routed(filename, ctxt) {
1690 RoutedFileOpen::Loaded(input) => ctxt_read_doc(ctxt, input, filename, options),
1691 RoutedFileOpen::Failed => {
1692 emit_io_warning(ctxt, io_load_failure_message(filename));
1693 ptr::null_mut()
1694 }
1695 RoutedFileOpen::EntityLoaderFailed => {
1696 // UPSTREAM-PARITY (parser.c xmlCtxtReadFile): a custom entity
1697 // loader returning NULL fails the read SILENTLY.
1698 ptr::null_mut()
1699 }
1700 RoutedFileOpen::Builtin => match helpers::input_from_file(filename) {
1701 Ok(input) => ctxt_read_doc(ctxt, input, filename, options),
1702 Err(_) => ptr::null_mut(),
1703 },
1704 }
1705 }
1706}
1707
1708/// Parse an XML in-memory block with a given context.
1709///
1710/// # UPSTREAM-PARITY
1711///
1712/// ```c
1713/// xmlDocPtr xmlCtxtReadMemory(xmlParserCtxtPtr ctxt, const char *buffer,
1714/// int size, const char *URL, const char *encoding,
1715/// int options);
1716/// ```
1717///
1718/// # SAFETY
1719///
1720/// - `ctxt` must be valid pointers (or NULL
1721/// where the upstream C contract allows), obtained from the
1722/// matching constructor/owner and not yet freed; the callee may
1723/// take or keep ownership exactly as the C API specifies.
1724///
1725/// - `buffer`, `URL`, `_encoding` must point to valid NUL-terminated
1726/// strings (or NULL where the C contract allows) for the lifetime
1727/// of the call.
1728///
1729/// The caller must not race this call with concurrent mutation of the
1730/// same objects from other threads (per-object state is not internally
1731/// synchronized). Violating any of the above is undefined behavior.
1732///
1733/// Exercised by the C-API differential courts
1734/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1735/// courts; those pass byte-for-byte against the upstream oracle.
1736#[no_mangle]
1737pub unsafe extern "C" fn xmlCtxtReadMemory(
1738 ctxt: *mut _xmlParserCtxt,
1739 buffer: *const c_char,
1740 size: c_int,
1741 URL: *const c_char,
1742 encoding: *const c_char,
1743 options: c_int,
1744) -> *mut _xmlDoc {
1745 if ctxt.is_null() || buffer.is_null() || size < 0 {
1746 return ptr::null_mut();
1747 }
1748 unsafe {
1749 let mut input = helpers::input_from_memory(buffer, size);
1750 // UPSTREAM-PARITY (xmlCtxtNewInputFromMemory): an explicit `encoding`
1751 // argument switches the input to that encoding BEFORE the parse, so
1752 // the parser never sees raw UCS-2/UCS-4/Latin-1 bytes (lxml feeds
1753 // PEP-393 python strings this way). Best-effort: when the name is
1754 // unknown or the buffer was already converted the raw bytes stay and
1755 // BOM/declaration detection decides as usual.
1756 if !encoding.is_null() {
1757 let name = core::ffi::CStr::from_ptr(encoding).to_bytes();
1758 input.apply_explicit_input_encoding(name);
1759 }
1760 ctxt_read_doc(ctxt, input, URL, options)
1761 }
1762}
1763
1764/// Parse an XML document from a file descriptor with a given context.
1765///
1766/// # UPSTREAM-PARITY
1767///
1768/// ```c
1769/// xmlDocPtr xmlCtxtReadFd(xmlParserCtxtPtr ctxt, int fd, const char *URL,
1770/// const char *encoding, int options);
1771/// ```
1772///
1773/// # SAFETY
1774///
1775/// - `ctxt` must be valid pointers (or NULL
1776/// where the upstream C contract allows), obtained from the
1777/// matching constructor/owner and not yet freed; the callee may
1778/// take or keep ownership exactly as the C API specifies.
1779///
1780/// - `URL`, `_encoding` must point to valid NUL-terminated
1781/// strings (or NULL where the C contract allows) for the lifetime
1782/// of the call.
1783///
1784/// The caller must not race this call with concurrent mutation of the
1785/// same objects from other threads (per-object state is not internally
1786/// synchronized). Violating any of the above is undefined behavior.
1787///
1788/// Exercised by the C-API differential courts
1789/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1790/// courts; those pass byte-for-byte against the upstream oracle.
1791#[no_mangle]
1792pub unsafe extern "C" fn xmlCtxtReadFd(
1793 ctxt: *mut _xmlParserCtxt,
1794 fd: c_int,
1795 URL: *const c_char,
1796 _encoding: *const c_char,
1797 options: c_int,
1798) -> *mut _xmlDoc {
1799 if ctxt.is_null() || fd < 0 {
1800 return ptr::null_mut();
1801 }
1802 unsafe {
1803 let mut buf = Vec::new();
1804 let mut tmp = [0u8; 4096];
1805 loop {
1806 let n = libc::read(fd, tmp.as_mut_ptr() as *mut c_void, tmp.len());
1807 if n <= 0 {
1808 break;
1809 }
1810 buf.extend_from_slice(&tmp[..n as usize]);
1811 }
1812 let input = helpers::input_from_memory(buf.as_ptr() as *const c_char, buf.len() as c_int);
1813 ctxt_read_doc(ctxt, input, URL, options)
1814 }
1815}
1816
1817/// Parse an XML document from I/O callbacks with a given context.
1818///
1819/// # UPSTREAM-PARITY
1820///
1821/// ```c
1822/// xmlDocPtr xmlCtxtReadIO(xmlParserCtxtPtr ctxt, xmlInputReadCallback ioread,
1823/// xmlInputCloseCallback ioclose, void *ioctx,
1824/// const char *URL, const char *encoding, int options);
1825/// ```
1826///
1827/// # SAFETY
1828///
1829/// - `ctxt`, `ioctx` must be valid pointers (or NULL
1830/// where the upstream C contract allows), obtained from the
1831/// matching constructor/owner and not yet freed; the callee may
1832/// take or keep ownership exactly as the C API specifies.
1833///
1834/// - `URL`, `_encoding` must point to valid NUL-terminated
1835/// strings (or NULL where the C contract allows) for the lifetime
1836/// of the call.
1837///
1838/// - `ioread`, `ioclose` must be a valid callback (or None);
1839/// the callback is invoked with the documented context pointer and
1840/// must itself uphold the same pointer invariants.
1841///
1842/// The caller must not race this call with concurrent mutation of the
1843/// same objects from other threads (per-object state is not internally
1844/// synchronized). Violating any of the above is undefined behavior.
1845///
1846/// Exercised by the C-API differential courts
1847/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1848/// courts; those pass byte-for-byte against the upstream oracle.
1849#[no_mangle]
1850pub unsafe extern "C" fn xmlCtxtReadIO(
1851 ctxt: *mut _xmlParserCtxt,
1852 ioread: Option<xmlInputReadCallback>,
1853 ioclose: Option<xmlInputCloseCallback>,
1854 ioctx: *mut c_void,
1855 URL: *const c_char,
1856 _encoding: *const c_char,
1857 options: c_int,
1858) -> *mut _xmlDoc {
1859 if ctxt.is_null() {
1860 return ptr::null_mut();
1861 }
1862 unsafe {
1863 let input = helpers::input_from_io(ioread, ioclose, ioctx);
1864 // UPSTREAM-PARITY (parser.c xmlCtxtNewInputFromIO): the URL becomes
1865 // the input's filename, which feeds the `file:line:` error prefix.
1866 let input = if !URL.is_null() {
1867 match std::ffi::CStr::from_ptr(URL).to_str() {
1868 Ok(s) => input.with_filename(s),
1869 Err(_) => input,
1870 }
1871 } else {
1872 input
1873 };
1874 ctxt_read_doc(ctxt, input, URL, options)
1875 }
1876}
1877
1878/// Parse a document from a raw parser input, taking ownership of `input`.
1879///
1880/// # UPSTREAM-PARITY
1881///
1882/// ```c
1883/// xmlDocPtr xmlCtxtParseDocument(xmlParserCtxtPtr ctxt, xmlParserInputPtr input);
1884/// ```
1885///
1886/// # SAFETY
1887///
1888/// - `ctxt`, `input` must be valid pointers (or NULL
1889/// where the upstream C contract allows), obtained from the
1890/// matching constructor/owner and not yet freed; the callee may
1891/// take or keep ownership exactly as the C API specifies.
1892///
1893/// The caller must not race this call with concurrent mutation of the
1894/// same objects from other threads (per-object state is not internally
1895/// synchronized). Violating any of the above is undefined behavior.
1896///
1897/// Exercised by the C-API differential courts
1898/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1899/// courts; those pass byte-for-byte against the upstream oracle.
1900#[no_mangle]
1901pub unsafe extern "C" fn xmlCtxtParseDocument(
1902 ctxt: *mut _xmlParserCtxt,
1903 input: *mut _xmlParserInput,
1904) -> *mut _xmlDoc {
1905 if ctxt.is_null() || input.is_null() {
1906 return ptr::null_mut();
1907 }
1908 unsafe {
1909 // Determine whether the caller's input is already owned by the
1910 // context's input stack (pushed via xmlPushInput).
1911 let mut owned = false;
1912 let nr = (*ctxt).inputNr;
1913 let tab = (*ctxt).inputTab;
1914 if !tab.is_null() {
1915 for i in 0..nr {
1916 if *tab.add(i as usize) == input {
1917 owned = true;
1918 break;
1919 }
1920 }
1921 }
1922 if (*ctxt).input == input {
1923 owned = true;
1924 }
1925
1926 // Copy the data first so the context reset cannot invalidate it.
1927 let ib = input_buffer_from_parser_input(input);
1928
1929 xmlCtxtReset(ctxt);
1930 helpers::setup_parser_input(ctxt, ib);
1931 helpers::parse_document(ctxt);
1932
1933 if !owned {
1934 helpers::free_parser_input(input);
1935 }
1936
1937 (*ctxt).myDoc
1938 }
1939}
1940
1941// ═══════════════════════════════════════════════════════════════════════════════
1942// Parser input buffers / streams
1943// ═══════════════════════════════════════════════════════════════════════════════
1944
1945/// Allocate a parser input buffer for the given encoding.
1946///
1947/// # UPSTREAM-PARITY
1948///
1949/// ```c
1950/// xmlParserInputBufferPtr xmlAllocParserInputBuffer(xmlCharEncoding enc);
1951/// ```
1952///
1953/// # SAFETY
1954///
1955/// The function touches crate-global state only; it is safe
1956/// as long as the caller respects the library's global
1957/// initialization/cleanup ordering (xmlInitParser before use,
1958/// xmlCleanupParser only after all users are done).
1959///
1960/// Violating the global lifecycle ordering, or calling this after
1961/// teardown or from a signal handler, is undefined behavior.
1962#[no_mangle]
1963pub unsafe extern "C" fn xmlAllocParserInputBuffer(enc: c_int) -> *mut _xmlParserInputBuffer {
1964 unsafe {
1965 let buf = xmlMallocZero(core::mem::size_of::<_xmlParserInputBuffer>())
1966 as *mut _xmlParserInputBuffer;
1967 if buf.is_null() {
1968 return ptr::null_mut();
1969 }
1970 let b = &mut *buf;
1971 b.buffer = io::buf_create(crate::abi::data_globals::xmlDefaultBufferSize) as *mut c_void;
1972 b.raw = io::buf_create(crate::abi::data_globals::xmlDefaultBufferSize) as *mut c_void;
1973 if b.buffer.is_null() || b.raw.is_null() {
1974 io::buf_free(b.buffer as *mut _xmlBuffer);
1975 io::buf_free(b.raw as *mut _xmlBuffer);
1976 xmlFreeImpl(buf as *mut c_void);
1977 return ptr::null_mut();
1978 }
1979 b.compressed = -1;
1980
1981 if enc != xmlCharEncoding::XML_CHAR_ENCODING_NONE as c_int
1982 && enc != xmlCharEncoding::XML_CHAR_ENCODING_ERROR as c_int
1983 {
1984 let handler = encoding_handler_for(enc);
1985 if !handler.is_null() {
1986 b.encoder = handler as *mut c_void;
1987 }
1988 }
1989 buf
1990 }
1991}
1992
1993/// Grow an input buffer by reading up to `len` bytes from its source.
1994///
1995/// # UPSTREAM-PARITY
1996///
1997/// ```c
1998/// int xmlParserInputBufferGrow(xmlParserInputBufferPtr in, int len);
1999/// ```
2000///
2001/// # SAFETY
2002///
2003/// - `in_` must be valid pointers (or NULL
2004/// where the upstream C contract allows), obtained from the
2005/// matching constructor/owner and not yet freed; the callee may
2006/// take or keep ownership exactly as the C API specifies.
2007///
2008/// The caller must not race this call with concurrent mutation of the
2009/// same objects from other threads (per-object state is not internally
2010/// synchronized). Violating any of the above is undefined behavior.
2011///
2012/// Exercised by the C-API differential courts
2013/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2014/// courts; those pass byte-for-byte against the upstream oracle.
2015#[no_mangle]
2016pub unsafe extern "C" fn xmlParserInputBufferGrow(
2017 in_: *mut _xmlParserInputBuffer,
2018 len: c_int,
2019) -> c_int {
2020 if in_.is_null() || len <= 0 {
2021 return 0;
2022 }
2023 unsafe {
2024 let b = &mut *in_;
2025 if b.error != 0 {
2026 return -1;
2027 }
2028 let Some(read_cb) = b.readcallback else {
2029 // Memory-based buffer: nothing to grow.
2030 return 0;
2031 };
2032 let mut tmp = vec![0u8; len as usize];
2033 let n = read_cb(b.context, tmp.as_mut_ptr() as *mut c_char, len);
2034 if n < 0 {
2035 b.error = 1;
2036 return -1;
2037 }
2038 if n == 0 {
2039 return 0;
2040 }
2041 io::input_buffer_push(in_, tmp.as_ptr() as *const c_char, n);
2042 n
2043 }
2044}
2045
2046/// Push `len` bytes into an input buffer (push parser).
2047///
2048/// # UPSTREAM-PARITY
2049///
2050/// ```c
2051/// int xmlParserInputBufferPush(xmlParserInputBufferPtr in, int len, const char *buf);
2052/// ```
2053///
2054/// # SAFETY
2055///
2056/// - `in_` must be valid pointers (or NULL
2057/// where the upstream C contract allows), obtained from the
2058/// matching constructor/owner and not yet freed; the callee may
2059/// take or keep ownership exactly as the C API specifies.
2060///
2061/// - `buf` must point to valid NUL-terminated
2062/// strings (or NULL where the C contract allows) for the lifetime
2063/// of the call.
2064///
2065/// The caller must not race this call with concurrent mutation of the
2066/// same objects from other threads (per-object state is not internally
2067/// synchronized). Violating any of the above is undefined behavior.
2068///
2069/// Exercised by the C-API differential courts
2070/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2071/// courts; those pass byte-for-byte against the upstream oracle.
2072#[no_mangle]
2073pub unsafe extern "C" fn xmlParserInputBufferPush(
2074 in_: *mut _xmlParserInputBuffer,
2075 len: c_int,
2076 buf: *const c_char,
2077) -> c_int {
2078 if in_.is_null() {
2079 return -1;
2080 }
2081 if len < 0 || (len > 0 && buf.is_null()) {
2082 return -1;
2083 }
2084 if len == 0 {
2085 return 0;
2086 }
2087 io::input_buffer_push(in_, buf, len)
2088}
2089
2090/// Read up to `len` bytes from an input buffer's source.
2091///
2092/// # UPSTREAM-PARITY
2093///
2094/// ```c
2095/// int xmlParserInputBufferRead(xmlParserInputBufferPtr in, int len);
2096/// ```
2097///
2098/// # SAFETY
2099///
2100/// - `in_` must be valid pointers (or NULL
2101/// where the upstream C contract allows), obtained from the
2102/// matching constructor/owner and not yet freed; the callee may
2103/// take or keep ownership exactly as the C API specifies.
2104///
2105/// The caller must not race this call with concurrent mutation of the
2106/// same objects from other threads (per-object state is not internally
2107/// synchronized). Violating any of the above is undefined behavior.
2108///
2109/// Exercised by the C-API differential courts
2110/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2111/// courts; those pass byte-for-byte against the upstream oracle.
2112#[no_mangle]
2113pub unsafe extern "C" fn xmlParserInputBufferRead(
2114 in_: *mut _xmlParserInputBuffer,
2115 len: c_int,
2116) -> c_int {
2117 xmlParserInputBufferGrow(in_, len)
2118}
2119
2120/// Deprecated: reading directly from an input stream is an error.
2121///
2122/// # UPSTREAM-PARITY
2123///
2124/// ```c
2125/// int xmlParserInputRead(xmlParserInputPtr in, int len);
2126/// ```
2127///
2128/// # SAFETY
2129///
2130/// - `_in_` must be valid pointers (or NULL
2131/// where the upstream C contract allows), obtained from the
2132/// matching constructor/owner and not yet freed; the callee may
2133/// take or keep ownership exactly as the C API specifies.
2134///
2135/// The caller must not race this call with concurrent mutation of the
2136/// same objects from other threads (per-object state is not internally
2137/// synchronized). Violating any of the above is undefined behavior.
2138///
2139/// Exercised by the C-API differential courts
2140/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2141/// courts; those pass byte-for-byte against the upstream oracle.
2142#[no_mangle]
2143pub const unsafe extern "C" fn xmlParserInputRead(
2144 _in_: *mut _xmlParserInput,
2145 _len: c_int,
2146) -> c_int {
2147 -1
2148}
2149
2150/// Grow a parser input's buffer by reading more data from its source.
2151///
2152/// # UPSTREAM-PARITY
2153///
2154/// ```c
2155/// int xmlParserInputGrow(xmlParserInputPtr in, int len);
2156/// ```
2157///
2158/// # SAFETY
2159///
2160/// - `in_` must be valid pointers (or NULL
2161/// where the upstream C contract allows), obtained from the
2162/// matching constructor/owner and not yet freed; the callee may
2163/// take or keep ownership exactly as the C API specifies.
2164///
2165/// The caller must not race this call with concurrent mutation of the
2166/// same objects from other threads (per-object state is not internally
2167/// synchronized). Violating any of the above is undefined behavior.
2168///
2169/// Exercised by the C-API differential courts
2170/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2171/// courts; those pass byte-for-byte against the upstream oracle.
2172#[no_mangle]
2173pub unsafe extern "C" fn xmlParserInputGrow(in_: *mut _xmlParserInput, len: c_int) -> c_int {
2174 if in_.is_null() || len < 0 {
2175 return -1;
2176 }
2177 unsafe {
2178 let pi = &*in_;
2179 if pi.base.is_null() || pi.cur.is_null() {
2180 return -1;
2181 }
2182 if pi.buf.is_null() {
2183 // Pure memory input: nothing to grow.
2184 return 0;
2185 }
2186 let b = &*pi.buf;
2187 // Memory buffers are not growable.
2188 if b.readcallback.is_none() && b.encoder.is_null() {
2189 return 0;
2190 }
2191 xmlParserInputBufferGrow(pi.buf, len)
2192 }
2193}
2194
2195/// Shrink a parser input, releasing already-consumed data from the buffer.
2196///
2197/// # UPSTREAM-PARITY
2198///
2199/// ```c
2200/// void xmlParserInputShrink(xmlParserInputPtr in);
2201/// ```
2202///
2203/// # SAFETY
2204///
2205/// - `in_` must be valid pointers (or NULL
2206/// where the upstream C contract allows), obtained from the
2207/// matching constructor/owner and not yet freed; the callee may
2208/// take or keep ownership exactly as the C API specifies.
2209///
2210/// The caller must not race this call with concurrent mutation of the
2211/// same objects from other threads (per-object state is not internally
2212/// synchronized). Violating any of the above is undefined behavior.
2213///
2214/// Exercised by the C-API differential courts
2215/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2216/// courts; those pass byte-for-byte against the upstream oracle.
2217#[no_mangle]
2218pub unsafe extern "C" fn xmlParserInputShrink(in_: *mut _xmlParserInput) {
2219 if in_.is_null() {
2220 return;
2221 }
2222 unsafe {
2223 let pi = &mut *in_;
2224 if pi.buf.is_null() || pi.base.is_null() || pi.cur.is_null() {
2225 return;
2226 }
2227 let used = (pi.cur as usize).saturating_sub(pi.base as usize);
2228 if used > LINE_LEN {
2229 // The candidate's inputs are backed by stable memory buffers, so
2230 // the base pointer cannot move; account for the consumed bytes.
2231 pi.consumed = pi.consumed.saturating_add((used - LINE_LEN) as c_ulong);
2232 }
2233 }
2234}
2235
2236/// Create a new (empty) parser input stream.
2237///
2238/// # UPSTREAM-PARITY
2239///
2240/// ```c
2241/// xmlParserInputPtr xmlNewInputStream(xmlParserCtxtPtr ctxt);
2242/// ```
2243///
2244/// # SAFETY
2245///
2246/// - `ctxt` must be valid pointers (or NULL
2247/// where the upstream C contract allows), obtained from the
2248/// matching constructor/owner and not yet freed; the callee may
2249/// take or keep ownership exactly as the C API specifies.
2250///
2251/// The caller must not race this call with concurrent mutation of the
2252/// same objects from other threads (per-object state is not internally
2253/// synchronized). Violating any of the above is undefined behavior.
2254///
2255/// Exercised by the C-API differential courts
2256/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2257/// courts; those pass byte-for-byte against the upstream oracle.
2258#[no_mangle]
2259pub unsafe extern "C" fn xmlNewInputStream(ctxt: *mut _xmlParserCtxt) -> *mut _xmlParserInput {
2260 unsafe {
2261 let input = xmlMallocZero(core::mem::size_of::<_xmlParserInput>()) as *mut _xmlParserInput;
2262 if input.is_null() {
2263 if !ctxt.is_null() {
2264 xmlCtxtErrMemory(ctxt);
2265 }
2266 return ptr::null_mut();
2267 }
2268 (*input).line = 1;
2269 (*input).col = 1;
2270 input
2271 }
2272}
2273
2274/// Wrap an input buffer in a parser input stream.
2275///
2276/// # UPSTREAM-PARITY
2277///
2278/// ```c
2279/// xmlParserInputPtr xmlNewIOInputStream(xmlParserCtxtPtr ctxt,
2280/// xmlParserInputBufferPtr input,
2281/// xmlCharEncoding enc);
2282/// ```
2283///
2284/// # SAFETY
2285///
2286/// - `ctxt`, `input` must be valid pointers (or NULL
2287/// where the upstream C contract allows), obtained from the
2288/// matching constructor/owner and not yet freed; the callee may
2289/// take or keep ownership exactly as the C API specifies.
2290///
2291/// The caller must not race this call with concurrent mutation of the
2292/// same objects from other threads (per-object state is not internally
2293/// synchronized). Violating any of the above is undefined behavior.
2294///
2295/// Exercised by the C-API differential courts
2296/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2297/// courts; those pass byte-for-byte against the upstream oracle.
2298#[no_mangle]
2299pub unsafe extern "C" fn xmlNewIOInputStream(
2300 ctxt: *mut _xmlParserCtxt,
2301 input: *mut _xmlParserInputBuffer,
2302 enc: c_int,
2303) -> *mut _xmlParserInput {
2304 if ctxt.is_null() || input.is_null() {
2305 return ptr::null_mut();
2306 }
2307 unsafe {
2308 let pi = xmlNewInputStream(ctxt);
2309 if pi.is_null() {
2310 return ptr::null_mut();
2311 }
2312 (*pi).buf = input;
2313 if enc != xmlCharEncoding::XML_CHAR_ENCODING_NONE as c_int
2314 && enc != xmlCharEncoding::XML_CHAR_ENCODING_ERROR as c_int
2315 {
2316 let handler = encoding_handler_for(enc);
2317 if !handler.is_null() {
2318 io::input_buffer_set_encoder(input, handler);
2319 }
2320 }
2321 pi
2322 }
2323}
2324
2325/// Create a parser input stream from a zero-terminated string. The string
2326/// must remain valid for the lifetime of the input (static mode).
2327///
2328/// # UPSTREAM-PARITY
2329///
2330/// ```c
2331/// xmlParserInputPtr xmlNewStringInputStream(xmlParserCtxtPtr ctxt,
2332/// const xmlChar *buffer);
2333/// ```
2334///
2335/// # SAFETY
2336///
2337/// - `ctxt` must be valid pointers (or NULL
2338/// where the upstream C contract allows), obtained from the
2339/// matching constructor/owner and not yet freed; the callee may
2340/// take or keep ownership exactly as the C API specifies.
2341///
2342/// - `buffer` must point to valid NUL-terminated
2343/// strings (or NULL where the C contract allows) for the lifetime
2344/// of the call.
2345///
2346/// The caller must not race this call with concurrent mutation of the
2347/// same objects from other threads (per-object state is not internally
2348/// synchronized). Violating any of the above is undefined behavior.
2349///
2350/// Exercised by the C-API differential courts
2351/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2352/// courts; those pass byte-for-byte against the upstream oracle.
2353#[no_mangle]
2354pub unsafe extern "C" fn xmlNewStringInputStream(
2355 ctxt: *mut _xmlParserCtxt,
2356 buffer: *const xmlChar,
2357) -> *mut _xmlParserInput {
2358 if ctxt.is_null() || buffer.is_null() {
2359 return ptr::null_mut();
2360 }
2361 unsafe {
2362 let input = xmlNewInputStream(ctxt);
2363 if input.is_null() {
2364 return ptr::null_mut();
2365 }
2366 let len = string::xml_strlen(buffer);
2367 (*input).base = buffer;
2368 (*input).cur = buffer;
2369 (*input).end = buffer.add(len);
2370 (*input).length = len as c_int;
2371 input
2372 }
2373}
2374
2375/// Setup the parser context to parse a new buffer (legacy API).
2376///
2377/// # UPSTREAM-PARITY
2378///
2379/// ```c
2380/// void xmlSetupParserForBuffer(xmlParserCtxtPtr ctxt, const xmlChar* buffer,
2381/// const char *filename);
2382/// ```
2383///
2384/// # SAFETY
2385///
2386/// - `ctxt` must be valid pointers (or NULL
2387/// where the upstream C contract allows), obtained from the
2388/// matching constructor/owner and not yet freed; the callee may
2389/// take or keep ownership exactly as the C API specifies.
2390///
2391/// - `buffer`, `filename` must point to valid NUL-terminated
2392/// strings (or NULL where the C contract allows) for the lifetime
2393/// of the call.
2394///
2395/// The caller must not race this call with concurrent mutation of the
2396/// same objects from other threads (per-object state is not internally
2397/// synchronized). Violating any of the above is undefined behavior.
2398///
2399/// Exercised by the C-API differential courts
2400/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2401/// courts; those pass byte-for-byte against the upstream oracle.
2402#[no_mangle]
2403pub unsafe extern "C" fn xmlSetupParserForBuffer(
2404 ctxt: *mut _xmlParserCtxt,
2405 buffer: *const xmlChar,
2406 filename: *const c_char,
2407) {
2408 if ctxt.is_null() || buffer.is_null() {
2409 return;
2410 }
2411 unsafe {
2412 xmlCtxtReset(ctxt);
2413 let len = string::xml_strlen(buffer);
2414 let uri = if filename.is_null() {
2415 None
2416 } else {
2417 CStr::from_ptr(filename).to_str().ok()
2418 };
2419 let input = InputBuffer::from_memory(core::slice::from_raw_parts(buffer, len), uri);
2420 helpers::setup_parser_input(ctxt, input);
2421 }
2422}
2423
2424/// Push an input stream onto the context's input stack.
2425///
2426/// # UPSTREAM-PARITY
2427///
2428/// ```c
2429/// int xmlPushInput(xmlParserCtxtPtr ctxt, xmlParserInputPtr input);
2430/// ```
2431///
2432/// # SAFETY
2433///
2434/// - `ctxt`, `input` must be valid pointers (or NULL
2435/// where the upstream C contract allows), obtained from the
2436/// matching constructor/owner and not yet freed; the callee may
2437/// take or keep ownership exactly as the C API specifies.
2438///
2439/// The caller must not race this call with concurrent mutation of the
2440/// same objects from other threads (per-object state is not internally
2441/// synchronized). Violating any of the above is undefined behavior.
2442///
2443/// Exercised by the C-API differential courts
2444/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2445/// courts; those pass byte-for-byte against the upstream oracle.
2446#[no_mangle]
2447pub unsafe extern "C" fn xmlPushInput(
2448 ctxt: *mut _xmlParserCtxt,
2449 input: *mut _xmlParserInput,
2450) -> c_int {
2451 if ctxt.is_null() || input.is_null() {
2452 return -1;
2453 }
2454 unsafe {
2455 let c = &mut *ctxt;
2456 if c.inputNr >= c.inputMax {
2457 let new_max = if c.inputMax == 0 { 5 } else { c.inputMax * 2 };
2458 let new_tab = xmlReallocImpl(
2459 c.inputTab as *mut c_void,
2460 (new_max as usize) * core::mem::size_of::<*mut _xmlParserInput>(),
2461 ) as *mut *mut _xmlParserInput;
2462 if new_tab.is_null() {
2463 return -1;
2464 }
2465 c.inputTab = new_tab;
2466 c.inputMax = new_max;
2467 }
2468 *c.inputTab.add(c.inputNr as usize) = input;
2469 c.input = input;
2470 (*input).id = c.input_id;
2471 c.input_id += 1;
2472 let idx = c.inputNr;
2473 c.inputNr += 1;
2474 idx
2475 }
2476}
2477
2478/// Pop the top input from the context's input stack and free it; returns the
2479/// current character after the pop (0 at end of input).
2480///
2481/// # UPSTREAM-PARITY
2482///
2483/// ```c
2484/// xmlChar xmlPopInput(xmlParserCtxtPtr ctxt);
2485/// ```
2486///
2487/// # SAFETY
2488///
2489/// - `ctxt` must be valid pointers (or NULL
2490/// where the upstream C contract allows), obtained from the
2491/// matching constructor/owner and not yet freed; the callee may
2492/// take or keep ownership exactly as the C API specifies.
2493///
2494/// The caller must not race this call with concurrent mutation of the
2495/// same objects from other threads (per-object state is not internally
2496/// synchronized). Violating any of the above is undefined behavior.
2497///
2498/// Exercised by the C-API differential courts
2499/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2500/// courts; those pass byte-for-byte against the upstream oracle.
2501#[no_mangle]
2502pub unsafe extern "C" fn xmlPopInput(ctxt: *mut _xmlParserCtxt) -> xmlChar {
2503 if ctxt.is_null() || (*ctxt).inputNr <= 1 {
2504 return 0;
2505 }
2506 unsafe {
2507 let c = &mut *ctxt;
2508 c.inputNr -= 1;
2509 let popped = *c.inputTab.add(c.inputNr as usize);
2510 *c.inputTab.add(c.inputNr as usize) = ptr::null_mut();
2511 if c.inputNr > 0 {
2512 c.input = *c.inputTab.add((c.inputNr - 1) as usize);
2513 } else {
2514 c.input = ptr::null_mut();
2515 }
2516 if !popped.is_null() {
2517 helpers::free_parser_input(popped);
2518 }
2519 if c.input.is_null() {
2520 return 0;
2521 }
2522 let cur = (*c.input).cur;
2523 let end = (*c.input).end;
2524 if cur.is_null() || cur >= end {
2525 0
2526 } else {
2527 *cur
2528 }
2529 }
2530}
2531
2532// ═══════════════════════════════════════════════════════════════════════════════
2533// Encoding switching
2534// ═══════════════════════════════════════════════════════════════════════════════
2535
2536/// Switch the input encoding of the current input.
2537///
2538/// # UPSTREAM-PARITY
2539///
2540/// ```c
2541/// int xmlSwitchEncoding(xmlParserCtxtPtr ctxt, xmlCharEncoding enc);
2542/// ```
2543///
2544/// # SAFETY
2545///
2546/// - `ctxt` must be valid pointers (or NULL
2547/// where the upstream C contract allows), obtained from the
2548/// matching constructor/owner and not yet freed; the callee may
2549/// take or keep ownership exactly as the C API specifies.
2550///
2551/// The caller must not race this call with concurrent mutation of the
2552/// same objects from other threads (per-object state is not internally
2553/// synchronized). Violating any of the above is undefined behavior.
2554///
2555/// Exercised by the C-API differential courts
2556/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2557/// courts; those pass byte-for-byte against the upstream oracle.
2558#[no_mangle]
2559pub unsafe extern "C" fn xmlSwitchEncoding(ctxt: *mut _xmlParserCtxt, enc: c_int) -> c_int {
2560 if ctxt.is_null() || (*ctxt).input.is_null() {
2561 return -1;
2562 }
2563 if enc == xmlCharEncoding::XML_CHAR_ENCODING_NONE as c_int {
2564 return 0;
2565 }
2566 unsafe {
2567 let handler = encoding_handler_for(enc);
2568 if handler.is_null() {
2569 return -1;
2570 }
2571 xmlSwitchToEncoding(ctxt, handler)
2572 }
2573}
2574
2575/// Switch the input encoding by name.
2576///
2577/// # UPSTREAM-PARITY
2578///
2579/// ```c
2580/// int xmlSwitchEncodingName(xmlParserCtxtPtr ctxt, const char *encoding);
2581/// ```
2582///
2583/// # SAFETY
2584///
2585/// - `ctxt` must be valid pointers (or NULL
2586/// where the upstream C contract allows), obtained from the
2587/// matching constructor/owner and not yet freed; the callee may
2588/// take or keep ownership exactly as the C API specifies.
2589///
2590/// - `encoding` must point to valid NUL-terminated
2591/// strings (or NULL where the C contract allows) for the lifetime
2592/// of the call.
2593///
2594/// The caller must not race this call with concurrent mutation of the
2595/// same objects from other threads (per-object state is not internally
2596/// synchronized). Violating any of the above is undefined behavior.
2597///
2598/// Exercised by the C-API differential courts
2599/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2600/// courts; those pass byte-for-byte against the upstream oracle.
2601#[no_mangle]
2602pub unsafe extern "C" fn xmlSwitchEncodingName(
2603 ctxt: *mut _xmlParserCtxt,
2604 encoding: *const c_char,
2605) -> c_int {
2606 if ctxt.is_null() || encoding.is_null() {
2607 return -1;
2608 }
2609 unsafe {
2610 let handler = encoding::find_encoding_handler(encoding as *const xmlChar);
2611 if handler.is_null() {
2612 return -1;
2613 }
2614 xmlSwitchToEncoding(ctxt, handler)
2615 }
2616}
2617
2618/// Switch the encoding of a specific parser input using an encoding handler.
2619///
2620/// # UPSTREAM-PARITY
2621///
2622/// ```c
2623/// int xmlSwitchInputEncoding(xmlParserCtxtPtr ctxt, xmlParserInputPtr input,
2624/// xmlCharEncodingHandlerPtr handler);
2625/// ```
2626///
2627/// # SAFETY
2628///
2629/// - `ctxt`, `input`, `handler` must be valid pointers (or NULL
2630/// where the upstream C contract allows), obtained from the
2631/// matching constructor/owner and not yet freed; the callee may
2632/// take or keep ownership exactly as the C API specifies.
2633///
2634/// The caller must not race this call with concurrent mutation of the
2635/// same objects from other threads (per-object state is not internally
2636/// synchronized). Violating any of the above is undefined behavior.
2637///
2638/// Exercised by the C-API differential courts
2639/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2640/// courts; those pass byte-for-byte against the upstream oracle.
2641#[no_mangle]
2642pub unsafe extern "C" fn xmlSwitchInputEncoding(
2643 ctxt: *mut _xmlParserCtxt,
2644 input: *mut _xmlParserInput,
2645 handler: *mut _xmlCharEncodingHandler,
2646) -> c_int {
2647 let _ = ctxt;
2648 if input.is_null() {
2649 return -1;
2650 }
2651 unsafe {
2652 if (*input).buf.is_null() {
2653 return -1;
2654 }
2655 io::input_buffer_set_encoder((*input).buf, handler);
2656 }
2657 0
2658}
2659
2660/// Switch the encoding of the current input using an encoding handler.
2661///
2662/// # UPSTREAM-PARITY
2663///
2664/// ```c
2665/// int xmlSwitchToEncoding(xmlParserCtxtPtr ctxt,
2666/// xmlCharEncodingHandlerPtr handler);
2667/// ```
2668///
2669/// # SAFETY
2670///
2671/// - `ctxt`, `handler` must be valid pointers (or NULL
2672/// where the upstream C contract allows), obtained from the
2673/// matching constructor/owner and not yet freed; the callee may
2674/// take or keep ownership exactly as the C API specifies.
2675///
2676/// The caller must not race this call with concurrent mutation of the
2677/// same objects from other threads (per-object state is not internally
2678/// synchronized). Violating any of the above is undefined behavior.
2679///
2680/// Exercised by the C-API differential courts
2681/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2682/// courts; those pass byte-for-byte against the upstream oracle.
2683#[no_mangle]
2684pub unsafe extern "C" fn xmlSwitchToEncoding(
2685 ctxt: *mut _xmlParserCtxt,
2686 handler: *mut _xmlCharEncodingHandler,
2687) -> c_int {
2688 if ctxt.is_null() {
2689 return -1;
2690 }
2691 unsafe {
2692 let input = (*ctxt).input;
2693 if input.is_null() {
2694 return -1;
2695 }
2696 // Memory-parser inputs (xmlCreateMemoryParserCtxt) carry buf == NULL;
2697 // their bytes live in the Rust-side InputBuffer (helpers.rs side
2698 // table), which already transcoded any BOM/declared encoding. A
2699 // caller-driven switch (PHP dom overrideEncoding) must therefore
2700 // transcode the whole buffered stream there (upstream applies the
2701 // input-buffer encoder before any read).
2702 if (*input).buf.is_null() && !(*handler).name.is_null() {
2703 return helpers::apply_memory_encoding_override(ctxt, (*handler).name);
2704 }
2705 io::input_buffer_set_encoder((*input).buf, handler);
2706 }
2707 0
2708}
2709
2710// ═══════════════════════════════════════════════════════════════════════════════
2711// Node info sequence (deprecated, parser.h)
2712// ═══════════════════════════════════════════════════════════════════════════════
2713
2714/// Initialise a node info sequence.
2715///
2716/// # UPSTREAM-PARITY
2717///
2718/// ```c
2719/// void xmlInitNodeInfoSeq(xmlParserNodeInfoSeqPtr seq);
2720/// ```
2721///
2722/// # SAFETY
2723///
2724/// - `seq` must be valid pointers (or NULL
2725/// where the upstream C contract allows), obtained from the
2726/// matching constructor/owner and not yet freed; the callee may
2727/// take or keep ownership exactly as the C API specifies.
2728///
2729/// The caller must not race this call with concurrent mutation of the
2730/// same objects from other threads (per-object state is not internally
2731/// synchronized). Violating any of the above is undefined behavior.
2732///
2733/// Exercised by the C-API differential courts
2734/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2735/// courts; those pass byte-for-byte against the upstream oracle.
2736#[no_mangle]
2737pub unsafe extern "C" fn xmlInitNodeInfoSeq(seq: *mut _xmlParserNodeInfoSeq) {
2738 if seq.is_null() {
2739 return;
2740 }
2741 unsafe {
2742 (*seq).length = 0;
2743 (*seq).maximum = 0;
2744 (*seq).buffer = ptr::null_mut();
2745 }
2746}
2747
2748/// Clear (release and reinitialise) a node info sequence.
2749///
2750/// # UPSTREAM-PARITY
2751///
2752/// ```c
2753/// void xmlClearNodeInfoSeq(xmlParserNodeInfoSeqPtr seq);
2754/// ```
2755///
2756/// # SAFETY
2757///
2758/// - `seq` must be valid pointers (or NULL
2759/// where the upstream C contract allows), obtained from the
2760/// matching constructor/owner and not yet freed; the callee may
2761/// take or keep ownership exactly as the C API specifies.
2762///
2763/// The caller must not race this call with concurrent mutation of the
2764/// same objects from other threads (per-object state is not internally
2765/// synchronized). Violating any of the above is undefined behavior.
2766///
2767/// Exercised by the C-API differential courts
2768/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2769/// courts; those pass byte-for-byte against the upstream oracle.
2770#[no_mangle]
2771pub unsafe extern "C" fn xmlClearNodeInfoSeq(seq: *mut _xmlParserNodeInfoSeq) {
2772 if seq.is_null() {
2773 return;
2774 }
2775 unsafe {
2776 if !(*seq).buffer.is_null() {
2777 xmlFreeImpl((*seq).buffer as *mut c_void);
2778 }
2779 xmlInitNodeInfoSeq(seq);
2780 }
2781}
2782
2783/// Find the index where the info record for `node` is (or should be) in the
2784/// sorted sequence; binary search by node pointer.
2785///
2786/// # UPSTREAM-PARITY
2787///
2788/// ```c
2789/// unsigned long xmlParserFindNodeInfoIndex(xmlParserNodeInfoSeqPtr seq,
2790/// xmlNodePtr node);
2791/// ```
2792///
2793/// # SAFETY
2794///
2795/// - `seq`, `node` must be valid pointers (or NULL
2796/// where the upstream C contract allows), obtained from the
2797/// matching constructor/owner and not yet freed; the callee may
2798/// take or keep ownership exactly as the C API specifies.
2799///
2800/// The caller must not race this call with concurrent mutation of the
2801/// same objects from other threads (per-object state is not internally
2802/// synchronized). Violating any of the above is undefined behavior.
2803///
2804/// Exercised by the C-API differential courts
2805/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2806/// courts; those pass byte-for-byte against the upstream oracle.
2807#[no_mangle]
2808pub unsafe extern "C" fn xmlParserFindNodeInfoIndex(
2809 seq: *mut _xmlParserNodeInfoSeq,
2810 node: *mut _xmlNode,
2811) -> c_ulong {
2812 if seq.is_null() || node.is_null() {
2813 return c_ulong::MAX;
2814 }
2815 unsafe {
2816 let s = &*seq;
2817 if s.buffer.is_null() || s.length == 0 {
2818 return 0;
2819 }
2820 let mut lower: usize = 0;
2821 let mut upper: usize = s.length as usize;
2822 while lower < upper {
2823 let middle = lower + (upper - lower) / 2;
2824 let cur_node = (*s.buffer.add(middle)).node;
2825 if cur_node == node {
2826 return middle as c_ulong;
2827 }
2828 if (cur_node as usize) < (node as usize) {
2829 lower = middle + 1;
2830 } else {
2831 upper = middle;
2832 }
2833 }
2834 lower as c_ulong
2835 }
2836}
2837
2838/// Find the node info record for a given node, or NULL.
2839///
2840/// # UPSTREAM-PARITY
2841///
2842/// ```c
2843/// const xmlParserNodeInfo *xmlParserFindNodeInfo(xmlParserCtxtPtr ctxt,
2844/// xmlNodePtr node);
2845/// ```
2846///
2847/// # SAFETY
2848///
2849/// - `ctxt`, `node` must be valid pointers (or NULL
2850/// where the upstream C contract allows), obtained from the
2851/// matching constructor/owner and not yet freed; the callee may
2852/// take or keep ownership exactly as the C API specifies.
2853///
2854/// The caller must not race this call with concurrent mutation of the
2855/// same objects from other threads (per-object state is not internally
2856/// synchronized). Violating any of the above is undefined behavior.
2857///
2858/// Exercised by the C-API differential courts
2859/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2860/// courts; those pass byte-for-byte against the upstream oracle.
2861#[no_mangle]
2862pub unsafe extern "C" fn xmlParserFindNodeInfo(
2863 ctxt: *mut _xmlParserCtxt,
2864 node: *mut _xmlNode,
2865) -> *const _xmlParserNodeInfo {
2866 if ctxt.is_null() || node.is_null() {
2867 return ptr::null();
2868 }
2869 unsafe {
2870 let seq = &(*ctxt).node_seq;
2871 let seq_mut = seq as *const _ as *mut _xmlParserNodeInfoSeq;
2872 let pos = xmlParserFindNodeInfoIndex(seq_mut, node);
2873 if !seq.buffer.is_null() && (pos as usize) < (seq.length as usize) {
2874 let info = &*seq.buffer.add(pos as usize);
2875 if info.node == node {
2876 return info;
2877 }
2878 }
2879 ptr::null()
2880 }
2881}
2882
2883/// Insert a node info record into the context's sorted sequence.
2884///
2885/// # UPSTREAM-PARITY
2886///
2887/// ```c
2888/// void xmlParserAddNodeInfo(xmlParserCtxtPtr ctxt, xmlParserNodeInfoPtr info);
2889/// ```
2890///
2891/// # SAFETY
2892///
2893/// - `ctxt`, `info` must be valid pointers (or NULL
2894/// where the upstream C contract allows), obtained from the
2895/// matching constructor/owner and not yet freed; the callee may
2896/// take or keep ownership exactly as the C API specifies.
2897///
2898/// The caller must not race this call with concurrent mutation of the
2899/// same objects from other threads (per-object state is not internally
2900/// synchronized). Violating any of the above is undefined behavior.
2901///
2902/// Exercised by the C-API differential courts
2903/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
2904/// courts; those pass byte-for-byte against the upstream oracle.
2905#[no_mangle]
2906pub unsafe extern "C" fn xmlParserAddNodeInfo(
2907 ctxt: *mut _xmlParserCtxt,
2908 info: *mut _xmlParserNodeInfo,
2909) {
2910 if ctxt.is_null() || info.is_null() {
2911 return;
2912 }
2913 unsafe {
2914 let seq = &mut (*ctxt).node_seq;
2915 let node = (*info).node;
2916 let pos = xmlParserFindNodeInfoIndex(seq, node as *mut _xmlNode) as usize;
2917
2918 if pos < seq.length as usize && !seq.buffer.is_null() && (*seq.buffer.add(pos)).node == node
2919 {
2920 // Node already recorded: update the record in place.
2921 ptr::copy_nonoverlapping(info, seq.buffer.add(pos), 1);
2922 return;
2923 }
2924
2925 // Grow the buffer (upstream xmlGrowCapacity: 50% growth from a
2926 // minimum of 4, capped at XML_MAX_ITEMS = 1 billion).
2927 if seq.length + 1 > seq.maximum {
2928 let new_max = xml_grow_capacity(seq.maximum);
2929 if new_max < 0 {
2930 xmlCtxtErrMemory(ctxt);
2931 return;
2932 }
2933 let new_buf = xmlReallocImpl(
2934 seq.buffer as *mut c_void,
2935 (new_max as usize) * core::mem::size_of::<_xmlParserNodeInfo>(),
2936 ) as *mut _xmlParserNodeInfo;
2937 if new_buf.is_null() {
2938 xmlCtxtErrMemory(ctxt);
2939 return;
2940 }
2941 seq.buffer = new_buf;
2942 seq.maximum = new_max as c_ulong;
2943 }
2944
2945 // Shift elements right to make room at `pos`.
2946 let length = seq.length as usize;
2947 for i in (pos + 1..=length).rev() {
2948 ptr::copy_nonoverlapping(seq.buffer.add(i - 1), seq.buffer.add(i), 1);
2949 }
2950 ptr::copy_nonoverlapping(info, seq.buffer.add(pos), 1);
2951 seq.length += 1;
2952 }
2953}
2954
2955/// Upstream `xmlGrowCapacity` (private/memory.h) for a zero-based capacity:
2956/// 50% growth, minimum initial allocation 4, capped at XML_MAX_ITEMS.
2957/// Returns the new capacity or -1 on overflow/cap exhaustion.
2958// The `as u64` casts are width-correcting for 32-bit platforms where
2959// `c_ulong` is 32 bits; on x86-64 they are identity casts.
2960#[allow(clippy::unnecessary_cast)]
2961const unsafe fn xml_grow_capacity(capacity: c_ulong) -> c_int {
2962 const XML_MAX_ITEMS: u64 = 1_000_000_000;
2963 const ELEM_SIZE: usize = core::mem::size_of::<_xmlParserNodeInfo>();
2964 if capacity == 0 {
2965 return 4;
2966 }
2967 if capacity as u64 >= XML_MAX_ITEMS || (capacity as usize) > usize::MAX / 2 / ELEM_SIZE {
2968 return -1;
2969 }
2970 let extra = capacity.div_ceil(2);
2971 if capacity as u64 > XML_MAX_ITEMS - extra as u64 {
2972 return XML_MAX_ITEMS as c_int;
2973 }
2974 (capacity + extra) as c_int
2975}
2976
2977// ═══════════════════════════════════════════════════════════════════════════════
2978// I/O callback registration (xmlIO.h)
2979// ═══════════════════════════════════════════════════════════════════════════════
2980
2981/// Register a new set of input I/O callbacks.
2982///
2983/// # UPSTREAM-PARITY
2984///
2985/// ```c
2986/// int xmlRegisterInputCallbacks(xmlInputMatchCallback matchFunc,
2987/// xmlInputOpenCallback openFunc,
2988/// xmlInputReadCallback readFunc,
2989/// xmlInputCloseCallback closeFunc);
2990/// ```
2991///
2992/// # SAFETY
2993///
2994///
2995/// - `matchFunc`, `openFunc`, `readFunc`, `closeFunc` must be a valid callback (or None);
2996/// the callback is invoked with the documented context pointer and
2997/// must itself uphold the same pointer invariants.
2998///
2999/// The caller must not race this call with concurrent mutation of the
3000/// same objects from other threads (per-object state is not internally
3001/// synchronized). Violating any of the above is undefined behavior.
3002///
3003/// Exercised by the C-API differential courts
3004/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
3005/// courts; those pass byte-for-byte against the upstream oracle.
3006#[no_mangle]
3007pub unsafe extern "C" fn xmlRegisterInputCallbacks(
3008 matchFunc: Option<xmlInputMatchCallback>,
3009 openFunc: Option<xmlInputOpenCallback>,
3010 readFunc: Option<xmlInputReadCallback>,
3011 closeFunc: Option<xmlInputCloseCallback>,
3012) -> c_int {
3013 unsafe {
3014 globals::init_parser();
3015 }
3016 let mut table = INPUT_CALLBACKS.lock();
3017 if table.len() >= 10 {
3018 return -1;
3019 }
3020 table.push(InputCallbackEntry {
3021 matchcb: matchFunc,
3022 opencb: openFunc,
3023 readcb: readFunc,
3024 closecb: closeFunc,
3025 });
3026 (table.len() - 1) as c_int
3027}
3028
3029/// Register the default compiled-in input callbacks (the `xmlFile*` pair).
3030///
3031/// # UPSTREAM-PARITY
3032///
3033/// ```c
3034/// void xmlRegisterDefaultInputCallbacks(void);
3035/// ```
3036///
3037/// # SAFETY
3038///
3039/// The function touches crate-global state only; it is safe
3040/// as long as the caller respects the library's global
3041/// initialization/cleanup ordering (xmlInitParser before use,
3042/// xmlCleanupParser only after all users are done).
3043///
3044/// Violating the global lifecycle ordering, or calling this after
3045/// teardown or from a signal handler, is undefined behavior.
3046#[no_mangle]
3047pub unsafe extern "C" fn xmlRegisterDefaultInputCallbacks() {
3048 unsafe {
3049 xmlRegisterInputCallbacks(
3050 Some(xmlFileMatch),
3051 Some(xmlFileOpen),
3052 Some(xmlFileRead),
3053 Some(xmlFileClose),
3054 );
3055 }
3056}
3057
3058/// Remove the top input callback from the stack.
3059///
3060/// # UPSTREAM-PARITY
3061///
3062/// ```c
3063/// int xmlPopInputCallbacks(void);
3064/// ```
3065///
3066/// # SAFETY
3067///
3068/// The function touches crate-global state only; it is safe
3069/// as long as the caller respects the library's global
3070/// initialization/cleanup ordering (xmlInitParser before use,
3071/// xmlCleanupParser only after all users are done).
3072///
3073/// Violating the global lifecycle ordering, or calling this after
3074/// teardown or from a signal handler, is undefined behavior.
3075#[no_mangle]
3076pub unsafe extern "C" fn xmlPopInputCallbacks() -> c_int {
3077 unsafe {
3078 globals::init_parser();
3079 }
3080 let mut table = INPUT_CALLBACKS.lock();
3081 if table.is_empty() {
3082 return -1;
3083 }
3084 table.pop();
3085 table.len() as c_int
3086}
3087
3088/// Clear the entire input callback table.
3089///
3090/// # UPSTREAM-PARITY
3091///
3092/// ```c
3093/// void xmlCleanupInputCallbacks(void);
3094/// ```
3095///
3096/// # SAFETY
3097///
3098/// The function touches crate-global state only; it is safe
3099/// as long as the caller respects the library's global
3100/// initialization/cleanup ordering (xmlInitParser before use,
3101/// xmlCleanupParser only after all users are done).
3102///
3103/// Violating the global lifecycle ordering, or calling this after
3104/// teardown or from a signal handler, is undefined behavior.
3105#[no_mangle]
3106pub unsafe extern "C" fn xmlCleanupInputCallbacks() {
3107 unsafe {
3108 globals::init_parser();
3109 }
3110 INPUT_CALLBACKS.lock().clear();
3111}
3112
3113/// Read a URI through the registered input callbacks (upstream
3114/// `xmlParserInputBufferCreateFilename`): the first registered pair whose
3115/// match callback accepts the URI is opened, read to EOF, and closed.
3116/// Returns `None` when no registered pair matches — callers fall back to
3117/// the regular file path. NULL callbacks inside a matching pair are treated
3118/// like upstream (an entry whose match callback is NULL is skipped).
3119///
3120/// Used by the XInclude loader so custom I/O schemes registered through
3121/// `xmlRegisterInputCallbacks` are honored (upstream xmlXIncludeLoadDoc →
3122/// xmlNewInputFromFile; Phase-12 EXTERNAL-CONSUMERS court: io1.c registers
3123/// an sql: scheme and XInclude hrefs route through it).
3124///
3125/// # SAFETY
3126///
3127/// - `uri` must be a valid NUL-terminated C string live for the call.
3128pub(crate) unsafe fn read_uri_via_input_callbacks(uri: *const c_char) -> Option<Vec<u8>> {
3129 let table = INPUT_CALLBACKS.lock();
3130 for e in table.iter() {
3131 let Some(matchcb) = e.matchcb else {
3132 continue;
3133 };
3134 // SAFETY: callbacks were registered by the caller and must uphold
3135 // the xmlInput*Callback contracts.
3136 if unsafe { matchcb(uri) } == 0 {
3137 continue;
3138 }
3139 let (Some(opencb), Some(readcb)) = (e.opencb, e.readcb) else {
3140 return None;
3141 };
3142 // SAFETY: the open callback returns a context for read/close.
3143 let ctx = unsafe { opencb(uri) };
3144 if ctx.is_null() {
3145 return None;
3146 }
3147 let mut data = Vec::new();
3148 let mut buf = [0u8; 4096];
3149 loop {
3150 // SAFETY: readcb fills `buf` per the xmlInputReadCallback contract.
3151 let n = unsafe { readcb(ctx, buf.as_mut_ptr() as *mut c_char, buf.len() as c_int) };
3152 if n < 0 {
3153 if let Some(closecb) = e.closecb {
3154 unsafe { closecb(ctx) };
3155 }
3156 return None;
3157 }
3158 if n == 0 {
3159 break;
3160 }
3161 data.extend_from_slice(&buf[..n as usize]);
3162 }
3163 if let Some(closecb) = e.closecb {
3164 unsafe { closecb(ctx) };
3165 }
3166 return Some(data);
3167 }
3168 None
3169}
3170
3171/// Register a new set of output I/O callbacks.
3172///
3173/// # UPSTREAM-PARITY
3174///
3175/// ```c
3176/// int xmlRegisterOutputCallbacks(xmlOutputMatchCallback matchFunc,
3177/// xmlOutputOpenCallback openFunc,
3178/// xmlOutputWriteCallback writeFunc,
3179/// xmlOutputCloseCallback closeFunc);
3180/// ```
3181///
3182/// # SAFETY
3183///
3184///
3185/// - `matchFunc`, `openFunc`, `writeFunc`, `closeFunc` must be a valid callback (or None);
3186/// the callback is invoked with the documented context pointer and
3187/// must itself uphold the same pointer invariants.
3188///
3189/// The caller must not race this call with concurrent mutation of the
3190/// same objects from other threads (per-object state is not internally
3191/// synchronized). Violating any of the above is undefined behavior.
3192///
3193/// Exercised by the C-API differential courts
3194/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
3195/// courts; those pass byte-for-byte against the upstream oracle.
3196#[no_mangle]
3197pub unsafe extern "C" fn xmlRegisterOutputCallbacks(
3198 matchFunc: Option<xmlOutputMatchCallback>,
3199 openFunc: Option<xmlOutputOpenCallback>,
3200 writeFunc: Option<xmlOutputWriteCallback>,
3201 closeFunc: Option<xmlOutputCloseCallback>,
3202) -> c_int {
3203 unsafe {
3204 globals::init_parser();
3205 }
3206 let mut table = OUTPUT_CALLBACKS.lock();
3207 if table.len() >= 10 {
3208 return -1;
3209 }
3210 table.push(OutputCallbackEntry {
3211 matchcb: matchFunc,
3212 opencb: openFunc,
3213 writecb: writeFunc,
3214 closecb: closeFunc,
3215 });
3216 (table.len() - 1) as c_int
3217}
3218
3219/// Register the default compiled-in output callbacks.
3220///
3221/// # UPSTREAM-PARITY
3222///
3223/// ```c
3224/// void xmlRegisterDefaultOutputCallbacks(void);
3225/// ```
3226///
3227/// # SAFETY
3228///
3229/// The function touches crate-global state only; it is safe
3230/// as long as the caller respects the library's global
3231/// initialization/cleanup ordering (xmlInitParser before use,
3232/// xmlCleanupParser only after all users are done).
3233///
3234/// Violating the global lifecycle ordering, or calling this after
3235/// teardown or from a signal handler, is undefined behavior.
3236#[no_mangle]
3237pub unsafe extern "C" fn xmlRegisterDefaultOutputCallbacks() {
3238 unsafe {
3239 xmlRegisterOutputCallbacks(Some(xmlFileMatch), None, None, None);
3240 }
3241}
3242
3243/// Register the HTTP POST output callbacks (upstream: default output callbacks).
3244///
3245/// # UPSTREAM-PARITY
3246///
3247/// ```c
3248/// void xmlRegisterHTTPPostCallbacks(void);
3249/// ```
3250///
3251/// # SAFETY
3252///
3253/// The function touches crate-global state only; it is safe
3254/// as long as the caller respects the library's global
3255/// initialization/cleanup ordering (xmlInitParser before use,
3256/// xmlCleanupParser only after all users are done).
3257///
3258/// Violating the global lifecycle ordering, or calling this after
3259/// teardown or from a signal handler, is undefined behavior.
3260#[no_mangle]
3261pub unsafe extern "C" fn xmlRegisterHTTPPostCallbacks() {
3262 unsafe { xmlRegisterDefaultOutputCallbacks() }
3263}
3264
3265/// Remove the top output callback from the stack.
3266///
3267/// # UPSTREAM-PARITY
3268///
3269/// ```c
3270/// int xmlPopOutputCallbacks(void);
3271/// ```
3272///
3273/// # SAFETY
3274///
3275/// The function touches crate-global state only; it is safe
3276/// as long as the caller respects the library's global
3277/// initialization/cleanup ordering (xmlInitParser before use,
3278/// xmlCleanupParser only after all users are done).
3279///
3280/// Violating the global lifecycle ordering, or calling this after
3281/// teardown or from a signal handler, is undefined behavior.
3282#[no_mangle]
3283pub unsafe extern "C" fn xmlPopOutputCallbacks() -> c_int {
3284 unsafe {
3285 globals::init_parser();
3286 }
3287 let mut table = OUTPUT_CALLBACKS.lock();
3288 if table.is_empty() {
3289 return -1;
3290 }
3291 table.pop();
3292 table.len() as c_int
3293}
3294
3295/// Clear the entire output callback table.
3296///
3297/// # UPSTREAM-PARITY
3298///
3299/// ```c
3300/// void xmlCleanupOutputCallbacks(void);
3301/// ```
3302///
3303/// # SAFETY
3304///
3305/// The function touches crate-global state only; it is safe
3306/// as long as the caller respects the library's global
3307/// initialization/cleanup ordering (xmlInitParser before use,
3308/// xmlCleanupParser only after all users are done).
3309///
3310/// Violating the global lifecycle ordering, or calling this after
3311/// teardown or from a signal handler, is undefined behavior.
3312#[no_mangle]
3313pub unsafe extern "C" fn xmlCleanupOutputCallbacks() {
3314 unsafe {
3315 globals::init_parser();
3316 }
3317 OUTPUT_CALLBACKS.lock().clear();
3318}
3319
3320// ═══════════════════════════════════════════════════════════════════════════════
3321// External entity loaders (parser.h)
3322// ═══════════════════════════════════════════════════════════════════════════════
3323
3324/// Default external entity loader: resolve `url` against the filesystem,
3325/// honouring XML_PARSE_NONET.
3326///
3327/// # Safety
3328///
3329/// `url`/`publicId` must be valid C strings or NULL; `ctxt` may be NULL.
3330unsafe extern "C" fn default_external_entity_loader(
3331 url: *const c_char,
3332 public_id: *const c_char,
3333 ctxt: *mut _xmlParserCtxt,
3334) -> *mut _xmlParserInput {
3335 let _ = public_id;
3336 if url.is_null() {
3337 return ptr::null_mut();
3338 }
3339 unsafe {
3340 // Refuse network access when NONET is set.
3341 if !ctxt.is_null() && (*ctxt).options & XML_PARSE_NONET != 0 {
3342 let len = libc::strlen(url);
3343 if len >= 7 && libc::strncasecmp(url, c"http://".as_ptr() as *const c_char, 7) == 0 {
3344 return ptr::null_mut();
3345 }
3346 }
3347 // UPSTREAM-PARITY (parserInternals.c xmlDefaultExternalEntityLoader
3348 // -> xmlNewInputFromFile -> xmlNewInputFromUrl): the registered
3349 // xmlParserInputBufferCreateFilenameDefault (php streams loader) is
3350 // consulted BEFORE the input-callback table and the built-in open. A
3351 // NULL loader result is XML_IO_ENOENT — xmlCtxtErrIO is raised and
3352 // there is no fallback.
3353 if globals::get_parser_input_buffer_create_filename_value_cross_dso().is_some() {
3354 // SAFETY: url is a valid NUL-terminated C string for the call.
3355 return match call_loader_materialize(url) {
3356 Err(()) => {
3357 emit_io_warning(ctxt, io_load_failure_message(url));
3358 ptr::null_mut()
3359 }
3360 Ok(data) => {
3361 // Build a MEMORY-backed C input: the entity machinery
3362 // consumes the loader result through base/end (upstream
3363 // buffers the external entity content the same way). A
3364 // zero-length result (php://memory, 0-byte file) is a
3365 // VALID empty input — the parse reports "Document is
3366 // empty" (php DOM createFromFile).
3367 let mem = if data.is_empty() {
3368 crate::xml::io::input_buffer_create_empty()
3369 } else {
3370 io::input_buffer_create_mem(
3371 data.as_ptr() as *const c_char,
3372 data.len() as c_int,
3373 xmlCharEncoding::XML_CHAR_ENCODING_NONE as c_int,
3374 )
3375 };
3376 if mem.is_null() {
3377 return ptr::null_mut();
3378 }
3379 parser_input_from_buf(mem)
3380 }
3381 };
3382 }
3383 // Try the registered input callbacks first.
3384 let table = INPUT_CALLBACKS.lock();
3385 for entry in table.iter() {
3386 if let (Some(match_cb), Some(open_cb)) = (entry.matchcb, entry.opencb) {
3387 if match_cb(url) != 0 {
3388 let ctx = open_cb(url);
3389 if !ctx.is_null() {
3390 let buf = helpers::alloc_parser_input_buffer();
3391 if buf.is_null() {
3392 if let Some(close_cb) = entry.closecb {
3393 close_cb(ctx);
3394 }
3395 return ptr::null_mut();
3396 }
3397 (*buf).context = ctx;
3398 (*buf).readcallback = entry.readcb;
3399 (*buf).closecallback = entry.closecb;
3400 return parser_input_from_buf(buf);
3401 }
3402 }
3403 }
3404 }
3405
3406 // Fall back to a plain file open.
3407 let buf =
3408 io::input_buffer_create_file(url, xmlCharEncoding::XML_CHAR_ENCODING_NONE as c_int);
3409 if buf.is_null() {
3410 // UPSTREAM-PARITY (parserInternals.c xmlNewInputFromFile): a
3411 // failed load raises xmlCtxtErrIO(ctxt, XML_IO_ENOENT, url) —
3412 // "I/O warning : failed to load \"%s\": %s\n" with the
3413 // strerror text (HOSTILE-FAILURE F7).
3414 let errno = *libc::__errno_location();
3415 let errstr = if errno == 0 {
3416 String::new()
3417 } else {
3418 std::ffi::CStr::from_ptr(libc::strerror(errno))
3419 .to_string_lossy()
3420 .into_owned()
3421 };
3422 let url_str = std::ffi::CStr::from_ptr(url).to_string_lossy();
3423 emit_io_warning(ctxt, format!("failed to load \"{url_str}\": {errstr}\n"));
3424 return ptr::null_mut();
3425 }
3426 parser_input_from_buf(buf)
3427 }
3428}
3429
3430/// UPSTREAM-PARITY (parserInternals.c xmlCtxtErrIO): raise an I/O warning
3431/// (XML_FROM_IO, XML_IO_ENOENT, XML_ERR_WARNING) through the parser's
3432/// channel — "I/O warning : <message>".
3433pub(crate) unsafe fn emit_io_warning(ctxt: *mut _xmlParserCtxt, message: String) {
3434 let msg_c = std::ffi::CString::new(message).unwrap_or_default();
3435 let delivery = if ctxt.is_null() {
3436 crate::xml::errors::GenericDelivery::Stream
3437 } else {
3438 unsafe { crate::xml::errors::parser_delivery(ctxt) }
3439 };
3440 unsafe {
3441 crate::xml::errors::raise_error_streamed(
3442 ctxt as *mut c_void,
3443 crate::abi::types::XML_FROM_IO,
3444 crate::abi::types::XML_IO_ENOENT,
3445 crate::abi::types::xmlErrorLevel::XML_ERR_WARNING as c_int,
3446 ptr::null(),
3447 0,
3448 0,
3449 ptr::null(),
3450 ptr::null(),
3451 ptr::null(),
3452 0,
3453 msg_c.as_ptr(),
3454 None,
3455 None,
3456 delivery,
3457 None,
3458 );
3459 }
3460}
3461
3462/// Result of routing a filename open through the registered loaders
3463/// (upstream 2.14+ `xmlLoadResource` layering).
3464#[allow(dead_code)]
3465pub(crate) enum RoutedFileOpen {
3466 /// No custom loader is registered — the caller falls back to the built-in
3467 /// file open (`helpers::input_from_file`).
3468 Builtin,
3469 /// A registered loader returned NULL: upstream reports `XML_IO_ENOENT`
3470 /// with NO built-in fallback (php streams loader: missing file, percent-
3471 /// encoded-NUL guard, disabled entity loader).
3472 Failed,
3473 /// A registered EXTERNAL ENTITY loader (`xmlSetExternalEntityLoader`)
3474 /// returned NULL for a file/URL open. Upstream `xmlCtxtNewInputFromUrl`
3475 /// propagates that NULL silently (no `xmlCtxtErrIO` — the custom loader
3476 /// owns its own error reporting), so callers fail without a warning.
3477 EntityLoaderFailed,
3478 /// The loader produced an input buffer whose bytes were materialized
3479 /// (filename = the original URI).
3480 Loaded(InputBuffer),
3481}
3482
3483/// UPSTREAM-PARITY (parserInternals.c `xmlNewInputFromUrl`): when a custom
3484/// `xmlParserInputBufferCreateFilenameDefault` is registered (PHP installs
3485/// its streams loader at request init), filename opens consult it FIRST —
3486/// php streams unescape `file://` URIs, enforce the percent-encoded-NUL
3487/// guard, honor stream contexts and emit their own failure warnings. A NULL
3488/// loader result is `XML_IO_ENOENT`; upstream does NOT fall back to the
3489/// built-in open in that case. Without a registered loader the caller keeps
3490/// the built-in path.
3491///
3492/// Invoke the registered loader and materialize the produced buffer's bytes
3493/// through its read callback, releasing the C buffer/stream exactly once
3494/// (the close callback runs when the buffer is freed). Returns `Err(())` on
3495/// a NULL loader result or a read-callback error.
3496///
3497/// # Safety
3498///
3499/// - `uri` must be a valid NUL-terminated C string live for the call; the
3500/// registered loader callback (if any) must uphold the
3501/// `xmlParserInputBufferCreateFilenameFunc` contract.
3502pub(crate) unsafe fn call_loader_materialize(uri: *const c_char) -> Result<Vec<u8>, ()> {
3503 // SAFETY: reads the per-thread loader slot — through the R-000177
3504 // cross-DSO bridge so the whole-archive facade copies observe the
3505 // loader a consumer registered via the core DSO's exported setter
3506 // (upstream: single core DSO, registration visible everywhere).
3507 let Some(func) = globals::get_parser_input_buffer_create_filename_value_cross_dso() else {
3508 return Err(());
3509 };
3510 // SAFETY: `func` is the consumer-registered C loader and must uphold the
3511 // xmlParserInputBufferCreateFilenameFunc contract (uri + enc in, buffer
3512 // out, or NULL on failure).
3513 let buf = unsafe { func(uri, xmlCharEncoding::XML_CHAR_ENCODING_NONE as c_int) };
3514 if buf.is_null() {
3515 return Err(());
3516 }
3517 let (read, ctx) = unsafe {
3518 let b = &*buf;
3519 (b.readcallback, b.context)
3520 };
3521 let mut data: Vec<u8> = Vec::new();
3522 let mut result = Err(());
3523 if let Some(read) = read {
3524 let mut tmp = [0u8; 4096];
3525 loop {
3526 // SAFETY: the loader's buffer carries the consumer's read
3527 // callback + context (xmlParserInputBufferCreateIO contract).
3528 let n = unsafe { read(ctx, tmp.as_mut_ptr() as *mut c_char, tmp.len() as c_int) };
3529 if n < 0 {
3530 break;
3531 }
3532 if n == 0 {
3533 result = Ok(());
3534 break;
3535 }
3536 data.extend_from_slice(&tmp[..n as usize]);
3537 }
3538 } else {
3539 // A memory-backed loader buffer (no read callback): copy its content.
3540 unsafe {
3541 let b = &*buf;
3542 if !b.buffer.is_null() {
3543 let xbuf = &*(b.buffer as *mut _xmlBuffer);
3544 if !xbuf.content.is_null() && xbuf.use_ > 0 {
3545 data.extend_from_slice(std::slice::from_raw_parts(
3546 xbuf.content as *const u8,
3547 xbuf.use_ as usize,
3548 ));
3549 }
3550 }
3551 }
3552 result = Ok(());
3553 }
3554 // Release the loader's C buffer: the close callback (php streams IO
3555 // close) runs exactly once now that the bytes are owned here.
3556 io::input_buffer_free(buf);
3557 result.map(|()| data)
3558}
3559
3560/// Route a filename open through the registered loaders, materializing the
3561/// result into an owned [`InputBuffer`] (filename = the original URI).
3562///
3563/// UPSTREAM LAYERING (2.14+ `xmlLoadResource`, R-000177): a REGISTERED
3564/// external entity loader (`xmlSetExternalEntityLoader`) is consulted first
3565/// for file/URL opens — main documents go through the same resource loader
3566/// as entities (`xmlCtxtNewInputFromUrl` -> `xmlLoadResource` ->
3567/// `xmlCurrentExternalEntityLoader`). A NULL custom-loader result is
3568/// `EntityLoaderFailed` (silent upstream — no `xmlCtxtErrIO`, the custom
3569/// loader reports its own errors). With NO custom entity loader the default
3570/// loader's tail is the `xmlParserInputBufferCreateFilenameDefault` (php
3571/// streams) loader, which is what the rest of this function implements
3572/// (upstream `xmlNewInputFromUrl`).
3573///
3574/// The registration is read through the R-000177 cross-DSO bridge (facade
3575/// copies must see a loader registered via the core DSO's exported setter).
3576///
3577/// # Safety
3578///
3579/// - `uri` must be a valid NUL-terminated C string live for the call.
3580/// - `ctxt` must be NULL or a valid parser context live for the call (passed
3581/// to the entity loader exactly as upstream `xmlLoadResource` does).
3582pub(crate) unsafe fn open_filename_routed(
3583 uri: *const c_char,
3584 ctxt: *mut _xmlParserCtxt,
3585) -> RoutedFileOpen {
3586 // A custom external entity loader governs file/URL opens too.
3587 if external_entity_loader_active() {
3588 // SAFETY: uri is a valid C string; ctxt is NULL or valid.
3589 let input = xmlLoadExternalEntity(uri, ptr::null(), ctxt);
3590 if input.is_null() {
3591 return RoutedFileOpen::EntityLoaderFailed;
3592 }
3593 // Materialize the loader's bytes into an owned InputBuffer (the
3594 // loader result is freed here; the parse consumes the copy).
3595 let loaded = input_bytes_owned(input);
3596 let named = if uri.is_null() {
3597 None
3598 } else {
3599 // SAFETY: uri is a valid NUL-terminated C string.
3600 Some(
3601 unsafe { CStr::from_ptr(uri) }
3602 .to_string_lossy()
3603 .into_owned(),
3604 )
3605 };
3606 return RoutedFileOpen::Loaded(InputBuffer::from_memory(
3607 loaded.as_deref().unwrap_or(&[]),
3608 named.as_deref(),
3609 ));
3610 }
3611 // No registered loader: the caller keeps the built-in open. The slot is
3612 // read through the R-000177 cross-DSO bridge (facade copies must see a
3613 // loader registered via the core DSO's exported setter).
3614 if globals::get_parser_input_buffer_create_filename_value_cross_dso().is_none() {
3615 return RoutedFileOpen::Builtin;
3616 }
3617 // SAFETY: uri is a valid NUL-terminated C string for the call.
3618 let loaded = unsafe { call_loader_materialize(uri) };
3619 match loaded {
3620 Err(()) => RoutedFileOpen::Failed,
3621 Ok(bytes) => {
3622 let named = if uri.is_null() {
3623 None
3624 } else {
3625 // SAFETY: uri is a valid NUL-terminated C string.
3626 Some(
3627 unsafe { CStr::from_ptr(uri) }
3628 .to_string_lossy()
3629 .into_owned(),
3630 )
3631 };
3632 RoutedFileOpen::Loaded(InputBuffer::from_memory(&bytes, named.as_deref()))
3633 }
3634 }
3635}
3636
3637/// True when a custom external entity loader is registered process-wide
3638/// (the core DSO's `xmlSetExternalEntityLoader` registration, or this DSO's
3639/// own when the accessor does not resolve in a single-DSO link). The
3640/// process-visible registration is authoritative (R-000177).
3641fn external_entity_loader_active() -> bool {
3642 match foreign_external_entity_loader() {
3643 Some(_) => true,
3644 None => EXTERNAL_ENTITY_LOADER.lock().is_some(),
3645 }
3646}
3647
3648/// Route a filename open through the `xmlParserInputBufferCreateFilenameDefault`
3649/// (php streams) loader ONLY — no external-entity-loader consult.
3650///
3651/// The xmlTextReader family reads through `xmlNewInputFromFile` upstream,
3652/// which does NOT go through the external entity loader (verified against
3653/// the executed 2.15.3 oracle), so the reader must not pick up an
3654/// `xmlSetExternalEntityLoader` registration.
3655///
3656/// # Safety
3657///
3658/// - `uri` must be a valid NUL-terminated C string live for the call.
3659pub(crate) unsafe fn open_filename_routed_input_only(uri: *const c_char) -> RoutedFileOpen {
3660 // No registered loader: the caller keeps the built-in open. The slot is
3661 // read through the R-000177 cross-DSO bridge (facade copies must see a
3662 // loader registered via the core DSO's exported setter).
3663 if globals::get_parser_input_buffer_create_filename_value_cross_dso().is_none() {
3664 return RoutedFileOpen::Builtin;
3665 }
3666 // SAFETY: uri is a valid NUL-terminated C string for the call.
3667 let loaded = unsafe { call_loader_materialize(uri) };
3668 match loaded {
3669 Err(()) => RoutedFileOpen::Failed,
3670 Ok(bytes) => {
3671 let named = if uri.is_null() {
3672 None
3673 } else {
3674 // SAFETY: uri is a valid NUL-terminated C string.
3675 Some(
3676 unsafe { CStr::from_ptr(uri) }
3677 .to_string_lossy()
3678 .into_owned(),
3679 )
3680 };
3681 RoutedFileOpen::Loaded(InputBuffer::from_memory(&bytes, named.as_deref()))
3682 }
3683 }
3684}
3685
3686/// Copy the bytes of a loader-produced `_xmlParserInput` into an owned
3687/// `Vec` and release the input (upstream `xmlCtxtParseDocument` consumes the
3688/// input; the candidate's parse paths own an [`InputBuffer`]). The input's
3689/// underlying buffer is freed with the input (xmlFreeInputStream).
3690///
3691/// # Safety
3692///
3693/// - `input` must be a valid `_xmlParserInput` produced by a registered
3694/// loader / `xmlLoadExternalEntity`, not yet freed.
3695unsafe fn input_bytes_owned(input: *mut _xmlParserInput) -> Option<Vec<u8>> {
3696 unsafe {
3697 let base = (*input).base;
3698 let end = (*input).end;
3699 let len = if base.is_null() {
3700 0
3701 } else {
3702 end.offset_from(base).max(0) as usize
3703 };
3704 let bytes = if base.is_null() || len == 0 {
3705 None
3706 } else {
3707 Some(core::slice::from_raw_parts(base, len).to_vec())
3708 };
3709 crate::abi::exports_xml2::xmlFreeInputStream(input);
3710 bytes
3711 }
3712}
3713
3714/// Compose the upstream `xmlCtxtErrIO(XML_IO_ENOENT, uri)` message text:
3715/// `failed to load "<uri>": <errno text>\n`. When errno is stale (the
3716/// registered php streams loader returned NULL without touching errno, e.g.
3717/// the percent-NUL guard) the `XML_IO_ENOENT` table text is used.
3718///
3719/// # Safety
3720///
3721/// - `uri` must be NULL or a valid NUL-terminated C string live for the call.
3722pub(crate) fn io_load_failure_message(uri: *const c_char) -> String {
3723 // SAFETY: reads errno only.
3724 let errno = unsafe { *libc::__errno_location() };
3725 let errstr = if errno == 0 {
3726 // xmlErrString(XML_IO_ENOENT) table text (error.c 2.15).
3727 "No such file or directory".to_string()
3728 } else {
3729 // SAFETY: strerror(errno) returns a static message for the value.
3730 unsafe { std::ffi::CStr::from_ptr(libc::strerror(errno)) }
3731 .to_string_lossy()
3732 .into_owned()
3733 };
3734 let url_str = if uri.is_null() {
3735 String::new()
3736 } else {
3737 // SAFETY: uri is a valid NUL-terminated C string.
3738 unsafe { std::ffi::CStr::from_ptr(uri) }
3739 .to_string_lossy()
3740 .into_owned()
3741 };
3742 format!("failed to load \"{url_str}\": {errstr}\n")
3743}
3744
3745/// Set the application-wide external entity loader.
3746///
3747/// # UPSTREAM-PARITY
3748///
3749/// ```c
3750/// void xmlSetExternalEntityLoader(xmlExternalEntityLoader f);
3751/// ```
3752///
3753/// # SAFETY
3754///
3755///
3756/// - `f` must be a valid callback (or None);
3757/// the callback is invoked with the documented context pointer and
3758/// must itself uphold the same pointer invariants.
3759///
3760/// The caller must not race this call with concurrent mutation of the
3761/// same objects from other threads (per-object state is not internally
3762/// synchronized). Violating any of the above is undefined behavior.
3763///
3764/// Exercised by the C-API differential courts
3765/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
3766/// courts; those pass byte-for-byte against the upstream oracle.
3767#[no_mangle]
3768pub unsafe extern "C" fn xmlSetExternalEntityLoader(f: Option<xmlExternalEntityLoader>) {
3769 *EXTERNAL_ENTITY_LOADER.lock() = f;
3770}
3771
3772/// Get the current external entity loader.
3773///
3774/// # UPSTREAM-PARITY
3775///
3776/// ```c
3777/// xmlExternalEntityLoader xmlGetExternalEntityLoader(void);
3778/// ```
3779///
3780/// # SAFETY
3781///
3782/// The function touches crate-global state only; it is safe
3783/// as long as the caller respects the library's global
3784/// initialization/cleanup ordering (xmlInitParser before use,
3785/// xmlCleanupParser only after all users are done).
3786///
3787/// Violating the global lifecycle ordering, or calling this after
3788/// teardown or from a signal handler, is undefined behavior.
3789#[no_mangle]
3790pub unsafe extern "C" fn xmlGetExternalEntityLoader() -> Option<xmlExternalEntityLoader> {
3791 *EXTERNAL_ENTITY_LOADER.lock()
3792}
3793
3794/// External entity loader that disables network access.
3795///
3796/// # UPSTREAM-PARITY
3797///
3798/// ```c
3799/// xmlParserInputPtr xmlNoNetExternalEntityLoader(const char *URL,
3800/// const char *ID,
3801/// xmlParserCtxtPtr ctxt);
3802/// ```
3803///
3804/// # SAFETY
3805///
3806/// - `ctxt` must be valid pointers (or NULL
3807/// where the upstream C contract allows), obtained from the
3808/// matching constructor/owner and not yet freed; the callee may
3809/// take or keep ownership exactly as the C API specifies.
3810///
3811/// - `URL`, `ID` must point to valid NUL-terminated
3812/// strings (or NULL where the C contract allows) for the lifetime
3813/// of the call.
3814///
3815/// The caller must not race this call with concurrent mutation of the
3816/// same objects from other threads (per-object state is not internally
3817/// synchronized). Violating any of the above is undefined behavior.
3818///
3819/// Exercised by the C-API differential courts
3820/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
3821/// courts; those pass byte-for-byte against the upstream oracle.
3822#[no_mangle]
3823pub unsafe extern "C" fn xmlNoNetExternalEntityLoader(
3824 URL: *const c_char,
3825 ID: *const c_char,
3826 ctxt: *mut _xmlParserCtxt,
3827) -> *mut _xmlParserInput {
3828 unsafe {
3829 let old_options = if ctxt.is_null() { 0 } else { (*ctxt).options };
3830 if !ctxt.is_null() {
3831 (*ctxt).options |= XML_PARSE_NONET;
3832 }
3833 let input = default_external_entity_loader(URL, ID, ctxt);
3834 if !ctxt.is_null() {
3835 (*ctxt).options = old_options;
3836 }
3837 input
3838 }
3839}
3840
3841/// Load an external entity using the registered loader.
3842///
3843/// # UPSTREAM-PARITY
3844///
3845/// ```c
3846/// xmlParserInputPtr xmlLoadExternalEntity(const char *URL, const char *ID,
3847/// xmlParserCtxtPtr ctxt);
3848/// ```
3849///
3850/// # SAFETY
3851///
3852/// - `ctxt` must be valid pointers (or NULL
3853/// where the upstream C contract allows), obtained from the
3854/// matching constructor/owner and not yet freed; the callee may
3855/// take or keep ownership exactly as the C API specifies.
3856///
3857/// - `URL`, `ID` must point to valid NUL-terminated
3858/// strings (or NULL where the C contract allows) for the lifetime
3859/// of the call.
3860///
3861/// The caller must not race this call with concurrent mutation of the
3862/// same objects from other threads (per-object state is not internally
3863/// synchronized). Violating any of the above is undefined behavior.
3864///
3865/// Exercised by the C-API differential courts
3866/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
3867/// courts; those pass byte-for-byte against the upstream oracle.
3868#[no_mangle]
3869pub unsafe extern "C" fn xmlLoadExternalEntity(
3870 URL: *const c_char,
3871 ID: *const c_char,
3872 ctxt: *mut _xmlParserCtxt,
3873) -> *mut _xmlParserInput {
3874 // R-000177: the loader xmlSetExternalEntityLoader registers binds to the
3875 // CORE DSO, so a load performed by a whole-archive facade's private copy
3876 // must consult the process-visible registration first (upstream: one
3877 // core instance, one loader). Single-DSO links resolve their own export.
3878 let loader = match foreign_external_entity_loader() {
3879 Some(f) => Some(f),
3880 None => *EXTERNAL_ENTITY_LOADER.lock(),
3881 };
3882 match loader {
3883 Some(f) => unsafe { f(URL, ID, ctxt) },
3884 None => unsafe { default_external_entity_loader(URL, ID, ctxt) },
3885 }
3886}
3887
3888/// Resolve the process-visible `xmlGetExternalEntityLoader` (the CORE DSO's
3889/// registration) via the dynamic symbol scope.
3890#[cfg(target_os = "linux")]
3891fn foreign_external_entity_loader() -> Option<xmlExternalEntityLoader> {
3892 use std::sync::OnceLock;
3893 type Getter = unsafe extern "C" fn() -> Option<xmlExternalEntityLoader>;
3894 static GETTER: OnceLock<Option<Getter>> = OnceLock::new();
3895 let getter = *GETTER.get_or_init(|| {
3896 // SAFETY: dlsym(RTLD_DEFAULT) returns the exported accessor address
3897 // or NULL; the transmute (pointer-sized) is sound.
3898 unsafe {
3899 let sym = libc::dlsym(libc::RTLD_DEFAULT, c"xmlGetExternalEntityLoader".as_ptr());
3900 if sym.is_null() {
3901 None
3902 } else {
3903 Some(std::mem::transmute::<*mut c_void, Getter>(sym))
3904 }
3905 }
3906 });
3907 getter.and_then(|g| unsafe { g() })
3908}
3909
3910#[cfg(not(target_os = "linux"))]
3911fn foreign_external_entity_loader() -> Option<xmlExternalEntityLoader> {
3912 None
3913}
3914
3915/// Check an input for HTTP access; with XML_PARSE_NONET set, HTTP inputs are
3916/// refused and freed.
3917///
3918/// # UPSTREAM-PARITY
3919///
3920/// ```c
3921/// xmlParserInputPtr xmlCheckHTTPInput(xmlParserCtxtPtr ctxt,
3922/// xmlParserInputPtr ret);
3923/// ```
3924///
3925/// # SAFETY
3926///
3927/// - `ctxt`, `ret` must be valid pointers (or NULL
3928/// where the upstream C contract allows), obtained from the
3929/// matching constructor/owner and not yet freed; the callee may
3930/// take or keep ownership exactly as the C API specifies.
3931///
3932/// The caller must not race this call with concurrent mutation of the
3933/// same objects from other threads (per-object state is not internally
3934/// synchronized). Violating any of the above is undefined behavior.
3935///
3936/// Exercised by the C-API differential courts
3937/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
3938/// courts; those pass byte-for-byte against the upstream oracle.
3939#[no_mangle]
3940pub unsafe extern "C" fn xmlCheckHTTPInput(
3941 ctxt: *mut _xmlParserCtxt,
3942 ret: *mut _xmlParserInput,
3943) -> *mut _xmlParserInput {
3944 if ret.is_null() {
3945 return ptr::null_mut();
3946 }
3947 unsafe {
3948 if !ctxt.is_null() && (*ctxt).options & XML_PARSE_NONET != 0 {
3949 let filename = (*ret).filename;
3950 if !filename.is_null() {
3951 let len = libc::strlen(filename);
3952 if len >= 7
3953 && libc::strncasecmp(filename, c"http://".as_ptr() as *const c_char, 7) == 0
3954 {
3955 // free_parser_input now frees the owned buffer (upstream
3956 // xmlFreeInputStream semantics); no separate buf free.
3957 helpers::free_parser_input(ret);
3958 return ptr::null_mut();
3959 }
3960 }
3961 }
3962 ret
3963 }
3964}
3965
3966// ═══════════════════════════════════════════════════════════════════════════════
3967// xmlFile* I/O callbacks (xmlIO.c)
3968// ═══════════════════════════════════════════════════════════════════════════════
3969
3970/// Match callback: the file I/O handlers accept every filename.
3971///
3972/// # UPSTREAM-PARITY
3973///
3974/// ```c
3975/// int xmlFileMatch(const char *filename);
3976/// ```
3977///
3978/// # SAFETY
3979///
3980///
3981/// - `_filename` must point to valid NUL-terminated
3982/// strings (or NULL where the C contract allows) for the lifetime
3983/// of the call.
3984///
3985/// The caller must not race this call with concurrent mutation of the
3986/// same objects from other threads (per-object state is not internally
3987/// synchronized). Violating any of the above is undefined behavior.
3988///
3989/// Exercised by the C-API differential courts
3990/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
3991/// courts; those pass byte-for-byte against the upstream oracle.
3992#[no_mangle]
3993pub const unsafe extern "C" fn xmlFileMatch(_filename: *const c_char) -> c_int {
3994 1
3995}
3996
3997/// Open a file and return a `FILE *` I/O context (cast to `void *`).
3998///
3999/// # UPSTREAM-PARITY
4000///
4001/// ```c
4002/// void *xmlFileOpen(const char *filename);
4003/// ```
4004///
4005/// # SAFETY
4006///
4007///
4008/// - `filename` must point to valid NUL-terminated
4009/// strings (or NULL where the C contract allows) for the lifetime
4010/// of the call.
4011///
4012/// The caller must not race this call with concurrent mutation of the
4013/// same objects from other threads (per-object state is not internally
4014/// synchronized). Violating any of the above is undefined behavior.
4015///
4016/// Exercised by the C-API differential courts
4017/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
4018/// courts; those pass byte-for-byte against the upstream oracle.
4019#[no_mangle]
4020pub unsafe extern "C" fn xmlFileOpen(filename: *const c_char) -> *mut c_void {
4021 if filename.is_null() {
4022 return ptr::null_mut();
4023 }
4024 unsafe { libc::fopen(filename, c"rb".as_ptr() as *const c_char) as *mut c_void }
4025}
4026
4027/// Read up to `len` bytes from a `FILE *` I/O context.
4028///
4029/// # UPSTREAM-PARITY
4030///
4031/// ```c
4032/// int xmlFileRead(void *context, char *buffer, int len);
4033/// ```
4034///
4035/// # SAFETY
4036///
4037/// - `context`, `buffer` must be valid pointers (or NULL
4038/// where the upstream C contract allows), obtained from the
4039/// matching constructor/owner and not yet freed; the callee may
4040/// take or keep ownership exactly as the C API specifies.
4041///
4042/// The caller must not race this call with concurrent mutation of the
4043/// same objects from other threads (per-object state is not internally
4044/// synchronized). Violating any of the above is undefined behavior.
4045///
4046/// Exercised by the C-API differential courts
4047/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
4048/// courts; those pass byte-for-byte against the upstream oracle.
4049#[no_mangle]
4050pub unsafe extern "C" fn xmlFileRead(
4051 context: *mut c_void,
4052 buffer: *mut c_char,
4053 len: c_int,
4054) -> c_int {
4055 if context.is_null() || buffer.is_null() || len <= 0 {
4056 return -1;
4057 }
4058 unsafe {
4059 let n = libc::fread(
4060 buffer as *mut c_void,
4061 1,
4062 len as usize,
4063 context as *mut libc::FILE,
4064 );
4065 if n < len as usize && libc::ferror(context as *mut libc::FILE) != 0 {
4066 return -1;
4067 }
4068 n as c_int
4069 }
4070}
4071
4072/// Close a `FILE *` I/O context.
4073///
4074/// # UPSTREAM-PARITY
4075///
4076/// ```c
4077/// int xmlFileClose(void *context);
4078/// ```
4079///
4080/// # SAFETY
4081///
4082/// - `context` must be valid pointers (or NULL
4083/// where the upstream C contract allows), obtained from the
4084/// matching constructor/owner and not yet freed; the callee may
4085/// take or keep ownership exactly as the C API specifies.
4086///
4087/// The caller must not race this call with concurrent mutation of the
4088/// same objects from other threads (per-object state is not internally
4089/// synchronized). Violating any of the above is undefined behavior.
4090///
4091/// Exercised by the C-API differential courts
4092/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
4093/// courts; those pass byte-for-byte against the upstream oracle.
4094#[no_mangle]
4095pub unsafe extern "C" fn xmlFileClose(context: *mut c_void) -> c_int {
4096 if context.is_null() {
4097 return -1;
4098 }
4099 unsafe {
4100 let file = context as *mut libc::FILE;
4101 let fd = libc::fileno(file);
4102 if fd == 0 {
4103 // stdin must not be closed.
4104 return 0;
4105 }
4106 if fd == 1 || fd == 2 {
4107 // stdout/stderr are only flushed.
4108 return if libc::fflush(file) == 0 { 0 } else { -1 };
4109 }
4110 libc::fclose(file)
4111 }
4112}
4113
4114// ═══════════════════════════════════════════════════════════════════════════════
4115// Low-level character scanning (parserInternals.c)
4116// ═══════════════════════════════════════════════════════════════════════════════
4117
4118/// Return the current character (UTF-8 decoded, EOL normalised) and its byte
4119/// length in `*len`. Does not advance the input pointer.
4120///
4121/// # UPSTREAM-PARITY
4122///
4123/// ```c
4124/// int xmlCurrentChar(xmlParserCtxtPtr ctxt, int *len);
4125/// ```
4126///
4127/// # SAFETY
4128///
4129/// - `ctxt`, `len` must be valid pointers (or NULL
4130/// where the upstream C contract allows), obtained from the
4131/// matching constructor/owner and not yet freed; the callee may
4132/// take or keep ownership exactly as the C API specifies.
4133///
4134/// The caller must not race this call with concurrent mutation of the
4135/// same objects from other threads (per-object state is not internally
4136/// synchronized). Violating any of the above is undefined behavior.
4137///
4138/// Exercised by the C-API differential courts
4139/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
4140/// courts; those pass byte-for-byte against the upstream oracle.
4141#[no_mangle]
4142pub unsafe extern "C" fn xmlCurrentChar(ctxt: *mut _xmlParserCtxt, len: *mut c_int) -> c_int {
4143 if ctxt.is_null() || len.is_null() || (*ctxt).input.is_null() {
4144 return 0;
4145 }
4146 unsafe {
4147 let pi = &*((*ctxt).input);
4148 let cur = pi.cur;
4149 if cur.is_null() {
4150 *len = 0;
4151 return 0;
4152 }
4153 let avail = (pi.end as usize).saturating_sub(cur as usize);
4154 let c = *cur;
4155
4156 if c < 0x80 {
4157 if c == b'\r' {
4158 // EOL normalisation: CR (optionally CRLF) becomes LF.
4159 if avail >= 2 && *cur.add(1) == b'\n' {
4160 (*(*ctxt).input).cur = cur.add(1);
4161 }
4162 *len = 1;
4163 return b'\n' as c_int;
4164 }
4165 if c == 0 {
4166 if avail == 0 {
4167 *len = 0;
4168 } else {
4169 *len = 1;
4170 }
4171 return 0;
4172 }
4173 *len = 1;
4174 return c as c_int;
4175 }
4176
4177 // Multi-byte UTF-8.
4178 if avail < 2 || (*cur.add(1) & 0xc0) != 0x80 {
4179 *len = 1;
4180 return XML_INVALID_CHAR;
4181 }
4182 if c < 0xe0 {
4183 if c < 0xc2 {
4184 *len = 1;
4185 return XML_INVALID_CHAR;
4186 }
4187 let val = (((c & 0x1f) as c_int) << 6) | ((*cur.add(1) & 0x3f) as c_int);
4188 *len = 2;
4189 return val;
4190 }
4191 if avail < 3 || (*cur.add(2) & 0xc0) != 0x80 {
4192 *len = 1;
4193 return XML_INVALID_CHAR;
4194 }
4195 if c < 0xf0 {
4196 let val = (((c & 0x0f) as c_int) << 12)
4197 | (((*cur.add(1) & 0x3f) as c_int) << 6)
4198 | ((*cur.add(2) & 0x3f) as c_int);
4199 if val < 0x800 || (0xd800..0xe000).contains(&val) {
4200 *len = 1;
4201 return XML_INVALID_CHAR;
4202 }
4203 *len = 3;
4204 return val;
4205 }
4206 if avail < 4 || (*cur.add(3) & 0xc0) != 0x80 {
4207 *len = 1;
4208 return XML_INVALID_CHAR;
4209 }
4210 let val = (((c & 0x07) as c_int) << 18)
4211 | (((*cur.add(1) & 0x3f) as c_int) << 12)
4212 | (((*cur.add(2) & 0x3f) as c_int) << 6)
4213 | ((*cur.add(3) & 0x3f) as c_int);
4214 if !(0x10000..0x110000).contains(&val) {
4215 *len = 1;
4216 return XML_INVALID_CHAR;
4217 }
4218 *len = 4;
4219 val
4220 }
4221}
4222
4223/// Advance to the next character, updating line/column accounting.
4224///
4225/// # UPSTREAM-PARITY
4226///
4227/// ```c
4228/// void xmlNextChar(xmlParserCtxtPtr ctxt);
4229/// ```
4230///
4231/// # SAFETY
4232///
4233/// - `ctxt` must be valid pointers (or NULL
4234/// where the upstream C contract allows), obtained from the
4235/// matching constructor/owner and not yet freed; the callee may
4236/// take or keep ownership exactly as the C API specifies.
4237///
4238/// The caller must not race this call with concurrent mutation of the
4239/// same objects from other threads (per-object state is not internally
4240/// synchronized). Violating any of the above is undefined behavior.
4241///
4242/// Exercised by the C-API differential courts
4243/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
4244/// courts; those pass byte-for-byte against the upstream oracle.
4245#[no_mangle]
4246pub unsafe extern "C" fn xmlNextChar(ctxt: *mut _xmlParserCtxt) {
4247 if ctxt.is_null() || (*ctxt).input.is_null() {
4248 return;
4249 }
4250 unsafe {
4251 let pi = &mut *((*ctxt).input);
4252 let cur = pi.cur;
4253 if cur.is_null() {
4254 return;
4255 }
4256 let avail = (pi.end as usize).saturating_sub(cur as usize);
4257 if avail == 0 {
4258 return;
4259 }
4260 let c = *cur;
4261
4262 if c < 0x80 {
4263 if c == b'\n' {
4264 pi.cur = cur.add(1);
4265 pi.line += 1;
4266 pi.col = 1;
4267 } else if c == b'\r' {
4268 // CRLF is a single line break.
4269 pi.cur = cur.add(if avail >= 2 && *cur.add(1) == b'\n' {
4270 2
4271 } else {
4272 1
4273 });
4274 pi.line += 1;
4275 pi.col = 1;
4276 } else {
4277 pi.cur = cur.add(1);
4278 pi.col += 1;
4279 }
4280 return;
4281 }
4282
4283 pi.col += 1;
4284
4285 if avail < 2 || (*cur.add(1) & 0xc0) != 0x80 {
4286 pi.cur = cur.add(1);
4287 return;
4288 }
4289 if c < 0xe0 {
4290 if c < 0xc2 {
4291 pi.cur = cur.add(1);
4292 return;
4293 }
4294 pi.cur = cur.add(2);
4295 return;
4296 }
4297 if avail < 3 || (*cur.add(2) & 0xc0) != 0x80 {
4298 pi.cur = cur.add(1);
4299 return;
4300 }
4301 if c < 0xf0 {
4302 let val = (((c as c_int) << 8) as u32) | (*cur.add(1) as u32);
4303 if (val < 0xe0a0) || (0xeda0..0xee00).contains(&val) {
4304 pi.cur = cur.add(1);
4305 return;
4306 }
4307 pi.cur = cur.add(3);
4308 return;
4309 }
4310 if avail < 4 || (*cur.add(3) & 0xc0) != 0x80 {
4311 pi.cur = cur.add(1);
4312 return;
4313 }
4314 let val = (((c as c_int) << 8) as u32) | (*cur.add(1) as u32);
4315 if !(0xf090..0xf490).contains(&val) {
4316 pi.cur = cur.add(1);
4317 return;
4318 }
4319 pi.cur = cur.add(4);
4320 }
4321}
4322
4323/// Skip blank characters (space, tab, LF, CR), updating line/column.
4324/// Returns the number of blanks skipped.
4325///
4326/// # UPSTREAM-PARITY
4327///
4328/// ```c
4329/// int xmlSkipBlankChars(xmlParserCtxtPtr ctxt);
4330/// ```
4331///
4332/// # SAFETY
4333///
4334/// - `ctxt` must be valid pointers (or NULL
4335/// where the upstream C contract allows), obtained from the
4336/// matching constructor/owner and not yet freed; the callee may
4337/// take or keep ownership exactly as the C API specifies.
4338///
4339/// The caller must not race this call with concurrent mutation of the
4340/// same objects from other threads (per-object state is not internally
4341/// synchronized). Violating any of the above is undefined behavior.
4342///
4343/// Exercised by the C-API differential courts
4344/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
4345/// courts; those pass byte-for-byte against the upstream oracle.
4346#[no_mangle]
4347pub unsafe extern "C" fn xmlSkipBlankChars(ctxt: *mut _xmlParserCtxt) -> c_int {
4348 if ctxt.is_null() || (*ctxt).input.is_null() {
4349 return 0;
4350 }
4351 unsafe {
4352 let pi = &mut *((*ctxt).input);
4353 let mut cur = pi.cur;
4354 if cur.is_null() {
4355 return 0;
4356 }
4357 let end = pi.end;
4358 let mut res = 0;
4359 while cur < end && (*cur == 0x20 || *cur == 0x09 || *cur == 0x0a || *cur == 0x0d) {
4360 if *cur == b'\n' {
4361 pi.line += 1;
4362 pi.col = 1;
4363 } else {
4364 pi.col += 1;
4365 }
4366 cur = cur.add(1);
4367 res += 1;
4368 }
4369 pi.cur = cur;
4370 res
4371 }
4372}
4373
4374/// XML 1.0 5th-edition NameStartChar predicate (upstream `xmlIsNameStartCharNew`).
4375const fn is_name_start_char_new(c: c_int) -> bool {
4376 if c == b' ' as c_int || c == b'>' as c_int || c == b'/' as c_int {
4377 return false;
4378 }
4379 (c >= b'a' as c_int && c <= b'z' as c_int)
4380 || (c >= b'A' as c_int && c <= b'Z' as c_int)
4381 || c == b'_' as c_int
4382 || c == b':' as c_int
4383 || (c >= 0xC0 && c <= 0xD6)
4384 || (c >= 0xD8 && c <= 0xF6)
4385 || (c >= 0xF8 && c <= 0x2FF)
4386 || (c >= 0x370 && c <= 0x37D)
4387 || (c >= 0x37F && c <= 0x1FFF)
4388 || (c >= 0x200C && c <= 0x200D)
4389 || (c >= 0x2070 && c <= 0x218F)
4390 || (c >= 0x2C00 && c <= 0x2FEF)
4391 || (c >= 0x3001 && c <= 0xD7FF)
4392 || (c >= 0xF900 && c <= 0xFDCF)
4393 || (c >= 0xFDF0 && c <= 0xFFFD)
4394 || (c >= 0x10000 && c <= 0xEFFFF)
4395}
4396
4397/// XML 1.0 5th-edition NameChar predicate (upstream `xmlIsNameCharNew`).
4398const fn is_name_char_new(c: c_int) -> bool {
4399 if c == b' ' as c_int || c == b'>' as c_int || c == b'/' as c_int {
4400 return false;
4401 }
4402 (c >= b'a' as c_int && c <= b'z' as c_int)
4403 || (c >= b'A' as c_int && c <= b'Z' as c_int)
4404 || (c >= b'0' as c_int && c <= b'9' as c_int)
4405 || c == b'_' as c_int
4406 || c == b':' as c_int
4407 || c == b'-' as c_int
4408 || c == b'.' as c_int
4409 || c == 0xB7
4410 || (c >= 0xC0 && c <= 0xD6)
4411 || (c >= 0xD8 && c <= 0xF6)
4412 || (c >= 0xF8 && c <= 0x2FF)
4413 || (c >= 0x300 && c <= 0x36F)
4414 || (c >= 0x370 && c <= 0x37D)
4415 || (c >= 0x37F && c <= 0x1FFF)
4416 || (c >= 0x200C && c <= 0x200D)
4417 || (c >= 0x203F && c <= 0x2040)
4418 || (c >= 0x2070 && c <= 0x218F)
4419 || (c >= 0x2C00 && c <= 0x2FEF)
4420 || (c >= 0x3001 && c <= 0xD7FF)
4421 || (c >= 0xF900 && c <= 0xFDCF)
4422 || (c >= 0xFDF0 && c <= 0xFFFD)
4423 || (c >= 0x10000 && c <= 0xEFFFF)
4424}
4425
4426/// Scan an XML Name (or NCName/Nmtoken) at `ctxt->input->cur`, advancing the
4427/// input pointer. Returns a pointer to the end of the name, or NULL when the
4428/// name exceeds `max` bytes.
4429///
4430/// # UPSTREAM-PARITY
4431///
4432/// ```c
4433/// const xmlChar *xmlScanName(xmlParserCtxtPtr ctxt, int max, int flags);
4434/// ```
4435///
4436/// # SAFETY
4437///
4438/// - `ctxt` must be valid pointers (or NULL
4439/// where the upstream C contract allows), obtained from the
4440/// matching constructor/owner and not yet freed; the callee may
4441/// take or keep ownership exactly as the C API specifies.
4442///
4443/// The caller must not race this call with concurrent mutation of the
4444/// same objects from other threads (per-object state is not internally
4445/// synchronized). Violating any of the above is undefined behavior.
4446///
4447/// Exercised by the C-API differential courts
4448/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
4449/// courts; those pass byte-for-byte against the upstream oracle.
4450#[no_mangle]
4451pub unsafe extern "C" fn xmlScanName(
4452 ctxt: *mut _xmlParserCtxt,
4453 max: c_int,
4454 flags: c_int,
4455) -> *const xmlChar {
4456 if ctxt.is_null() || (*ctxt).input.is_null() || max <= 0 {
4457 return ptr::null();
4458 }
4459 unsafe {
4460 let pi = &mut *((*ctxt).input);
4461 let mut ptr = pi.cur;
4462 if ptr.is_null() {
4463 return ptr::null();
4464 }
4465 let end = pi.end;
4466 let mut remaining = max as usize;
4467 let stop: u8 = if flags & XML_SCAN_NC != 0 { b':' } else { 0 };
4468 let old10 = flags & XML_SCAN_OLD10 != 0;
4469 let mut f = flags;
4470
4471 loop {
4472 if ptr >= end {
4473 break;
4474 }
4475 let c = *ptr;
4476 let (cp, len) = if c < 0x80 {
4477 if stop != 0 && c == stop {
4478 break;
4479 }
4480 (c as c_int, 1usize)
4481 } else {
4482 // Decode a multi-byte UTF-8 character.
4483 let avail = (end as usize).saturating_sub(ptr as usize);
4484 let mut l = 4usize;
4485 let cp = decode_utf8_char(ptr, avail, &mut l);
4486 if cp < 0 {
4487 break;
4488 }
4489 (cp, l)
4490 };
4491
4492 let ok = if f & XML_SCAN_NMTOKEN != 0 {
4493 if old10 {
4494 is_name_char_old10(cp)
4495 } else {
4496 is_name_char_new(cp)
4497 }
4498 } else if old10 {
4499 is_name_start_char_old10(cp)
4500 } else {
4501 is_name_start_char_new(cp)
4502 };
4503 if !ok {
4504 break;
4505 }
4506 if len > remaining {
4507 return ptr::null();
4508 }
4509 ptr = ptr.add(len);
4510 remaining -= len;
4511 f |= XML_SCAN_NMTOKEN;
4512 }
4513
4514 pi.cur = ptr;
4515 ptr
4516 }
4517}
4518
4519/// Decode a UTF-8 character at `ptr` with `avail` bytes available; returns the
4520/// codepoint (or -1 on invalid/truncated input) and sets `*len` to its length.
4521const unsafe fn decode_utf8_char(ptr: *const u8, avail: usize, len: &mut usize) -> c_int {
4522 unsafe {
4523 let c = *ptr;
4524 if avail < 2 || (*ptr.add(1) & 0xc0) != 0x80 {
4525 return -1;
4526 }
4527 if c < 0xe0 {
4528 if c < 0xc2 {
4529 return -1;
4530 }
4531 *len = 2;
4532 return (((c & 0x1f) as c_int) << 6) | ((*ptr.add(1) & 0x3f) as c_int);
4533 }
4534 if avail < 3 || (*ptr.add(2) & 0xc0) != 0x80 {
4535 return -1;
4536 }
4537 if c < 0xf0 {
4538 let val = (((c & 0x0f) as c_int) << 12)
4539 | (((*ptr.add(1) & 0x3f) as c_int) << 6)
4540 | ((*ptr.add(2) & 0x3f) as c_int);
4541 if val < 0x800 || (val >= 0xd800 && val < 0xe000) {
4542 return -1;
4543 }
4544 *len = 3;
4545 return val;
4546 }
4547 if avail < 4 || (*ptr.add(3) & 0xc0) != 0x80 {
4548 return -1;
4549 }
4550 let val = (((c & 0x07) as c_int) << 18)
4551 | (((*ptr.add(1) & 0x3f) as c_int) << 12)
4552 | (((*ptr.add(2) & 0x3f) as c_int) << 6)
4553 | ((*ptr.add(3) & 0x3f) as c_int);
4554 if val < 0x10000 || val >= 0x110000 {
4555 return -1;
4556 }
4557 *len = 4;
4558 val
4559 }
4560}
4561
4562/// XML 1.0 (pre-revision-5) NameStartChar predicate: Letter, '_' or ':'.
4563const fn is_name_start_char_old10(c: c_int) -> bool {
4564 (c >= b'a' as c_int && c <= b'z' as c_int)
4565 || (c >= b'A' as c_int && c <= b'Z' as c_int)
4566 || c == b'_' as c_int
4567 || c == b':' as c_int
4568 || (c >= 0xC0 && c <= 0xD6)
4569 || (c >= 0xD8 && c <= 0xF6)
4570 || (c >= 0xF8 && c <= 0x2FF)
4571 || (c >= 0x370 && c <= 0x37D)
4572 || (c >= 0x37F && c <= 0x1FFF)
4573 || (c >= 0x200C && c <= 0x200D)
4574 || (c >= 0x2070 && c <= 0x218F)
4575 || (c >= 0x2C00 && c <= 0x2FEF)
4576 || (c >= 0x3001 && c <= 0xD7FF)
4577 || (c >= 0xF900 && c <= 0xFDCF)
4578 || (c >= 0xFDF0 && c <= 0xFFFD)
4579 || (c >= 0x10000 && c <= 0xEFFFF)
4580}
4581
4582/// XML 1.0 (pre-revision-5) NameChar predicate: NameStartChar, digits, '.',
4583/// '-', combining chars and extenders.
4584const fn is_name_char_old10(c: c_int) -> bool {
4585 is_name_start_char_old10(c)
4586 || (c >= b'0' as c_int && c <= b'9' as c_int)
4587 || c == b'.' as c_int
4588 || c == b'-' as c_int
4589 || c == 0xB7
4590 || (c >= 0x300 && c <= 0x36F)
4591 || c == 0x02D0
4592 || c == 0x02D1
4593 || c == 0x0387
4594 || c == 0x0640
4595 || c == 0x0E46
4596 || c == 0x0EC6
4597 || c == 0x3005
4598 || (c >= 0x3031 && c <= 0x3035)
4599 || (c >= 0x309D && c <= 0x309E)
4600 || (c >= 0x30FC && c <= 0x30FE)
4601}
4602
4603/// Decode entities from the current input position: char references and
4604/// (predefined and DTD-declared) entity references are substituted. Stops at
4605/// the first of `end`/`end2`/`end3`, or after `len` bytes (`len < 0` = rest).
4606///
4607/// # UPSTREAM-PARITY
4608///
4609/// ```c
4610/// xmlChar *xmlDecodeEntities(xmlParserCtxtPtr ctxt, int len, xmlChar end,
4611/// xmlChar end2, xmlChar end3);
4612/// ```
4613///
4614/// # SAFETY
4615///
4616/// - `ctxt` must be valid pointers (or NULL
4617/// where the upstream C contract allows), obtained from the
4618/// matching constructor/owner and not yet freed; the callee may
4619/// take or keep ownership exactly as the C API specifies.
4620///
4621/// The caller must not race this call with concurrent mutation of the
4622/// same objects from other threads (per-object state is not internally
4623/// synchronized). Violating any of the above is undefined behavior.
4624///
4625/// Exercised by the C-API differential courts
4626/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
4627/// courts; those pass byte-for-byte against the upstream oracle.
4628#[no_mangle]
4629pub unsafe extern "C" fn xmlDecodeEntities(
4630 ctxt: *mut _xmlParserCtxt,
4631 len: c_int,
4632 end: xmlChar,
4633 end2: xmlChar,
4634 end3: xmlChar,
4635) -> *mut xmlChar {
4636 if ctxt.is_null() || (*ctxt).input.is_null() {
4637 return ptr::null_mut();
4638 }
4639 unsafe {
4640 let pi = &*((*ctxt).input);
4641 let cur = pi.cur;
4642 if cur.is_null() {
4643 return ptr::null_mut();
4644 }
4645 let avail = (pi.end as usize).saturating_sub(cur as usize);
4646 let n = if len < 0 {
4647 avail
4648 } else {
4649 (len as usize).min(avail)
4650 };
4651
4652 let mut out: Vec<u8> = Vec::new();
4653 let mut i = 0usize;
4654
4655 while i < n {
4656 let c = *cur.add(i);
4657 if c == end || c == end2 || c == end3 {
4658 break;
4659 }
4660 if c != b'&' {
4661 out.push(c);
4662 i += 1;
4663 continue;
4664 }
4665
4666 // Character reference: &#...; or &#x...;
4667 if i + 1 < n && *cur.add(i + 1) == b'#' {
4668 let (value, consumed) = parse_char_ref(cur.add(i), n - i);
4669 if consumed == 0 {
4670 out.push(b'&');
4671 i += 1;
4672 continue;
4673 }
4674 let mut buf = [0u8; 4];
4675 let blen = copy_char_utf8(&mut buf, value);
4676 out.extend_from_slice(&buf[..blen]);
4677 i += consumed;
4678 continue;
4679 }
4680
4681 // Entity reference: &name;
4682 let mut j = i + 1;
4683 while j < n
4684 && ((*cur.add(j)).is_ascii_alphanumeric()
4685 || *cur.add(j) == b'_'
4686 || *cur.add(j) == b'-'
4687 || *cur.add(j) == b'.'
4688 || *cur.add(j) == b':')
4689 {
4690 j += 1;
4691 }
4692 if j < n && *cur.add(j) == b';' {
4693 let name = core::slice::from_raw_parts(cur.add(i + 1), j - i - 1);
4694 let mut replaced = false;
4695 // Predefined entities.
4696 let content: Option<&[u8]> = match name {
4697 b"amp" => Some(b"&"),
4698 b"lt" => Some(b"<"),
4699 b"gt" => Some(b">"),
4700 b"quot" => Some(b"\""),
4701 b"apos" => Some(b"'"),
4702 _ => None,
4703 };
4704 if let Some(c) = content {
4705 out.extend_from_slice(c);
4706 replaced = true;
4707 } else {
4708 // DTD-declared entity.
4709 let mut name_nul = name.to_vec();
4710 name_nul.push(0);
4711 let ent = entities::get_entity((*ctxt).myDoc, name_nul.as_ptr());
4712 if !ent.is_null() && !(*ent).content.is_null() {
4713 let clen = string::xml_strlen((*ent).content);
4714 out.extend_from_slice(core::slice::from_raw_parts((*ent).content, clen));
4715 replaced = true;
4716 }
4717 }
4718 if replaced {
4719 i = j + 1;
4720 continue;
4721 }
4722 }
4723 out.push(b'&');
4724 i += 1;
4725 }
4726
4727 out.push(0);
4728 let result = xmlMallocImpl(out.len()) as *mut xmlChar;
4729 if result.is_null() {
4730 return ptr::null_mut();
4731 }
4732 ptr::copy_nonoverlapping(out.as_ptr(), result, out.len());
4733 result
4734 }
4735}
4736
4737/// Parse a numeric character reference at `ptr` (starting at '&#'); returns
4738/// the value and total bytes consumed, or (0, 0) when malformed.
4739const unsafe fn parse_char_ref(ptr: *const u8, avail: usize) -> (c_int, usize) {
4740 unsafe {
4741 if avail < 3 || *ptr != b'&' || *ptr.add(1) != b'#' {
4742 return (0, 0);
4743 }
4744 let mut i = 2usize;
4745 let hex = i < avail && (*ptr.add(i) == b'x' || *ptr.add(i) == b'X');
4746 if hex {
4747 i += 1;
4748 }
4749 let start = i;
4750 let mut value: u32 = 0;
4751 while i < avail && *ptr.add(i) != b';' {
4752 let d = (*ptr.add(i) as char).to_digit(if hex { 16 } else { 10 });
4753 match d {
4754 Some(d) => {
4755 value = value
4756 .saturating_mul(if hex { 16 } else { 10 })
4757 .saturating_add(d);
4758 i += 1;
4759 }
4760 None => return (0, 0),
4761 }
4762 }
4763 if i == start || i >= avail || *ptr.add(i) != b';' {
4764 return (0, 0);
4765 }
4766 (value as c_int, i + 1)
4767 }
4768}
4769
4770/// Encode a codepoint into a UTF-8 byte sequence; returns the byte count.
4771const fn copy_char_utf8(out: &mut [u8; 4], val: c_int) -> usize {
4772 if val < 0x80 {
4773 out[0] = val as u8;
4774 1
4775 } else if val < 0x800 {
4776 out[0] = 0xC0 | ((val >> 6) as u8);
4777 out[1] = 0x80 | ((val & 0x3F) as u8);
4778 2
4779 } else if val < 0x10000 {
4780 out[0] = 0xE0 | ((val >> 12) as u8);
4781 out[1] = 0x80 | (((val >> 6) & 0x3F) as u8);
4782 out[2] = 0x80 | ((val & 0x3F) as u8);
4783 3
4784 } else if val < 0x110000 {
4785 out[0] = 0xF0 | ((val >> 18) as u8);
4786 out[1] = 0x80 | (((val >> 12) & 0x3F) as u8);
4787 out[2] = 0x80 | (((val >> 6) & 0x3F) as u8);
4788 out[3] = 0x80 | ((val & 0x3F) as u8);
4789 4
4790 } else {
4791 out[0] = 0;
4792 1
4793 }
4794}
4795
4796/// Detect the character encoding of a buffer from its initial bytes.
4797///
4798/// # UPSTREAM-PARITY
4799///
4800/// ```c
4801/// xmlCharEncoding xmlDetectCharEncoding(const unsigned char *in, int len);
4802/// ```
4803///
4804/// # SAFETY
4805///
4806/// - `in_` must be valid pointers (or NULL
4807/// where the upstream C contract allows), obtained from the
4808/// matching constructor/owner and not yet freed; the callee may
4809/// take or keep ownership exactly as the C API specifies.
4810///
4811/// The caller must not race this call with concurrent mutation of the
4812/// same objects from other threads (per-object state is not internally
4813/// synchronized). Violating any of the above is undefined behavior.
4814///
4815/// Exercised by the C-API differential courts
4816/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
4817/// courts; those pass byte-for-byte against the upstream oracle.
4818#[no_mangle]
4819pub const unsafe extern "C" fn xmlDetectCharEncoding(in_: *const c_uchar, len: c_int) -> c_int {
4820 if in_.is_null() {
4821 return xmlCharEncoding::XML_CHAR_ENCODING_NONE as c_int;
4822 }
4823 unsafe {
4824 if len >= 4 {
4825 if *in_ == 0x00 && *in_.add(1) == 0x00 && *in_.add(2) == 0x00 && *in_.add(3) == 0x3C {
4826 return xmlCharEncoding::XML_CHAR_ENCODING_UCS4BE as c_int;
4827 }
4828 if *in_ == 0x3C && *in_.add(1) == 0x00 && *in_.add(2) == 0x00 && *in_.add(3) == 0x00 {
4829 return xmlCharEncoding::XML_CHAR_ENCODING_UCS4LE as c_int;
4830 }
4831 if *in_ == 0x4C && *in_.add(1) == 0x6F && *in_.add(2) == 0xA7 && *in_.add(3) == 0x94 {
4832 return xmlCharEncoding::XML_CHAR_ENCODING_EBCDIC as c_int;
4833 }
4834 if *in_ == 0x3C && *in_.add(1) == 0x3F && *in_.add(2) == 0x78 && *in_.add(3) == 0x6D {
4835 return xmlCharEncoding::XML_CHAR_ENCODING_UTF8 as c_int;
4836 }
4837 if *in_ == 0x3C && *in_.add(1) == 0x00 && *in_.add(2) == 0x3F && *in_.add(3) == 0x00 {
4838 return xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE as c_int;
4839 }
4840 if *in_ == 0x00 && *in_.add(1) == 0x3C && *in_.add(2) == 0x00 && *in_.add(3) == 0x3F {
4841 return xmlCharEncoding::XML_CHAR_ENCODING_UTF16BE as c_int;
4842 }
4843 }
4844 if len >= 3 && *in_ == 0xEF && *in_.add(1) == 0xBB && *in_.add(2) == 0xBF {
4845 return xmlCharEncoding::XML_CHAR_ENCODING_UTF8 as c_int;
4846 }
4847 if len >= 2 {
4848 if *in_ == 0xFE && *in_.add(1) == 0xFF {
4849 return xmlCharEncoding::XML_CHAR_ENCODING_UTF16BE as c_int;
4850 }
4851 if *in_ == 0xFF && *in_.add(1) == 0xFE {
4852 return xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE as c_int;
4853 }
4854 }
4855 }
4856 xmlCharEncoding::XML_CHAR_ENCODING_NONE as c_int
4857}
4858
4859/// Convert the first line of `in` using the encoding handler, appending the
4860/// result to `out`.
4861///
4862/// # UPSTREAM-PARITY
4863///
4864/// ```c
4865/// int xmlCharEncFirstLine(xmlCharEncodingHandlerPtr handler,
4866/// struct _xmlBuffer *out, struct _xmlBuffer *in);
4867/// ```
4868///
4869/// # SAFETY
4870///
4871/// - `handler`, `out`, `in_` must be valid pointers (or NULL
4872/// where the upstream C contract allows), obtained from the
4873/// matching constructor/owner and not yet freed; the callee may
4874/// take or keep ownership exactly as the C API specifies.
4875///
4876/// The caller must not race this call with concurrent mutation of the
4877/// same objects from other threads (per-object state is not internally
4878/// synchronized). Violating any of the above is undefined behavior.
4879///
4880/// Exercised by the C-API differential courts
4881/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
4882/// courts; those pass byte-for-byte against the upstream oracle.
4883#[no_mangle]
4884pub unsafe extern "C" fn xmlCharEncFirstLine(
4885 handler: *mut _xmlCharEncodingHandler,
4886 out: *mut _xmlBuffer,
4887 in_: *mut _xmlBuffer,
4888) -> c_int {
4889 encoding::xmlCharEncInFunc(handler, out, in_)
4890}
4891
4892/// Check whether the current thread is the main thread.
4893///
4894/// # UPSTREAM-PARITY
4895///
4896/// ```c
4897/// int xmlIsMainThread(void);
4898/// ```
4899///
4900/// # SAFETY
4901///
4902/// The function touches crate-global state only; it is safe
4903/// as long as the caller respects the library's global
4904/// initialization/cleanup ordering (xmlInitParser before use,
4905/// xmlCleanupParser only after all users are done).
4906///
4907/// Violating the global lifecycle ordering, or calling this after
4908/// teardown or from a signal handler, is undefined behavior.
4909#[no_mangle]
4910pub const unsafe extern "C" fn xmlIsMainThread() -> c_int {
4911 1
4912}
4913
4914// ═══════════════════════════════════════════════════════════════════════════════
4915// Error reporting helpers (xmlerror.h)
4916// ═══════════════════════════════════════════════════════════════════════════════
4917
4918/// Print file and line information for a parser input to the generic error
4919/// channel.
4920///
4921/// # UPSTREAM-PARITY
4922///
4923/// ```c
4924/// void xmlParserPrintFileInfo(struct _xmlParserInput *input);
4925/// ```
4926///
4927/// # SAFETY
4928///
4929/// - `input` must be valid pointers (or NULL
4930/// where the upstream C contract allows), obtained from the
4931/// matching constructor/owner and not yet freed; the callee may
4932/// take or keep ownership exactly as the C API specifies.
4933///
4934/// The caller must not race this call with concurrent mutation of the
4935/// same objects from other threads (per-object state is not internally
4936/// synchronized). Violating any of the above is undefined behavior.
4937///
4938/// Exercised by the C-API differential courts
4939/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
4940/// courts; those pass byte-for-byte against the upstream oracle.
4941#[no_mangle]
4942pub unsafe extern "C" fn xmlParserPrintFileInfo(input: *mut _xmlParserInput) {
4943 if input.is_null() {
4944 return;
4945 }
4946 unsafe {
4947 let channel: Option<xmlGenericErrorFunc> = globals::get_generic_error_func();
4948 let data = globals::get_generic_error_ctx();
4949 let Some(ch) = channel else { return };
4950
4951 let msg = if !(*input).filename.is_null() {
4952 let file = CStr::from_ptr((*input).filename);
4953 let s = format!("{}:{}: ", file.to_string_lossy(), (*input).line);
4954 std::ffi::CString::new(s).unwrap_or_default()
4955 } else {
4956 let s = format!("Entity: line {}: ", (*input).line);
4957 std::ffi::CString::new(s).unwrap_or_default()
4958 };
4959 ch(data, msg.as_ptr());
4960 }
4961}
4962
4963/// Print the input context around the current error position to the generic
4964/// error channel.
4965///
4966/// # UPSTREAM-PARITY
4967///
4968/// ```c
4969/// void xmlParserPrintFileContext(struct _xmlParserInput *input);
4970/// ```
4971///
4972/// # SAFETY
4973///
4974/// - `input` must be valid pointers (or NULL
4975/// where the upstream C contract allows), obtained from the
4976/// matching constructor/owner and not yet freed; the callee may
4977/// take or keep ownership exactly as the C API specifies.
4978///
4979/// The caller must not race this call with concurrent mutation of the
4980/// same objects from other threads (per-object state is not internally
4981/// synchronized). Violating any of the above is undefined behavior.
4982///
4983/// Exercised by the C-API differential courts
4984/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
4985/// courts; those pass byte-for-byte against the upstream oracle.
4986#[no_mangle]
4987pub unsafe extern "C" fn xmlParserPrintFileContext(input: *mut _xmlParserInput) {
4988 if input.is_null() || (*input).cur.is_null() {
4989 return;
4990 }
4991 unsafe {
4992 let channel: Option<xmlGenericErrorFunc> = globals::get_generic_error_func();
4993 let data = globals::get_generic_error_ctx();
4994 let Some(ch) = channel else { return };
4995
4996 let pi = &*input;
4997 let cur = pi.cur;
4998 let base = pi.base;
4999 let end = pi.end;
5000
5001 // Build a window of up to 80 bytes ending at `cur`.
5002 let before = if base.is_null() {
5003 0
5004 } else {
5005 (cur as usize).saturating_sub(base as usize)
5006 };
5007 let take = before.min(LINE_LEN);
5008 let start = cur.sub(take);
5009 let n = (end as usize).saturating_sub(start as usize).min(LINE_LEN);
5010
5011 let mut content = vec![0u8; n];
5012 if n > 0 {
5013 ptr::copy_nonoverlapping(start, content.as_mut_ptr(), n);
5014 }
5015 let line = std::ffi::CString::new(content.clone()).unwrap_or_default();
5016 ch(data, line.as_ptr());
5017
5018 // Caret line pointing at the current character.
5019 let mut caret = vec![b' '; take];
5020 if take < LINE_LEN + 1 {
5021 caret.push(b'^');
5022 }
5023 let caret_c = std::ffi::CString::new(caret).unwrap_or_default();
5024 ch(data, caret_c.as_ptr());
5025 }
5026}
5027
5028// ═══════════════════════════════════════════════════════════════════════════════
5029// SAX/DTD parse front-ends
5030// ═══════════════════════════════════════════════════════════════════════════════
5031
5032/// Handle an entity reference by pushing the entity's content as a new input
5033/// stream (deprecated internal API).
5034///
5035/// # UPSTREAM-PARITY
5036///
5037/// ```c
5038/// void xmlHandleEntity(xmlParserCtxtPtr ctxt, xmlEntityPtr entity);
5039/// ```
5040///
5041/// # SAFETY
5042///
5043/// - `ctxt`, `entity` must be valid pointers (or NULL
5044/// where the upstream C contract allows), obtained from the
5045/// matching constructor/owner and not yet freed; the callee may
5046/// take or keep ownership exactly as the C API specifies.
5047///
5048/// The caller must not race this call with concurrent mutation of the
5049/// same objects from other threads (per-object state is not internally
5050/// synchronized). Violating any of the above is undefined behavior.
5051///
5052/// Exercised by the C-API differential courts
5053/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
5054/// courts; those pass byte-for-byte against the upstream oracle.
5055#[no_mangle]
5056pub unsafe extern "C" fn xmlHandleEntity(ctxt: *mut _xmlParserCtxt, entity: *mut c_void) {
5057 if ctxt.is_null() {
5058 return;
5059 }
5060 unsafe {
5061 let ent = entity as *mut _xmlEntity;
5062 if ent.is_null() {
5063 return;
5064 }
5065 // Unparsed entities cannot be included by reference.
5066 if (*ent).etype == xmlEntityType::XML_EXTERNAL_GENERAL_UNPARSED_ENTITY as c_int {
5067 return;
5068 }
5069
5070 let mut input = ptr::null_mut();
5071 if !(*ent).content.is_null() {
5072 // Internal entity: push its replacement text as a new stream.
5073 let content = (*ent).content;
5074 let pi = xmlNewInputStream(ctxt);
5075 if pi.is_null() {
5076 return;
5077 }
5078 let len = string::xml_strlen(content);
5079 (*pi).base = content;
5080 (*pi).cur = content;
5081 (*pi).end = content.add(len);
5082 (*pi).length = len as c_int;
5083 (*pi).entity = ent;
5084 input = pi;
5085 } else if !(*ent).URI.is_null() {
5086 // External parsed entity: load it through the entity loader.
5087 input = xmlLoadExternalEntity(
5088 (*ent).URI as *const c_char,
5089 (*ent).ExternalID as *const c_char,
5090 ctxt,
5091 );
5092 if !input.is_null() {
5093 (*input).entity = ent;
5094 }
5095 }
5096
5097 if input.is_null() {
5098 return;
5099 }
5100 xmlPushInput(ctxt, input);
5101 }
5102}
5103
5104/// Load and parse a DTD, returning the resulting `xmlDtd` (detached from any
5105/// document).
5106///
5107/// # UPSTREAM-PARITY
5108///
5109/// ```c
5110/// xmlDtdPtr xmlSAXParseDTD(xmlSAXHandlerPtr sax, const xmlChar *publicId,
5111/// const xmlChar *systemId);
5112/// ```
5113///
5114/// # SAFETY
5115///
5116/// - `sax` must be valid pointers (or NULL
5117/// where the upstream C contract allows), obtained from the
5118/// matching constructor/owner and not yet freed; the callee may
5119/// take or keep ownership exactly as the C API specifies.
5120///
5121/// - `publicId`, `systemId` must point to valid NUL-terminated
5122/// strings (or NULL where the C contract allows) for the lifetime
5123/// of the call.
5124///
5125/// The caller must not race this call with concurrent mutation of the
5126/// same objects from other threads (per-object state is not internally
5127/// synchronized). Violating any of the above is undefined behavior.
5128///
5129/// Exercised by the C-API differential courts
5130/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
5131/// courts; those pass byte-for-byte against the upstream oracle.
5132#[no_mangle]
5133pub unsafe extern "C" fn xmlSAXParseDTD(
5134 sax: *mut _xmlSAXHandler,
5135 publicId: *const xmlChar,
5136 systemId: *const xmlChar,
5137) -> *mut _xmlDtd {
5138 if publicId.is_null() && systemId.is_null() {
5139 return ptr::null_mut();
5140 }
5141 unsafe {
5142 let ctxt = xmlNewSAXParserCtxt(sax, ptr::null_mut());
5143 if ctxt.is_null() {
5144 return ptr::null_mut();
5145 }
5146 apply_options(ctxt, XML_PARSE_DTDLOAD);
5147
5148 // Resolve via the SAX resolveEntity callback when available, else
5149 // load the system ID directly.
5150 let mut input = ptr::null_mut();
5151 if !sax.is_null() {
5152 if let Some(resolve) = (*sax).resolveEntity {
5153 input = resolve((*ctxt).userData, publicId, systemId);
5154 }
5155 }
5156 if input.is_null() {
5157 if systemId.is_null() {
5158 helpers::free_parser_ctxt(ctxt);
5159 return ptr::null_mut();
5160 }
5161 input = xmlLoadExternalEntity(systemId as *const c_char, ptr::null(), ctxt);
5162 }
5163 if input.is_null() {
5164 helpers::free_parser_ctxt(ctxt);
5165 return ptr::null_mut();
5166 }
5167
5168 // Materialise the DTD text before freeing the input struct.
5169 let data: Vec<u8> = {
5170 let pi = &*input;
5171 if !pi.base.is_null() && !pi.end.is_null() && pi.end >= pi.base {
5172 let len = (pi.end as usize).saturating_sub(pi.base as usize);
5173 core::slice::from_raw_parts(pi.base, len).to_vec()
5174 } else if !pi.buf.is_null() {
5175 input_buffer_data(pi.buf)
5176 } else {
5177 Vec::new()
5178 }
5179 };
5180 helpers::free_parser_input(input);
5181
5182 let dtd = parse_dtd_text(ctxt, &data, publicId, systemId);
5183 helpers::free_parser_ctxt(ctxt);
5184 dtd
5185 }
5186}
5187
5188/// Load and parse a DTD from an input buffer.
5189///
5190/// # UPSTREAM-PARITY
5191///
5192/// ```c
5193/// xmlDtdPtr xmlIOParseDTD(xmlSAXHandlerPtr sax, xmlParserInputBufferPtr input,
5194/// xmlCharEncoding enc);
5195/// ```
5196///
5197/// # SAFETY
5198///
5199/// - `sax`, `input` must be valid pointers (or NULL
5200/// where the upstream C contract allows), obtained from the
5201/// matching constructor/owner and not yet freed; the callee may
5202/// take or keep ownership exactly as the C API specifies.
5203///
5204/// The caller must not race this call with concurrent mutation of the
5205/// same objects from other threads (per-object state is not internally
5206/// synchronized). Violating any of the above is undefined behavior.
5207///
5208/// Exercised by the C-API differential courts
5209/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
5210/// courts; those pass byte-for-byte against the upstream oracle.
5211#[no_mangle]
5212pub unsafe extern "C" fn xmlIOParseDTD(
5213 sax: *mut _xmlSAXHandler,
5214 input: *mut _xmlParserInputBuffer,
5215 enc: c_int,
5216) -> *mut _xmlDtd {
5217 if input.is_null() {
5218 return ptr::null_mut();
5219 }
5220 unsafe {
5221 let ctxt = xmlNewSAXParserCtxt(sax, ptr::null_mut());
5222 if ctxt.is_null() {
5223 io::input_buffer_free(input);
5224 return ptr::null_mut();
5225 }
5226 apply_options(ctxt, XML_PARSE_DTDLOAD);
5227 if enc != xmlCharEncoding::XML_CHAR_ENCODING_NONE as c_int {
5228 (*ctxt).charset = enc;
5229 }
5230
5231 // Materialise the data from the input buffer.
5232 let data: Vec<u8> = input_buffer_data(input);
5233 io::input_buffer_free(input);
5234
5235 let dtd = parse_dtd_text(ctxt, &data, ptr::null(), ptr::null());
5236 helpers::free_parser_ctxt(ctxt);
5237 dtd
5238 }
5239}
5240
5241/// Extract the buffered data of an input buffer as an owned byte vector.
5242unsafe fn input_buffer_data(buf: *mut _xmlParserInputBuffer) -> Vec<u8> {
5243 unsafe {
5244 if buf.is_null() {
5245 return Vec::new();
5246 }
5247 let b = &*buf;
5248 if let Some(read) = b.readcallback {
5249 let mut out = Vec::new();
5250 let mut tmp = [0u8; 4096];
5251 loop {
5252 let n = read(
5253 b.context,
5254 tmp.as_mut_ptr() as *mut c_char,
5255 tmp.len() as c_int,
5256 );
5257 if n <= 0 {
5258 break;
5259 }
5260 out.extend_from_slice(&tmp[..n as usize]);
5261 }
5262 return out;
5263 }
5264 if !b.buffer.is_null() {
5265 let xbuf = &*(b.buffer as *mut _xmlBuffer);
5266 if !xbuf.content.is_null() && xbuf.use_ > 0 {
5267 return core::slice::from_raw_parts(xbuf.content, xbuf.use_ as usize).to_vec();
5268 }
5269 }
5270 Vec::new()
5271 }
5272}
5273
5274/// Parse an external general entity and build a tree.
5275///
5276/// # UPSTREAM-PARITY
5277///
5278/// ```c
5279/// xmlDocPtr xmlSAXParseEntity(xmlSAXHandlerPtr sax, const char *filename);
5280/// ```
5281///
5282/// # SAFETY
5283///
5284/// - `sax` must be valid pointers (or NULL
5285/// where the upstream C contract allows), obtained from the
5286/// matching constructor/owner and not yet freed; the callee may
5287/// take or keep ownership exactly as the C API specifies.
5288///
5289/// - `filename` must point to valid NUL-terminated
5290/// strings (or NULL where the C contract allows) for the lifetime
5291/// of the call.
5292///
5293/// The caller must not race this call with concurrent mutation of the
5294/// same objects from other threads (per-object state is not internally
5295/// synchronized). Violating any of the above is undefined behavior.
5296///
5297/// Exercised by the C-API differential courts
5298/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
5299/// courts; those pass byte-for-byte against the upstream oracle.
5300#[no_mangle]
5301pub unsafe extern "C" fn xmlSAXParseEntity(
5302 sax: *mut _xmlSAXHandler,
5303 filename: *const c_char,
5304) -> *mut _xmlDoc {
5305 if filename.is_null() {
5306 return ptr::null_mut();
5307 }
5308 unsafe {
5309 let ctxt = xmlNewSAXParserCtxt(sax, ptr::null_mut());
5310 if ctxt.is_null() {
5311 return ptr::null_mut();
5312 }
5313 let input = match open_filename_routed(filename, ctxt) {
5314 RoutedFileOpen::Loaded(i) => i,
5315 RoutedFileOpen::Failed => {
5316 emit_io_warning(ctxt, io_load_failure_message(filename));
5317 helpers::free_parser_ctxt(ctxt);
5318 return ptr::null_mut();
5319 }
5320 RoutedFileOpen::EntityLoaderFailed => {
5321 helpers::free_parser_ctxt(ctxt);
5322 return ptr::null_mut();
5323 }
5324 RoutedFileOpen::Builtin => match helpers::input_from_file(filename) {
5325 Ok(i) => i,
5326 Err(_) => {
5327 helpers::free_parser_ctxt(ctxt);
5328 return ptr::null_mut();
5329 }
5330 },
5331 };
5332 helpers::setup_parser_input(ctxt, input);
5333 let rc = helpers::parse_document(ctxt);
5334 let doc = (*ctxt).myDoc;
5335 (*ctxt).myDoc = ptr::null_mut();
5336 if rc != 0 || (*ctxt).wellFormed == 0 {
5337 if !doc.is_null() {
5338 tree::free_doc(doc);
5339 }
5340 helpers::free_parser_ctxt(ctxt);
5341 return ptr::null_mut();
5342 }
5343 helpers::free_parser_ctxt(ctxt);
5344 doc
5345 }
5346}
5347
5348// ═══════════════════════════════════════════════════════════════════════════════
5349// C14N: xmlC14NDocSave
5350// ═══════════════════════════════════════════════════════════════════════════════
5351
5352/// Canonicalise a document (or node set) and save it to a file.
5353///
5354/// # UPSTREAM-PARITY
5355///
5356/// ```c
5357/// int xmlC14NDocSave(xmlDocPtr doc, xmlNodeSetPtr nodes, int mode,
5358/// xmlChar **inclusive_ns_prefixes, int with_comments,
5359/// const char *filename, int compression);
5360/// ```
5361///
5362/// # SAFETY
5363///
5364/// - `doc`, `nodes`, `inclusive_ns_prefixes` must be valid pointers (or NULL
5365/// where the upstream C contract allows), obtained from the
5366/// matching constructor/owner and not yet freed; the callee may
5367/// take or keep ownership exactly as the C API specifies.
5368///
5369/// - `filename` must point to valid NUL-terminated
5370/// strings (or NULL where the C contract allows) for the lifetime
5371/// of the call.
5372///
5373/// The caller must not race this call with concurrent mutation of the
5374/// same objects from other threads (per-object state is not internally
5375/// synchronized). Violating any of the above is undefined behavior.
5376///
5377/// Exercised by the C-API differential courts
5378/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
5379/// courts; those pass byte-for-byte against the upstream oracle.
5380#[no_mangle]
5381pub unsafe extern "C" fn xmlC14NDocSave(
5382 doc: *mut _xmlDoc,
5383 nodes: *mut _xmlNodeSet,
5384 mode: c_int,
5385 inclusive_ns_prefixes: *mut *mut xmlChar,
5386 with_comments: c_int,
5387 filename: *const c_char,
5388 compression: c_int,
5389) -> c_int {
5390 if filename.is_null() {
5391 return -1;
5392 }
5393 unsafe {
5394 let output = io::output_buffer_create_filename(filename, ptr::null_mut(), compression);
5395 if output.is_null() {
5396 return -1;
5397 }
5398 let ret = crate::xml::c14n::xmlC14NDocSaveTo(
5399 doc,
5400 nodes,
5401 mode,
5402 inclusive_ns_prefixes,
5403 with_comments,
5404 output,
5405 );
5406 if ret < 0 {
5407 io::output_buffer_close(output);
5408 return -1;
5409 }
5410 let close_ret = io::output_buffer_close(output);
5411 if close_ret < 0 {
5412 -1
5413 } else {
5414 ret
5415 }
5416 }
5417}
5418
5419// ═══════════════════════════════════════════════════════════════════════════════
5420// Parser-internal stack primitives (parserInternals.h)
5421//
5422// Debian/2.9-era distro binaries link these directly (they are exported data
5423// in the distro DSO, not hidden). They manipulate the parser context's
5424// deprecated `inputTab`/`nameTab`/`nodeTab` arrays exactly as upstream
5425// parserInternals.c does. `valuePush`/`valuePop` operate on the XPath parser
5426// context value stack (xpath.c valuePush/valuePop).
5427// ═══════════════════════════════════════════════════════════════════════════════
5428
5429/// `inputPush` (parserInternals.h) — push an input onto the parser input stack.
5430#[no_mangle]
5431pub unsafe extern "C" fn inputPush(
5432 ctxt: *mut _xmlParserCtxt,
5433 value: *mut _xmlParserInput,
5434) -> *mut _xmlParserInput {
5435 if ctxt.is_null() || value.is_null() {
5436 return ptr::null_mut();
5437 }
5438 unsafe {
5439 let c = &mut *ctxt;
5440 if c.inputNr >= c.inputMax {
5441 let new_max = if c.inputMax > 0 { c.inputMax * 2 } else { 4 };
5442 let new_tab = crate::abi::allocator::xmlReallocImpl(
5443 c.inputTab as *mut core::ffi::c_void,
5444 (new_max as usize) * core::mem::size_of::<*mut _xmlParserInput>(),
5445 ) as *mut *mut _xmlParserInput;
5446 if new_tab.is_null() {
5447 return ptr::null_mut();
5448 }
5449 c.inputTab = new_tab;
5450 c.inputMax = new_max;
5451 }
5452 *c.inputTab.add(c.inputNr as usize) = value;
5453 c.inputNr += 1;
5454 value
5455 }
5456}
5457
5458/// `inputPop` (parserInternals.h) — pop an input off the parser input stack.
5459#[no_mangle]
5460pub unsafe extern "C" fn inputPop(ctxt: *mut _xmlParserCtxt) -> *mut _xmlParserInput {
5461 if ctxt.is_null() {
5462 return ptr::null_mut();
5463 }
5464 unsafe {
5465 let c = &mut *ctxt;
5466 if c.inputNr <= 0 {
5467 return ptr::null_mut();
5468 }
5469 c.inputNr -= 1;
5470 let ret = *c.inputTab.add(c.inputNr as usize);
5471 *c.inputTab.add(c.inputNr as usize) = ptr::null_mut();
5472 ret
5473 }
5474}
5475
5476/// `namePush` (parserInternals.h) — push a name onto the parser name stack.
5477#[no_mangle]
5478pub unsafe extern "C" fn namePush(
5479 ctxt: *mut _xmlParserCtxt,
5480 value: *const xmlChar,
5481) -> *const xmlChar {
5482 if ctxt.is_null() || value.is_null() {
5483 return ptr::null();
5484 }
5485 unsafe {
5486 let c = &mut *ctxt;
5487 if c.nameNr >= c.nameMax {
5488 let new_max = if c.nameMax > 0 { c.nameMax * 2 } else { 4 };
5489 let new_tab = crate::abi::allocator::xmlReallocImpl(
5490 c.nameTab as *mut core::ffi::c_void,
5491 (new_max as usize) * core::mem::size_of::<*const xmlChar>(),
5492 ) as *mut *const xmlChar;
5493 if new_tab.is_null() {
5494 return ptr::null();
5495 }
5496 c.nameTab = new_tab;
5497 c.nameMax = new_max;
5498 }
5499 *c.nameTab.add(c.nameNr as usize) = value;
5500 c.nameNr += 1;
5501 value
5502 }
5503}
5504
5505/// `namePop` (parserInternals.h) — pop a name off the parser name stack.
5506#[no_mangle]
5507pub unsafe extern "C" fn namePop(ctxt: *mut _xmlParserCtxt) -> *const xmlChar {
5508 if ctxt.is_null() {
5509 return ptr::null();
5510 }
5511 unsafe {
5512 let c = &mut *ctxt;
5513 if c.nameNr <= 0 {
5514 return ptr::null();
5515 }
5516 c.nameNr -= 1;
5517 let ret = *c.nameTab.add(c.nameNr as usize);
5518 *c.nameTab.add(c.nameNr as usize) = ptr::null();
5519 ret
5520 }
5521}
5522
5523/// `nodePush` (parserInternals.h) — push a node onto the parser node stack.
5524#[no_mangle]
5525pub unsafe extern "C" fn nodePush(
5526 ctxt: *mut _xmlParserCtxt,
5527 value: *mut _xmlNode,
5528) -> *mut _xmlNode {
5529 if ctxt.is_null() || value.is_null() {
5530 return ptr::null_mut();
5531 }
5532 unsafe {
5533 let c = &mut *ctxt;
5534 if c.nodeNr >= c.nodeMax {
5535 let new_max = if c.nodeMax > 0 { c.nodeMax * 2 } else { 4 };
5536 let new_tab = crate::abi::allocator::xmlReallocImpl(
5537 c.nodeTab as *mut core::ffi::c_void,
5538 (new_max as usize) * core::mem::size_of::<*mut _xmlNode>(),
5539 ) as *mut *mut _xmlNode;
5540 if new_tab.is_null() {
5541 return ptr::null_mut();
5542 }
5543 c.nodeTab = new_tab;
5544 c.nodeMax = new_max;
5545 }
5546 *c.nodeTab.add(c.nodeNr as usize) = value;
5547 c.nodeNr += 1;
5548 value
5549 }
5550}
5551
5552/// `nodePop` (parserInternals.h) — pop a node off the parser node stack.
5553#[no_mangle]
5554pub unsafe extern "C" fn nodePop(ctxt: *mut _xmlParserCtxt) -> *mut _xmlNode {
5555 if ctxt.is_null() {
5556 return ptr::null_mut();
5557 }
5558 unsafe {
5559 let c = &mut *ctxt;
5560 if c.nodeNr <= 0 {
5561 return ptr::null_mut();
5562 }
5563 c.nodeNr -= 1;
5564 let ret = *c.nodeTab.add(c.nodeNr as usize);
5565 *c.nodeTab.add(c.nodeNr as usize) = ptr::null_mut();
5566 ret
5567 }
5568}
5569
5570/// `valuePush` (xpath.c) — push an XPath object onto the parser value stack.
5571#[no_mangle]
5572pub unsafe extern "C" fn valuePush(
5573 ctxt: *mut crate::xml::xpath::parser_context::XmlXPathParserContext,
5574 value: *mut _xmlXPathObject,
5575) -> c_int {
5576 if crate::xml::xpath::parser_context::value_push(ctxt, value).is_null() {
5577 -1
5578 } else {
5579 0
5580 }
5581}
5582
5583/// `valuePop` (xpath.c) — pop an XPath object off the parser value stack.
5584#[no_mangle]
5585pub unsafe extern "C" fn valuePop(
5586 ctxt: *mut crate::xml::xpath::parser_context::XmlXPathParserContext,
5587) -> *mut _xmlXPathObject {
5588 crate::xml::xpath::parser_context::value_pop(ctxt)
5589}
5590
5591#[cfg(test)]
5592mod tests {
5593 use super::*;
5594
5595 /// A read-callback state pair mirroring PHP's streams IO loader: the
5596 /// registered `xmlParserInputBufferCreateFilenameDefault` serves bytes
5597 /// through an `xmlParserInputBufferCreateIO` buffer (php builds exactly
5598 /// this shape with php_libxml_streams_IO_read/close over a php_stream).
5599 /// The loader reaches the state through a thread-local pointer (the
5600 /// loader slot itself is per-thread TLS, so there is no cross-thread
5601 /// aliasing).
5602 struct ServeState {
5603 data: &'static [u8],
5604 pos: usize,
5605 closed: bool,
5606 }
5607
5608 thread_local! {
5609 static SERVE_STATE: std::cell::Cell<*mut ServeState> =
5610 std::cell::Cell::new(std::ptr::null_mut());
5611 }
5612
5613 unsafe extern "C" fn serve_read(ctx: *mut c_void, buffer: *mut c_char, len: c_int) -> c_int {
5614 // SAFETY: ctx is the ServeState set up by the test; buffer is a
5615 // writable len-byte region per the xmlInputReadCallback contract.
5616 let st = unsafe { &mut *(ctx as *mut ServeState) };
5617 if st.pos >= st.data.len() {
5618 return 0;
5619 }
5620 let n = (len as usize).min(st.data.len() - st.pos);
5621 unsafe {
5622 core::ptr::copy_nonoverlapping(st.data.as_ptr().add(st.pos), buffer as *mut u8, n);
5623 }
5624 st.pos += n;
5625 n as c_int
5626 }
5627
5628 unsafe extern "C" fn serve_close(ctx: *mut c_void) -> c_int {
5629 // SAFETY: ctx is the ServeState set up by the test.
5630 let st = unsafe { &mut *(ctx as *mut ServeState) };
5631 st.closed = true;
5632 0
5633 }
5634
5635 /// The php-shaped loader: build an IO buffer over the thread-local serve
5636 /// state. `uri` is deliberately ignored — php's loader opens whatever the
5637 /// php streams layer resolves, so a "file://" URI or a non-existent path
5638 /// both reach the stream; this guard proves the ENGINE consults the
5639 /// loader instead of the built-in path (which would fail on the bogus
5640 /// URI used here).
5641 unsafe extern "C" fn serving_loader(
5642 _uri: *const c_char,
5643 _enc: c_int,
5644 ) -> *mut _xmlParserInputBuffer {
5645 SERVE_STATE.with(|cell| {
5646 let st = cell.get();
5647 if st.is_null() {
5648 return ptr::null_mut();
5649 }
5650 crate::abi::exports_xml2::xmlParserInputBufferCreateIO(
5651 Some(serve_read as xmlInputReadCallback),
5652 Some(serve_close as xmlInputCloseCallback),
5653 st as *mut c_void,
5654 xmlCharEncoding::XML_CHAR_ENCODING_NONE as c_int,
5655 )
5656 })
5657 }
5658
5659 unsafe extern "C" fn record_message(ctx: *mut c_void, err: *const _xmlError) {
5660 if err.is_null() {
5661 return;
5662 }
5663 // SAFETY: ctx is the recording Vec set up by the test; the error is
5664 // live for the call and its message is NUL-terminated.
5665 let out = unsafe { &mut *(ctx as *mut Vec<u8>) };
5666 let msg = unsafe { (*err).message };
5667 if !msg.is_null() {
5668 // SAFETY: message is a NUL-terminated C string for the call.
5669 let bytes = unsafe { std::ffi::CStr::from_ptr(msg) }.to_bytes();
5670 out.extend_from_slice(bytes);
5671 }
5672 }
5673
5674 /// SP-14.3.2 S8 / dom-L2 (bug79971_1): a registered
5675 /// `xmlParserInputBufferCreateFilenameDefault` (PHP's streams loader) is
5676 /// consulted by the main-document file open (`xmlReadFile`/
5677 /// `xmlCtxtReadFile` -> xmlNewInputFromFile -> xmlNewInputFromUrl): its
5678 /// bytes are parsed even when the URI is not a real file, and a NULL
5679 /// loader result reports the xmlCtxtErrIO "failed to load" warning with
5680 /// NO built-in fallback.
5681 ///
5682 /// # Safety
5683 ///
5684 /// - the callbacks and stack state are valid for the duration of each
5685 /// call; the loader/generic-handler TLS slots are restored before the
5686 /// test ends (serialized via the error-handler test lock).
5687 #[test]
5688 fn test_main_doc_open_consults_registered_input_loader() {
5689 use crate::xml::globals::ERROR_HANDLER_TEST_LOCK;
5690
5691 // Serialize against the handler-slot tests (the generic func slot is
5692 // shared global state); the loader slot is this thread's TLS but it
5693 // is restored so later engine state stays pristine.
5694 let _guard = ERROR_HANDLER_TEST_LOCK.lock();
5695 let old_loader = globals::get_parser_input_buffer_create_filename_value();
5696 let old_struct = globals::get_structured_error_func();
5697 let old_struct_ctx = globals::get_structured_error_ctx();
5698
5699 let mut captured: Vec<u8> = Vec::new();
5700 let captured_ptr = &mut captured as *mut Vec<u8> as *mut c_void;
5701 // SAFETY: set/restore of the handler slots is serialized under
5702 // ERROR_HANDLER_TEST_LOCK for the test's duration.
5703 unsafe {
5704 globals::set_structured_error_func(
5705 captured_ptr,
5706 Some(record_message as xmlStructuredErrorFunc),
5707 );
5708 }
5709
5710 unsafe {
5711 let mut serve = ServeState {
5712 data: b"<root><a>1</a></root>",
5713 pos: 0,
5714 closed: false,
5715 };
5716 SERVE_STATE.with(|cell| cell.set(&mut serve as *mut ServeState));
5717 globals::set_parser_input_buffer_create_filename_value(Some(serving_loader));
5718
5719 // The URI names no real file — only the loader can satisfy it.
5720 let ctxt = helpers::create_parser_ctxt();
5721 assert!(!ctxt.is_null());
5722 let doc = xmlCtxtReadFile(
5723 ctxt,
5724 c"file:///definitely-not-a-file.xml".as_ptr(),
5725 ptr::null(),
5726 0,
5727 );
5728 assert!(
5729 !doc.is_null(),
5730 "registered loader must be consulted for the main document open"
5731 );
5732 let root = (*doc).children;
5733 assert!(
5734 !root.is_null() && !(*root).name.is_null(),
5735 "served document must produce a root element"
5736 );
5737 assert_eq!(
5738 crate::xml::string::xmlstr_to_bytes((*root).name as *const u8),
5739 b"root",
5740 "document served by the loader must be parsed"
5741 );
5742 assert!(serve.closed, "loader stream must be closed exactly once");
5743 tree::free_doc(doc);
5744 helpers::free_parser_ctxt(ctxt);
5745
5746 // A loader result of NULL is XML_IO_ENOENT: the built-in open is
5747 // NOT attempted and the xmlCtxtErrIO ENOENT report ("failed to
5748 // load") reaches the structured handler.
5749 globals::set_parser_input_buffer_create_filename_value(None);
5750 SERVE_STATE.with(|cell| cell.set(ptr::null_mut()));
5751 unsafe extern "C" fn null_loader(
5752 _uri: *const c_char,
5753 _enc: c_int,
5754 ) -> *mut _xmlParserInputBuffer {
5755 ptr::null_mut()
5756 }
5757 globals::set_parser_input_buffer_create_filename_value(Some(null_loader));
5758 let ctxt2 = helpers::create_parser_ctxt();
5759 assert!(!ctxt2.is_null());
5760 let doc2 = xmlCtxtReadFile(
5761 ctxt2,
5762 c"file:///definitely-not-a-file.xml".as_ptr(),
5763 ptr::null(),
5764 0,
5765 );
5766 assert!(doc2.is_null(), "NULL loader result must fail the open");
5767 let got = String::from_utf8_lossy(&captured);
5768 assert!(
5769 got.contains("failed to load"),
5770 "xmlCtxtErrIO report must reach the error channel: {got:?}"
5771 );
5772 helpers::free_parser_ctxt(ctxt2);
5773 }
5774
5775 // Restore both slots.
5776 unsafe {
5777 globals::set_parser_input_buffer_create_filename_value(old_loader);
5778 globals::set_structured_error_func(old_struct_ctx, old_struct);
5779 }
5780 }
5781}