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///
500/// # SAFETY
501///
502///
503/// - `arg` must point to valid NUL-terminated
504///   strings (or NULL where the C contract allows) for the lifetime
505///   of the call.
506///
507/// The caller must not race this call with concurrent mutation of the
508/// same objects from other threads (per-object state is not internally
509/// synchronized). Violating any of the above is undefined behavior.
510///
511/// Exercised by the C-API differential courts
512/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
513/// courts; those pass byte-for-byte against the upstream oracle.
514#[no_mangle]
515pub unsafe extern "C" fn xmlShellPrintXPathError(errorType: c_int, arg: *const c_char) {
516    let default_arg = b"Result\0";
517    let arg = if arg.is_null() {
518        default_arg.as_ptr() as *const c_char
519    } else {
520        arg
521    };
522
523    if errorType == xmlXPathObjectType::XPATH_UNDEFINED as c_int {
524        unsafe { shell_generic_error(arg, b": ", b"no such node\n") };
525    } else if errorType == xmlXPathObjectType::XPATH_BOOLEAN as c_int {
526        unsafe { shell_generic_error(arg, b" is a Boolean", b"\n") };
527    } else if errorType == xmlXPathObjectType::XPATH_NUMBER as c_int {
528        unsafe { shell_generic_error(arg, b" is a number", b"\n") };
529    } else if errorType == xmlXPathObjectType::XPATH_STRING as c_int {
530        unsafe { shell_generic_error(arg, b" is a string", b"\n") };
531    } else if errorType == xmlXPathObjectType::XPATH_POINT as c_int {
532        unsafe { shell_generic_error(arg, b" is a point", b"\n") };
533    } else if errorType == xmlXPathObjectType::XPATH_RANGE as c_int
534        || errorType == xmlXPathObjectType::XPATH_LOCATIONSET as c_int
535    {
536        unsafe { shell_generic_error(arg, b" is a range", b"\n") };
537    } else if errorType == xmlXPathObjectType::XPATH_USERS as c_int {
538        unsafe { shell_generic_error(arg, b" is user-defined", b"\n") };
539    } else if errorType == xmlXPathObjectType::XPATH_XSLT_TREE as c_int {
540        unsafe { shell_generic_error(arg, b" is an XSLT value tree", b"\n") };
541    }
542}
543
544/// Print a node to the output `FILE*` of the context (stdout when `ctxt` is
545/// NULL) — upstream static `xmlShellPrintNodeCtxt`.
546unsafe fn xmlShellPrintNodeCtxt(ctxt: *mut _xmlShellCtxt, node: *mut _xmlNode) {
547    if node.is_null() {
548        return;
549    }
550    let fp = if ctxt.is_null() {
551        unsafe { stdout }
552    } else {
553        (*ctxt).output
554    };
555
556    let typ = unsafe { (*node).type_ };
557    if typ == xmlElementType::XML_DOCUMENT_NODE as c_int {
558        tree::xmlDocDump(fp, node as *mut _xmlDoc);
559    } else if typ == xmlElementType::XML_ATTRIBUTE_NODE as c_int {
560        unsafe {
561            debug::xmlDebugDumpAttrList(fp as *mut debug::_IO_FILE, node as *mut _xmlAttr, 0);
562        }
563    } else {
564        unsafe {
565            shell_elem_dump(fp, (*node).doc, node);
566        }
567    }
568    unsafe {
569        out_bytes(fp, b"\n");
570    }
571}
572
573/// `void xmlShellPrintNode(xmlNodePtr node)` — print a node to stdout.
574///
575/// # SAFETY
576///
577/// - `node` must be valid pointers (or NULL
578///   where the upstream C contract allows), obtained from the
579///   matching constructor/owner and not yet freed; the callee may
580///   take or keep ownership exactly as the C API specifies.
581///
582/// The caller must not race this call with concurrent mutation of the
583/// same objects from other threads (per-object state is not internally
584/// synchronized). Violating any of the above is undefined behavior.
585///
586/// Exercised by the C-API differential courts
587/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
588/// courts; those pass byte-for-byte against the upstream oracle.
589#[no_mangle]
590pub unsafe extern "C" fn xmlShellPrintNode(node: *mut _xmlNode) {
591    unsafe { xmlShellPrintNodeCtxt(ptr::null_mut(), node) };
592}
593
594/// `void xmlShellPrintXPathResult(xmlXPathObjectPtr list)`.
595///
596/// Prints an XPath result object to stdout via `xmlXPathDebugDumpObject`
597/// (the same printer the 2.12.x shell loop uses for its `xpath` command).
598///
599/// # SAFETY
600///
601/// - `list` must be valid pointers (or NULL
602///   where the upstream C contract allows), obtained from the
603///   matching constructor/owner and not yet freed; the callee may
604///   take or keep ownership exactly as the C API specifies.
605///
606/// The caller must not race this call with concurrent mutation of the
607/// same objects from other threads (per-object state is not internally
608/// synchronized). Violating any of the above is undefined behavior.
609///
610/// Exercised by the C-API differential courts
611/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
612/// courts; those pass byte-for-byte against the upstream oracle.
613#[no_mangle]
614pub unsafe extern "C" fn xmlShellPrintXPathResult(list: *mut _xmlXPathObject) {
615    unsafe {
616        xmlXPathDebugDumpObject(stdout, list, 0);
617    }
618}
619
620// ═══════════════════════════════════════════════════════════════════════════════
621// xmlShellList / xmlShellBase / xmlShellDir
622// ═══════════════════════════════════════════════════════════════════════════════
623
624/// `int xmlShellList(xmlShellCtxtPtr ctxt, char *arg, xmlNodePtr node,
625/// xmlNodePtr node2)` — the shell "ls" command.
626///
627/// # SAFETY
628///
629/// - `ctxt`, `_arg`, `node`, `_node2` must be valid pointers (or NULL
630///   where the upstream C contract allows), obtained from the
631///   matching constructor/owner and not yet freed; the callee may
632///   take or keep ownership exactly as the C API specifies.
633///
634/// The caller must not race this call with concurrent mutation of the
635/// same objects from other threads (per-object state is not internally
636/// synchronized). Violating any of the above is undefined behavior.
637///
638/// Exercised by the C-API differential courts
639/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
640/// courts; those pass byte-for-byte against the upstream oracle.
641#[no_mangle]
642pub unsafe extern "C" fn xmlShellList(
643    ctxt: *mut _xmlShellCtxt,
644    _arg: *mut c_char,
645    node: *mut _xmlNode,
646    _node2: *mut _xmlNode,
647) -> c_int {
648    if ctxt.is_null() {
649        return 0;
650    }
651    if node.is_null() {
652        unsafe {
653            out_bytes((*ctxt).output, b"NULL\n");
654        }
655        return 0;
656    }
657    let typ = unsafe { (*node).type_ };
658    let mut cur: *mut _xmlNode;
659    if typ == xmlElementType::XML_DOCUMENT_NODE as c_int
660        || typ == xmlElementType::XML_HTML_DOCUMENT_NODE as c_int
661    {
662        cur = unsafe { (*(node as *mut _xmlDoc)).children };
663    } else if typ == xmlElementType::XML_NAMESPACE_DECL as c_int {
664        unsafe {
665            debug::xmlLsOneNode((*ctxt).output as *mut debug::_IO_FILE, node);
666        }
667        return 0;
668    } else if !unsafe { (*node).children }.is_null() {
669        cur = unsafe { (*node).children };
670    } else {
671        unsafe {
672            debug::xmlLsOneNode((*ctxt).output as *mut debug::_IO_FILE, node);
673        }
674        return 0;
675    }
676    while !cur.is_null() {
677        unsafe {
678            debug::xmlLsOneNode((*ctxt).output as *mut debug::_IO_FILE, cur);
679            cur = (*cur).next;
680        }
681    }
682    0
683}
684
685/// `int xmlShellBase(xmlShellCtxtPtr ctxt, char *arg, xmlNodePtr node,
686/// xmlNodePtr node2)` — the shell "base" command.
687///
688/// # SAFETY
689///
690/// - `ctxt`, `_arg`, `node`, `_node2` must be valid pointers (or NULL
691///   where the upstream C contract allows), obtained from the
692///   matching constructor/owner and not yet freed; the callee may
693///   take or keep ownership exactly as the C API specifies.
694///
695/// The caller must not race this call with concurrent mutation of the
696/// same objects from other threads (per-object state is not internally
697/// synchronized). Violating any of the above is undefined behavior.
698///
699/// Exercised by the C-API differential courts
700/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
701/// courts; those pass byte-for-byte against the upstream oracle.
702#[no_mangle]
703pub unsafe extern "C" fn xmlShellBase(
704    ctxt: *mut _xmlShellCtxt,
705    _arg: *mut c_char,
706    node: *mut _xmlNode,
707    _node2: *mut _xmlNode,
708) -> c_int {
709    if ctxt.is_null() {
710        return 0;
711    }
712    if node.is_null() {
713        unsafe {
714            out_bytes((*ctxt).output, b"NULL\n");
715        }
716        return 0;
717    }
718
719    let base = unsafe { xmlNodeGetBase((*node).doc, node) };
720
721    if base.is_null() {
722        unsafe {
723            out_bytes((*ctxt).output, b" No base found !!!\n");
724        }
725    } else {
726        unsafe {
727            out_cstr((*ctxt).output, base as *const c_char);
728            out_bytes((*ctxt).output, b"\n");
729            xmlFreeImpl(base as *mut c_void);
730        }
731    }
732    0
733}
734
735/// `int xmlShellDir(xmlShellCtxtPtr ctxt, char *arg, xmlNodePtr node,
736/// xmlNodePtr node2)` — the shell "dir" command.
737///
738/// # SAFETY
739///
740/// - `ctxt`, `_arg`, `node`, `_node2` must be valid pointers (or NULL
741///   where the upstream C contract allows), obtained from the
742///   matching constructor/owner and not yet freed; the callee may
743///   take or keep ownership exactly as the C API specifies.
744///
745/// The caller must not race this call with concurrent mutation of the
746/// same objects from other threads (per-object state is not internally
747/// synchronized). Violating any of the above is undefined behavior.
748///
749/// Exercised by the C-API differential courts
750/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
751/// courts; those pass byte-for-byte against the upstream oracle.
752#[no_mangle]
753pub unsafe extern "C" fn xmlShellDir(
754    ctxt: *mut _xmlShellCtxt,
755    _arg: *mut c_char,
756    node: *mut _xmlNode,
757    _node2: *mut _xmlNode,
758) -> c_int {
759    if ctxt.is_null() {
760        return 0;
761    }
762    if node.is_null() {
763        unsafe {
764            out_bytes((*ctxt).output, b"NULL\n");
765        }
766        return 0;
767    }
768    let typ = unsafe { (*node).type_ };
769    unsafe {
770        if typ == xmlElementType::XML_DOCUMENT_NODE as c_int
771            || typ == xmlElementType::XML_HTML_DOCUMENT_NODE as c_int
772        {
773            debug::xmlDebugDumpDocumentHead(
774                (*ctxt).output as *mut debug::_IO_FILE,
775                node as *mut _xmlDoc,
776            );
777        } else if typ == xmlElementType::XML_ATTRIBUTE_NODE as c_int {
778            debug::xmlDebugDumpAttr(
779                (*ctxt).output as *mut debug::_IO_FILE,
780                node as *mut _xmlAttr,
781                0,
782            );
783        } else {
784            debug::xmlDebugDumpOneNode((*ctxt).output as *mut debug::_IO_FILE, node, 0);
785        }
786    }
787    0
788}
789
790// ═══════════════════════════════════════════════════════════════════════════════
791// xmlShellCat / xmlShellLoad / xmlShellWrite / xmlShellSave
792// ═══════════════════════════════════════════════════════════════════════════════
793
794/// `int xmlShellCat(xmlShellCtxtPtr ctxt, char *arg, xmlNodePtr node,
795/// xmlNodePtr node2)` — the shell "cat" command: dump the serialization of
796/// the node (XML or HTML, matching the document type).
797///
798/// # SAFETY
799///
800/// - `ctxt`, `_arg`, `node`, `_node2` must be valid pointers (or NULL
801///   where the upstream C contract allows), obtained from the
802///   matching constructor/owner and not yet freed; the callee may
803///   take or keep ownership exactly as the C API specifies.
804///
805/// The caller must not race this call with concurrent mutation of the
806/// same objects from other threads (per-object state is not internally
807/// synchronized). Violating any of the above is undefined behavior.
808///
809/// Exercised by the C-API differential courts
810/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
811/// courts; those pass byte-for-byte against the upstream oracle.
812#[no_mangle]
813pub unsafe extern "C" fn xmlShellCat(
814    ctxt: *mut _xmlShellCtxt,
815    _arg: *mut c_char,
816    node: *mut _xmlNode,
817    _node2: *mut _xmlNode,
818) -> c_int {
819    if ctxt.is_null() {
820        return 0;
821    }
822    if node.is_null() {
823        unsafe {
824            out_bytes((*ctxt).output, b"NULL\n");
825        }
826        return 0;
827    }
828    let out = unsafe { (*ctxt).output };
829    let is_html =
830        unsafe { (*(*ctxt).doc).type_ == xmlElementType::XML_HTML_DOCUMENT_NODE as c_int };
831    let typ = unsafe { (*node).type_ };
832    unsafe {
833        if is_html {
834            if typ == xmlElementType::XML_HTML_DOCUMENT_NODE as c_int {
835                shell_html_doc_dump(out, node as *mut _xmlDoc);
836            } else {
837                shell_html_node_dump_file(out, node);
838            }
839        } else if typ == xmlElementType::XML_DOCUMENT_NODE as c_int {
840            tree::xmlDocDump(out, node as *mut _xmlDoc);
841        } else {
842            shell_elem_dump(out, (*ctxt).doc, node);
843        }
844        out_bytes(out, b"\n");
845    }
846    0
847}
848
849/// `int xmlShellLoad(xmlShellCtxtPtr ctxt, char *filename, xmlNodePtr node,
850/// xmlNodePtr node2)` — the shell "load" command.
851///
852/// # SAFETY
853///
854/// - `ctxt`, `filename`, `_node`, `_node2` must be valid pointers (or NULL
855///   where the upstream C contract allows), obtained from the
856///   matching constructor/owner and not yet freed; the callee may
857///   take or keep ownership exactly as the C API specifies.
858///
859/// The caller must not race this call with concurrent mutation of the
860/// same objects from other threads (per-object state is not internally
861/// synchronized). Violating any of the above is undefined behavior.
862///
863/// Exercised by the C-API differential courts
864/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
865/// courts; those pass byte-for-byte against the upstream oracle.
866#[no_mangle]
867pub unsafe extern "C" fn xmlShellLoad(
868    ctxt: *mut _xmlShellCtxt,
869    filename: *mut c_char,
870    _node: *mut _xmlNode,
871    _node2: *mut _xmlNode,
872) -> c_int {
873    if ctxt.is_null() || filename.is_null() {
874        return -1;
875    }
876    let mut html = 0;
877    if !unsafe { (*ctxt).doc }.is_null() {
878        html = unsafe {
879            ((*(*ctxt).doc).type_ == xmlElementType::XML_HTML_DOCUMENT_NODE as c_int) as c_int
880        };
881    }
882
883    let doc: *mut _xmlDoc = if html != 0 {
884        // Upstream htmlParseFile (the exports_xml2 htmlParseFile is a Phase-1
885        // stub; the HTML module provides the real parser).
886        unsafe { crate::xml::html::parse_file(filename, ptr::null()) }
887    } else {
888        unsafe { xmlReadFile(filename, ptr::null(), 0) }
889    };
890
891    if !doc.is_null() {
892        unsafe {
893            if (*ctxt).loaded == 1 {
894                tree::free_doc((*ctxt).doc);
895            }
896            (*ctxt).loaded = 1;
897            xmlXPathFreeContext((*ctxt).pctxt);
898            if !(*ctxt).filename.is_null() {
899                xmlFreeImpl((*ctxt).filename as *mut c_void);
900            }
901            (*ctxt).doc = doc;
902            (*ctxt).node = doc as *mut _xmlNode;
903            (*ctxt).pctxt = xmlXPathNewContext(doc);
904            (*ctxt).filename = xmlCanonicPath(filename) as *mut c_char;
905        }
906        0
907    } else {
908        -1
909    }
910}
911
912/// `int xmlShellWrite(xmlShellCtxtPtr ctxt, char *filename, xmlNodePtr node,
913/// xmlNodePtr node2)` — the shell "write" command: write the subtree under
914/// `node` to `filename`.
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 xmlShellWrite(
932    ctxt: *mut _xmlShellCtxt,
933    filename: *mut c_char,
934    node: *mut _xmlNode,
935    _node2: *mut _xmlNode,
936) -> c_int {
937    if node.is_null() {
938        return -1;
939    }
940    if filename.is_null() || *filename == 0 {
941        unsafe {
942            shell_generic_error(
943                c"Write command requires a filename argument\n".as_ptr() as *const c_char,
944                b"",
945                b"",
946            );
947        }
948        return -1;
949    }
950    // Upstream: `if (access(filename, W_OK))` under `#ifdef W_OK`.
951    if libc::access(filename, libc::W_OK) != 0 {
952        unsafe {
953            shell_generic_error(c"Cannot write to ".as_ptr() as *const c_char, b"", b"");
954            shell_generic_error(filename, b"", b"\n");
955        }
956        return -1;
957    }
958    let typ = unsafe { (*node).type_ };
959    unsafe {
960        match typ {
961            t if t == xmlElementType::XML_DOCUMENT_NODE as c_int => {
962                if xmlSaveFile(filename, (*ctxt).doc) < -1 {
963                    shell_generic_error(c"Failed to write to ".as_ptr() as *const c_char, b"", b"");
964                    shell_generic_error(filename, b"", b"\n");
965                    return -1;
966                }
967            }
968            t if t == xmlElementType::XML_HTML_DOCUMENT_NODE as c_int => {
969                // Upstream htmlSaveFile: serialize the HTML doc to the file.
970                if shell_save_html_doc(filename, (*ctxt).doc) < 0 {
971                    shell_generic_error(c"Failed to write to ".as_ptr() as *const c_char, b"", b"");
972                    shell_generic_error(filename, b"", b"\n");
973                    return -1;
974                }
975            }
976            _ => {
977                let f = libc::fopen(filename, c"w".as_ptr() as *const c_char);
978                if f.is_null() {
979                    shell_generic_error(c"Failed to write to ".as_ptr() as *const c_char, b"", b"");
980                    shell_generic_error(filename, b"", b"\n");
981                    return -1;
982                }
983                shell_elem_dump(f as *mut c_void, (*ctxt).doc, node);
984                libc::fclose(f);
985            }
986        }
987    }
988    0
989}
990
991/// Serialize an HTML document to a file (upstream `htmlSaveFile`).
992unsafe fn shell_save_html_doc(filename: *const c_char, doc: *mut _xmlDoc) -> c_int {
993    if filename.is_null() || doc.is_null() {
994        return -1;
995    }
996    let buf = io::buf_create(-1);
997    if buf.is_null() {
998        return -1;
999    }
1000    let ret = crate::xml::html::doc_dump(buf, doc);
1001    if ret < 0 {
1002        io::buf_free(buf);
1003        return -1;
1004    }
1005    let content = io::buf_content(buf);
1006    let len = io::buf_length(buf);
1007    let written = if !content.is_null() && len > 0 {
1008        let f = libc::fopen(filename, c"w".as_ptr() as *const c_char);
1009        if f.is_null() {
1010            io::buf_free(buf);
1011            return -1;
1012        }
1013        let n = libc::fwrite(content as *const c_void, 1, len as usize, f);
1014        libc::fclose(f);
1015        n as c_int
1016    } else {
1017        0
1018    };
1019    io::buf_free(buf);
1020    written
1021}
1022
1023/// `int xmlShellSave(xmlShellCtxtPtr ctxt, char *filename, xmlNodePtr node,
1024/// xmlNodePtr node2)` — the shell "save" command: write the current document
1025/// to `filename`, or to its original name when no filename is given.
1026///
1027/// # SAFETY
1028///
1029/// - `ctxt`, `filename`, `_node`, `_node2` must be valid pointers (or NULL
1030///   where the upstream C contract allows), obtained from the
1031///   matching constructor/owner and not yet freed; the callee may
1032///   take or keep ownership exactly as the C API specifies.
1033///
1034/// The caller must not race this call with concurrent mutation of the
1035/// same objects from other threads (per-object state is not internally
1036/// synchronized). Violating any of the above is undefined behavior.
1037///
1038/// Exercised by the C-API differential courts
1039/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1040/// courts; those pass byte-for-byte against the upstream oracle.
1041#[no_mangle]
1042pub unsafe extern "C" fn xmlShellSave(
1043    ctxt: *mut _xmlShellCtxt,
1044    filename: *mut c_char,
1045    _node: *mut _xmlNode,
1046    _node2: *mut _xmlNode,
1047) -> c_int {
1048    if ctxt.is_null() || unsafe { (*ctxt).doc }.is_null() {
1049        return -1;
1050    }
1051    let mut filename = filename;
1052    if filename.is_null() || *filename == 0 {
1053        filename = unsafe { (*ctxt).filename };
1054    }
1055    if filename.is_null() {
1056        return -1;
1057    }
1058    // Upstream: `if (access(filename, W_OK))` under `#ifdef W_OK`.
1059    if libc::access(filename, libc::W_OK) != 0 {
1060        unsafe {
1061            shell_generic_error(c"Cannot save to ".as_ptr() as *const c_char, b"", b"");
1062            shell_generic_error(filename, b"", b"\n");
1063        }
1064        return -1;
1065    }
1066    let typ = unsafe { (*(*ctxt).doc).type_ };
1067    unsafe {
1068        match typ {
1069            t if t == xmlElementType::XML_DOCUMENT_NODE as c_int => {
1070                if xmlSaveFile(filename, (*ctxt).doc) < 0 {
1071                    shell_generic_error(c"Failed to save to ".as_ptr() as *const c_char, b"", b"");
1072                    shell_generic_error(filename, b"", b"\n");
1073                }
1074            }
1075            t if t == xmlElementType::XML_HTML_DOCUMENT_NODE as c_int => {
1076                if shell_save_html_doc(filename, (*ctxt).doc) < 0 {
1077                    shell_generic_error(c"Failed to save to ".as_ptr() as *const c_char, b"", b"");
1078                    shell_generic_error(filename, b"", b"\n");
1079                }
1080            }
1081            _ => {
1082                shell_generic_error(
1083                    c"To save to subparts of a document use the 'write' command\n".as_ptr()
1084                        as *const c_char,
1085                    b"",
1086                    b"",
1087                );
1088                return -1;
1089            }
1090        }
1091    }
1092    0
1093}
1094
1095// ═══════════════════════════════════════════════════════════════════════════════
1096// xmlShellValidate
1097// ═══════════════════════════════════════════════════════════════════════════════
1098
1099/// Validity callback matching upstream's use of `xmlGenericError` as the
1100/// valid-ctxt error/warning handler (default generic channel: stderr).
1101unsafe extern "C" fn shell_valid_error(_ctx: *mut c_void, msg: *const c_char) {
1102    unsafe {
1103        if !msg.is_null() {
1104            out_cstr(stderr, msg);
1105        }
1106    }
1107}
1108
1109/// Minimal external-DTD loader (upstream `xmlParseDTD(NULL, dtd)`).
1110///
1111/// Scans the file for a `<!DOCTYPE name (PUBLIC|SYSTEM) "..." ...>` header
1112/// and builds a `_xmlDtd` node from it. Declarations are not parsed, so
1113/// validation against an external DTD is permissive (empty declaration
1114/// tables). Returns NULL when the file cannot be read/scanned, in which
1115/// case the caller behaves exactly like upstream's failed `xmlParseDTD`.
1116unsafe fn shell_parse_dtd(dtd: *const c_char) -> *mut _xmlDtd {
1117    if dtd.is_null() {
1118        return ptr::null_mut();
1119    }
1120    let path = match unsafe { core::ffi::CStr::from_ptr(dtd) }.to_str() {
1121        Ok(p) => p,
1122        Err(_) => return ptr::null_mut(),
1123    };
1124    let content = match std::fs::read(path) {
1125        Ok(c) => c,
1126        Err(_) => return ptr::null_mut(),
1127    };
1128    // Locate the `<!DOCTYPE` keyword (case-insensitive per XML).
1129    let lower: Vec<u8> = content.iter().map(|b| b.to_ascii_lowercase()).collect();
1130    let pos = match find_subslice(&lower, b"<!doctype") {
1131        Some(p) => p,
1132        // No DOCTYPE header (plain external-subset .dtd files): use the
1133        // file name as the DTD name so validation can still proceed.
1134        None => {
1135            let base = path.rsplit(['/', '\\']).next().unwrap_or(path);
1136            let name_c = bytes_to_xmlstr(base.as_bytes());
1137            let dtd_node =
1138                crate::xml::dtd::new_dtd(ptr::null_mut(), name_c, ptr::null_mut(), ptr::null_mut());
1139            if !name_c.is_null() {
1140                xmlFreeImpl(name_c as *mut c_void);
1141            }
1142            return dtd_node;
1143        }
1144    };
1145    let mut i = pos + b"<!doctype".len();
1146    // Skip whitespace.
1147    while i < content.len() && (content[i] as char).is_ascii_whitespace() {
1148        i += 1;
1149    }
1150    // DTD name (up to the next whitespace).
1151    let name_start = i;
1152    while i < content.len()
1153        && !(content[i] as char).is_ascii_whitespace()
1154        && content[i] != b'>'
1155        && content[i] != b'['
1156    {
1157        i += 1;
1158    }
1159    if i == name_start {
1160        return ptr::null_mut();
1161    }
1162    let name = &content[name_start..i];
1163
1164    // Optional PUBLIC/SYSTEM identifiers.
1165    while i < content.len() && (content[i] as char).is_ascii_whitespace() {
1166        i += 1;
1167    }
1168    let mut public_id: Option<&[u8]> = None;
1169    let mut system_id: Option<&[u8]> = None;
1170    if i < content.len() && content[i..].to_ascii_lowercase().starts_with(b"public") {
1171        i += b"public".len();
1172        while i < content.len() && (content[i] as char).is_ascii_whitespace() {
1173            i += 1;
1174        }
1175        if i < content.len() && content[i] == b'"' {
1176            let s = i + 1;
1177            let e = content[s..].iter().position(|&c| c == b'"').map(|p| s + p);
1178            if let Some(e) = e {
1179                public_id = Some(&content[s..e]);
1180            }
1181        }
1182        while i < content.len() && (content[i] as char).is_ascii_whitespace() {
1183            i += 1;
1184        }
1185        if i < content.len() && content[i] == b'"' {
1186            let s = i + 1;
1187            let e = content[s..].iter().position(|&c| c == b'"').map(|p| s + p);
1188            if let Some(e) = e {
1189                system_id = Some(&content[s..e]);
1190            }
1191        }
1192    } else if i < content.len() && content[i..].to_ascii_lowercase().starts_with(b"system") {
1193        i += b"system".len();
1194        while i < content.len() && (content[i] as char).is_ascii_whitespace() {
1195            i += 1;
1196        }
1197        if i < content.len() && content[i] == b'"' {
1198            let s = i + 1;
1199            let e = content[s..].iter().position(|&c| c == b'"').map(|p| s + p);
1200            if let Some(e) = e {
1201                system_id = Some(&content[s..e]);
1202            }
1203        }
1204    }
1205
1206    let name_c = bytes_to_xmlstr(name);
1207    let ext_c = match public_id {
1208        Some(v) => bytes_to_xmlstr(v),
1209        None => ptr::null_mut(),
1210    };
1211    let sys_c = match system_id {
1212        Some(v) => bytes_to_xmlstr(v),
1213        None => ptr::null_mut(),
1214    };
1215    let dtd_node = crate::xml::dtd::new_dtd(ptr::null_mut(), name_c, ext_c, sys_c);
1216    if !name_c.is_null() {
1217        xmlFreeImpl(name_c as *mut c_void);
1218    }
1219    if !ext_c.is_null() {
1220        xmlFreeImpl(ext_c as *mut c_void);
1221    }
1222    if !sys_c.is_null() {
1223        xmlFreeImpl(sys_c as *mut c_void);
1224    }
1225    dtd_node
1226}
1227
1228/// Find `needle` in `haystack` (byte-wise).
1229fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option<usize> {
1230    if needle.is_empty() || haystack.len() < needle.len() {
1231        return None;
1232    }
1233    haystack.windows(needle.len()).position(|w| w == needle)
1234}
1235
1236/// Copy a byte slice into a NUL-terminated xmlMalloc'd xmlChar string.
1237unsafe fn bytes_to_xmlstr(bytes: &[u8]) -> *mut xmlChar {
1238    let buf = xmlMallocImpl(bytes.len() + 1) as *mut xmlChar;
1239    if buf.is_null() {
1240        return ptr::null_mut();
1241    }
1242    unsafe {
1243        ptr::copy_nonoverlapping(bytes.as_ptr(), buf, bytes.len());
1244        *buf.add(bytes.len()) = 0;
1245    }
1246    buf
1247}
1248
1249/// `int xmlShellValidate(xmlShellCtxtPtr ctxt, char *dtd, xmlNodePtr node,
1250/// xmlNodePtr node2)` — the shell "validate" command.
1251///
1252/// # SAFETY
1253///
1254/// - `ctxt`, `dtd`, `_node`, `_node2` must be valid pointers (or NULL
1255///   where the upstream C contract allows), obtained from the
1256///   matching constructor/owner and not yet freed; the callee may
1257///   take or keep ownership exactly as the C API specifies.
1258///
1259/// The caller must not race this call with concurrent mutation of the
1260/// same objects from other threads (per-object state is not internally
1261/// synchronized). Violating any of the above is undefined behavior.
1262///
1263/// Exercised by the C-API differential courts
1264/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1265/// courts; those pass byte-for-byte against the upstream oracle.
1266#[no_mangle]
1267pub unsafe extern "C" fn xmlShellValidate(
1268    ctxt: *mut _xmlShellCtxt,
1269    dtd: *mut c_char,
1270    _node: *mut _xmlNode,
1271    _node2: *mut _xmlNode,
1272) -> c_int {
1273    if ctxt.is_null() || unsafe { (*ctxt).doc }.is_null() {
1274        return -1;
1275    }
1276    // Upstream: `xmlValidCtxt vctxt; memset(&vctxt, 0, sizeof(vctxt));`
1277    let vctxt = xmlMallocZero(size_of::<_xmlValidCtxt>()) as *mut _xmlValidCtxt;
1278    if vctxt.is_null() {
1279        return -1;
1280    }
1281    unsafe {
1282        (*vctxt).error = Some(shell_valid_error);
1283        (*vctxt).warning = Some(shell_valid_error);
1284    }
1285    let mut res = -1;
1286    unsafe {
1287        if dtd.is_null() || *dtd == 0 {
1288            res = xmlValidateDocument(vctxt, (*ctxt).doc);
1289        } else {
1290            let subset = shell_parse_dtd(dtd as *const c_char);
1291            if !subset.is_null() {
1292                res = xmlValidateDtd(vctxt, (*ctxt).doc, subset);
1293                crate::xml::dtd::free_dtd(subset);
1294            }
1295        }
1296    }
1297    unsafe {
1298        xmlFreeImpl(vctxt as *mut c_void);
1299    }
1300    res
1301}
1302
1303// ═══════════════════════════════════════════════════════════════════════════════
1304// xmlShellDu / xmlShellPwd
1305// ═══════════════════════════════════════════════════════════════════════════════
1306
1307/// `int xmlShellDu(xmlShellCtxtPtr ctxt, char *arg, xmlNodePtr tree,
1308/// xmlNodePtr node2)` — the shell "du" command: show the structure of the
1309/// subtree under `tree`, deep-first.
1310///
1311/// # SAFETY
1312///
1313/// - `ctxt`, `_arg`, `tree`, `_node2` must be valid pointers (or NULL
1314///   where the upstream C contract allows), obtained from the
1315///   matching constructor/owner and not yet freed; the callee may
1316///   take or keep ownership exactly as the C API specifies.
1317///
1318/// The caller must not race this call with concurrent mutation of the
1319/// same objects from other threads (per-object state is not internally
1320/// synchronized). Violating any of the above is undefined behavior.
1321///
1322/// Exercised by the C-API differential courts
1323/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1324/// courts; those pass byte-for-byte against the upstream oracle.
1325#[no_mangle]
1326pub unsafe extern "C" fn xmlShellDu(
1327    ctxt: *mut _xmlShellCtxt,
1328    _arg: *mut c_char,
1329    tree: *mut _xmlNode,
1330    _node2: *mut _xmlNode,
1331) -> c_int {
1332    if ctxt.is_null() {
1333        return -1;
1334    }
1335    if tree.is_null() {
1336        return -1;
1337    }
1338    let out = unsafe { (*ctxt).output };
1339    let mut indent: c_int = 0;
1340    let mut node: *mut _xmlNode = tree;
1341    unsafe {
1342        while !node.is_null() {
1343            let typ = (*node).type_;
1344            if typ == xmlElementType::XML_DOCUMENT_NODE as c_int
1345                || typ == xmlElementType::XML_HTML_DOCUMENT_NODE as c_int
1346            {
1347                out_bytes(out, b"/\n");
1348            } else if typ == xmlElementType::XML_ELEMENT_NODE as c_int {
1349                let mut line = Vec::new();
1350                for _ in 0..indent {
1351                    line.extend_from_slice(b"  ");
1352                }
1353                if !(*node).ns.is_null() && !(*(*node).ns).prefix.is_null() {
1354                    push_cstr(&mut line, (*(*node).ns).prefix as *const c_char);
1355                    line.push(b':');
1356                }
1357                push_cstr(&mut line, (*node).name as *const c_char);
1358                line.push(b'\n');
1359                out_bytes(out, &line);
1360            }
1361
1362            /*
1363             * Browse the full subtree, deep first
1364             */
1365            if typ == xmlElementType::XML_DOCUMENT_NODE as c_int
1366                || typ == xmlElementType::XML_HTML_DOCUMENT_NODE as c_int
1367            {
1368                node = (*(node as *mut _xmlDoc)).children;
1369            } else if !(*node).children.is_null()
1370                && typ != xmlElementType::XML_ENTITY_REF_NODE as c_int
1371            {
1372                node = (*node).children;
1373                indent += 1;
1374            } else if node != tree && !(*node).next.is_null() {
1375                node = (*node).next;
1376            } else if node != tree {
1377                while node != tree {
1378                    if !(*node).parent.is_null() {
1379                        node = (*node).parent;
1380                        indent -= 1;
1381                    }
1382                    if node != tree && !(*node).next.is_null() {
1383                        node = (*node).next;
1384                        break;
1385                    }
1386                    if (*node).parent.is_null() {
1387                        node = ptr::null_mut();
1388                        break;
1389                    }
1390                    if node == tree {
1391                        node = ptr::null_mut();
1392                        break;
1393                    }
1394                }
1395                if node == tree {
1396                    node = ptr::null_mut();
1397                }
1398            } else {
1399                node = ptr::null_mut();
1400            }
1401        }
1402    }
1403    0
1404}
1405
1406/// `int xmlShellPwd(xmlShellCtxtPtr ctxt, char *buffer, xmlNodePtr node,
1407/// xmlNodePtr node2)` — the shell "pwd" command: full path of `node` into
1408/// `buffer` (which must hold at least 500 chars).
1409///
1410/// # SAFETY
1411///
1412/// - `_ctxt`, `buffer`, `node`, `_node2` must be valid pointers (or NULL
1413///   where the upstream C contract allows), obtained from the
1414///   matching constructor/owner and not yet freed; the callee may
1415///   take or keep ownership exactly as the C API specifies.
1416///
1417/// The caller must not race this call with concurrent mutation of the
1418/// same objects from other threads (per-object state is not internally
1419/// synchronized). Violating any of the above is undefined behavior.
1420///
1421/// Exercised by the C-API differential courts
1422/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1423/// courts; those pass byte-for-byte against the upstream oracle.
1424#[no_mangle]
1425pub unsafe extern "C" fn xmlShellPwd(
1426    _ctxt: *mut _xmlShellCtxt,
1427    buffer: *mut c_char,
1428    node: *mut _xmlNode,
1429    _node2: *mut _xmlNode,
1430) -> c_int {
1431    if node.is_null() || buffer.is_null() {
1432        return -1;
1433    }
1434
1435    let path = unsafe { shell_get_node_path(node) };
1436    if path.is_null() {
1437        return -1;
1438    }
1439
1440    // Upstream: `snprintf(buffer, 499, "%s", path); buffer[499] = '0';`
1441    let plen = unsafe { tree::xml_strlen(path) } as usize;
1442    let n = plen.min(498);
1443    unsafe {
1444        ptr::copy_nonoverlapping(path as *const u8, buffer as *mut u8, n);
1445        *buffer.add(n) = 0;
1446        *buffer.add(499) = b'0' as c_char;
1447    }
1448    unsafe {
1449        xmlFreeImpl(path as *mut c_void);
1450    }
1451    0
1452}
1453
1454// ═══════════════════════════════════════════════════════════════════════════════
1455// Static command helpers used by the shell loop
1456// ═══════════════════════════════════════════════════════════════════════════════
1457
1458/// Upstream static `xmlShellSetBase` — the shell "setbase" command.
1459unsafe fn xmlShellSetBase(ctxt: *mut _xmlShellCtxt, arg: *mut c_char, node: *mut _xmlNode) {
1460    let _ = ctxt;
1461    if !node.is_null() {
1462        unsafe {
1463            xmlNodeSetBase(node, arg as *const xmlChar);
1464        }
1465    }
1466}
1467
1468/// Upstream static `xmlShellRegisterNamespace` — the shell "setns" command:
1469/// register/unregister `prefix=nsuri` pairs on the XPath context.
1470unsafe fn xmlShellRegisterNamespace(ctxt: *mut _xmlShellCtxt, arg: *mut c_char) -> c_int {
1471    let ns_list_dup = unsafe { xmlStrdup(arg as *const xmlChar) };
1472    if ns_list_dup.is_null() {
1473        return -1;
1474    }
1475    let mut next: *mut xmlChar = ns_list_dup;
1476    loop {
1477        if unsafe { *next == 0 } {
1478            break;
1479        }
1480        // find prefix
1481        let prefix = next;
1482        let eq = unsafe { xmlStrchr(next, b'=' as xmlChar) };
1483        if eq.is_null() {
1484            unsafe {
1485                out_cstr(
1486                    (*ctxt).output,
1487                    c"setns: prefix=[nsuri] required\n".as_ptr() as *const c_char,
1488                );
1489            }
1490            unsafe {
1491                xmlFreeImpl(ns_list_dup as *mut c_void);
1492            }
1493            return -1;
1494        }
1495        // split at '='
1496        unsafe {
1497            *(eq as *mut xmlChar) = 0;
1498        }
1499        let href = unsafe { eq.add(1) };
1500        // find href
1501        let space = unsafe { xmlStrchr(href, b' ' as xmlChar) };
1502        if !space.is_null() {
1503            unsafe {
1504                *(space as *mut xmlChar) = 0;
1505            }
1506            next = unsafe { space.add(1) as *mut xmlChar };
1507        } else {
1508            next = unsafe { href.add(tree::xml_strlen(href) as usize) as *mut xmlChar };
1509        }
1510
1511        // do register namespace
1512        if unsafe { xmlXPathRegisterNs((*ctxt).pctxt, prefix, href) } != 0 {
1513            unsafe {
1514                let mut msg = Vec::new();
1515                msg.extend_from_slice(b"Error: unable to register NS with prefix=\"");
1516                push_cstr(&mut msg, prefix as *const c_char);
1517                msg.extend_from_slice(b"\" and href=\"");
1518                push_cstr(&mut msg, href as *const c_char);
1519                msg.extend_from_slice(b"\"\n");
1520                out_bytes((*ctxt).output, &msg);
1521            }
1522            unsafe {
1523                xmlFreeImpl(ns_list_dup as *mut c_void);
1524            }
1525            return -1;
1526        }
1527    }
1528    unsafe {
1529        xmlFreeImpl(ns_list_dup as *mut c_void);
1530    }
1531    0
1532}
1533
1534/// Upstream static `xmlShellRegisterRootNamespaces` — the shell "setrootns"
1535/// command: register all namespace declarations found on the root element.
1536unsafe fn xmlShellRegisterRootNamespaces(ctxt: *mut _xmlShellCtxt, root: *mut _xmlNode) -> c_int {
1537    if root.is_null()
1538        || unsafe { (*root).type_ != xmlElementType::XML_ELEMENT_NODE as c_int }
1539        || unsafe { (*root).nsDef.is_null() }
1540        || ctxt.is_null()
1541        || unsafe { (*ctxt).pctxt.is_null() }
1542    {
1543        return -1;
1544    }
1545    let mut ns = unsafe { (*root).nsDef };
1546    while !ns.is_null() {
1547        if unsafe { (*ns).prefix.is_null() } {
1548            unsafe {
1549                xmlXPathRegisterNs(
1550                    (*ctxt).pctxt,
1551                    c"defaultns".as_ptr() as *const xmlChar,
1552                    (*ns).href,
1553                );
1554            }
1555        } else {
1556            unsafe {
1557                xmlXPathRegisterNs((*ctxt).pctxt, (*ns).prefix, (*ns).href);
1558            }
1559        }
1560        ns = unsafe { (*ns).next };
1561    }
1562    0
1563}
1564
1565/// Upstream static `xmlShellGrep` — the shell "grep" command: search a
1566/// string in the subtree under `node`, deep first.
1567unsafe fn xmlShellGrep(ctxt: *mut _xmlShellCtxt, arg: *mut c_char, node: *mut _xmlNode) {
1568    if ctxt.is_null() || node.is_null() || arg.is_null() {
1569        return;
1570    }
1571    let mut node = node;
1572    while !node.is_null() {
1573        unsafe {
1574            let typ = (*node).type_;
1575            if typ == xmlElementType::XML_COMMENT_NODE as c_int {
1576                if !xmlStrstr((*node).content, arg as *const xmlChar).is_null() {
1577                    let path = shell_get_node_path(node);
1578                    if !path.is_null() {
1579                        let mut line = Vec::new();
1580                        push_cstr(&mut line, path as *const c_char);
1581                        line.extend_from_slice(b" : ");
1582                        out_bytes((*ctxt).output, &line);
1583                        xmlFreeImpl(path as *mut c_void);
1584                    }
1585                    xmlShellList(ctxt, ptr::null_mut(), node, ptr::null_mut());
1586                }
1587            } else if typ == xmlElementType::XML_TEXT_NODE as c_int
1588                && !xmlStrstr((*node).content, arg as *const xmlChar).is_null()
1589            {
1590                let path = shell_get_node_path((*node).parent);
1591                if !path.is_null() {
1592                    let mut line = Vec::new();
1593                    push_cstr(&mut line, path as *const c_char);
1594                    line.extend_from_slice(b" : ");
1595                    out_bytes((*ctxt).output, &line);
1596                    xmlFreeImpl(path as *mut c_void);
1597                }
1598                xmlShellList(ctxt, ptr::null_mut(), (*node).parent, ptr::null_mut());
1599            }
1600
1601            /*
1602             * Browse the full subtree, deep first
1603             */
1604            if typ == xmlElementType::XML_DOCUMENT_NODE as c_int
1605                || typ == xmlElementType::XML_HTML_DOCUMENT_NODE as c_int
1606            {
1607                node = (*(node as *mut _xmlDoc)).children;
1608            } else if !(*node).children.is_null()
1609                && typ != xmlElementType::XML_ENTITY_REF_NODE as c_int
1610            {
1611                node = (*node).children;
1612            } else if !(*node).next.is_null() {
1613                node = (*node).next;
1614            } else {
1615                while !node.is_null() {
1616                    if !(*node).parent.is_null() {
1617                        node = (*node).parent;
1618                    }
1619                    if !(*node).next.is_null() {
1620                        node = (*node).next;
1621                        break;
1622                    }
1623                    if (*node).parent.is_null() {
1624                        node = ptr::null_mut();
1625                        break;
1626                    }
1627                }
1628            }
1629        }
1630    }
1631}
1632
1633/// Emit the per-type error message for a non-node-set XPath result
1634/// (upstream's repeated `switch (list->type)` blocks in the shell loop).
1635unsafe fn shell_result_type_error(arg: *const c_char, typ: c_int) {
1636    if typ == xmlXPathObjectType::XPATH_UNDEFINED as c_int {
1637        unsafe { shell_generic_error(arg, b": ", b"no such node\n") };
1638    } else if typ == xmlXPathObjectType::XPATH_BOOLEAN as c_int {
1639        unsafe { shell_generic_error(arg, b" is a Boolean", b"\n") };
1640    } else if typ == xmlXPathObjectType::XPATH_NUMBER as c_int {
1641        unsafe { shell_generic_error(arg, b" is a number", b"\n") };
1642    } else if typ == xmlXPathObjectType::XPATH_STRING as c_int {
1643        unsafe { shell_generic_error(arg, b" is a string", b"\n") };
1644    } else if typ == xmlXPathObjectType::XPATH_POINT as c_int {
1645        unsafe { shell_generic_error(arg, b" is a point", b"\n") };
1646    } else if typ == xmlXPathObjectType::XPATH_RANGE as c_int
1647        || typ == xmlXPathObjectType::XPATH_LOCATIONSET as c_int
1648    {
1649        unsafe { shell_generic_error(arg, b" is a range", b"\n") };
1650    } else if typ == xmlXPathObjectType::XPATH_USERS as c_int {
1651        unsafe { shell_generic_error(arg, b" is user-defined", b"\n") };
1652    } else if typ == xmlXPathObjectType::XPATH_XSLT_TREE as c_int {
1653        unsafe { shell_generic_error(arg, b" is an XSLT value tree", b"\n") };
1654    }
1655}
1656
1657// ═══════════════════════════════════════════════════════════════════════════════
1658// xmlShell — the interactive loop
1659// ═══════════════════════════════════════════════════════════════════════════════
1660
1661/// Build the prompt string for the current node ("/ > ", "name > ",
1662/// "prefix:name > " or "? > "), NUL-terminated.
1663unsafe fn shell_build_prompt(ctxt: *mut _xmlShellCtxt) -> Vec<u8> {
1664    let mut p = Vec::new();
1665    let node = unsafe { (*ctxt).node };
1666    let doc = unsafe { (*ctxt).doc };
1667    if node == doc as *mut _xmlNode {
1668        p.extend_from_slice(b"/ > ");
1669    } else if !node.is_null() && !unsafe { (*node).name }.is_null() {
1670        unsafe {
1671            if !(*node).ns.is_null() && !(*(*node).ns).prefix.is_null() {
1672                push_cstr(&mut p, (*(*node).ns).prefix as *const c_char);
1673                p.push(b':');
1674            }
1675            push_cstr(&mut p, (*node).name as *const c_char);
1676        }
1677        p.extend_from_slice(b" > ");
1678    } else {
1679        p.extend_from_slice(b"? > ");
1680    }
1681    p.push(0);
1682    p
1683}
1684
1685/// The shell's "help" output (upstream 2.12.6), written to `ctxt->output`.
1686unsafe fn shell_print_help(ctxt: *mut _xmlShellCtxt) {
1687    let out = unsafe { (*ctxt).output };
1688    const HELP: &[&[u8]] = &[
1689        b"\tbase         display XML base of the node\n",
1690        b"\tsetbase URI  change the XML base of the node\n",
1691        b"\tbye          leave shell\n",
1692        b"\tcat [node]   display node or current node\n",
1693        b"\tcd [path]    change directory to path or to root\n",
1694        b"\tdir [path]   dumps information about the node (namespace, attributes, content)\n",
1695        b"\tdu [path]    show the structure of the subtree under path or the current node\n",
1696        b"\texit         leave shell\n",
1697        b"\thelp         display this help\n",
1698        b"\tfree         display memory usage\n",
1699        b"\tload [name]  load a new document with name\n",
1700        b"\tls [path]    list contents of path or the current directory\n",
1701        b"\txpath expr   evaluate the XPath expression in that context and print the result\n",
1702        b"\tsetns nsreg  register a namespace to a prefix in the XPath evaluation context\n",
1703        b"\t             format for nsreg is: prefix=[nsuri] (i.e. prefix= unsets a prefix)\n",
1704        b"\tsetrootns    register all namespace found on the root element\n",
1705        b"\t             the default namespace if any uses 'defaultns' prefix\n",
1706        b"\tpwd          display current working directory\n",
1707        b"\twhereis      display absolute path of [path] or current working directory\n",
1708        b"\tquit         leave shell\n",
1709        b"\tsave [name]  save this document to name or the original name\n",
1710        b"\twrite [name] write the current node to the filename\n",
1711        b"\tvalidate     check the document for errors\n",
1712        b"\tgrep string  search for a string in the subtree\n",
1713    ];
1714    for line in HELP {
1715        unsafe {
1716            out_bytes(out, line);
1717        }
1718    }
1719}
1720
1721/// `void xmlShell(xmlDocPtr doc, char *filename, xmlShellReadlineFunc input,
1722/// FILE *output)` — the XML shell: an interactive loop allowing to load,
1723/// validate, view, modify and save a document.
1724///
1725/// # SAFETY
1726///
1727/// - `doc`, `filename`, `output` must be valid pointers (or NULL
1728///   where the upstream C contract allows), obtained from the
1729///   matching constructor/owner and not yet freed; the callee may
1730///   take or keep ownership exactly as the C API specifies.
1731///
1732/// The caller must not race this call with concurrent mutation of the
1733/// same objects from other threads (per-object state is not internally
1734/// synchronized). Violating any of the above is undefined behavior.
1735///
1736/// Exercised by the C-API differential courts
1737/// (courts/suites/data-abi/*-family-probe.c) and the CLI differential
1738/// courts; those pass byte-for-byte against the upstream oracle.
1739#[no_mangle]
1740pub unsafe extern "C" fn xmlShell(
1741    doc: *mut _xmlDoc,
1742    filename: *mut c_char,
1743    input: xmlShellReadlineFunc,
1744    output: *mut c_void,
1745) {
1746    if doc.is_null() || filename.is_null() || input.is_none() {
1747        return;
1748    }
1749    let output = if output.is_null() {
1750        unsafe { stdout }
1751    } else {
1752        output
1753    };
1754
1755    let ctxt = xmlMallocZero(size_of::<_xmlShellCtxt>()) as *mut _xmlShellCtxt;
1756    if ctxt.is_null() {
1757        return;
1758    }
1759    unsafe {
1760        (*ctxt).loaded = 0;
1761        (*ctxt).doc = doc;
1762        (*ctxt).input = input;
1763        (*ctxt).output = output;
1764        (*ctxt).filename = xmlStrdup(filename as *const xmlChar) as *mut c_char;
1765        (*ctxt).node = doc as *mut _xmlNode;
1766        (*ctxt).pctxt = xmlXPathNewContext(doc);
1767    }
1768    if unsafe { (*ctxt).pctxt }.is_null() {
1769        unsafe {
1770            xmlFreeImpl(ctxt as *mut c_void);
1771        }
1772        return;
1773    }
1774
1775    let mut cmdline: *mut c_char = ptr::null_mut();
1776    loop {
1777        // Prompt.
1778        let prompt = unsafe { shell_build_prompt(ctxt) };
1779        let readline = unsafe { (*ctxt).input };
1780        cmdline = match readline {
1781            Some(f) => f(prompt.as_ptr() as *mut c_char),
1782            None => break,
1783        };
1784        if cmdline.is_null() {
1785            break;
1786        }
1787
1788        // Parse the command itself (skip leading spaces/tabs).
1789        let clen = unsafe { libc::strlen(cmdline) } as usize;
1790        let cbytes = unsafe { core::slice::from_raw_parts(cmdline as *const u8, clen) };
1791        let mut i = 0usize;
1792        while i < clen && (cbytes[i] == b' ' || cbytes[i] == b'\t') {
1793            i += 1;
1794        }
1795        let mut command: Vec<u8> = Vec::new();
1796        while i < clen
1797            && cbytes[i] != b' '
1798            && cbytes[i] != b'\t'
1799            && cbytes[i] != b'\n'
1800            && cbytes[i] != b'\r'
1801        {
1802            command.push(cbytes[i]);
1803            i += 1;
1804        }
1805        if command.is_empty() {
1806            unsafe {
1807                libc::free(cmdline as *mut c_void);
1808            }
1809            cmdline = ptr::null_mut();
1810            continue;
1811        }
1812
1813        // Parse the argument (rest of the line).
1814        while i < clen && (cbytes[i] == b' ' || cbytes[i] == b'\t') {
1815            i += 1;
1816        }
1817        let mut arg: Vec<u8> = Vec::new();
1818        while i < clen && cbytes[i] != b'\n' && cbytes[i] != b'\r' {
1819            arg.push(cbytes[i]);
1820            i += 1;
1821        }
1822
1823        // NUL-terminated views for the C API.
1824        command.push(0);
1825        let cmd: &[u8] = &command;
1826        let mut argn = arg.clone();
1827        argn.push(0);
1828        let arg_cstr: *mut c_char = argn.as_mut_ptr() as *mut c_char;
1829        let arg_xml: *const xmlChar = argn.as_ptr() as *const xmlChar;
1830
1831        // start interpreting the command
1832        if cmd == b"exit\0" || cmd == b"quit\0" || cmd == b"bye\0" {
1833            break;
1834        }
1835        if cmd == b"help\0" {
1836            unsafe { shell_print_help(ctxt) };
1837        } else if cmd == b"validate\0" {
1838            unsafe {
1839                xmlShellValidate(ctxt, arg_cstr, ptr::null_mut(), ptr::null_mut());
1840            }
1841        } else if cmd == b"load\0" {
1842            unsafe {
1843                xmlShellLoad(ctxt, arg_cstr, ptr::null_mut(), ptr::null_mut());
1844            }
1845        } else if cmd == b"save\0" {
1846            unsafe {
1847                xmlShellSave(ctxt, arg_cstr, ptr::null_mut(), ptr::null_mut());
1848            }
1849        } else if cmd == b"write\0" {
1850            if arg.is_empty() {
1851                unsafe {
1852                    shell_generic_error(
1853                        c"Write command requires a filename argument\n".as_ptr() as *const c_char,
1854                        b"",
1855                        b"",
1856                    );
1857                }
1858            } else {
1859                unsafe {
1860                    xmlShellWrite(ctxt, arg_cstr, (*ctxt).node, ptr::null_mut());
1861                }
1862            }
1863        } else if cmd == b"grep\0" {
1864            unsafe {
1865                xmlShellGrep(ctxt, arg_cstr, (*ctxt).node);
1866            }
1867        } else if cmd == b"free\0" {
1868            unsafe {
1869                if arg.is_empty() {
1870                    crate::abi::allocator::xmlMemShow((*ctxt).output, 0);
1871                } else {
1872                    let mut len: c_int = 0;
1873                    let arg_s = core::str::from_utf8(&argn[..argn.len() - 1]).unwrap_or("");
1874                    if let Ok(v) = arg_s.trim().parse::<c_int>() {
1875                        len = v;
1876                    }
1877                    crate::abi::allocator::xmlMemShow((*ctxt).output, len);
1878                }
1879            }
1880        } else if cmd == b"pwd\0" {
1881            let mut dir = [0 as c_char; 500];
1882            unsafe {
1883                if xmlShellPwd(ctxt, dir.as_mut_ptr(), (*ctxt).node, ptr::null_mut()) == 0 {
1884                    let mut line = Vec::new();
1885                    push_cstr(&mut line, dir.as_mut_ptr());
1886                    line.extend_from_slice(b"\n");
1887                    out_bytes((*ctxt).output, &line);
1888                }
1889            }
1890        } else if cmd == b"du\0" {
1891            unsafe {
1892                if arg.is_empty() {
1893                    xmlShellDu(ctxt, ptr::null_mut(), (*ctxt).node, ptr::null_mut());
1894                } else {
1895                    (*(*ctxt).pctxt).node = (*ctxt).node;
1896                    let list = xmlXPathEval(arg_xml, (*ctxt).pctxt);
1897                    if !list.is_null() {
1898                        let typ = (*list).type_;
1899                        if typ == xmlXPathObjectType::XPATH_NODESET as c_int {
1900                            let ns = (*list).nodesetval as *mut _xmlNodeSet;
1901                            if !ns.is_null() {
1902                                for indx in 0..(*ns).nodeNr {
1903                                    let n = *(*ns).nodeTab.add(indx as usize);
1904                                    xmlShellDu(ctxt, ptr::null_mut(), n, ptr::null_mut());
1905                                }
1906                            }
1907                        } else {
1908                            shell_result_type_error(arg_cstr, typ);
1909                        }
1910                        xmlXPathFreeObject(list);
1911                    } else {
1912                        shell_generic_error(arg_cstr, b": ", b"no such node\n");
1913                    }
1914                    (*(*ctxt).pctxt).node = ptr::null_mut();
1915                }
1916            }
1917        } else if cmd == b"base\0" {
1918            unsafe {
1919                xmlShellBase(ctxt, ptr::null_mut(), (*ctxt).node, ptr::null_mut());
1920            }
1921        } else if cmd == b"setns\0" {
1922            unsafe {
1923                if arg.is_empty() {
1924                    shell_generic_error(
1925                        c"setns: prefix=[nsuri] required\n".as_ptr() as *const c_char,
1926                        b"",
1927                        b"",
1928                    );
1929                } else {
1930                    xmlShellRegisterNamespace(ctxt, arg_cstr);
1931                }
1932            }
1933        } else if cmd == b"setrootns\0" {
1934            unsafe {
1935                let root = tree::doc_get_root_element((*ctxt).doc);
1936                xmlShellRegisterRootNamespaces(ctxt, root);
1937            }
1938        } else if cmd == b"xpath\0" {
1939            unsafe {
1940                if arg.is_empty() {
1941                    shell_generic_error(
1942                        c"xpath: expression required\n".as_ptr() as *const c_char,
1943                        b"",
1944                        b"",
1945                    );
1946                } else {
1947                    (*(*ctxt).pctxt).node = (*ctxt).node;
1948                    let list = xmlXPathEval(arg_xml, (*ctxt).pctxt);
1949                    xmlXPathDebugDumpObject((*ctxt).output, list, 0);
1950                    xmlXPathFreeObject(list);
1951                }
1952            }
1953        } else if cmd == b"setbase\0" {
1954            unsafe {
1955                xmlShellSetBase(ctxt, arg_cstr, (*ctxt).node);
1956            }
1957        } else if cmd == b"ls\0" || cmd == b"dir\0" {
1958            let is_dir = cmd == b"dir\0";
1959            unsafe {
1960                if arg.is_empty() {
1961                    if is_dir {
1962                        xmlShellDir(ctxt, ptr::null_mut(), (*ctxt).node, ptr::null_mut());
1963                    } else {
1964                        xmlShellList(ctxt, ptr::null_mut(), (*ctxt).node, ptr::null_mut());
1965                    }
1966                } else {
1967                    (*(*ctxt).pctxt).node = (*ctxt).node;
1968                    let list = xmlXPathEval(arg_xml, (*ctxt).pctxt);
1969                    if !list.is_null() {
1970                        let typ = (*list).type_;
1971                        if typ == xmlXPathObjectType::XPATH_NODESET as c_int {
1972                            let ns = (*list).nodesetval as *mut _xmlNodeSet;
1973                            if !ns.is_null() {
1974                                for indx in 0..(*ns).nodeNr {
1975                                    let n = *(*ns).nodeTab.add(indx as usize);
1976                                    if is_dir {
1977                                        xmlShellDir(ctxt, ptr::null_mut(), n, ptr::null_mut());
1978                                    } else {
1979                                        xmlShellList(ctxt, ptr::null_mut(), n, ptr::null_mut());
1980                                    }
1981                                }
1982                            }
1983                        } else {
1984                            shell_result_type_error(arg_cstr, typ);
1985                        }
1986                        xmlXPathFreeObject(list);
1987                    } else {
1988                        shell_generic_error(arg_cstr, b": ", b"no such node\n");
1989                    }
1990                    (*(*ctxt).pctxt).node = ptr::null_mut();
1991                }
1992            }
1993        } else if cmd == b"whereis\0" {
1994            let mut dir = [0 as c_char; 500];
1995            unsafe {
1996                if arg.is_empty() {
1997                    if xmlShellPwd(ctxt, dir.as_mut_ptr(), (*ctxt).node, ptr::null_mut()) == 0 {
1998                        let mut line = Vec::new();
1999                        push_cstr(&mut line, dir.as_mut_ptr());
2000                        line.extend_from_slice(b"\n");
2001                        out_bytes((*ctxt).output, &line);
2002                    }
2003                } else {
2004                    (*(*ctxt).pctxt).node = (*ctxt).node;
2005                    let list = xmlXPathEval(arg_xml, (*ctxt).pctxt);
2006                    if !list.is_null() {
2007                        let typ = (*list).type_;
2008                        if typ == xmlXPathObjectType::XPATH_NODESET as c_int {
2009                            let ns = (*list).nodesetval as *mut _xmlNodeSet;
2010                            if !ns.is_null() {
2011                                for indx in 0..(*ns).nodeNr {
2012                                    let n = *(*ns).nodeTab.add(indx as usize);
2013                                    if xmlShellPwd(ctxt, dir.as_mut_ptr(), n, ptr::null_mut()) == 0
2014                                    {
2015                                        let mut line = Vec::new();
2016                                        push_cstr(&mut line, dir.as_mut_ptr());
2017                                        line.extend_from_slice(b"\n");
2018                                        out_bytes((*ctxt).output, &line);
2019                                    }
2020                                }
2021                            }
2022                        } else {
2023                            shell_result_type_error(arg_cstr, typ);
2024                        }
2025                        xmlXPathFreeObject(list);
2026                    } else {
2027                        shell_generic_error(arg_cstr, b": ", b"no such node\n");
2028                    }
2029                    (*(*ctxt).pctxt).node = ptr::null_mut();
2030                }
2031            }
2032        } else if cmd == b"cd\0" {
2033            unsafe {
2034                if arg.is_empty() {
2035                    (*ctxt).node = (*ctxt).doc as *mut _xmlNode;
2036                } else {
2037                    // Upstream strips a trailing '/' from the argument.
2038                    let mut argn = argn;
2039                    let l = argn.len();
2040                    if l >= 3 && argn[l - 2] == b'/' {
2041                        argn[l - 2] = 0;
2042                    }
2043                    (*(*ctxt).pctxt).node = (*ctxt).node;
2044                    let list = xmlXPathEval(argn.as_ptr() as *const xmlChar, (*ctxt).pctxt);
2045                    if !list.is_null() {
2046                        let typ = (*list).type_;
2047                        if typ == xmlXPathObjectType::XPATH_NODESET as c_int {
2048                            let ns = (*list).nodesetval as *mut _xmlNodeSet;
2049                            if !ns.is_null() {
2050                                if (*ns).nodeNr == 1 {
2051                                    (*ctxt).node = *(*ns).nodeTab;
2052                                    if !(*ctxt).node.is_null()
2053                                        && (*(*ctxt).node).type_
2054                                            == xmlElementType::XML_NAMESPACE_DECL as c_int
2055                                    {
2056                                        shell_generic_error(
2057                                            c"cannot cd to namespace\n".as_ptr() as *const c_char,
2058                                            b"",
2059                                            b"",
2060                                        );
2061                                        (*ctxt).node = ptr::null_mut();
2062                                    }
2063                                } else {
2064                                    let mut msg = Vec::new();
2065                                    push_cstr(&mut msg, arg_cstr);
2066                                    msg.extend_from_slice(b" is a ");
2067                                    msg.extend_from_slice((*ns).nodeNr.to_string().as_bytes());
2068                                    msg.extend_from_slice(b" Node Set\n");
2069                                    out_bytes(stderr, &msg);
2070                                }
2071                            } else {
2072                                let mut msg = Vec::new();
2073                                push_cstr(&mut msg, arg_cstr);
2074                                msg.extend_from_slice(b" is an empty Node Set\n");
2075                                out_bytes(stderr, &msg);
2076                            }
2077                        } else {
2078                            shell_result_type_error(arg_cstr, typ);
2079                        }
2080                        xmlXPathFreeObject(list);
2081                    } else {
2082                        shell_generic_error(arg_cstr, b": ", b"no such node\n");
2083                    }
2084                    (*(*ctxt).pctxt).node = ptr::null_mut();
2085                }
2086            }
2087        } else if cmd == b"cat\0" {
2088            unsafe {
2089                if arg.is_empty() {
2090                    xmlShellCat(ctxt, ptr::null_mut(), (*ctxt).node, ptr::null_mut());
2091                } else {
2092                    // UPSTREAM-PARITY: the 2.12.x loop reuses the outer
2093                    // `i` (the argument length) in `if (i > 0)`, which is
2094                    // always true here, so the separator is emitted before
2095                    // every node of the node-set.
2096                    (*(*ctxt).pctxt).node = (*ctxt).node;
2097                    let list = xmlXPathEval(arg_xml, (*ctxt).pctxt);
2098                    if !list.is_null() {
2099                        let typ = (*list).type_;
2100                        if typ == xmlXPathObjectType::XPATH_NODESET as c_int {
2101                            let ns = (*list).nodesetval as *mut _xmlNodeSet;
2102                            if !ns.is_null() {
2103                                for indx in 0..(*ns).nodeNr {
2104                                    if i > 0 {
2105                                        out_bytes((*ctxt).output, b" -------\n");
2106                                    }
2107                                    let n = *(*ns).nodeTab.add(indx as usize);
2108                                    xmlShellCat(ctxt, ptr::null_mut(), n, ptr::null_mut());
2109                                }
2110                            }
2111                        } else {
2112                            shell_result_type_error(arg_cstr, typ);
2113                        }
2114                        xmlXPathFreeObject(list);
2115                    } else {
2116                        shell_generic_error(arg_cstr, b": ", b"no such node\n");
2117                    }
2118                    (*(*ctxt).pctxt).node = ptr::null_mut();
2119                }
2120            }
2121        } else {
2122            let mut msg = Vec::new();
2123            msg.extend_from_slice(b"Unknown command ");
2124            msg.extend_from_slice(&command[..command.len() - 1]);
2125            msg.extend_from_slice(b"\n");
2126            unsafe {
2127                out_bytes(stderr, &msg);
2128            }
2129        }
2130
2131        unsafe {
2132            libc::free(cmdline as *mut c_void);
2133        }
2134        cmdline = ptr::null_mut();
2135    }
2136
2137    // Cleanup (upstream xmlShell epilogue).
2138    unsafe {
2139        xmlXPathFreeContext((*ctxt).pctxt);
2140        if (*ctxt).loaded != 0 {
2141            tree::free_doc((*ctxt).doc);
2142        }
2143        if !(*ctxt).filename.is_null() {
2144            xmlFreeImpl((*ctxt).filename as *mut c_void);
2145        }
2146        xmlFreeImpl(ctxt as *mut c_void);
2147        if !cmdline.is_null() {
2148            libc::free(cmdline as *mut c_void);
2149        }
2150    }
2151}