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