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::allocator::xmlFreeImpl;
70use crate::abi::callbacks::{
71    xmlCharEncodingOutputFunc, xmlOutputCloseCallback, xmlOutputWriteCallback,
72};
73use crate::abi::structs::{_xmlDoc, _xmlNode, _xmlOutputBuffer};
74use crate::abi::types::xmlChar;
75use crate::xml::io;
76use std::os::raw::{c_char, c_int, c_long};
77use std::ptr;
78
79/// XML_SAVE_FORMAT — format output (newlines + indentation).
80pub const XML_SAVE_FORMAT: c_int = 1 << 0;
81/// XML_SAVE_NO_DECL — don't emit an XML declaration.
82pub const XML_SAVE_NO_DECL: c_int = 1 << 1;
83/// XML_SAVE_NO_EMPTY — don't emit empty tags.
84pub const XML_SAVE_NO_EMPTY: c_int = 1 << 2;
85
86/// Candidate-internal save context (opaque upstream).
87#[derive(Debug)]
88#[repr(C)]
89pub struct _xmlSaveCtxt {
90    /// The output buffer the context serializes into.
91    pub buf: *mut _xmlOutputBuffer,
92    /// The `XML_SAVE_*` option bitmask passed to `xmlSaveTo*`.
93    pub options: c_int,
94    /// Whether `XML_SAVE_FORMAT` (newlines + indentation) is enabled.
95    pub format: c_int,
96    /// Whether the XML declaration is suppressed (`XML_SAVE_NO_DECL`).
97    pub no_decl: c_int,
98    /// Whether empty elements must be written with an explicit end tag
99    /// (`XML_SAVE_NO_EMPTY`).
100    pub no_empty: c_int,
101    /// Optional indentation string used when formatting is enabled.
102    pub indent: *mut xmlChar,
103    /// The encoding name carried into the XML declaration (upstream
104    /// `ctxt->encoding`, xmlStrdup'd at context creation, freed by
105    /// `xmlSaveClose`/`xmlSaveFinish` like upstream xmlFreeSaveCtxt).
106    /// NULL means "use the document's own encoding" (upstream
107    /// `if (encoding == NULL) encoding = cur->encoding;`).
108    pub encoding: *mut xmlChar,
109    /// Character-escaping callback for text content (deprecated upstream).
110    pub escape: Option<xmlCharEncodingOutputFunc>,
111    /// Character-escaping callback for attribute values (deprecated upstream).
112    pub attrEscape: Option<xmlCharEncodingOutputFunc>,
113}
114
115/// Create a save context around an output buffer.
116///
117/// `encoding` is the encoding name passed to the `xmlSaveTo*` constructor
118/// (upstream `xmlNewSaveCtxt` xmlStrdups it into `ctxt->encoding`); it is
119/// emitted in the XML declaration and used to pick the encoder.
120///
121/// # SAFETY
122///
123/// - `encoding` must be NULL or a valid NUL-terminated string.
124unsafe fn save_ctxt_new(
125    buf: *mut _xmlOutputBuffer,
126    options: c_int,
127    encoding: *const c_char,
128) -> *mut _xmlSaveCtxt {
129    if buf.is_null() {
130        return ptr::null_mut();
131    }
132    let ctxt = libc::calloc(1, core::mem::size_of::<_xmlSaveCtxt>()) as *mut _xmlSaveCtxt;
133    if ctxt.is_null() {
134        io::output_buffer_close(buf);
135        return ptr::null_mut();
136    }
137    (*ctxt).buf = buf;
138    (*ctxt).options = options;
139    (*ctxt).format = if (options & XML_SAVE_FORMAT) != 0 {
140        1
141    } else {
142        0
143    };
144    (*ctxt).no_decl = if (options & XML_SAVE_NO_DECL) != 0 {
145        1
146    } else {
147        0
148    };
149    (*ctxt).no_empty = if (options & XML_SAVE_NO_EMPTY) != 0 {
150        1
151    } else {
152        0
153    };
154    (*ctxt).encoding = if encoding.is_null() {
155        ptr::null_mut()
156    } else {
157        crate::abi::exports_xml2::xmlStrdup(encoding as *const xmlChar)
158    };
159    ctxt
160}
161
162/// Resolve an encoding name to an encoding handler.
163unsafe fn encoding_handler(
164    encoding: *const c_char,
165) -> *mut crate::abi::structs::_xmlCharEncodingHandler {
166    if encoding.is_null() {
167        return ptr::null_mut();
168    }
169    crate::xml::encoding::xmlFindCharEncodingHandler(encoding)
170}
171
172/// `xmlSaveCtxt *xmlSaveToFd(int fd, const char *encoding, int options)`.
173///
174/// # SAFETY
175///
176/// - `fd` must be a valid open file descriptor.
177#[no_mangle]
178pub unsafe extern "C" fn xmlSaveToFd(
179    fd: c_int,
180    encoding: *const c_char,
181    options: c_int,
182) -> *mut _xmlSaveCtxt {
183    let enc = unsafe { encoding_handler(encoding) };
184    let out = io::output_buffer_create_fd(fd, enc);
185    unsafe { save_ctxt_new(out, options, encoding) }
186}
187
188/// `xmlSaveCtxt *xmlSaveToFilename(const char *filename, const char *encoding, int options)`.
189///
190/// # SAFETY
191///
192/// - `filename` must be a valid NUL-terminated path.
193#[no_mangle]
194pub unsafe extern "C" fn xmlSaveToFilename(
195    filename: *const c_char,
196    encoding: *const c_char,
197    options: c_int,
198) -> *mut _xmlSaveCtxt {
199    let enc = unsafe { encoding_handler(encoding) };
200    let out = io::output_buffer_create_filename(filename, enc, 0);
201    unsafe { save_ctxt_new(out, options, encoding) }
202}
203
204/// `xmlSaveCtxt *xmlSaveToBuffer(xmlBuffer *buffer, const char *encoding, int options)`.
205///
206/// # SAFETY
207///
208/// - `buffer` must be a valid `_xmlBuffer`.
209#[no_mangle]
210pub unsafe extern "C" fn xmlSaveToBuffer(
211    buffer: *mut crate::abi::structs::_xmlBuffer,
212    encoding: *const c_char,
213    options: c_int,
214) -> *mut _xmlSaveCtxt {
215    let enc = unsafe { encoding_handler(encoding) };
216    let out = io::output_buffer_create_buffer(buffer, enc);
217    unsafe { save_ctxt_new(out, options, encoding) }
218}
219
220/// `xmlSaveCtxt *xmlSaveToIO(xmlOutputWriteCallback iowrite, xmlOutputCloseCallback ioclose, void *ioctx, const char *encoding, int options)`.
221///
222/// # SAFETY
223///
224/// - The callbacks must be valid function pointers or NULL.
225#[no_mangle]
226pub unsafe extern "C" fn xmlSaveToIO(
227    iowrite: Option<xmlOutputWriteCallback>,
228    ioclose: Option<xmlOutputCloseCallback>,
229    ioctx: *mut core::ffi::c_void,
230    encoding: *const c_char,
231    options: c_int,
232) -> *mut _xmlSaveCtxt {
233    let enc = unsafe { encoding_handler(encoding) };
234    let out = io::output_buffer_create_io(iowrite, ioclose, ioctx, enc);
235    unsafe { save_ctxt_new(out, options, encoding) }
236}
237
238/// Serialize `doc` into the save context's output buffer.
239///
240/// Returns the number of bytes written, or -1 on error.
241///
242/// # SAFETY
243///
244/// - `ctxt` must be a valid save context.
245/// - `doc` must be a valid document or NULL.
246#[no_mangle]
247pub unsafe extern "C" fn xmlSaveDoc(ctxt: *mut _xmlSaveCtxt, doc: *mut _xmlDoc) -> c_long {
248    unsafe { save_doc_or_tree(ctxt, doc as *mut _xmlNode) }
249}
250
251/// Serialize a node tree into the save context's output buffer.
252///
253/// Returns the number of bytes written, or -1 on error.
254///
255/// # SAFETY
256///
257/// - `ctxt` must be a valid save context.
258/// - `node` must be a valid node or NULL.
259#[no_mangle]
260pub unsafe extern "C" fn xmlSaveTree(ctxt: *mut _xmlSaveCtxt, node: *mut _xmlNode) -> c_long {
261    unsafe { save_doc_or_tree(ctxt, node) }
262}
263
264unsafe fn save_doc_or_tree(ctxt: *mut _xmlSaveCtxt, node: *mut _xmlNode) -> c_long {
265    if ctxt.is_null() || node.is_null() {
266        return -1;
267    }
268    let buf = io::buf_create(-1);
269    if buf.is_null() {
270        return -1;
271    }
272    let indent = (*ctxt).indent;
273    let format = (*ctxt).format;
274    let no_decl = (*ctxt).no_decl;
275    let encoding = (*ctxt).encoding as *const xmlChar;
276    crate::xml::tree::serialize_node_opts_enc(node, buf, format, 0, indent, no_decl, encoding);
277
278    let before = io::buf_length(buf);
279    let content = io::buf_content(buf);
280    let ret = if before > 0 && !content.is_null() {
281        io::output_buffer_write((*ctxt).buf, before, content as *const c_char)
282    } else {
283        0
284    };
285    io::buf_free(buf);
286    if ret < 0 {
287        -1
288    } else {
289        ret as c_long
290    }
291}
292
293/// `int xmlSaveFlush(xmlSaveCtxt *ctxt)` — flush the output buffer.
294///
295/// # SAFETY
296///
297/// - `ctxt` must be a valid save context.
298#[no_mangle]
299pub unsafe extern "C" fn xmlSaveFlush(ctxt: *mut _xmlSaveCtxt) -> c_int {
300    if ctxt.is_null() {
301        return -1;
302    }
303    io::output_buffer_flush((*ctxt).buf)
304}
305
306/// `int xmlSaveClose(xmlSaveCtxt *ctxt)` — flush, close and free the context.
307///
308/// # UPSTREAM-PARITY
309///
310/// Returns the number of bytes written (the flush result), like upstream
311/// xmlSaveClose (xmlsave.c 2.15); the underlying output buffer is closed by
312/// xmlFreeSaveCtxt.
313///
314/// # SAFETY
315///
316/// - `ctxt` must be a valid save context; it is freed by this call.
317#[no_mangle]
318pub unsafe extern "C" fn xmlSaveClose(ctxt: *mut _xmlSaveCtxt) -> c_int {
319    if ctxt.is_null() {
320        return -1;
321    }
322    let flush_ret = if (*ctxt).buf.is_null() {
323        -1
324    } else {
325        io::output_buffer_flush((*ctxt).buf)
326    };
327    // xmlFreeSaveCtxt closes the output buffer and frees the context.
328    if !(*ctxt).buf.is_null() {
329        io::output_buffer_close((*ctxt).buf);
330    }
331    if !(*ctxt).indent.is_null() {
332        libc::free((*ctxt).indent as *mut libc::c_void);
333    }
334    if !(*ctxt).encoding.is_null() {
335        xmlFreeImpl((*ctxt).encoding as *mut core::ffi::c_void);
336    }
337    libc::free(ctxt as *mut libc::c_void);
338    flush_ret
339}
340
341/// `xmlParserErrors xmlSaveFinish(xmlSaveCtxt *ctxt)` — flush, close, free;
342/// returns an xmlParserErrors code (XML_ERR_OK on success).
343///
344/// # UPSTREAM-PARITY
345///
346/// Upstream xmlSaveFinish returns `xmlOutputBufferClose(ctxt->buf)`'s error
347/// code (negated when negative), i.e. XML_ERR_OK (0) on success.
348///
349/// # SAFETY
350///
351/// - `ctxt` must be a valid save context; it is freed by this call.
352#[no_mangle]
353pub unsafe extern "C" fn xmlSaveFinish(ctxt: *mut _xmlSaveCtxt) -> c_int {
354    if ctxt.is_null() {
355        return -1;
356    }
357    let ret = if (*ctxt).buf.is_null() {
358        -1
359    } else {
360        io::output_buffer_close((*ctxt).buf)
361    };
362    if !(*ctxt).indent.is_null() {
363        libc::free((*ctxt).indent as *mut libc::c_void);
364    }
365    if !(*ctxt).encoding.is_null() {
366        xmlFreeImpl((*ctxt).encoding as *mut core::ffi::c_void);
367    }
368    libc::free(ctxt as *mut libc::c_void);
369    if ret < 0 {
370        -ret
371    } else {
372        0
373    }
374}
375
376/// `int xmlSaveSetIndentString(xmlSaveCtxt *ctxt, const char *indent)`.
377///
378/// # SAFETY
379///
380/// - `ctxt` must be a valid save context.
381/// - `indent` must be a valid NUL-terminated string or NULL (reset to
382///   default).
383#[no_mangle]
384pub unsafe extern "C" fn xmlSaveSetIndentString(
385    ctxt: *mut _xmlSaveCtxt,
386    indent: *const c_char,
387) -> c_int {
388    // UPSTREAM-PARITY: xmlSaveSetIndentString rejects NULL/empty/overlong
389    // indents (xmlsave.c 2.15: (ctxt==NULL)||(indent==NULL) -> -1,
390    // len<=0 || len>MAX_INDENT -> -1).
391    if ctxt.is_null() || indent.is_null() {
392        return -1;
393    }
394    let len = libc::strlen(indent) as usize;
395    if len == 0 || len > 60 {
396        return -1;
397    }
398    if !(*ctxt).indent.is_null() {
399        libc::free((*ctxt).indent as *mut libc::c_void);
400        (*ctxt).indent = ptr::null_mut();
401    }
402    let copy = libc::malloc(len + 1) as *mut xmlChar;
403    if copy.is_null() {
404        return -1;
405    }
406    libc::memcpy(
407        copy as *mut libc::c_void,
408        indent as *const libc::c_void,
409        len + 1,
410    );
411    (*ctxt).indent = copy;
412    0
413}
414
415/// `int xmlSaveSetEscape(xmlSaveCtxt *ctxt, xmlCharEncodingOutputFunc escape)`.
416///
417/// # SAFETY
418///
419/// - `ctxt` must be a valid save context.
420#[no_mangle]
421pub unsafe extern "C" fn xmlSaveSetEscape(
422    ctxt: *mut _xmlSaveCtxt,
423    escape: Option<xmlCharEncodingOutputFunc>,
424) -> c_int {
425    if ctxt.is_null() {
426        return -1;
427    }
428    (*ctxt).escape = escape;
429    0
430}
431
432/// `int xmlSaveSetAttrEscape(xmlSaveCtxt *ctxt, xmlCharEncodingOutputFunc escape)`.
433///
434/// # SAFETY
435///
436/// - `ctxt` must be a valid save context.
437#[no_mangle]
438pub unsafe extern "C" fn xmlSaveSetAttrEscape(
439    ctxt: *mut _xmlSaveCtxt,
440    escape: Option<xmlCharEncodingOutputFunc>,
441) -> c_int {
442    if ctxt.is_null() {
443        return -1;
444    }
445    (*ctxt).attrEscape = escape;
446    0
447}
448
449/// Wrap an existing output buffer in a save context (candidate-internal;
450/// does not close the buffer on allocation failure — upstream xmlSaveFormatFileTo
451/// semantics). `encoding` is threaded into the XML declaration like upstream
452/// xmlDocDumpInternal (buf->encoder takes precedence there; the candidate
453/// resolves the encoder at xmlSaveTo* time).
454///
455/// # SAFETY
456///
457/// - `encoding` must be NULL or a valid NUL-terminated string.
458unsafe fn save_ctxt_wrap(
459    buf: *mut _xmlOutputBuffer,
460    options: c_int,
461    encoding: *const c_char,
462) -> *mut _xmlSaveCtxt {
463    if buf.is_null() {
464        return ptr::null_mut();
465    }
466    let ctxt = libc::calloc(1, core::mem::size_of::<_xmlSaveCtxt>()) as *mut _xmlSaveCtxt;
467    if ctxt.is_null() {
468        return ptr::null_mut();
469    }
470    (*ctxt).buf = buf;
471    (*ctxt).options = options;
472    (*ctxt).format = if (options & XML_SAVE_FORMAT) != 0 {
473        1
474    } else {
475        0
476    };
477    (*ctxt).no_decl = if (options & XML_SAVE_NO_DECL) != 0 {
478        1
479    } else {
480        0
481    };
482    (*ctxt).no_empty = if (options & XML_SAVE_NO_EMPTY) != 0 {
483        1
484    } else {
485        0
486    };
487    (*ctxt).encoding = if encoding.is_null() {
488        ptr::null_mut()
489    } else {
490        crate::abi::exports_xml2::xmlStrdup(encoding as *const xmlChar)
491    };
492    ctxt
493}
494
495/// `int xmlSaveFormatFileTo(xmlOutputBufferPtr buf, xmlDocPtr cur, const char *encoding, int format)`
496/// — serialize `cur` into an existing output buffer and close it (upstream
497/// xmlsave.c).
498///
499/// # SAFETY
500///
501/// - `buf` must be a valid output buffer (closed by this call).
502/// - `cur` must be a valid document.
503#[no_mangle]
504pub unsafe extern "C" fn xmlSaveFormatFileTo(
505    buf: *mut _xmlOutputBuffer,
506    cur: *mut _xmlDoc,
507    encoding: *const c_char,
508    format: c_int,
509) -> c_int {
510    let options = if format != 0 { XML_SAVE_FORMAT } else { 0 };
511    let ctxt = unsafe { save_ctxt_wrap(buf, options, encoding) };
512    if ctxt.is_null() {
513        return -1;
514    }
515    let ret = unsafe { xmlSaveDoc(ctxt, cur) };
516    let close_ret = unsafe { xmlSaveClose(ctxt) };
517    if ret < 0 {
518        -1
519    } else {
520        close_ret
521    }
522}
523
524/// `int xmlSaveFileTo(xmlOutputBufferPtr buf, xmlDocPtr cur, const char *encoding)`
525/// — upstream xmlsave.c delegates to xmlSaveFormatFileTo(buf, cur, encoding, 0).
526///
527/// # SAFETY
528///
529/// - `buf` must be a valid output buffer (closed by this call).
530/// - `cur` must be a valid document.
531#[no_mangle]
532pub unsafe extern "C" fn xmlSaveFileTo(
533    buf: *mut _xmlOutputBuffer,
534    cur: *mut _xmlDoc,
535    encoding: *const c_char,
536) -> c_int {
537    unsafe { xmlSaveFormatFileTo(buf, cur, encoding, 0) }
538}
539
540#[cfg(test)]
541mod tests {
542    use super::*;
543    use crate::xml::tree::new_doc;
544
545    /// Build a document with a single `root` element.
546    ///
547    /// # Safety
548    ///
549    /// - The returned document is non-NULL and owns its root element; the
550    ///   caller must free it with `tree::free_doc` exactly once.
551    fn doc_with_root() -> *mut _xmlDoc {
552        unsafe {
553            let doc = new_doc(c"1.0".as_ptr() as *const xmlChar);
554            let root =
555                crate::xml::tree::new_node(ptr::null_mut(), c"root".as_ptr() as *const xmlChar);
556            crate::xml::tree::doc_set_root_element(doc, root);
557            doc
558        }
559    }
560
561    /// Save a formatted doc to a buffer and compare the serialized bytes.
562    ///
563    /// # Safety
564    ///
565    /// - `doc` and `buf` are non-NULL (asserted) and valid until freed with
566    ///   `tree::free_doc`/`io::buf_free`; `ctxt` is non-NULL and valid
567    ///   until `xmlSaveFinish`; the buffer content/pointers are valid while
568    ///   the byte slice is constructed and read.
569    #[test]
570    fn test_save_to_buffer_format_and_nodes() {
571        unsafe {
572            let doc = doc_with_root();
573            let buf = io::buf_create(-1);
574            let ctxt = xmlSaveToBuffer(buf, ptr::null(), XML_SAVE_FORMAT);
575            assert!(!ctxt.is_null());
576            assert!(xmlSaveDoc(ctxt, doc) >= 0);
577            assert_eq!(xmlSaveFinish(ctxt), 0);
578            let content = io::buf_content(buf);
579            let len = io::buf_length(buf);
580            let s = core::slice::from_raw_parts(content, len as usize);
581            let expected = "<?xml version=\"1.0\"?>\n<root/>\n";
582            assert_eq!(s, expected.as_bytes());
583            crate::xml::tree::free_doc(doc);
584            io::buf_free(buf);
585        }
586    }
587
588    /// xmlSaveFormatFileTo with an encoding name: the XML declaration
589    /// carries `encoding="..."` (upstream xmlsave.c xmlDocDumpInternal —
590    /// tree2.c's xmlSaveFormatFileEnc("-", doc, "UTF-8", 1) path; Phase-12
591    /// EXTERNAL-CONSUMERS court).
592    ///
593    /// # Safety
594    ///
595    /// - `doc` and `buf` are non-NULL (asserted) and valid until freed;
596    ///   the buffer content is valid while the byte slice is read.
597    #[test]
598    fn test_save_format_file_to_encoding_decl() {
599        unsafe {
600            let doc = doc_with_root();
601            let buf = io::buf_create(-1);
602            let obuf = io::output_buffer_create_buffer(buf, ptr::null_mut());
603            assert!(!obuf.is_null());
604            assert!(xmlSaveFormatFileTo(obuf, doc, c"UTF-8".as_ptr(), 1) >= 0);
605            let content = io::buf_content(buf);
606            let len = io::buf_length(buf);
607            let s = core::slice::from_raw_parts(content, len as usize);
608            assert_eq!(s, b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root/>\n");
609            crate::xml::tree::free_doc(doc);
610            io::buf_free(buf);
611        }
612    }
613
614    /// Save a doc without an XML declaration and compare the output.
615    ///
616    /// # Safety
617    ///
618    /// - `doc` and `buf` are non-NULL (asserted) and valid until freed;
619    ///   `ctxt` is valid until `xmlSaveFinish`; the buffer content is
620    ///   valid while the byte slice is read.
621    #[test]
622    fn test_save_no_decl() {
623        unsafe {
624            let doc = doc_with_root();
625            let buf = io::buf_create(-1);
626            let ctxt = xmlSaveToBuffer(buf, ptr::null(), XML_SAVE_NO_DECL);
627            assert!(!ctxt.is_null());
628            xmlSaveDoc(ctxt, doc);
629            xmlSaveFinish(ctxt);
630            let content = io::buf_content(buf);
631            let len = io::buf_length(buf);
632            let s = core::slice::from_raw_parts(content, len as usize);
633            assert_eq!(s, b"<root/>\n");
634            crate::xml::tree::free_doc(doc);
635            io::buf_free(buf);
636        }
637    }
638
639    /// Set an indent string and verify it appears in the serialized output.
640    ///
641    /// # Safety
642    ///
643    /// - `doc`, `buf` and `ctxt` are non-NULL (asserted) and valid until
644    ///   their respective frees; the indent string is a static
645    ///   NUL-terminated string valid for `xmlSaveSetIndentString`; the
646    ///   buffer content is valid while the byte slice is read.
647    #[test]
648    fn test_save_set_indent_string() {
649        unsafe {
650            let doc = doc_with_root();
651            let child =
652                crate::xml::tree::new_node(ptr::null_mut(), c"child".as_ptr() as *const xmlChar);
653            crate::xml::tree::add_child(crate::xml::tree::doc_get_root_element(doc), child);
654            let buf = io::buf_create(-1);
655            let ctxt = xmlSaveToBuffer(buf, ptr::null(), XML_SAVE_FORMAT);
656            assert!(!ctxt.is_null());
657            assert_eq!(
658                xmlSaveSetIndentString(ctxt, c"\t".as_ptr() as *const c_char),
659                0
660            );
661            xmlSaveDoc(ctxt, doc);
662            xmlSaveFinish(ctxt);
663            let content = io::buf_content(buf);
664            let len = io::buf_length(buf);
665            let s = core::slice::from_raw_parts(content, len as usize);
666            let expected = "<?xml version=\"1.0\"?>\n<root>\n\t<child/>\n</root>\n";
667            assert_eq!(s, expected.as_bytes());
668            crate::xml::tree::free_doc(doc);
669            io::buf_free(buf);
670        }
671    }
672
673    /// NULL and invalid arguments must be rejected without crashing.
674    ///
675    /// # Safety
676    ///
677    /// - `xmlSaveToFd`, `xmlSaveFlush`, `xmlSaveFinish`, `xmlSaveClose`,
678    ///   `xmlSaveSetIndentString`, `xmlSaveSetEscape`,
679    ///   `xmlSaveSetAttrEscape`, `xmlSaveDoc` and `xmlSaveTree` handle NULL
680    ///   contexts/documents as documented no-ops returning an error code;
681    ///   no pointer is dereferenced.
682    #[test]
683    fn test_save_close_null_and_errors() {
684        unsafe {
685            assert!(xmlSaveToFd(-1, ptr::null(), 0).is_null());
686            assert_eq!(xmlSaveFlush(ptr::null_mut()), -1);
687            assert_eq!(xmlSaveFinish(ptr::null_mut()), -1);
688            assert_eq!(xmlSaveClose(ptr::null_mut()), -1);
689            assert_eq!(xmlSaveSetIndentString(ptr::null_mut(), ptr::null()), -1);
690            assert_eq!(xmlSaveSetEscape(ptr::null_mut(), None), -1);
691            assert_eq!(xmlSaveSetAttrEscape(ptr::null_mut(), None), -1);
692            assert_eq!(xmlSaveDoc(ptr::null_mut(), ptr::null_mut()), -1);
693            assert_eq!(xmlSaveTree(ptr::null_mut(), ptr::null_mut()), -1);
694        }
695    }
696}