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