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