Skip to main content

libxml_rs/xml/
save.rs

1//! XML save-context API (upstream xmlsave.c, 2.15.3).
2//!
3//! `xmlSaveToFd` / `xmlSaveToFilename` / `xmlSaveToBuffer` / `xmlSaveToIO`
4//! create a save context; `xmlSaveDoc` / `xmlSaveTree` serialize into it;
5//! `xmlSaveFlush` / `xmlSaveClose` / `xmlSaveFinish` finalize it.
6//!
7//! # UPSTREAM-PARITY
8//!
9//! `xmlSaveCtxt` is opaque in the public headers (xmlsave.h); the candidate
10//! defines its own internal representation — there is no ABI constraint on
11//! its layout. Behavior mirrors xmlsave.c: options XML_SAVE_FORMAT,
12//! XML_SAVE_NO_DECL, XML_SAVE_NO_EMPTY and the deprecated escape callbacks.
13//! Formatting/decl handling is provided by the tree serializer
14//! (`serialize_node_opts`), which mirrors upstream `xmlSaveDoc`/
15//! `xmlSaveTree`/DumpState mechanics.
16//!
17//! # Courts
18//!
19//! SAVE-* differential cases compare `xmlSave*` output byte-for-byte with
20//! the oracle DSO across option combinations.
21//!
22//! # Upstream contract
23//!
24//! Mirrors upstream `xmlsave.c` (+ xmlIO.c output buffers) at libxml2
25//! 2.15.3 (`SRC-LIBXML2-2.15.0-XMLSAVE-C`): `xmlSaveToFd` / `xmlSaveToIO` /
26//! `xmlSaveToFilename` / `xmlSaveToBuffer`, `xmlSaveDoc`, `xmlSaveTree`,
27//! `xmlSaveFlush` / `xmlSaveFinish` / `xmlSaveClose`, and the deprecated
28//! `xmlSaveSetEscape` / `xmlSaveSetAttrEscape` hooks.
29//!
30//! # Conceptual behavior
31//!
32//! A save context wraps an output buffer plus the `XML_SAVE_*` option mask;
33//! `xmlSaveDoc`/`xmlSaveTree` delegate to the tree serializer
34//! (`serialize_node_opts`), which mirrors upstream DumpState mechanics
35//! (format/indent, XML declaration suppression, empty-element policy).
36//!
37//! # Ownership & safety invariants
38//!
39//! `xmlSaveTo*` adopts the output buffer; `xmlSaveClose` flushes and frees
40//! it. The escape/attrEscape callback slots are stored verbatim and never
41//! dereferenced by the context (deprecated upstream).
42//!
43//! # Historical quirks & epochs
44//!
45//! The escape/attrEscape hooks are deprecated since the 2.x era and kept
46//! only for source compatibility; the serializer behavior targets the
47//! 2.15.3 epoch (e.g. the html-dump single-line epoch E-007 applies to the
48//! HTML serializer, and XSLT output relies on these options).
49//!
50//! # Deliberate oddities
51//!
52//! `xmlSaveCtxt` is opaque in the public header, so the candidate-internal
53//! layout is unconstrained — the deliberate fidelity surface is the
54//! behavior, not the struct bytes.
55//!
56//! # Proving courts
57//!
58//! SAVE-* differential probes (courts/suites/data-abi/*) compare output
59//! byte-identical against the oracle DSO; the CLI differential courts
60//! (xmllint save paths) and cargo test round-trips cover the options.
61//!
62//! # Tempting simplifications that would break parity
63//!
64//! Do not drop the deprecated escape callback slots: consumers still set
65//! them and observe them firing during serialization. Do not bypass the
66//! output-buffer layer (xmlIO.c): flush counts and encoder interaction
67//! (R-000151) are observable through `xmlSaveFlush`/`xmlSaveClose`.
68
69use crate::abi::callbacks::{
70    xmlCharEncodingOutputFunc, xmlOutputCloseCallback, xmlOutputWriteCallback,
71};
72use crate::abi::structs::{_xmlDoc, _xmlNode, _xmlOutputBuffer};
73use crate::abi::types::xmlChar;
74use crate::xml::io;
75use std::os::raw::{c_char, c_int, c_long};
76use std::ptr;
77
78/// XML_SAVE_FORMAT — format output (newlines + indentation).
79pub const XML_SAVE_FORMAT: c_int = 1 << 0;
80/// XML_SAVE_NO_DECL — don't emit an XML declaration.
81pub const XML_SAVE_NO_DECL: c_int = 1 << 1;
82/// XML_SAVE_NO_EMPTY — don't emit empty tags.
83pub const XML_SAVE_NO_EMPTY: c_int = 1 << 2;
84
85/// Candidate-internal save context (opaque upstream).
86#[derive(Debug)]
87#[repr(C)]
88pub struct _xmlSaveCtxt {
89    /// The output buffer the context serializes into.
90    pub buf: *mut _xmlOutputBuffer,
91    /// The `XML_SAVE_*` option bitmask passed to `xmlSaveTo*`.
92    pub options: c_int,
93    /// Whether `XML_SAVE_FORMAT` (newlines + indentation) is enabled.
94    pub format: c_int,
95    /// Whether the XML declaration is suppressed (`XML_SAVE_NO_DECL`).
96    pub no_decl: c_int,
97    /// Whether empty elements must be written with an explicit end tag
98    /// (`XML_SAVE_NO_EMPTY`).
99    pub no_empty: c_int,
100    /// Optional indentation string used when formatting is enabled.
101    pub indent: *mut xmlChar,
102    /// Character-escaping callback for text content (deprecated upstream).
103    pub escape: Option<xmlCharEncodingOutputFunc>,
104    /// Character-escaping callback for attribute values (deprecated upstream).
105    pub attrEscape: Option<xmlCharEncodingOutputFunc>,
106}
107
108/// Create a save context around an output buffer.
109unsafe fn save_ctxt_new(buf: *mut _xmlOutputBuffer, options: c_int) -> *mut _xmlSaveCtxt {
110    if buf.is_null() {
111        return ptr::null_mut();
112    }
113    let ctxt = libc::calloc(1, core::mem::size_of::<_xmlSaveCtxt>()) as *mut _xmlSaveCtxt;
114    if ctxt.is_null() {
115        io::output_buffer_close(buf);
116        return ptr::null_mut();
117    }
118    (*ctxt).buf = buf;
119    (*ctxt).options = options;
120    (*ctxt).format = if (options & XML_SAVE_FORMAT) != 0 {
121        1
122    } else {
123        0
124    };
125    (*ctxt).no_decl = if (options & XML_SAVE_NO_DECL) != 0 {
126        1
127    } else {
128        0
129    };
130    (*ctxt).no_empty = if (options & XML_SAVE_NO_EMPTY) != 0 {
131        1
132    } else {
133        0
134    };
135    ctxt
136}
137
138/// Resolve an encoding name to an encoding handler.
139unsafe fn encoding_handler(
140    encoding: *const c_char,
141) -> *mut crate::abi::structs::_xmlCharEncodingHandler {
142    if encoding.is_null() {
143        return ptr::null_mut();
144    }
145    crate::xml::encoding::xmlFindCharEncodingHandler(encoding)
146}
147
148/// `xmlSaveCtxt *xmlSaveToFd(int fd, const char *encoding, int options)`.
149///
150/// # SAFETY
151///
152/// - `fd` must be a valid open file descriptor.
153#[no_mangle]
154pub unsafe extern "C" fn xmlSaveToFd(
155    fd: c_int,
156    encoding: *const c_char,
157    options: c_int,
158) -> *mut _xmlSaveCtxt {
159    let enc = unsafe { encoding_handler(encoding) };
160    let out = io::output_buffer_create_fd(fd, enc);
161    unsafe { save_ctxt_new(out, options) }
162}
163
164/// `xmlSaveCtxt *xmlSaveToFilename(const char *filename, const char *encoding, int options)`.
165///
166/// # SAFETY
167///
168/// - `filename` must be a valid NUL-terminated path.
169#[no_mangle]
170pub unsafe extern "C" fn xmlSaveToFilename(
171    filename: *const c_char,
172    encoding: *const c_char,
173    options: c_int,
174) -> *mut _xmlSaveCtxt {
175    let enc = unsafe { encoding_handler(encoding) };
176    let out = io::output_buffer_create_filename(filename, enc, 0);
177    unsafe { save_ctxt_new(out, options) }
178}
179
180/// `xmlSaveCtxt *xmlSaveToBuffer(xmlBuffer *buffer, const char *encoding, int options)`.
181///
182/// # SAFETY
183///
184/// - `buffer` must be a valid `_xmlBuffer`.
185#[no_mangle]
186pub unsafe extern "C" fn xmlSaveToBuffer(
187    buffer: *mut crate::abi::structs::_xmlBuffer,
188    encoding: *const c_char,
189    options: c_int,
190) -> *mut _xmlSaveCtxt {
191    let enc = unsafe { encoding_handler(encoding) };
192    let out = io::output_buffer_create_buffer(buffer, enc);
193    unsafe { save_ctxt_new(out, options) }
194}
195
196/// `xmlSaveCtxt *xmlSaveToIO(xmlOutputWriteCallback iowrite, xmlOutputCloseCallback ioclose, void *ioctx, const char *encoding, int options)`.
197///
198/// # SAFETY
199///
200/// - The callbacks must be valid function pointers or NULL.
201#[no_mangle]
202pub unsafe extern "C" fn xmlSaveToIO(
203    iowrite: Option<xmlOutputWriteCallback>,
204    ioclose: Option<xmlOutputCloseCallback>,
205    ioctx: *mut core::ffi::c_void,
206    encoding: *const c_char,
207    options: c_int,
208) -> *mut _xmlSaveCtxt {
209    let enc = unsafe { encoding_handler(encoding) };
210    let out = io::output_buffer_create_io(iowrite, ioclose, ioctx, enc);
211    unsafe { save_ctxt_new(out, options) }
212}
213
214/// Serialize `doc` into the save context's output buffer.
215///
216/// Returns the number of bytes written, or -1 on error.
217///
218/// # SAFETY
219///
220/// - `ctxt` must be a valid save context.
221/// - `doc` must be a valid document or NULL.
222#[no_mangle]
223pub unsafe extern "C" fn xmlSaveDoc(ctxt: *mut _xmlSaveCtxt, doc: *mut _xmlDoc) -> c_long {
224    unsafe { save_doc_or_tree(ctxt, doc as *mut _xmlNode) }
225}
226
227/// Serialize a node tree into the save context's output buffer.
228///
229/// Returns the number of bytes written, or -1 on error.
230///
231/// # SAFETY
232///
233/// - `ctxt` must be a valid save context.
234/// - `node` must be a valid node or NULL.
235#[no_mangle]
236pub unsafe extern "C" fn xmlSaveTree(ctxt: *mut _xmlSaveCtxt, node: *mut _xmlNode) -> c_long {
237    unsafe { save_doc_or_tree(ctxt, node) }
238}
239
240unsafe fn save_doc_or_tree(ctxt: *mut _xmlSaveCtxt, node: *mut _xmlNode) -> c_long {
241    if ctxt.is_null() || node.is_null() {
242        return -1;
243    }
244    let buf = io::buf_create(-1);
245    if buf.is_null() {
246        return -1;
247    }
248    let indent = (*ctxt).indent;
249    let format = (*ctxt).format;
250    let no_decl = (*ctxt).no_decl;
251    crate::xml::tree::serialize_node_opts(node, buf, format, 0, indent, no_decl);
252
253    let before = io::buf_length(buf);
254    let content = io::buf_content(buf);
255    let ret = if before > 0 && !content.is_null() {
256        io::output_buffer_write((*ctxt).buf, before, content as *const c_char)
257    } else {
258        0
259    };
260    io::buf_free(buf);
261    if ret < 0 {
262        -1
263    } else {
264        ret as c_long
265    }
266}
267
268/// `int xmlSaveFlush(xmlSaveCtxt *ctxt)` — flush the output buffer.
269///
270/// # SAFETY
271///
272/// - `ctxt` must be a valid save context.
273#[no_mangle]
274pub unsafe extern "C" fn xmlSaveFlush(ctxt: *mut _xmlSaveCtxt) -> c_int {
275    if ctxt.is_null() {
276        return -1;
277    }
278    io::output_buffer_flush((*ctxt).buf)
279}
280
281/// `int xmlSaveClose(xmlSaveCtxt *ctxt)` — flush, close and free the context.
282///
283/// # UPSTREAM-PARITY
284///
285/// Returns the number of bytes written (the flush result), like upstream
286/// xmlSaveClose (xmlsave.c 2.15); the underlying output buffer is closed by
287/// xmlFreeSaveCtxt.
288///
289/// # SAFETY
290///
291/// - `ctxt` must be a valid save context; it is freed by this call.
292#[no_mangle]
293pub unsafe extern "C" fn xmlSaveClose(ctxt: *mut _xmlSaveCtxt) -> c_int {
294    if ctxt.is_null() {
295        return -1;
296    }
297    let flush_ret = if (*ctxt).buf.is_null() {
298        -1
299    } else {
300        io::output_buffer_flush((*ctxt).buf)
301    };
302    // xmlFreeSaveCtxt closes the output buffer and frees the context.
303    if !(*ctxt).buf.is_null() {
304        io::output_buffer_close((*ctxt).buf);
305    }
306    if !(*ctxt).indent.is_null() {
307        libc::free((*ctxt).indent as *mut libc::c_void);
308    }
309    libc::free(ctxt as *mut libc::c_void);
310    flush_ret
311}
312
313/// `xmlParserErrors xmlSaveFinish(xmlSaveCtxt *ctxt)` — flush, close, free;
314/// returns an xmlParserErrors code (XML_ERR_OK on success).
315///
316/// # UPSTREAM-PARITY
317///
318/// Upstream xmlSaveFinish returns `xmlOutputBufferClose(ctxt->buf)`'s error
319/// code (negated when negative), i.e. XML_ERR_OK (0) on success.
320///
321/// # SAFETY
322///
323/// - `ctxt` must be a valid save context; it is freed by this call.
324#[no_mangle]
325pub unsafe extern "C" fn xmlSaveFinish(ctxt: *mut _xmlSaveCtxt) -> c_int {
326    if ctxt.is_null() {
327        return -1;
328    }
329    let ret = if (*ctxt).buf.is_null() {
330        -1
331    } else {
332        io::output_buffer_close((*ctxt).buf)
333    };
334    if !(*ctxt).indent.is_null() {
335        libc::free((*ctxt).indent as *mut libc::c_void);
336    }
337    libc::free(ctxt as *mut libc::c_void);
338    if ret < 0 {
339        -ret
340    } else {
341        0
342    }
343}
344
345/// `int xmlSaveSetIndentString(xmlSaveCtxt *ctxt, const char *indent)`.
346///
347/// # SAFETY
348///
349/// - `ctxt` must be a valid save context.
350/// - `indent` must be a valid NUL-terminated string or NULL (reset to
351///   default).
352#[no_mangle]
353pub unsafe extern "C" fn xmlSaveSetIndentString(
354    ctxt: *mut _xmlSaveCtxt,
355    indent: *const c_char,
356) -> c_int {
357    // UPSTREAM-PARITY: xmlSaveSetIndentString rejects NULL/empty/overlong
358    // indents (xmlsave.c 2.15: (ctxt==NULL)||(indent==NULL) -> -1,
359    // len<=0 || len>MAX_INDENT -> -1).
360    if ctxt.is_null() || indent.is_null() {
361        return -1;
362    }
363    let len = libc::strlen(indent) as usize;
364    if len == 0 || len > 60 {
365        return -1;
366    }
367    if !(*ctxt).indent.is_null() {
368        libc::free((*ctxt).indent as *mut libc::c_void);
369        (*ctxt).indent = ptr::null_mut();
370    }
371    let copy = libc::malloc(len + 1) as *mut xmlChar;
372    if copy.is_null() {
373        return -1;
374    }
375    libc::memcpy(
376        copy as *mut libc::c_void,
377        indent as *const libc::c_void,
378        len + 1,
379    );
380    (*ctxt).indent = copy;
381    0
382}
383
384/// `int xmlSaveSetEscape(xmlSaveCtxt *ctxt, xmlCharEncodingOutputFunc escape)`.
385///
386/// # SAFETY
387///
388/// - `ctxt` must be a valid save context.
389#[no_mangle]
390pub unsafe extern "C" fn xmlSaveSetEscape(
391    ctxt: *mut _xmlSaveCtxt,
392    escape: Option<xmlCharEncodingOutputFunc>,
393) -> c_int {
394    if ctxt.is_null() {
395        return -1;
396    }
397    (*ctxt).escape = escape;
398    0
399}
400
401/// `int xmlSaveSetAttrEscape(xmlSaveCtxt *ctxt, xmlCharEncodingOutputFunc escape)`.
402///
403/// # SAFETY
404///
405/// - `ctxt` must be a valid save context.
406#[no_mangle]
407pub unsafe extern "C" fn xmlSaveSetAttrEscape(
408    ctxt: *mut _xmlSaveCtxt,
409    escape: Option<xmlCharEncodingOutputFunc>,
410) -> c_int {
411    if ctxt.is_null() {
412        return -1;
413    }
414    (*ctxt).attrEscape = escape;
415    0
416}
417
418/// Wrap an existing output buffer in a save context (candidate-internal;
419/// does not close the buffer on allocation failure — upstream xmlSaveFormatFileTo
420/// semantics).
421unsafe fn save_ctxt_wrap(buf: *mut _xmlOutputBuffer, options: c_int) -> *mut _xmlSaveCtxt {
422    if buf.is_null() {
423        return ptr::null_mut();
424    }
425    let ctxt = libc::calloc(1, core::mem::size_of::<_xmlSaveCtxt>()) as *mut _xmlSaveCtxt;
426    if ctxt.is_null() {
427        return ptr::null_mut();
428    }
429    (*ctxt).buf = buf;
430    (*ctxt).options = options;
431    (*ctxt).format = if (options & XML_SAVE_FORMAT) != 0 {
432        1
433    } else {
434        0
435    };
436    (*ctxt).no_decl = if (options & XML_SAVE_NO_DECL) != 0 {
437        1
438    } else {
439        0
440    };
441    (*ctxt).no_empty = if (options & XML_SAVE_NO_EMPTY) != 0 {
442        1
443    } else {
444        0
445    };
446    ctxt
447}
448
449/// `int xmlSaveFormatFileTo(xmlOutputBufferPtr buf, xmlDocPtr cur, const char *encoding, int format)`
450/// — serialize `cur` into an existing output buffer and close it (upstream
451/// xmlsave.c).
452///
453/// # SAFETY
454///
455/// - `buf` must be a valid output buffer (closed by this call).
456/// - `cur` must be a valid document.
457#[no_mangle]
458pub unsafe extern "C" fn xmlSaveFormatFileTo(
459    buf: *mut _xmlOutputBuffer,
460    cur: *mut _xmlDoc,
461    encoding: *const c_char,
462    format: c_int,
463) -> c_int {
464    let _ = encoding;
465    let options = if format != 0 { XML_SAVE_FORMAT } else { 0 };
466    let ctxt = unsafe { save_ctxt_wrap(buf, options) };
467    if ctxt.is_null() {
468        return -1;
469    }
470    let ret = unsafe { xmlSaveDoc(ctxt, cur) };
471    let close_ret = unsafe { xmlSaveClose(ctxt) };
472    if ret < 0 {
473        -1
474    } else {
475        close_ret
476    }
477}
478
479/// `int xmlSaveFileTo(xmlOutputBufferPtr buf, xmlDocPtr cur, const char *encoding)`
480/// — upstream xmlsave.c delegates to xmlSaveFormatFileTo(buf, cur, encoding, 0).
481///
482/// # SAFETY
483///
484/// - `buf` must be a valid output buffer (closed by this call).
485/// - `cur` must be a valid document.
486#[no_mangle]
487pub unsafe extern "C" fn xmlSaveFileTo(
488    buf: *mut _xmlOutputBuffer,
489    cur: *mut _xmlDoc,
490    encoding: *const c_char,
491) -> c_int {
492    unsafe { xmlSaveFormatFileTo(buf, cur, encoding, 0) }
493}
494
495#[cfg(test)]
496mod tests {
497    use super::*;
498    use crate::xml::tree::new_doc;
499
500    fn doc_with_root() -> *mut _xmlDoc {
501        unsafe {
502            let doc = new_doc(c"1.0".as_ptr() as *const xmlChar);
503            let root =
504                crate::xml::tree::new_node(ptr::null_mut(), c"root".as_ptr() as *const xmlChar);
505            crate::xml::tree::doc_set_root_element(doc, root);
506            doc
507        }
508    }
509
510    #[test]
511    fn test_save_to_buffer_format_and_nodes() {
512        unsafe {
513            let doc = doc_with_root();
514            let buf = io::buf_create(-1);
515            let ctxt = xmlSaveToBuffer(buf, ptr::null(), XML_SAVE_FORMAT);
516            assert!(!ctxt.is_null());
517            assert!(xmlSaveDoc(ctxt, doc) >= 0);
518            assert_eq!(xmlSaveFinish(ctxt), 0);
519            let content = io::buf_content(buf);
520            let len = io::buf_length(buf);
521            let s = core::slice::from_raw_parts(content, len as usize);
522            let expected = "<?xml version=\"1.0\"?>\n<root/>\n";
523            assert_eq!(s, expected.as_bytes());
524            crate::xml::tree::free_doc(doc);
525            io::buf_free(buf);
526        }
527    }
528
529    #[test]
530    fn test_save_no_decl() {
531        unsafe {
532            let doc = doc_with_root();
533            let buf = io::buf_create(-1);
534            let ctxt = xmlSaveToBuffer(buf, ptr::null(), XML_SAVE_NO_DECL);
535            assert!(!ctxt.is_null());
536            xmlSaveDoc(ctxt, doc);
537            xmlSaveFinish(ctxt);
538            let content = io::buf_content(buf);
539            let len = io::buf_length(buf);
540            let s = core::slice::from_raw_parts(content, len as usize);
541            assert_eq!(s, b"<root/>\n");
542            crate::xml::tree::free_doc(doc);
543            io::buf_free(buf);
544        }
545    }
546
547    #[test]
548    fn test_save_set_indent_string() {
549        unsafe {
550            let doc = doc_with_root();
551            let child =
552                crate::xml::tree::new_node(ptr::null_mut(), c"child".as_ptr() as *const xmlChar);
553            crate::xml::tree::add_child(crate::xml::tree::doc_get_root_element(doc), child);
554            let buf = io::buf_create(-1);
555            let ctxt = xmlSaveToBuffer(buf, ptr::null(), XML_SAVE_FORMAT);
556            assert!(!ctxt.is_null());
557            assert_eq!(
558                xmlSaveSetIndentString(ctxt, c"\t".as_ptr() as *const c_char),
559                0
560            );
561            xmlSaveDoc(ctxt, doc);
562            xmlSaveFinish(ctxt);
563            let content = io::buf_content(buf);
564            let len = io::buf_length(buf);
565            let s = core::slice::from_raw_parts(content, len as usize);
566            let expected = "<?xml version=\"1.0\"?>\n<root>\n\t<child/>\n</root>\n";
567            assert_eq!(s, expected.as_bytes());
568            crate::xml::tree::free_doc(doc);
569            io::buf_free(buf);
570        }
571    }
572
573    #[test]
574    fn test_save_close_null_and_errors() {
575        unsafe {
576            assert!(xmlSaveToFd(-1, ptr::null(), 0).is_null());
577            assert_eq!(xmlSaveFlush(ptr::null_mut()), -1);
578            assert_eq!(xmlSaveFinish(ptr::null_mut()), -1);
579            assert_eq!(xmlSaveClose(ptr::null_mut()), -1);
580            assert_eq!(xmlSaveSetIndentString(ptr::null_mut(), ptr::null()), -1);
581            assert_eq!(xmlSaveSetEscape(ptr::null_mut(), None), -1);
582            assert_eq!(xmlSaveSetAttrEscape(ptr::null_mut(), None), -1);
583            assert_eq!(xmlSaveDoc(ptr::null_mut(), ptr::null_mut()), -1);
584            assert_eq!(xmlSaveTree(ptr::null_mut(), ptr::null_mut()), -1);
585        }
586    }
587}