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