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