Skip to main content

libxml_rs/abi/
exports_shell.rs

1//! exports_shell — XML shell family C ABI exports (11.1-I shell closure).
2//!
3//! Faithful port of the libxml2 shell section that lived in `debugXML.c`
4//! (the file was named `xmlshell.c` before libxml2 2.0 and `shell.c` after
5//! the 2.13 refactor). The fourteen `xmlShell*` symbols were removed from
6//! `debugXML.h` in 2.13+ but are still exported from the DSO; the reference
7//! ABI is libxml2 2.12.6 (the last release declaring them in the public
8//! header).
9//!
10//! # UPSTREAM-PARITY
11//!
12//! Signatures follow `debugXML.h` (2.0.0 → 2.12.6 are identical):
13//!
14//! ```c
15//! typedef char * (* xmlShellReadlineFunc)(char *prompt);
16//! struct _xmlShellCtxt {
17//!     char *filename;
18//!     xmlDocPtr doc;
19//!     xmlNodePtr node;
20//!     xmlXPathContextPtr pctxt;
21//!     int loaded;
22//!     FILE *output;
23//!     xmlShellReadlineFunc input;
24//! };
25//! typedef int (* xmlShellCmd)(xmlShellCtxtPtr ctxt, char *arg,
26//!                             xmlNodePtr node, xmlNodePtr node2);
27//! ```
28//!
29//! The `FILE*` is opaque at the ABI boundary (`*mut c_void`); string output
30//! goes through the `fwrite`/`fputs` libc symbols declared below. Command
31//! functions write their results to `ctxt->output` (or stdout when no
32//! context is given), matching upstream's `xmlShellPrintNodeCtxt`/`xmlShellCat`
33//! convention. Error messages use the generic error channel, which defaults
34//! to stderr (upstream `xmlGenericError` / `xmlGenericErrorContext`).
35//!
36//! # DIVERGENCES (recorded residuals)
37//!
38//! - `xmlShellPrintXPathResult`: upstream 2.12.x exported a no-op for a NULL
39//!   shell context; per the shell family closure directive it prints the
40//!   object with `xmlXPathDebugDumpObject(stdout, list, 0)` — the same call
41//!   the 2.12.x shell loop uses for its `xpath` command.
42//! - `xmlShellValidate` with a DTD argument parses the DTD file with a
43//!   minimal `<!DOCTYPE>` header scan (the crate has no `xmlParseDTD`
44//!   external-subset parser); a file that cannot be scanned yields NULL and
45//!   `-1`, exactly like upstream's failed `xmlParseDTD`.
46//! - The `set` (fragment parse) and `relaxng` shell commands are not
47//!   dispatched (`xmlParseInNodeContext` / RELAX NG integration not yet
48//!   available in the crate); their help lines are omitted.
49//!
50//! # Upstream contract
51//!
52//! Parity target is the libxml2 shell section of `debugXML.c` (the file named
53//! `xmlshell.c` before 2.0 and `shell.c` after the 2.13 refactor); the
54//! fourteen `xmlShell*` symbols were removed from `debugXML.h` in 2.13+ but
55//! are still exported from the DSO, so the reference ABI is 2.12.6 (last
56//! release declaring them) while the runtime target is 2.15.3. R-000168
57//! (11.1-U) recorded the c_char(u8-on-aarch64) buffer typing fix in the shell
58//! debugger.
59//!
60//! # Conceptual behavior
61//!
62//! This module implements the interactive XML shell commands: node printing,
63//! XPath evaluation and printing, directory/base traversal, element/attr/
64//! namespace listing, document validation and the load/save commands, writing
65//! through `ctxt->output` (or stdout) and the generic error channel exactly
66//! like upstream.
67//!
68//! # Ownership & safety invariants
69//!
70//! The `_xmlShellCtxt` is caller-owned; the shell borrows its doc/node/pctxt
71//! (never frees them — the caller owns the document). Strings returned by
72//! `xmlShellReadlineFunc` are caller-allocated. The `FILE*` output is opaque
73//! at the ABI boundary.
74//!
75//! # Historical quirks & epochs
76//!
77//! The shell API is the 2.0-era debugging surface kept exported for xmllint
78//! compatibility; E-005/E-006 (exit-code reworks in 2.13.0 and 2.15.0)
79//! changed the CLI validation behavior that the shells validate command
80//! feeds. The 2.13 header removal is itself a historical oddity the candidate
81//! mirrors by keeping the exports.
82//!
83//! # Deliberate oddities
84//!
85//! The DIVERGENCES listed above are deliberate: `xmlShellPrintXPathResult`
86//! prints via `xmlXPathDebugDumpObject` per the shell closure directive,
87//! `xmlShellValidate` uses a minimal DOCTYPE header scan, and the `set`/
88//! `relaxng` commands are not dispatched.
89//!
90//! # Proving courts
91//!
92//! The C14N, CLI-XMLLINT, DTD, HTML, PARSER, RELAXNG, SCHEMATRON, XINCLUDE
93//! and XSD court families plus DSO-LOADER cover this module.
94//!
95//! # Tempting simplifications that would break parity
96//!
97//! A tempting simplification is to drop the shell exports because the
98//! interactive shell is rarely used — they are still oracle-DSO symbols and
99//! the DSO-LOADER court resolves them; removing them would break downstream
100//! embedding of xmllints shell. Another shortcut, routing shell output
101//! straight to stdout instead of `ctxt->output`, would break the
102//! output-redirection contract the CLI-XMLLINT courts exercise.
103
104#![allow(
105    missing_docs,
106    missing_debug_implementations,
107    non_snake_case,
108    non_camel_case_types,
109    non_upper_case_globals
110)]
111
112use core::ffi::c_void;
113use core::ptr;
114use std::os::raw::{c_char, c_int};
115
116use crate::abi::allocator::{xmlFreeImpl, xmlMallocImpl, xmlMallocZero};
117use crate::abi::exports_string::xmlStrstr;
118use crate::abi::exports_tree::{xmlNodeGetBase, xmlNodeSetBase};
119use crate::abi::exports_uri::xmlCanonicPath;
120use crate::abi::exports_xml2::{
121    xmlReadFile, xmlSaveFile, xmlStrEqual, xmlStrchr, xmlStrdup, xmlValidateDocument,
122    xmlValidateDtd, xmlXPathEval, xmlXPathFreeContext, xmlXPathFreeObject, xmlXPathNewContext,
123    xmlXPathRegisterNs,
124};
125use crate::abi::structs::{
126    _xmlAttr, _xmlDoc, _xmlDtd, _xmlNode, _xmlNodeSet, _xmlValidCtxt, _xmlXPathContext,
127    _xmlXPathObject,
128};
129use crate::abi::types::{xmlChar, xmlElementType, xmlXPathObjectType};
130use crate::xml::xpath::exports::xmlXPathDebugDumpObject;
131use crate::xml::{debug, io, tree};
132
133// ═══════════════════════════════════════════════════════════════════════════════
134// Shell context / callbacks (debugXML.h layout)
135// ═══════════════════════════════════════════════════════════════════════════════
136
137/// `char *(*xmlShellReadlineFunc)(char *prompt)` — returns a NUL-terminated
138/// line allocated by the provider; freed by the shell with `free()`.
139pub type xmlShellReadlineFunc = Option<unsafe extern "C" fn(prompt: *mut c_char) -> *mut c_char>;
140
141/// The shell context (`struct _xmlShellCtxt`, upstream debugXML.h layout).
142#[repr(C)]
143pub struct _xmlShellCtxt {
144    /// The file name the shell was started on (xmlStrdup'ed / xmlCanonicPath'ed).
145    pub filename: *mut c_char,
146    /// The current document.
147    pub doc: *mut _xmlDoc,
148    /// The current node.
149    pub node: *mut _xmlNode,
150    /// The XPath evaluation context.
151    pub pctxt: *mut _xmlXPathContext,
152    /// Whether `doc` was loaded by the shell itself (and must be freed).
153    pub loaded: c_int,
154    /// The output `FILE*` the shell writes results to.
155    pub output: *mut c_void,
156    /// The line reading callback.
157    pub input: xmlShellReadlineFunc,
158}
159
160/// `typedef int (*xmlShellCmd)(xmlShellCtxtPtr, char*, xmlNodePtr, xmlNodePtr)`
161/// — generic signature of the shell command functions (debugXML.h).
162pub type xmlShellCmd = Option<
163    unsafe extern "C" fn(*mut _xmlShellCtxt, *mut c_char, *mut _xmlNode, *mut _xmlNode) -> c_int,
164>;
165
166extern "C" {
167    /// `size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)`.
168    fn fwrite(ptr: *const c_void, size: usize, nmemb: usize, stream: *mut c_void) -> usize;
169    /// `int fputs(const char *s, FILE *stream)`.
170    fn fputs(s: *const c_char, stream: *mut c_void) -> c_int;
171    /// The libc `FILE *stdout` variable.
172    static mut stdout: *mut c_void;
173    /// The libc `FILE *stderr` variable.
174    static mut stderr: *mut c_void;
175}
176
177// ═══════════════════════════════════════════════════════════════════════════════
178// Small output helpers (the shell has no variadic fprintf in Rust)
179// ═══════════════════════════════════════════════════════════════════════════════
180
181/// Append the bytes of a NUL-terminated C string to `v`.
182unsafe fn push_cstr(v: &mut Vec<u8>, s: *const c_char) {
183    if s.is_null() {
184        return;
185    }
186    let len = libc::strlen(s);
187    v.extend_from_slice(core::slice::from_raw_parts(s as *const u8, len));
188}
189
190/// Write raw bytes to a `FILE*`.
191unsafe fn out_bytes(fp: *mut c_void, bytes: &[u8]) {
192    if fp.is_null() || bytes.is_empty() {
193        return;
194    }
195    unsafe {
196        fwrite(bytes.as_ptr() as *const c_void, 1, bytes.len(), fp);
197    }
198}
199
200/// Write a NUL-terminated C string to a `FILE*`.
201unsafe fn out_cstr(fp: *mut c_void, s: *const c_char) {
202    if fp.is_null() || s.is_null() {
203        return;
204    }
205    unsafe {
206        fputs(s, fp);
207    }
208}
209
210/// Generic error channel (upstream `xmlGenericError(xmlGenericErrorContext,
211/// fmt, ...)`); the default context is stderr.
212///
213/// Renders `arg` + `mid` + `end`, i.e. the "%s..."-style messages the
214/// shell emits.
215unsafe fn shell_generic_error(arg: *const c_char, mid: &[u8], end: &[u8]) {
216    let mut v = Vec::new();
217    unsafe {
218        push_cstr(&mut v, arg);
219    }
220    v.extend_from_slice(mid);
221    v.extend_from_slice(end);
222    unsafe {
223        out_bytes(stderr, &v);
224    }
225}
226
227/// Dump a node subtree (upstream `xmlElemDump(output, doc, cur)`, i.e.
228/// `xmlNodeDumpOutput(output, doc, cur, 0, 0, NULL)`) to a `FILE*` using the
229/// crate serializer.
230unsafe fn shell_elem_dump(fp: *mut c_void, doc: *mut _xmlDoc, node: *mut _xmlNode) -> c_int {
231    if fp.is_null() || node.is_null() {
232        return -1;
233    }
234    let buf = io::buf_create(-1);
235    if buf.is_null() {
236        return -1;
237    }
238    let ret = tree::node_dump(buf, doc, node, 0, 0);
239    if ret < 0 {
240        io::buf_free(buf);
241        return -1;
242    }
243    let content = io::buf_content(buf);
244    let len = io::buf_length(buf);
245    if !content.is_null() && len > 0 {
246        unsafe {
247            fwrite(content as *const c_void, 1, len as usize, fp);
248        }
249    }
250    io::buf_free(buf);
251    0
252}
253
254/// Dump an HTML document to a `FILE*` (upstream `htmlDocDump`).
255unsafe fn shell_html_doc_dump(fp: *mut c_void, doc: *mut _xmlDoc) -> c_int {
256    if fp.is_null() || doc.is_null() {
257        return -1;
258    }
259    let buf = io::buf_create(-1);
260    if buf.is_null() {
261        return -1;
262    }
263    let ret = crate::xml::html::doc_dump(buf, doc);
264    if ret < 0 {
265        io::buf_free(buf);
266        return -1;
267    }
268    let content = io::buf_content(buf);
269    let len = io::buf_length(buf);
270    if !content.is_null() && len > 0 {
271        unsafe {
272            fwrite(content as *const c_void, 1, len as usize, fp);
273        }
274    }
275    io::buf_free(buf);
276    ret
277}
278
279/// Dump an HTML node subtree to a `FILE*` (upstream `htmlNodeDumpFile`).
280unsafe fn shell_html_node_dump_file(fp: *mut c_void, node: *mut _xmlNode) -> c_int {
281    if fp.is_null() || node.is_null() {
282        return -1;
283    }
284    let buf = io::buf_create(-1);
285    if buf.is_null() {
286        return -1;
287    }
288    let before = io::buf_length(buf);
289    crate::xml::html::serialize_node(node, buf, 0, 0);
290    let after = io::buf_length(buf);
291    if after < 0 || before < 0 {
292        io::buf_free(buf);
293        return -1;
294    }
295    let content = io::buf_content(buf);
296    let len = io::buf_length(buf);
297    if !content.is_null() && len > 0 {
298        unsafe {
299            fwrite(content as *const c_void, 1, len as usize, fp);
300        }
301    }
302    io::buf_free(buf);
303    after - before
304}
305
306/// `xmlChar *xmlGetNodePath(const xmlNode *node)` — build the XPath-like path
307/// of a node (tree.c 2.12.6). Returns a xmlMalloc'd string or NULL.
308unsafe fn shell_get_node_path(node: *const _xmlNode) -> *mut xmlChar {
309    if node.is_null() || (*node).type_ == xmlElementType::XML_NAMESPACE_DECL as c_int {
310        return ptr::null_mut();
311    }
312
313    // Segments are collected from the node up to the root, then reversed;
314    // each segment is "sep" + name (+ "[occur]").
315    let mut segments: Vec<Vec<u8>> = Vec::new();
316    let mut cur: *const _xmlNode = node;
317
318    loop {
319        if cur.is_null() {
320            break;
321        }
322        let typ = (*cur).type_;
323        let mut seg: Vec<u8> = Vec::new();
324        let mut occur: c_int = 0;
325        let mut generic: bool;
326        let next: *const _xmlNode;
327
328        if typ == xmlElementType::XML_DOCUMENT_NODE as c_int
329            || typ == xmlElementType::XML_HTML_DOCUMENT_NODE as c_int
330        {
331            // Upstream: `if (buffer[0] == '/') break;` — any built segment
332            // starts with '/', so a non-empty segment list means we stop here.
333            if !segments.is_empty() {
334                break;
335            }
336            seg.extend_from_slice(b"/");
337            next = ptr::null();
338        } else if typ == xmlElementType::XML_ELEMENT_NODE as c_int {
339            generic = false;
340            seg.extend_from_slice(b"/");
341            let name = (*cur).name;
342            let ns = (*cur).ns;
343            if !name.is_null() {
344                if !ns.is_null() && !(*ns).prefix.is_null() {
345                    unsafe {
346                        push_cstr(&mut seg, (*ns).prefix as *const c_char);
347                    }
348                    seg.push(b':');
349                    unsafe {
350                        push_cstr(&mut seg, name as *const c_char);
351                    }
352                } else if !ns.is_null() {
353                    // Elements in the default namespace are expressed as "*".
354                    generic = true;
355                    seg.extend_from_slice(b"*");
356                } else {
357                    unsafe {
358                        push_cstr(&mut seg, name as *const c_char);
359                    }
360                }
361            }
362            next = (*cur).parent;
363
364            // Thumbler index computation (occurrence among same-name siblings).
365            let mut tmp = (*cur).prev;
366            while !tmp.is_null() {
367                if (*tmp).type_ == xmlElementType::XML_ELEMENT_NODE as c_int
368                    && (generic || unsafe { shell_same_element_name(cur, tmp) })
369                {
370                    occur += 1;
371                }
372                tmp = (*tmp).prev;
373            }
374            if occur == 0 {
375                let mut tmp = (*cur).next;
376                while !tmp.is_null() && occur == 0 {
377                    if (*tmp).type_ == xmlElementType::XML_ELEMENT_NODE as c_int
378                        && (generic || unsafe { shell_same_element_name(cur, tmp) })
379                    {
380                        occur += 1;
381                    }
382                    tmp = (*tmp).next;
383                }
384                if occur != 0 {
385                    occur = 1;
386                }
387            } else {
388                occur += 1;
389            }
390        } else if typ == xmlElementType::XML_COMMENT_NODE as c_int {
391            seg.extend_from_slice(b"/comment()");
392            next = (*cur).parent;
393
394            let mut tmp = (*cur).prev;
395            while !tmp.is_null() {
396                if (*tmp).type_ == xmlElementType::XML_COMMENT_NODE as c_int {
397                    occur += 1;
398                }
399                tmp = (*tmp).prev;
400            }
401            if occur == 0 {
402                let mut tmp = (*cur).next;
403                while !tmp.is_null() && occur == 0 {
404                    if (*tmp).type_ == xmlElementType::XML_COMMENT_NODE as c_int {
405                        occur += 1;
406                    }
407                    tmp = (*tmp).next;
408                }
409                if occur != 0 {
410                    occur = 1;
411                }
412            } else {
413                occur += 1;
414            }
415        } else if typ == xmlElementType::XML_TEXT_NODE as c_int
416            || typ == xmlElementType::XML_CDATA_SECTION_NODE as c_int
417        {
418            seg.extend_from_slice(b"/text()");
419            next = (*cur).parent;
420
421            let mut tmp = (*cur).prev;
422            while !tmp.is_null() {
423                if (*tmp).type_ == xmlElementType::XML_TEXT_NODE as c_int
424                    || (*tmp).type_ == xmlElementType::XML_CDATA_SECTION_NODE as c_int
425                {
426                    occur += 1;
427                }
428                tmp = (*tmp).prev;
429            }
430            if occur == 0 {
431                let mut tmp = (*cur).next;
432                while !tmp.is_null() {
433                    if (*tmp).type_ == xmlElementType::XML_TEXT_NODE as c_int
434                        || (*tmp).type_ == xmlElementType::XML_CDATA_SECTION_NODE as c_int
435                    {
436                        occur = 1;
437                        break;
438                    }
439                    tmp = (*tmp).next;
440                }
441            } else {
442                occur += 1;
443            }
444        } else if typ == xmlElementType::XML_PI_NODE as c_int {
445            let mut nm = Vec::new();
446            nm.extend_from_slice(b"processing-instruction('");
447            unsafe {
448                push_cstr(&mut nm, (*cur).name as *const c_char);
449            }
450            nm.extend_from_slice(b"')");
451            seg.extend_from_slice(b"/");
452            seg.extend_from_slice(&nm);
453            next = (*cur).parent;
454
455            let mut tmp = (*cur).prev;
456            while !tmp.is_null() {
457                if (*tmp).type_ == xmlElementType::XML_PI_NODE as c_int
458                    && unsafe { xmlStrEqual((*cur).name, (*tmp).name) != 0 }
459                {
460                    occur += 1;
461                }
462                tmp = (*tmp).prev;
463            }
464            if occur == 0 {
465                let mut tmp = (*cur).next;
466                while !tmp.is_null() && occur == 0 {
467                    if (*tmp).type_ == xmlElementType::XML_PI_NODE as c_int
468                        && unsafe { xmlStrEqual((*cur).name, (*tmp).name) != 0 }
469                    {
470                        occur += 1;
471                    }
472                    tmp = (*tmp).next;
473                }
474                if occur != 0 {
475                    occur = 1;
476                }
477            } else {
478                occur += 1;
479            }
480        } else if typ == xmlElementType::XML_ATTRIBUTE_NODE as c_int {
481            seg.extend_from_slice(b"/@");
482            let attr = cur as *const _xmlAttr;
483            let name = (*attr).name;
484            if !name.is_null() {
485                if !(*attr).ns.is_null() && !(*(*attr).ns).prefix.is_null() {
486                    unsafe {
487                        push_cstr(&mut seg, (*(*attr).ns).prefix as *const c_char);
488                    }
489                    seg.push(b':');
490                }
491                unsafe {
492                    push_cstr(&mut seg, name as *const c_char);
493                }
494            }
495            next = (*attr).parent;
496        } else {
497            return ptr::null_mut();
498        }
499
500        if occur != 0 {
501            seg.extend_from_slice(format!("[{}]", occur).as_bytes());
502        }
503        segments.push(seg);
504        if next.is_null() {
505            break;
506        }
507        cur = next;
508    }
509
510    // Assemble the final path: root-most segment first.
511    let mut path: Vec<u8> = Vec::new();
512    for seg in segments.iter().rev() {
513        path.extend_from_slice(seg);
514    }
515    path.push(0);
516
517    let ret = xmlMallocImpl(path.len()) as *mut xmlChar;
518    if ret.is_null() {
519        return ptr::null_mut();
520    }
521    unsafe {
522        ptr::copy_nonoverlapping(path.as_ptr(), ret, path.len());
523    }
524    ret
525}
526
527/// Upstream element-name equality for the path thumbler: same local name and
528/// same namespace (pointer equality, or equal prefixes when both are set).
529unsafe fn shell_same_element_name(a: *const _xmlNode, b: *const _xmlNode) -> bool {
530    unsafe {
531        if xmlStrEqual((*a).name, (*b).name) == 0 {
532            return false;
533        }
534        let ans = (*a).ns;
535        let bns = (*b).ns;
536        if ans == bns {
537            return true;
538        }
539        if !ans.is_null() && !bns.is_null() {
540            return xmlStrEqual((*ans).prefix, (*bns).prefix) != 0;
541        }
542        false
543    }
544}
545
546// ═══════════════════════════════════════════════════════════════════════════════
547// xmlShellPrintXPathError / xmlShellPrintNode / xmlShellPrintXPathResult
548// ═══════════════════════════════════════════════════════════════════════════════
549
550/// `void xmlShellPrintXPathError(int errorType, const char *arg)`.
551///
552/// Print the XPath error to the default error channel (stderr).
553///
554/// # SAFETY
555///
556///
557/// - `arg` must point to valid NUL-terminated
558///   strings (or NULL where the C contract allows) for the lifetime
559///   of the call.
560///
561/// The caller must not race this call with concurrent mutation of the
562/// same objects from other threads (per-object state is not internally
563/// synchronized). Violating any of the above is undefined behavior.
564///
565/// Exercised by the C-API differential courts
566/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
567/// courts; those pass byte-for-byte against the upstream oracle.
568#[no_mangle]
569pub unsafe extern "C" fn xmlShellPrintXPathError(errorType: c_int, arg: *const c_char) {
570    let default_arg = b"Result\0";
571    let arg = if arg.is_null() {
572        default_arg.as_ptr() as *const c_char
573    } else {
574        arg
575    };
576
577    if errorType == xmlXPathObjectType::XPATH_UNDEFINED as c_int {
578        unsafe { shell_generic_error(arg, b": ", b"no such node\n") };
579    } else if errorType == xmlXPathObjectType::XPATH_BOOLEAN as c_int {
580        unsafe { shell_generic_error(arg, b" is a Boolean", b"\n") };
581    } else if errorType == xmlXPathObjectType::XPATH_NUMBER as c_int {
582        unsafe { shell_generic_error(arg, b" is a number", b"\n") };
583    } else if errorType == xmlXPathObjectType::XPATH_STRING as c_int {
584        unsafe { shell_generic_error(arg, b" is a string", b"\n") };
585    } else if errorType == xmlXPathObjectType::XPATH_POINT as c_int {
586        unsafe { shell_generic_error(arg, b" is a point", b"\n") };
587    } else if errorType == xmlXPathObjectType::XPATH_RANGE as c_int
588        || errorType == xmlXPathObjectType::XPATH_LOCATIONSET as c_int
589    {
590        unsafe { shell_generic_error(arg, b" is a range", b"\n") };
591    } else if errorType == xmlXPathObjectType::XPATH_USERS as c_int {
592        unsafe { shell_generic_error(arg, b" is user-defined", b"\n") };
593    } else if errorType == xmlXPathObjectType::XPATH_XSLT_TREE as c_int {
594        unsafe { shell_generic_error(arg, b" is an XSLT value tree", b"\n") };
595    }
596}
597
598/// Print a node to the output `FILE*` of the context (stdout when `ctxt` is
599/// NULL) — upstream static `xmlShellPrintNodeCtxt`.
600unsafe fn xmlShellPrintNodeCtxt(ctxt: *mut _xmlShellCtxt, node: *mut _xmlNode) {
601    if node.is_null() {
602        return;
603    }
604    let fp = if ctxt.is_null() {
605        unsafe { stdout }
606    } else {
607        (*ctxt).output
608    };
609
610    let typ = unsafe { (*node).type_ };
611    if typ == xmlElementType::XML_DOCUMENT_NODE as c_int {
612        tree::xmlDocDump(fp, node as *mut _xmlDoc);
613    } else if typ == xmlElementType::XML_ATTRIBUTE_NODE as c_int {
614        unsafe {
615            debug::xmlDebugDumpAttrList(fp as *mut debug::_IO_FILE, node as *mut _xmlAttr, 0);
616        }
617    } else {
618        unsafe {
619            shell_elem_dump(fp, (*node).doc, node);
620        }
621    }
622    unsafe {
623        out_bytes(fp, b"\n");
624    }
625}
626
627/// `void xmlShellPrintNode(xmlNodePtr node)` — print a node to stdout.
628///
629/// # SAFETY
630///
631/// - `node` must be valid pointers (or NULL
632///   where the upstream C contract allows), obtained from the
633///   matching constructor/owner and not yet freed; the callee may
634///   take or keep ownership exactly as the C API specifies.
635///
636/// The caller must not race this call with concurrent mutation of the
637/// same objects from other threads (per-object state is not internally
638/// synchronized). Violating any of the above is undefined behavior.
639///
640/// Exercised by the C-API differential courts
641/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
642/// courts; those pass byte-for-byte against the upstream oracle.
643#[no_mangle]
644pub unsafe extern "C" fn xmlShellPrintNode(node: *mut _xmlNode) {
645    unsafe { xmlShellPrintNodeCtxt(ptr::null_mut(), node) };
646}
647
648/// `void xmlShellPrintXPathResult(xmlXPathObjectPtr list)`.
649///
650/// Prints an XPath result object to stdout via `xmlXPathDebugDumpObject`
651/// (the same printer the 2.12.x shell loop uses for its `xpath` command).
652///
653/// # SAFETY
654///
655/// - `list` must be valid pointers (or NULL
656///   where the upstream C contract allows), obtained from the
657///   matching constructor/owner and not yet freed; the callee may
658///   take or keep ownership exactly as the C API specifies.
659///
660/// The caller must not race this call with concurrent mutation of the
661/// same objects from other threads (per-object state is not internally
662/// synchronized). Violating any of the above is undefined behavior.
663///
664/// Exercised by the C-API differential courts
665/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
666/// courts; those pass byte-for-byte against the upstream oracle.
667#[no_mangle]
668pub unsafe extern "C" fn xmlShellPrintXPathResult(list: *mut _xmlXPathObject) {
669    unsafe {
670        xmlXPathDebugDumpObject(stdout, list, 0);
671    }
672}
673
674// ═══════════════════════════════════════════════════════════════════════════════
675// xmlShellList / xmlShellBase / xmlShellDir
676// ═══════════════════════════════════════════════════════════════════════════════
677
678/// `int xmlShellList(xmlShellCtxtPtr ctxt, char *arg, xmlNodePtr node,
679/// xmlNodePtr node2)` — the shell "ls" command.
680///
681/// # SAFETY
682///
683/// - `ctxt`, `_arg`, `node`, `_node2` must be valid pointers (or NULL
684///   where the upstream C contract allows), obtained from the
685///   matching constructor/owner and not yet freed; the callee may
686///   take or keep ownership exactly as the C API specifies.
687///
688/// The caller must not race this call with concurrent mutation of the
689/// same objects from other threads (per-object state is not internally
690/// synchronized). Violating any of the above is undefined behavior.
691///
692/// Exercised by the C-API differential courts
693/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
694/// courts; those pass byte-for-byte against the upstream oracle.
695#[no_mangle]
696pub unsafe extern "C" fn xmlShellList(
697    ctxt: *mut _xmlShellCtxt,
698    _arg: *mut c_char,
699    node: *mut _xmlNode,
700    _node2: *mut _xmlNode,
701) -> c_int {
702    if ctxt.is_null() {
703        return 0;
704    }
705    if node.is_null() {
706        unsafe {
707            out_bytes((*ctxt).output, b"NULL\n");
708        }
709        return 0;
710    }
711    let typ = unsafe { (*node).type_ };
712    let mut cur: *mut _xmlNode;
713    if typ == xmlElementType::XML_DOCUMENT_NODE as c_int
714        || typ == xmlElementType::XML_HTML_DOCUMENT_NODE as c_int
715    {
716        cur = unsafe { (*(node as *mut _xmlDoc)).children };
717    } else if typ == xmlElementType::XML_NAMESPACE_DECL as c_int {
718        unsafe {
719            debug::xmlLsOneNode((*ctxt).output as *mut debug::_IO_FILE, node);
720        }
721        return 0;
722    } else if !unsafe { (*node).children }.is_null() {
723        cur = unsafe { (*node).children };
724    } else {
725        unsafe {
726            debug::xmlLsOneNode((*ctxt).output as *mut debug::_IO_FILE, node);
727        }
728        return 0;
729    }
730    while !cur.is_null() {
731        unsafe {
732            debug::xmlLsOneNode((*ctxt).output as *mut debug::_IO_FILE, cur);
733            cur = (*cur).next;
734        }
735    }
736    0
737}
738
739/// `int xmlShellBase(xmlShellCtxtPtr ctxt, char *arg, xmlNodePtr node,
740/// xmlNodePtr node2)` — the shell "base" command.
741///
742/// # SAFETY
743///
744/// - `ctxt`, `_arg`, `node`, `_node2` must be valid pointers (or NULL
745///   where the upstream C contract allows), obtained from the
746///   matching constructor/owner and not yet freed; the callee may
747///   take or keep ownership exactly as the C API specifies.
748///
749/// The caller must not race this call with concurrent mutation of the
750/// same objects from other threads (per-object state is not internally
751/// synchronized). Violating any of the above is undefined behavior.
752///
753/// Exercised by the C-API differential courts
754/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
755/// courts; those pass byte-for-byte against the upstream oracle.
756#[no_mangle]
757pub unsafe extern "C" fn xmlShellBase(
758    ctxt: *mut _xmlShellCtxt,
759    _arg: *mut c_char,
760    node: *mut _xmlNode,
761    _node2: *mut _xmlNode,
762) -> c_int {
763    if ctxt.is_null() {
764        return 0;
765    }
766    if node.is_null() {
767        unsafe {
768            out_bytes((*ctxt).output, b"NULL\n");
769        }
770        return 0;
771    }
772
773    let base = unsafe { xmlNodeGetBase((*node).doc, node) };
774
775    if base.is_null() {
776        unsafe {
777            out_bytes((*ctxt).output, b" No base found !!!\n");
778        }
779    } else {
780        unsafe {
781            out_cstr((*ctxt).output, base as *const c_char);
782            out_bytes((*ctxt).output, b"\n");
783            xmlFreeImpl(base as *mut c_void);
784        }
785    }
786    0
787}
788
789/// `int xmlShellDir(xmlShellCtxtPtr ctxt, char *arg, xmlNodePtr node,
790/// xmlNodePtr node2)` — the shell "dir" command.
791///
792/// # SAFETY
793///
794/// - `ctxt`, `_arg`, `node`, `_node2` must be valid pointers (or NULL
795///   where the upstream C contract allows), obtained from the
796///   matching constructor/owner and not yet freed; the callee may
797///   take or keep ownership exactly as the C API specifies.
798///
799/// The caller must not race this call with concurrent mutation of the
800/// same objects from other threads (per-object state is not internally
801/// synchronized). Violating any of the above is undefined behavior.
802///
803/// Exercised by the C-API differential courts
804/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
805/// courts; those pass byte-for-byte against the upstream oracle.
806#[no_mangle]
807pub unsafe extern "C" fn xmlShellDir(
808    ctxt: *mut _xmlShellCtxt,
809    _arg: *mut c_char,
810    node: *mut _xmlNode,
811    _node2: *mut _xmlNode,
812) -> c_int {
813    if ctxt.is_null() {
814        return 0;
815    }
816    if node.is_null() {
817        unsafe {
818            out_bytes((*ctxt).output, b"NULL\n");
819        }
820        return 0;
821    }
822    let typ = unsafe { (*node).type_ };
823    unsafe {
824        if typ == xmlElementType::XML_DOCUMENT_NODE as c_int
825            || typ == xmlElementType::XML_HTML_DOCUMENT_NODE as c_int
826        {
827            debug::xmlDebugDumpDocumentHead(
828                (*ctxt).output as *mut debug::_IO_FILE,
829                node as *mut _xmlDoc,
830            );
831        } else if typ == xmlElementType::XML_ATTRIBUTE_NODE as c_int {
832            debug::xmlDebugDumpAttr(
833                (*ctxt).output as *mut debug::_IO_FILE,
834                node as *mut _xmlAttr,
835                0,
836            );
837        } else {
838            debug::xmlDebugDumpOneNode((*ctxt).output as *mut debug::_IO_FILE, node, 0);
839        }
840    }
841    0
842}
843
844// ═══════════════════════════════════════════════════════════════════════════════
845// xmlShellCat / xmlShellLoad / xmlShellWrite / xmlShellSave
846// ═══════════════════════════════════════════════════════════════════════════════
847
848/// `int xmlShellCat(xmlShellCtxtPtr ctxt, char *arg, xmlNodePtr node,
849/// xmlNodePtr node2)` — the shell "cat" command: dump the serialization of
850/// the node (XML or HTML, matching the document type).
851///
852/// # SAFETY
853///
854/// - `ctxt`, `_arg`, `node`, `_node2` must be valid pointers (or NULL
855///   where the upstream C contract allows), obtained from the
856///   matching constructor/owner and not yet freed; the callee may
857///   take or keep ownership exactly as the C API specifies.
858///
859/// The caller must not race this call with concurrent mutation of the
860/// same objects from other threads (per-object state is not internally
861/// synchronized). Violating any of the above is undefined behavior.
862///
863/// Exercised by the C-API differential courts
864/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
865/// courts; those pass byte-for-byte against the upstream oracle.
866#[no_mangle]
867pub unsafe extern "C" fn xmlShellCat(
868    ctxt: *mut _xmlShellCtxt,
869    _arg: *mut c_char,
870    node: *mut _xmlNode,
871    _node2: *mut _xmlNode,
872) -> c_int {
873    if ctxt.is_null() {
874        return 0;
875    }
876    if node.is_null() {
877        unsafe {
878            out_bytes((*ctxt).output, b"NULL\n");
879        }
880        return 0;
881    }
882    let out = unsafe { (*ctxt).output };
883    let is_html =
884        unsafe { (*(*ctxt).doc).type_ == xmlElementType::XML_HTML_DOCUMENT_NODE as c_int };
885    let typ = unsafe { (*node).type_ };
886    unsafe {
887        if is_html {
888            if typ == xmlElementType::XML_HTML_DOCUMENT_NODE as c_int {
889                shell_html_doc_dump(out, node as *mut _xmlDoc);
890            } else {
891                shell_html_node_dump_file(out, node);
892            }
893        } else if typ == xmlElementType::XML_DOCUMENT_NODE as c_int {
894            tree::xmlDocDump(out, node as *mut _xmlDoc);
895        } else {
896            shell_elem_dump(out, (*ctxt).doc, node);
897        }
898        out_bytes(out, b"\n");
899    }
900    0
901}
902
903/// `int xmlShellLoad(xmlShellCtxtPtr ctxt, char *filename, xmlNodePtr node,
904/// xmlNodePtr node2)` — the shell "load" command.
905///
906/// # SAFETY
907///
908/// - `ctxt`, `filename`, `_node`, `_node2` must be valid pointers (or NULL
909///   where the upstream C contract allows), obtained from the
910///   matching constructor/owner and not yet freed; the callee may
911///   take or keep ownership exactly as the C API specifies.
912///
913/// The caller must not race this call with concurrent mutation of the
914/// same objects from other threads (per-object state is not internally
915/// synchronized). Violating any of the above is undefined behavior.
916///
917/// Exercised by the C-API differential courts
918/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
919/// courts; those pass byte-for-byte against the upstream oracle.
920#[no_mangle]
921pub unsafe extern "C" fn xmlShellLoad(
922    ctxt: *mut _xmlShellCtxt,
923    filename: *mut c_char,
924    _node: *mut _xmlNode,
925    _node2: *mut _xmlNode,
926) -> c_int {
927    if ctxt.is_null() || filename.is_null() {
928        return -1;
929    }
930    let mut html = 0;
931    if !unsafe { (*ctxt).doc }.is_null() {
932        html = unsafe {
933            ((*(*ctxt).doc).type_ == xmlElementType::XML_HTML_DOCUMENT_NODE as c_int) as c_int
934        };
935    }
936
937    let doc: *mut _xmlDoc = if html != 0 {
938        // Upstream htmlParseFile (the exports_xml2 htmlParseFile is a Phase-1
939        // stub; the HTML module provides the real parser).
940        unsafe { crate::xml::html::parse_file(filename, ptr::null()) }
941    } else {
942        unsafe { xmlReadFile(filename, ptr::null(), 0) }
943    };
944
945    if !doc.is_null() {
946        unsafe {
947            if (*ctxt).loaded == 1 {
948                tree::free_doc((*ctxt).doc);
949            }
950            (*ctxt).loaded = 1;
951            xmlXPathFreeContext((*ctxt).pctxt);
952            if !(*ctxt).filename.is_null() {
953                xmlFreeImpl((*ctxt).filename as *mut c_void);
954            }
955            (*ctxt).doc = doc;
956            (*ctxt).node = doc as *mut _xmlNode;
957            (*ctxt).pctxt = xmlXPathNewContext(doc);
958            (*ctxt).filename = xmlCanonicPath(filename) as *mut c_char;
959        }
960        0
961    } else {
962        -1
963    }
964}
965
966/// `int xmlShellWrite(xmlShellCtxtPtr ctxt, char *filename, xmlNodePtr node,
967/// xmlNodePtr node2)` — the shell "write" command: write the subtree under
968/// `node` to `filename`.
969///
970/// # SAFETY
971///
972/// - `ctxt`, `filename`, `node`, `_node2` must be valid pointers (or NULL
973///   where the upstream C contract allows), obtained from the
974///   matching constructor/owner and not yet freed; the callee may
975///   take or keep ownership exactly as the C API specifies.
976///
977/// The caller must not race this call with concurrent mutation of the
978/// same objects from other threads (per-object state is not internally
979/// synchronized). Violating any of the above is undefined behavior.
980///
981/// Exercised by the C-API differential courts
982/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
983/// courts; those pass byte-for-byte against the upstream oracle.
984#[no_mangle]
985pub unsafe extern "C" fn xmlShellWrite(
986    ctxt: *mut _xmlShellCtxt,
987    filename: *mut c_char,
988    node: *mut _xmlNode,
989    _node2: *mut _xmlNode,
990) -> c_int {
991    if node.is_null() {
992        return -1;
993    }
994    if filename.is_null() || *filename == 0 {
995        unsafe {
996            shell_generic_error(
997                c"Write command requires a filename argument\n".as_ptr() as *const c_char,
998                b"",
999                b"",
1000            );
1001        }
1002        return -1;
1003    }
1004    // Upstream: `if (access(filename, W_OK))` under `#ifdef W_OK`.
1005    if libc::access(filename, libc::W_OK) != 0 {
1006        unsafe {
1007            shell_generic_error(c"Cannot write to ".as_ptr() as *const c_char, b"", b"");
1008            shell_generic_error(filename, b"", b"\n");
1009        }
1010        return -1;
1011    }
1012    let typ = unsafe { (*node).type_ };
1013    unsafe {
1014        match typ {
1015            t if t == xmlElementType::XML_DOCUMENT_NODE as c_int => {
1016                if xmlSaveFile(filename, (*ctxt).doc) < -1 {
1017                    shell_generic_error(c"Failed to write to ".as_ptr() as *const c_char, b"", b"");
1018                    shell_generic_error(filename, b"", b"\n");
1019                    return -1;
1020                }
1021            }
1022            t if t == xmlElementType::XML_HTML_DOCUMENT_NODE as c_int => {
1023                // Upstream htmlSaveFile: serialize the HTML doc to the file.
1024                if shell_save_html_doc(filename, (*ctxt).doc) < 0 {
1025                    shell_generic_error(c"Failed to write to ".as_ptr() as *const c_char, b"", b"");
1026                    shell_generic_error(filename, b"", b"\n");
1027                    return -1;
1028                }
1029            }
1030            _ => {
1031                let f = libc::fopen(filename, c"w".as_ptr() as *const c_char);
1032                if f.is_null() {
1033                    shell_generic_error(c"Failed to write to ".as_ptr() as *const c_char, b"", b"");
1034                    shell_generic_error(filename, b"", b"\n");
1035                    return -1;
1036                }
1037                shell_elem_dump(f as *mut c_void, (*ctxt).doc, node);
1038                libc::fclose(f);
1039            }
1040        }
1041    }
1042    0
1043}
1044
1045/// Serialize an HTML document to a file (upstream `htmlSaveFile`).
1046unsafe fn shell_save_html_doc(filename: *const c_char, doc: *mut _xmlDoc) -> c_int {
1047    if filename.is_null() || doc.is_null() {
1048        return -1;
1049    }
1050    let buf = io::buf_create(-1);
1051    if buf.is_null() {
1052        return -1;
1053    }
1054    let ret = crate::xml::html::doc_dump(buf, doc);
1055    if ret < 0 {
1056        io::buf_free(buf);
1057        return -1;
1058    }
1059    let content = io::buf_content(buf);
1060    let len = io::buf_length(buf);
1061    let written = if !content.is_null() && len > 0 {
1062        let f = libc::fopen(filename, c"w".as_ptr() as *const c_char);
1063        if f.is_null() {
1064            io::buf_free(buf);
1065            return -1;
1066        }
1067        let n = libc::fwrite(content as *const c_void, 1, len as usize, f);
1068        libc::fclose(f);
1069        n as c_int
1070    } else {
1071        0
1072    };
1073    io::buf_free(buf);
1074    written
1075}
1076
1077/// `int xmlShellSave(xmlShellCtxtPtr ctxt, char *filename, xmlNodePtr node,
1078/// xmlNodePtr node2)` — the shell "save" command: write the current document
1079/// to `filename`, or to its original name when no filename is given.
1080///
1081/// # SAFETY
1082///
1083/// - `ctxt`, `filename`, `_node`, `_node2` must be valid pointers (or NULL
1084///   where the upstream C contract allows), obtained from the
1085///   matching constructor/owner and not yet freed; the callee may
1086///   take or keep ownership exactly as the C API specifies.
1087///
1088/// The caller must not race this call with concurrent mutation of the
1089/// same objects from other threads (per-object state is not internally
1090/// synchronized). Violating any of the above is undefined behavior.
1091///
1092/// Exercised by the C-API differential courts
1093/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1094/// courts; those pass byte-for-byte against the upstream oracle.
1095#[no_mangle]
1096pub unsafe extern "C" fn xmlShellSave(
1097    ctxt: *mut _xmlShellCtxt,
1098    filename: *mut c_char,
1099    _node: *mut _xmlNode,
1100    _node2: *mut _xmlNode,
1101) -> c_int {
1102    if ctxt.is_null() || unsafe { (*ctxt).doc }.is_null() {
1103        return -1;
1104    }
1105    let mut filename = filename;
1106    if filename.is_null() || *filename == 0 {
1107        filename = unsafe { (*ctxt).filename };
1108    }
1109    if filename.is_null() {
1110        return -1;
1111    }
1112    // Upstream: `if (access(filename, W_OK))` under `#ifdef W_OK`.
1113    if libc::access(filename, libc::W_OK) != 0 {
1114        unsafe {
1115            shell_generic_error(c"Cannot save to ".as_ptr() as *const c_char, b"", b"");
1116            shell_generic_error(filename, b"", b"\n");
1117        }
1118        return -1;
1119    }
1120    let typ = unsafe { (*(*ctxt).doc).type_ };
1121    unsafe {
1122        match typ {
1123            t if t == xmlElementType::XML_DOCUMENT_NODE as c_int => {
1124                if xmlSaveFile(filename, (*ctxt).doc) < 0 {
1125                    shell_generic_error(c"Failed to save to ".as_ptr() as *const c_char, b"", b"");
1126                    shell_generic_error(filename, b"", b"\n");
1127                }
1128            }
1129            t if t == xmlElementType::XML_HTML_DOCUMENT_NODE as c_int => {
1130                if shell_save_html_doc(filename, (*ctxt).doc) < 0 {
1131                    shell_generic_error(c"Failed to save to ".as_ptr() as *const c_char, b"", b"");
1132                    shell_generic_error(filename, b"", b"\n");
1133                }
1134            }
1135            _ => {
1136                shell_generic_error(
1137                    c"To save to subparts of a document use the 'write' command\n".as_ptr()
1138                        as *const c_char,
1139                    b"",
1140                    b"",
1141                );
1142                return -1;
1143            }
1144        }
1145    }
1146    0
1147}
1148
1149// ═══════════════════════════════════════════════════════════════════════════════
1150// xmlShellValidate
1151// ═══════════════════════════════════════════════════════════════════════════════
1152
1153/// Validity callback matching upstream's use of `xmlGenericError` as the
1154/// valid-ctxt error/warning handler (default generic channel: stderr).
1155unsafe extern "C" fn shell_valid_error(_ctx: *mut c_void, msg: *const c_char) {
1156    unsafe {
1157        if !msg.is_null() {
1158            out_cstr(stderr, msg);
1159        }
1160    }
1161}
1162
1163/// Minimal external-DTD loader (upstream `xmlParseDTD(NULL, dtd)`).
1164///
1165/// Scans the file for a `<!DOCTYPE name (PUBLIC|SYSTEM) "..." ...>` header
1166/// and builds a `_xmlDtd` node from it. Declarations are not parsed, so
1167/// validation against an external DTD is permissive (empty declaration
1168/// tables). Returns NULL when the file cannot be read/scanned, in which
1169/// case the caller behaves exactly like upstream's failed `xmlParseDTD`.
1170unsafe fn shell_parse_dtd(dtd: *const c_char) -> *mut _xmlDtd {
1171    if dtd.is_null() {
1172        return ptr::null_mut();
1173    }
1174    let path = match unsafe { core::ffi::CStr::from_ptr(dtd) }.to_str() {
1175        Ok(p) => p,
1176        Err(_) => return ptr::null_mut(),
1177    };
1178    let content = match std::fs::read(path) {
1179        Ok(c) => c,
1180        Err(_) => return ptr::null_mut(),
1181    };
1182    // Locate the `<!DOCTYPE` keyword (case-insensitive per XML).
1183    let lower: Vec<u8> = content.iter().map(|b| b.to_ascii_lowercase()).collect();
1184    let pos = match find_subslice(&lower, b"<!doctype") {
1185        Some(p) => p,
1186        // No DOCTYPE header (plain external-subset .dtd files): use the
1187        // file name as the DTD name so validation can still proceed.
1188        None => {
1189            let base = path.rsplit(['/', '\\']).next().unwrap_or(path);
1190            let name_c = bytes_to_xmlstr(base.as_bytes());
1191            let dtd_node =
1192                crate::xml::dtd::new_dtd(ptr::null_mut(), name_c, ptr::null_mut(), ptr::null_mut());
1193            if !name_c.is_null() {
1194                xmlFreeImpl(name_c as *mut c_void);
1195            }
1196            return dtd_node;
1197        }
1198    };
1199    let mut i = pos + b"<!doctype".len();
1200    // Skip whitespace.
1201    while i < content.len() && (content[i] as char).is_ascii_whitespace() {
1202        i += 1;
1203    }
1204    // DTD name (up to the next whitespace).
1205    let name_start = i;
1206    while i < content.len()
1207        && !(content[i] as char).is_ascii_whitespace()
1208        && content[i] != b'>'
1209        && content[i] != b'['
1210    {
1211        i += 1;
1212    }
1213    if i == name_start {
1214        return ptr::null_mut();
1215    }
1216    let name = &content[name_start..i];
1217
1218    // Optional PUBLIC/SYSTEM identifiers.
1219    while i < content.len() && (content[i] as char).is_ascii_whitespace() {
1220        i += 1;
1221    }
1222    let mut public_id: Option<&[u8]> = None;
1223    let mut system_id: Option<&[u8]> = None;
1224    if i < content.len() && content[i..].to_ascii_lowercase().starts_with(b"public") {
1225        i += b"public".len();
1226        while i < content.len() && (content[i] as char).is_ascii_whitespace() {
1227            i += 1;
1228        }
1229        if i < content.len() && content[i] == b'"' {
1230            let s = i + 1;
1231            let e = content[s..].iter().position(|&c| c == b'"').map(|p| s + p);
1232            if let Some(e) = e {
1233                public_id = Some(&content[s..e]);
1234            }
1235        }
1236        while i < content.len() && (content[i] as char).is_ascii_whitespace() {
1237            i += 1;
1238        }
1239        if i < content.len() && content[i] == b'"' {
1240            let s = i + 1;
1241            let e = content[s..].iter().position(|&c| c == b'"').map(|p| s + p);
1242            if let Some(e) = e {
1243                system_id = Some(&content[s..e]);
1244            }
1245        }
1246    } else if i < content.len() && content[i..].to_ascii_lowercase().starts_with(b"system") {
1247        i += b"system".len();
1248        while i < content.len() && (content[i] as char).is_ascii_whitespace() {
1249            i += 1;
1250        }
1251        if i < content.len() && content[i] == b'"' {
1252            let s = i + 1;
1253            let e = content[s..].iter().position(|&c| c == b'"').map(|p| s + p);
1254            if let Some(e) = e {
1255                system_id = Some(&content[s..e]);
1256            }
1257        }
1258    }
1259
1260    let name_c = bytes_to_xmlstr(name);
1261    let ext_c = match public_id {
1262        Some(v) => bytes_to_xmlstr(v),
1263        None => ptr::null_mut(),
1264    };
1265    let sys_c = match system_id {
1266        Some(v) => bytes_to_xmlstr(v),
1267        None => ptr::null_mut(),
1268    };
1269    let dtd_node = crate::xml::dtd::new_dtd(ptr::null_mut(), name_c, ext_c, sys_c);
1270    if !name_c.is_null() {
1271        xmlFreeImpl(name_c as *mut c_void);
1272    }
1273    if !ext_c.is_null() {
1274        xmlFreeImpl(ext_c as *mut c_void);
1275    }
1276    if !sys_c.is_null() {
1277        xmlFreeImpl(sys_c as *mut c_void);
1278    }
1279    dtd_node
1280}
1281
1282/// Find `needle` in `haystack` (byte-wise).
1283fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option<usize> {
1284    if needle.is_empty() || haystack.len() < needle.len() {
1285        return None;
1286    }
1287    haystack.windows(needle.len()).position(|w| w == needle)
1288}
1289
1290/// Copy a byte slice into a NUL-terminated xmlMalloc'd xmlChar string.
1291unsafe fn bytes_to_xmlstr(bytes: &[u8]) -> *mut xmlChar {
1292    let buf = xmlMallocImpl(bytes.len() + 1) as *mut xmlChar;
1293    if buf.is_null() {
1294        return ptr::null_mut();
1295    }
1296    unsafe {
1297        ptr::copy_nonoverlapping(bytes.as_ptr(), buf, bytes.len());
1298        *buf.add(bytes.len()) = 0;
1299    }
1300    buf
1301}
1302
1303/// `int xmlShellValidate(xmlShellCtxtPtr ctxt, char *dtd, xmlNodePtr node,
1304/// xmlNodePtr node2)` — the shell "validate" command.
1305///
1306/// # SAFETY
1307///
1308/// - `ctxt`, `dtd`, `_node`, `_node2` must be valid pointers (or NULL
1309///   where the upstream C contract allows), obtained from the
1310///   matching constructor/owner and not yet freed; the callee may
1311///   take or keep ownership exactly as the C API specifies.
1312///
1313/// The caller must not race this call with concurrent mutation of the
1314/// same objects from other threads (per-object state is not internally
1315/// synchronized). Violating any of the above is undefined behavior.
1316///
1317/// Exercised by the C-API differential courts
1318/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1319/// courts; those pass byte-for-byte against the upstream oracle.
1320#[no_mangle]
1321pub unsafe extern "C" fn xmlShellValidate(
1322    ctxt: *mut _xmlShellCtxt,
1323    dtd: *mut c_char,
1324    _node: *mut _xmlNode,
1325    _node2: *mut _xmlNode,
1326) -> c_int {
1327    if ctxt.is_null() || unsafe { (*ctxt).doc }.is_null() {
1328        return -1;
1329    }
1330    // Upstream: `xmlValidCtxt vctxt; memset(&vctxt, 0, sizeof(vctxt));`
1331    let vctxt = xmlMallocZero(size_of::<_xmlValidCtxt>()) as *mut _xmlValidCtxt;
1332    if vctxt.is_null() {
1333        return -1;
1334    }
1335    unsafe {
1336        (*vctxt).error = Some(shell_valid_error);
1337        (*vctxt).warning = Some(shell_valid_error);
1338    }
1339    let mut res = -1;
1340    unsafe {
1341        if dtd.is_null() || *dtd == 0 {
1342            res = xmlValidateDocument(vctxt, (*ctxt).doc);
1343        } else {
1344            let subset = shell_parse_dtd(dtd as *const c_char);
1345            if !subset.is_null() {
1346                res = xmlValidateDtd(vctxt, (*ctxt).doc, subset);
1347                crate::xml::dtd::free_dtd(subset);
1348            }
1349        }
1350    }
1351    unsafe {
1352        xmlFreeImpl(vctxt as *mut c_void);
1353    }
1354    res
1355}
1356
1357// ═══════════════════════════════════════════════════════════════════════════════
1358// xmlShellDu / xmlShellPwd
1359// ═══════════════════════════════════════════════════════════════════════════════
1360
1361/// `int xmlShellDu(xmlShellCtxtPtr ctxt, char *arg, xmlNodePtr tree,
1362/// xmlNodePtr node2)` — the shell "du" command: show the structure of the
1363/// subtree under `tree`, deep-first.
1364///
1365/// # SAFETY
1366///
1367/// - `ctxt`, `_arg`, `tree`, `_node2` must be valid pointers (or NULL
1368///   where the upstream C contract allows), obtained from the
1369///   matching constructor/owner and not yet freed; the callee may
1370///   take or keep ownership exactly as the C API specifies.
1371///
1372/// The caller must not race this call with concurrent mutation of the
1373/// same objects from other threads (per-object state is not internally
1374/// synchronized). Violating any of the above is undefined behavior.
1375///
1376/// Exercised by the C-API differential courts
1377/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1378/// courts; those pass byte-for-byte against the upstream oracle.
1379#[no_mangle]
1380pub unsafe extern "C" fn xmlShellDu(
1381    ctxt: *mut _xmlShellCtxt,
1382    _arg: *mut c_char,
1383    tree: *mut _xmlNode,
1384    _node2: *mut _xmlNode,
1385) -> c_int {
1386    if ctxt.is_null() {
1387        return -1;
1388    }
1389    if tree.is_null() {
1390        return -1;
1391    }
1392    let out = unsafe { (*ctxt).output };
1393    let mut indent: c_int = 0;
1394    let mut node: *mut _xmlNode = tree;
1395    unsafe {
1396        while !node.is_null() {
1397            let typ = (*node).type_;
1398            if typ == xmlElementType::XML_DOCUMENT_NODE as c_int
1399                || typ == xmlElementType::XML_HTML_DOCUMENT_NODE as c_int
1400            {
1401                out_bytes(out, b"/\n");
1402            } else if typ == xmlElementType::XML_ELEMENT_NODE as c_int {
1403                let mut line = Vec::new();
1404                for _ in 0..indent {
1405                    line.extend_from_slice(b"  ");
1406                }
1407                if !(*node).ns.is_null() && !(*(*node).ns).prefix.is_null() {
1408                    push_cstr(&mut line, (*(*node).ns).prefix as *const c_char);
1409                    line.push(b':');
1410                }
1411                push_cstr(&mut line, (*node).name as *const c_char);
1412                line.push(b'\n');
1413                out_bytes(out, &line);
1414            }
1415
1416            /*
1417             * Browse the full subtree, deep first
1418             */
1419            if typ == xmlElementType::XML_DOCUMENT_NODE as c_int
1420                || typ == xmlElementType::XML_HTML_DOCUMENT_NODE as c_int
1421            {
1422                node = (*(node as *mut _xmlDoc)).children;
1423            } else if !(*node).children.is_null()
1424                && typ != xmlElementType::XML_ENTITY_REF_NODE as c_int
1425            {
1426                node = (*node).children;
1427                indent += 1;
1428            } else if node != tree && !(*node).next.is_null() {
1429                node = (*node).next;
1430            } else if node != tree {
1431                while node != tree {
1432                    if !(*node).parent.is_null() {
1433                        node = (*node).parent;
1434                        indent -= 1;
1435                    }
1436                    if node != tree && !(*node).next.is_null() {
1437                        node = (*node).next;
1438                        break;
1439                    }
1440                    if (*node).parent.is_null() {
1441                        node = ptr::null_mut();
1442                        break;
1443                    }
1444                    if node == tree {
1445                        node = ptr::null_mut();
1446                        break;
1447                    }
1448                }
1449                if node == tree {
1450                    node = ptr::null_mut();
1451                }
1452            } else {
1453                node = ptr::null_mut();
1454            }
1455        }
1456    }
1457    0
1458}
1459
1460/// `int xmlShellPwd(xmlShellCtxtPtr ctxt, char *buffer, xmlNodePtr node,
1461/// xmlNodePtr node2)` — the shell "pwd" command: full path of `node` into
1462/// `buffer` (which must hold at least 500 chars).
1463///
1464/// # SAFETY
1465///
1466/// - `_ctxt`, `buffer`, `node`, `_node2` must be valid pointers (or NULL
1467///   where the upstream C contract allows), obtained from the
1468///   matching constructor/owner and not yet freed; the callee may
1469///   take or keep ownership exactly as the C API specifies.
1470///
1471/// The caller must not race this call with concurrent mutation of the
1472/// same objects from other threads (per-object state is not internally
1473/// synchronized). Violating any of the above is undefined behavior.
1474///
1475/// Exercised by the C-API differential courts
1476/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1477/// courts; those pass byte-for-byte against the upstream oracle.
1478#[no_mangle]
1479pub unsafe extern "C" fn xmlShellPwd(
1480    _ctxt: *mut _xmlShellCtxt,
1481    buffer: *mut c_char,
1482    node: *mut _xmlNode,
1483    _node2: *mut _xmlNode,
1484) -> c_int {
1485    if node.is_null() || buffer.is_null() {
1486        return -1;
1487    }
1488
1489    let path = unsafe { shell_get_node_path(node) };
1490    if path.is_null() {
1491        return -1;
1492    }
1493
1494    // Upstream: `snprintf(buffer, 499, "%s", path); buffer[499] = '0';`
1495    let plen = unsafe { tree::xml_strlen(path) } as usize;
1496    let n = plen.min(498);
1497    unsafe {
1498        ptr::copy_nonoverlapping(path as *const u8, buffer as *mut u8, n);
1499        *buffer.add(n) = 0;
1500        *buffer.add(499) = b'0' as c_char;
1501    }
1502    unsafe {
1503        xmlFreeImpl(path as *mut c_void);
1504    }
1505    0
1506}
1507
1508// ═══════════════════════════════════════════════════════════════════════════════
1509// Static command helpers used by the shell loop
1510// ═══════════════════════════════════════════════════════════════════════════════
1511
1512/// Upstream static `xmlShellSetBase` — the shell "setbase" command.
1513unsafe fn xmlShellSetBase(ctxt: *mut _xmlShellCtxt, arg: *mut c_char, node: *mut _xmlNode) {
1514    let _ = ctxt;
1515    if !node.is_null() {
1516        unsafe {
1517            xmlNodeSetBase(node, arg as *const xmlChar);
1518        }
1519    }
1520}
1521
1522/// Upstream static `xmlShellRegisterNamespace` — the shell "setns" command:
1523/// register/unregister `prefix=nsuri` pairs on the XPath context.
1524unsafe fn xmlShellRegisterNamespace(ctxt: *mut _xmlShellCtxt, arg: *mut c_char) -> c_int {
1525    let ns_list_dup = unsafe { xmlStrdup(arg as *const xmlChar) };
1526    if ns_list_dup.is_null() {
1527        return -1;
1528    }
1529    let mut next: *mut xmlChar = ns_list_dup;
1530    loop {
1531        if unsafe { *next == 0 } {
1532            break;
1533        }
1534        // find prefix
1535        let prefix = next;
1536        let eq = unsafe { xmlStrchr(next, b'=' as xmlChar) };
1537        if eq.is_null() {
1538            unsafe {
1539                out_cstr(
1540                    (*ctxt).output,
1541                    c"setns: prefix=[nsuri] required\n".as_ptr() as *const c_char,
1542                );
1543            }
1544            unsafe {
1545                xmlFreeImpl(ns_list_dup as *mut c_void);
1546            }
1547            return -1;
1548        }
1549        // split at '='
1550        unsafe {
1551            *(eq as *mut xmlChar) = 0;
1552        }
1553        let href = unsafe { eq.add(1) };
1554        // find href
1555        let space = unsafe { xmlStrchr(href, b' ' as xmlChar) };
1556        if !space.is_null() {
1557            unsafe {
1558                *(space as *mut xmlChar) = 0;
1559            }
1560            next = unsafe { space.add(1) as *mut xmlChar };
1561        } else {
1562            next = unsafe { href.add(tree::xml_strlen(href) as usize) as *mut xmlChar };
1563        }
1564
1565        // do register namespace
1566        if unsafe { xmlXPathRegisterNs((*ctxt).pctxt, prefix, href) } != 0 {
1567            unsafe {
1568                let mut msg = Vec::new();
1569                msg.extend_from_slice(b"Error: unable to register NS with prefix=\"");
1570                push_cstr(&mut msg, prefix as *const c_char);
1571                msg.extend_from_slice(b"\" and href=\"");
1572                push_cstr(&mut msg, href as *const c_char);
1573                msg.extend_from_slice(b"\"\n");
1574                out_bytes((*ctxt).output, &msg);
1575            }
1576            unsafe {
1577                xmlFreeImpl(ns_list_dup as *mut c_void);
1578            }
1579            return -1;
1580        }
1581    }
1582    unsafe {
1583        xmlFreeImpl(ns_list_dup as *mut c_void);
1584    }
1585    0
1586}
1587
1588/// Upstream static `xmlShellRegisterRootNamespaces` — the shell "setrootns"
1589/// command: register all namespace declarations found on the root element.
1590unsafe fn xmlShellRegisterRootNamespaces(ctxt: *mut _xmlShellCtxt, root: *mut _xmlNode) -> c_int {
1591    if root.is_null()
1592        || unsafe { (*root).type_ != xmlElementType::XML_ELEMENT_NODE as c_int }
1593        || unsafe { (*root).nsDef.is_null() }
1594        || ctxt.is_null()
1595        || unsafe { (*ctxt).pctxt.is_null() }
1596    {
1597        return -1;
1598    }
1599    let mut ns = unsafe { (*root).nsDef };
1600    while !ns.is_null() {
1601        if unsafe { (*ns).prefix.is_null() } {
1602            unsafe {
1603                xmlXPathRegisterNs(
1604                    (*ctxt).pctxt,
1605                    c"defaultns".as_ptr() as *const xmlChar,
1606                    (*ns).href,
1607                );
1608            }
1609        } else {
1610            unsafe {
1611                xmlXPathRegisterNs((*ctxt).pctxt, (*ns).prefix, (*ns).href);
1612            }
1613        }
1614        ns = unsafe { (*ns).next };
1615    }
1616    0
1617}
1618
1619/// Upstream static `xmlShellGrep` — the shell "grep" command: search a
1620/// string in the subtree under `node`, deep first.
1621unsafe fn xmlShellGrep(ctxt: *mut _xmlShellCtxt, arg: *mut c_char, node: *mut _xmlNode) {
1622    if ctxt.is_null() || node.is_null() || arg.is_null() {
1623        return;
1624    }
1625    let mut node = node;
1626    while !node.is_null() {
1627        unsafe {
1628            let typ = (*node).type_;
1629            if typ == xmlElementType::XML_COMMENT_NODE as c_int {
1630                if !xmlStrstr((*node).content, arg as *const xmlChar).is_null() {
1631                    let path = shell_get_node_path(node);
1632                    if !path.is_null() {
1633                        let mut line = Vec::new();
1634                        push_cstr(&mut line, path as *const c_char);
1635                        line.extend_from_slice(b" : ");
1636                        out_bytes((*ctxt).output, &line);
1637                        xmlFreeImpl(path as *mut c_void);
1638                    }
1639                    xmlShellList(ctxt, ptr::null_mut(), node, ptr::null_mut());
1640                }
1641            } else if typ == xmlElementType::XML_TEXT_NODE as c_int
1642                && !xmlStrstr((*node).content, arg as *const xmlChar).is_null()
1643            {
1644                let path = shell_get_node_path((*node).parent);
1645                if !path.is_null() {
1646                    let mut line = Vec::new();
1647                    push_cstr(&mut line, path as *const c_char);
1648                    line.extend_from_slice(b" : ");
1649                    out_bytes((*ctxt).output, &line);
1650                    xmlFreeImpl(path as *mut c_void);
1651                }
1652                xmlShellList(ctxt, ptr::null_mut(), (*node).parent, ptr::null_mut());
1653            }
1654
1655            /*
1656             * Browse the full subtree, deep first
1657             */
1658            if typ == xmlElementType::XML_DOCUMENT_NODE as c_int
1659                || typ == xmlElementType::XML_HTML_DOCUMENT_NODE as c_int
1660            {
1661                node = (*(node as *mut _xmlDoc)).children;
1662            } else if !(*node).children.is_null()
1663                && typ != xmlElementType::XML_ENTITY_REF_NODE as c_int
1664            {
1665                node = (*node).children;
1666            } else if !(*node).next.is_null() {
1667                node = (*node).next;
1668            } else {
1669                while !node.is_null() {
1670                    if !(*node).parent.is_null() {
1671                        node = (*node).parent;
1672                    }
1673                    if !(*node).next.is_null() {
1674                        node = (*node).next;
1675                        break;
1676                    }
1677                    if (*node).parent.is_null() {
1678                        node = ptr::null_mut();
1679                        break;
1680                    }
1681                }
1682            }
1683        }
1684    }
1685}
1686
1687/// Emit the per-type error message for a non-node-set XPath result
1688/// (upstream's repeated `switch (list->type)` blocks in the shell loop).
1689unsafe fn shell_result_type_error(arg: *const c_char, typ: c_int) {
1690    if typ == xmlXPathObjectType::XPATH_UNDEFINED as c_int {
1691        unsafe { shell_generic_error(arg, b": ", b"no such node\n") };
1692    } else if typ == xmlXPathObjectType::XPATH_BOOLEAN as c_int {
1693        unsafe { shell_generic_error(arg, b" is a Boolean", b"\n") };
1694    } else if typ == xmlXPathObjectType::XPATH_NUMBER as c_int {
1695        unsafe { shell_generic_error(arg, b" is a number", b"\n") };
1696    } else if typ == xmlXPathObjectType::XPATH_STRING as c_int {
1697        unsafe { shell_generic_error(arg, b" is a string", b"\n") };
1698    } else if typ == xmlXPathObjectType::XPATH_POINT as c_int {
1699        unsafe { shell_generic_error(arg, b" is a point", b"\n") };
1700    } else if typ == xmlXPathObjectType::XPATH_RANGE as c_int
1701        || typ == xmlXPathObjectType::XPATH_LOCATIONSET as c_int
1702    {
1703        unsafe { shell_generic_error(arg, b" is a range", b"\n") };
1704    } else if typ == xmlXPathObjectType::XPATH_USERS as c_int {
1705        unsafe { shell_generic_error(arg, b" is user-defined", b"\n") };
1706    } else if typ == xmlXPathObjectType::XPATH_XSLT_TREE as c_int {
1707        unsafe { shell_generic_error(arg, b" is an XSLT value tree", b"\n") };
1708    }
1709}
1710
1711// ═══════════════════════════════════════════════════════════════════════════════
1712// xmlShell — the interactive loop
1713// ═══════════════════════════════════════════════════════════════════════════════
1714
1715/// Build the prompt string for the current node ("/ > ", "name > ",
1716/// "prefix:name > " or "? > "), NUL-terminated.
1717unsafe fn shell_build_prompt(ctxt: *mut _xmlShellCtxt) -> Vec<u8> {
1718    let mut p = Vec::new();
1719    let node = unsafe { (*ctxt).node };
1720    let doc = unsafe { (*ctxt).doc };
1721    if node == doc as *mut _xmlNode {
1722        p.extend_from_slice(b"/ > ");
1723    } else if !node.is_null() && !unsafe { (*node).name }.is_null() {
1724        unsafe {
1725            if !(*node).ns.is_null() && !(*(*node).ns).prefix.is_null() {
1726                push_cstr(&mut p, (*(*node).ns).prefix as *const c_char);
1727                p.push(b':');
1728            }
1729            push_cstr(&mut p, (*node).name as *const c_char);
1730        }
1731        p.extend_from_slice(b" > ");
1732    } else {
1733        p.extend_from_slice(b"? > ");
1734    }
1735    p.push(0);
1736    p
1737}
1738
1739/// The shell's "help" output (upstream 2.12.6), written to `ctxt->output`.
1740unsafe fn shell_print_help(ctxt: *mut _xmlShellCtxt) {
1741    let out = unsafe { (*ctxt).output };
1742    const HELP: &[&[u8]] = &[
1743        b"\tbase         display XML base of the node\n",
1744        b"\tsetbase URI  change the XML base of the node\n",
1745        b"\tbye          leave shell\n",
1746        b"\tcat [node]   display node or current node\n",
1747        b"\tcd [path]    change directory to path or to root\n",
1748        b"\tdir [path]   dumps information about the node (namespace, attributes, content)\n",
1749        b"\tdu [path]    show the structure of the subtree under path or the current node\n",
1750        b"\texit         leave shell\n",
1751        b"\thelp         display this help\n",
1752        b"\tfree         display memory usage\n",
1753        b"\tload [name]  load a new document with name\n",
1754        b"\tls [path]    list contents of path or the current directory\n",
1755        b"\txpath expr   evaluate the XPath expression in that context and print the result\n",
1756        b"\tsetns nsreg  register a namespace to a prefix in the XPath evaluation context\n",
1757        b"\t             format for nsreg is: prefix=[nsuri] (i.e. prefix= unsets a prefix)\n",
1758        b"\tsetrootns    register all namespace found on the root element\n",
1759        b"\t             the default namespace if any uses 'defaultns' prefix\n",
1760        b"\tpwd          display current working directory\n",
1761        b"\twhereis      display absolute path of [path] or current working directory\n",
1762        b"\tquit         leave shell\n",
1763        b"\tsave [name]  save this document to name or the original name\n",
1764        b"\twrite [name] write the current node to the filename\n",
1765        b"\tvalidate     check the document for errors\n",
1766        b"\tgrep string  search for a string in the subtree\n",
1767    ];
1768    for line in HELP {
1769        unsafe {
1770            out_bytes(out, line);
1771        }
1772    }
1773}
1774
1775/// `void xmlShell(xmlDocPtr doc, char *filename, xmlShellReadlineFunc input,
1776/// FILE *output)` — the XML shell: an interactive loop allowing to load,
1777/// validate, view, modify and save a document.
1778///
1779/// # SAFETY
1780///
1781/// - `doc`, `filename`, `output` must be valid pointers (or NULL
1782///   where the upstream C contract allows), obtained from the
1783///   matching constructor/owner and not yet freed; the callee may
1784///   take or keep ownership exactly as the C API specifies.
1785///
1786/// The caller must not race this call with concurrent mutation of the
1787/// same objects from other threads (per-object state is not internally
1788/// synchronized). Violating any of the above is undefined behavior.
1789///
1790/// Exercised by the C-API differential courts
1791/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1792/// courts; those pass byte-for-byte against the upstream oracle.
1793#[no_mangle]
1794pub unsafe extern "C" fn xmlShell(
1795    doc: *mut _xmlDoc,
1796    filename: *mut c_char,
1797    input: xmlShellReadlineFunc,
1798    output: *mut c_void,
1799) {
1800    if doc.is_null() || filename.is_null() || input.is_none() {
1801        return;
1802    }
1803    let output = if output.is_null() {
1804        unsafe { stdout }
1805    } else {
1806        output
1807    };
1808
1809    let ctxt = xmlMallocZero(size_of::<_xmlShellCtxt>()) as *mut _xmlShellCtxt;
1810    if ctxt.is_null() {
1811        return;
1812    }
1813    unsafe {
1814        (*ctxt).loaded = 0;
1815        (*ctxt).doc = doc;
1816        (*ctxt).input = input;
1817        (*ctxt).output = output;
1818        (*ctxt).filename = xmlStrdup(filename as *const xmlChar) as *mut c_char;
1819        (*ctxt).node = doc as *mut _xmlNode;
1820        (*ctxt).pctxt = xmlXPathNewContext(doc);
1821    }
1822    if unsafe { (*ctxt).pctxt }.is_null() {
1823        unsafe {
1824            xmlFreeImpl(ctxt as *mut c_void);
1825        }
1826        return;
1827    }
1828
1829    let mut cmdline: *mut c_char = ptr::null_mut();
1830    loop {
1831        // Prompt.
1832        let prompt = unsafe { shell_build_prompt(ctxt) };
1833        let readline = unsafe { (*ctxt).input };
1834        cmdline = match readline {
1835            Some(f) => f(prompt.as_ptr() as *mut c_char),
1836            None => break,
1837        };
1838        if cmdline.is_null() {
1839            break;
1840        }
1841
1842        // Parse the command itself (skip leading spaces/tabs).
1843        let clen = unsafe { libc::strlen(cmdline) } as usize;
1844        let cbytes = unsafe { core::slice::from_raw_parts(cmdline as *const u8, clen) };
1845        let mut i = 0usize;
1846        while i < clen && (cbytes[i] == b' ' || cbytes[i] == b'\t') {
1847            i += 1;
1848        }
1849        let mut command: Vec<u8> = Vec::new();
1850        while i < clen
1851            && cbytes[i] != b' '
1852            && cbytes[i] != b'\t'
1853            && cbytes[i] != b'\n'
1854            && cbytes[i] != b'\r'
1855        {
1856            command.push(cbytes[i]);
1857            i += 1;
1858        }
1859        if command.is_empty() {
1860            unsafe {
1861                libc::free(cmdline as *mut c_void);
1862            }
1863            cmdline = ptr::null_mut();
1864            continue;
1865        }
1866
1867        // Parse the argument (rest of the line).
1868        while i < clen && (cbytes[i] == b' ' || cbytes[i] == b'\t') {
1869            i += 1;
1870        }
1871        let mut arg: Vec<u8> = Vec::new();
1872        while i < clen && cbytes[i] != b'\n' && cbytes[i] != b'\r' {
1873            arg.push(cbytes[i]);
1874            i += 1;
1875        }
1876
1877        // NUL-terminated views for the C API.
1878        command.push(0);
1879        let cmd: &[u8] = &command;
1880        let mut argn = arg.clone();
1881        argn.push(0);
1882        let arg_cstr: *mut c_char = argn.as_mut_ptr() as *mut c_char;
1883        let arg_xml: *const xmlChar = argn.as_ptr() as *const xmlChar;
1884
1885        // start interpreting the command
1886        if cmd == b"exit\0" || cmd == b"quit\0" || cmd == b"bye\0" {
1887            break;
1888        }
1889        if cmd == b"help\0" {
1890            unsafe { shell_print_help(ctxt) };
1891        } else if cmd == b"validate\0" {
1892            unsafe {
1893                xmlShellValidate(ctxt, arg_cstr, ptr::null_mut(), ptr::null_mut());
1894            }
1895        } else if cmd == b"load\0" {
1896            unsafe {
1897                xmlShellLoad(ctxt, arg_cstr, ptr::null_mut(), ptr::null_mut());
1898            }
1899        } else if cmd == b"save\0" {
1900            unsafe {
1901                xmlShellSave(ctxt, arg_cstr, ptr::null_mut(), ptr::null_mut());
1902            }
1903        } else if cmd == b"write\0" {
1904            if arg.is_empty() {
1905                unsafe {
1906                    shell_generic_error(
1907                        c"Write command requires a filename argument\n".as_ptr() as *const c_char,
1908                        b"",
1909                        b"",
1910                    );
1911                }
1912            } else {
1913                unsafe {
1914                    xmlShellWrite(ctxt, arg_cstr, (*ctxt).node, ptr::null_mut());
1915                }
1916            }
1917        } else if cmd == b"grep\0" {
1918            unsafe {
1919                xmlShellGrep(ctxt, arg_cstr, (*ctxt).node);
1920            }
1921        } else if cmd == b"free\0" {
1922            unsafe {
1923                if arg.is_empty() {
1924                    crate::abi::allocator::xmlMemShow((*ctxt).output, 0);
1925                } else {
1926                    let mut len: c_int = 0;
1927                    let arg_s = core::str::from_utf8(&argn[..argn.len() - 1]).unwrap_or("");
1928                    if let Ok(v) = arg_s.trim().parse::<c_int>() {
1929                        len = v;
1930                    }
1931                    crate::abi::allocator::xmlMemShow((*ctxt).output, len);
1932                }
1933            }
1934        } else if cmd == b"pwd\0" {
1935            let mut dir = [0 as c_char; 500];
1936            unsafe {
1937                if xmlShellPwd(ctxt, dir.as_mut_ptr(), (*ctxt).node, ptr::null_mut()) == 0 {
1938                    let mut line = Vec::new();
1939                    push_cstr(&mut line, dir.as_mut_ptr());
1940                    line.extend_from_slice(b"\n");
1941                    out_bytes((*ctxt).output, &line);
1942                }
1943            }
1944        } else if cmd == b"du\0" {
1945            unsafe {
1946                if arg.is_empty() {
1947                    xmlShellDu(ctxt, ptr::null_mut(), (*ctxt).node, ptr::null_mut());
1948                } else {
1949                    (*(*ctxt).pctxt).node = (*ctxt).node;
1950                    let list = xmlXPathEval(arg_xml, (*ctxt).pctxt);
1951                    if !list.is_null() {
1952                        let typ = (*list).type_;
1953                        if typ == xmlXPathObjectType::XPATH_NODESET as c_int {
1954                            let ns = (*list).nodesetval as *mut _xmlNodeSet;
1955                            if !ns.is_null() {
1956                                for indx in 0..(*ns).nodeNr {
1957                                    let n = *(*ns).nodeTab.add(indx as usize);
1958                                    xmlShellDu(ctxt, ptr::null_mut(), n, ptr::null_mut());
1959                                }
1960                            }
1961                        } else {
1962                            shell_result_type_error(arg_cstr, typ);
1963                        }
1964                        xmlXPathFreeObject(list);
1965                    } else {
1966                        shell_generic_error(arg_cstr, b": ", b"no such node\n");
1967                    }
1968                    (*(*ctxt).pctxt).node = ptr::null_mut();
1969                }
1970            }
1971        } else if cmd == b"base\0" {
1972            unsafe {
1973                xmlShellBase(ctxt, ptr::null_mut(), (*ctxt).node, ptr::null_mut());
1974            }
1975        } else if cmd == b"setns\0" {
1976            unsafe {
1977                if arg.is_empty() {
1978                    shell_generic_error(
1979                        c"setns: prefix=[nsuri] required\n".as_ptr() as *const c_char,
1980                        b"",
1981                        b"",
1982                    );
1983                } else {
1984                    xmlShellRegisterNamespace(ctxt, arg_cstr);
1985                }
1986            }
1987        } else if cmd == b"setrootns\0" {
1988            unsafe {
1989                let root = tree::doc_get_root_element((*ctxt).doc);
1990                xmlShellRegisterRootNamespaces(ctxt, root);
1991            }
1992        } else if cmd == b"xpath\0" {
1993            unsafe {
1994                if arg.is_empty() {
1995                    shell_generic_error(
1996                        c"xpath: expression required\n".as_ptr() as *const c_char,
1997                        b"",
1998                        b"",
1999                    );
2000                } else {
2001                    (*(*ctxt).pctxt).node = (*ctxt).node;
2002                    let list = xmlXPathEval(arg_xml, (*ctxt).pctxt);
2003                    xmlXPathDebugDumpObject((*ctxt).output, list, 0);
2004                    xmlXPathFreeObject(list);
2005                }
2006            }
2007        } else if cmd == b"setbase\0" {
2008            unsafe {
2009                xmlShellSetBase(ctxt, arg_cstr, (*ctxt).node);
2010            }
2011        } else if cmd == b"ls\0" || cmd == b"dir\0" {
2012            let is_dir = cmd == b"dir\0";
2013            unsafe {
2014                if arg.is_empty() {
2015                    if is_dir {
2016                        xmlShellDir(ctxt, ptr::null_mut(), (*ctxt).node, ptr::null_mut());
2017                    } else {
2018                        xmlShellList(ctxt, ptr::null_mut(), (*ctxt).node, ptr::null_mut());
2019                    }
2020                } else {
2021                    (*(*ctxt).pctxt).node = (*ctxt).node;
2022                    let list = xmlXPathEval(arg_xml, (*ctxt).pctxt);
2023                    if !list.is_null() {
2024                        let typ = (*list).type_;
2025                        if typ == xmlXPathObjectType::XPATH_NODESET as c_int {
2026                            let ns = (*list).nodesetval as *mut _xmlNodeSet;
2027                            if !ns.is_null() {
2028                                for indx in 0..(*ns).nodeNr {
2029                                    let n = *(*ns).nodeTab.add(indx as usize);
2030                                    if is_dir {
2031                                        xmlShellDir(ctxt, ptr::null_mut(), n, ptr::null_mut());
2032                                    } else {
2033                                        xmlShellList(ctxt, ptr::null_mut(), n, ptr::null_mut());
2034                                    }
2035                                }
2036                            }
2037                        } else {
2038                            shell_result_type_error(arg_cstr, typ);
2039                        }
2040                        xmlXPathFreeObject(list);
2041                    } else {
2042                        shell_generic_error(arg_cstr, b": ", b"no such node\n");
2043                    }
2044                    (*(*ctxt).pctxt).node = ptr::null_mut();
2045                }
2046            }
2047        } else if cmd == b"whereis\0" {
2048            let mut dir = [0 as c_char; 500];
2049            unsafe {
2050                if arg.is_empty() {
2051                    if xmlShellPwd(ctxt, dir.as_mut_ptr(), (*ctxt).node, ptr::null_mut()) == 0 {
2052                        let mut line = Vec::new();
2053                        push_cstr(&mut line, dir.as_mut_ptr());
2054                        line.extend_from_slice(b"\n");
2055                        out_bytes((*ctxt).output, &line);
2056                    }
2057                } else {
2058                    (*(*ctxt).pctxt).node = (*ctxt).node;
2059                    let list = xmlXPathEval(arg_xml, (*ctxt).pctxt);
2060                    if !list.is_null() {
2061                        let typ = (*list).type_;
2062                        if typ == xmlXPathObjectType::XPATH_NODESET as c_int {
2063                            let ns = (*list).nodesetval as *mut _xmlNodeSet;
2064                            if !ns.is_null() {
2065                                for indx in 0..(*ns).nodeNr {
2066                                    let n = *(*ns).nodeTab.add(indx as usize);
2067                                    if xmlShellPwd(ctxt, dir.as_mut_ptr(), n, ptr::null_mut()) == 0
2068                                    {
2069                                        let mut line = Vec::new();
2070                                        push_cstr(&mut line, dir.as_mut_ptr());
2071                                        line.extend_from_slice(b"\n");
2072                                        out_bytes((*ctxt).output, &line);
2073                                    }
2074                                }
2075                            }
2076                        } else {
2077                            shell_result_type_error(arg_cstr, typ);
2078                        }
2079                        xmlXPathFreeObject(list);
2080                    } else {
2081                        shell_generic_error(arg_cstr, b": ", b"no such node\n");
2082                    }
2083                    (*(*ctxt).pctxt).node = ptr::null_mut();
2084                }
2085            }
2086        } else if cmd == b"cd\0" {
2087            unsafe {
2088                if arg.is_empty() {
2089                    (*ctxt).node = (*ctxt).doc as *mut _xmlNode;
2090                } else {
2091                    // Upstream strips a trailing '/' from the argument.
2092                    let mut argn = argn;
2093                    let l = argn.len();
2094                    if l >= 3 && argn[l - 2] == b'/' {
2095                        argn[l - 2] = 0;
2096                    }
2097                    (*(*ctxt).pctxt).node = (*ctxt).node;
2098                    let list = xmlXPathEval(argn.as_ptr() as *const xmlChar, (*ctxt).pctxt);
2099                    if !list.is_null() {
2100                        let typ = (*list).type_;
2101                        if typ == xmlXPathObjectType::XPATH_NODESET as c_int {
2102                            let ns = (*list).nodesetval as *mut _xmlNodeSet;
2103                            if !ns.is_null() {
2104                                if (*ns).nodeNr == 1 {
2105                                    (*ctxt).node = *(*ns).nodeTab;
2106                                    if !(*ctxt).node.is_null()
2107                                        && (*(*ctxt).node).type_
2108                                            == xmlElementType::XML_NAMESPACE_DECL as c_int
2109                                    {
2110                                        shell_generic_error(
2111                                            c"cannot cd to namespace\n".as_ptr() as *const c_char,
2112                                            b"",
2113                                            b"",
2114                                        );
2115                                        (*ctxt).node = ptr::null_mut();
2116                                    }
2117                                } else {
2118                                    let mut msg = Vec::new();
2119                                    push_cstr(&mut msg, arg_cstr);
2120                                    msg.extend_from_slice(b" is a ");
2121                                    msg.extend_from_slice((*ns).nodeNr.to_string().as_bytes());
2122                                    msg.extend_from_slice(b" Node Set\n");
2123                                    out_bytes(stderr, &msg);
2124                                }
2125                            } else {
2126                                let mut msg = Vec::new();
2127                                push_cstr(&mut msg, arg_cstr);
2128                                msg.extend_from_slice(b" is an empty Node Set\n");
2129                                out_bytes(stderr, &msg);
2130                            }
2131                        } else {
2132                            shell_result_type_error(arg_cstr, typ);
2133                        }
2134                        xmlXPathFreeObject(list);
2135                    } else {
2136                        shell_generic_error(arg_cstr, b": ", b"no such node\n");
2137                    }
2138                    (*(*ctxt).pctxt).node = ptr::null_mut();
2139                }
2140            }
2141        } else if cmd == b"cat\0" {
2142            unsafe {
2143                if arg.is_empty() {
2144                    xmlShellCat(ctxt, ptr::null_mut(), (*ctxt).node, ptr::null_mut());
2145                } else {
2146                    // UPSTREAM-PARITY: the 2.12.x loop reuses the outer
2147                    // `i` (the argument length) in `if (i > 0)`, which is
2148                    // always true here, so the separator is emitted before
2149                    // every node of the node-set.
2150                    (*(*ctxt).pctxt).node = (*ctxt).node;
2151                    let list = xmlXPathEval(arg_xml, (*ctxt).pctxt);
2152                    if !list.is_null() {
2153                        let typ = (*list).type_;
2154                        if typ == xmlXPathObjectType::XPATH_NODESET as c_int {
2155                            let ns = (*list).nodesetval as *mut _xmlNodeSet;
2156                            if !ns.is_null() {
2157                                for indx in 0..(*ns).nodeNr {
2158                                    if i > 0 {
2159                                        out_bytes((*ctxt).output, b" -------\n");
2160                                    }
2161                                    let n = *(*ns).nodeTab.add(indx as usize);
2162                                    xmlShellCat(ctxt, ptr::null_mut(), n, ptr::null_mut());
2163                                }
2164                            }
2165                        } else {
2166                            shell_result_type_error(arg_cstr, typ);
2167                        }
2168                        xmlXPathFreeObject(list);
2169                    } else {
2170                        shell_generic_error(arg_cstr, b": ", b"no such node\n");
2171                    }
2172                    (*(*ctxt).pctxt).node = ptr::null_mut();
2173                }
2174            }
2175        } else {
2176            let mut msg = Vec::new();
2177            msg.extend_from_slice(b"Unknown command ");
2178            msg.extend_from_slice(&command[..command.len() - 1]);
2179            msg.extend_from_slice(b"\n");
2180            unsafe {
2181                out_bytes(stderr, &msg);
2182            }
2183        }
2184
2185        unsafe {
2186            libc::free(cmdline as *mut c_void);
2187        }
2188        cmdline = ptr::null_mut();
2189    }
2190
2191    // Cleanup (upstream xmlShell epilogue).
2192    unsafe {
2193        xmlXPathFreeContext((*ctxt).pctxt);
2194        if (*ctxt).loaded != 0 {
2195            tree::free_doc((*ctxt).doc);
2196        }
2197        if !(*ctxt).filename.is_null() {
2198            xmlFreeImpl((*ctxt).filename as *mut c_void);
2199        }
2200        xmlFreeImpl(ctxt as *mut c_void);
2201        if !cmdline.is_null() {
2202            libc::free(cmdline as *mut c_void);
2203        }
2204    }
2205}