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/// Candidate-internal: nokogiri passes `SaveOptions::AS_HTML` (64) to emit
86/// HTML-style output (HTML-void elements stay `<br>`-style, non-void empty
87/// elements get an explicit end tag). Mirrors nokogiri's flag value, which is
88/// 64 (2^6); upstream uses `XML_SAVE_AS_HTML = (1<<10)`.
89pub const XML_SAVE_AS_HTML: c_int = 1 << 6;
90
91/// Candidate-internal save context (opaque upstream).
92#[derive(Debug)]
93#[repr(C)]
94pub struct _xmlSaveCtxt {
95 /// The output buffer the context serializes into.
96 pub buf: *mut _xmlOutputBuffer,
97 /// The `XML_SAVE_*` option bitmask passed to `xmlSaveTo*`.
98 pub options: c_int,
99 /// Whether `XML_SAVE_FORMAT` (newlines + indentation) is enabled.
100 pub format: c_int,
101 /// Whether the XML declaration is suppressed (`XML_SAVE_NO_DECL`).
102 pub no_decl: c_int,
103 /// Whether empty elements must be written with an explicit end tag
104 /// (`XML_SAVE_NO_EMPTY`).
105 pub no_empty: c_int,
106 /// HTML output mode (nokogiri `SaveOptions::AS_HTML` = 64): empty HTML
107 /// void elements (br/img/…) serialise as `<br>` and other empty
108 /// non-void elements as `<a></a>`.
109 pub as_html: c_int,
110 /// Optional indentation string used when formatting is enabled.
111 pub indent: *mut xmlChar,
112 /// The encoding name carried into the XML declaration (upstream
113 /// `ctxt->encoding`, xmlStrdup'd at context creation, freed by
114 /// `xmlSaveClose`/`xmlSaveFinish` like upstream xmlFreeSaveCtxt).
115 /// NULL means "use the document's own encoding" (upstream
116 /// `if (encoding == NULL) encoding = cur->encoding;`).
117 pub encoding: *mut xmlChar,
118 /// Character-escaping callback for text content (deprecated upstream).
119 pub escape: Option<xmlCharEncodingOutputFunc>,
120 /// Character-escaping callback for attribute values (deprecated upstream).
121 pub attrEscape: Option<xmlCharEncodingOutputFunc>,
122}
123
124/// Create a save context around an output buffer.
125///
126/// `encoding` is the encoding name passed to the `xmlSaveTo*` constructor
127/// (upstream `xmlNewSaveCtxt` xmlStrdups it into `ctxt->encoding`); it is
128/// emitted in the XML declaration and used to pick the encoder.
129///
130/// # SAFETY
131///
132/// - `encoding` must be NULL or a valid NUL-terminated string.
133unsafe fn save_ctxt_new(
134 buf: *mut _xmlOutputBuffer,
135 options: c_int,
136 encoding: *const c_char,
137) -> *mut _xmlSaveCtxt {
138 if buf.is_null() {
139 return ptr::null_mut();
140 }
141 let ctxt = libc::calloc(1, core::mem::size_of::<_xmlSaveCtxt>()) as *mut _xmlSaveCtxt;
142 if ctxt.is_null() {
143 io::output_buffer_close(buf);
144 return ptr::null_mut();
145 }
146 (*ctxt).buf = buf;
147 (*ctxt).options = options;
148 (*ctxt).format = if (options & XML_SAVE_FORMAT) != 0 {
149 1
150 } else {
151 0
152 };
153 (*ctxt).no_decl = if (options & XML_SAVE_NO_DECL) != 0 {
154 1
155 } else {
156 0
157 };
158 (*ctxt).no_empty = if (options & XML_SAVE_NO_EMPTY) != 0 {
159 1
160 } else {
161 0
162 };
163 (*ctxt).as_html = if (options & XML_SAVE_AS_HTML) != 0 {
164 1
165 } else {
166 0
167 };
168 (*ctxt).encoding = if encoding.is_null() {
169 ptr::null_mut()
170 } else {
171 crate::abi::exports_xml2::xmlStrdup(encoding as *const xmlChar)
172 };
173 ctxt
174}
175
176/// Resolve an encoding name to an encoding handler.
177unsafe fn encoding_handler(
178 encoding: *const c_char,
179) -> *mut crate::abi::structs::_xmlCharEncodingHandler {
180 if encoding.is_null() {
181 return ptr::null_mut();
182 }
183 crate::xml::encoding::xmlFindCharEncodingHandler(encoding)
184}
185
186/// `xmlSaveCtxt *xmlSaveToFd(int fd, const char *encoding, int options)`.
187///
188/// # SAFETY
189///
190/// - `fd` must be a valid open file descriptor.
191#[no_mangle]
192pub unsafe extern "C" fn xmlSaveToFd(
193 fd: c_int,
194 encoding: *const c_char,
195 options: c_int,
196) -> *mut _xmlSaveCtxt {
197 let enc = unsafe { encoding_handler(encoding) };
198 let out = io::output_buffer_create_fd(fd, enc);
199 unsafe { save_ctxt_new(out, options, encoding) }
200}
201
202/// `xmlSaveCtxt *xmlSaveToFilename(const char *filename, const char *encoding, int options)`.
203///
204/// # SAFETY
205///
206/// - `filename` must be a valid NUL-terminated path.
207#[no_mangle]
208pub unsafe extern "C" fn xmlSaveToFilename(
209 filename: *const c_char,
210 encoding: *const c_char,
211 options: c_int,
212) -> *mut _xmlSaveCtxt {
213 let enc = unsafe { encoding_handler(encoding) };
214 // UPSTREAM-PARITY (xmlsave.c xmlSaveToFilename): the file open funnels
215 // through xmlOutputBufferCreateFilename, honoring a registered default
216 // create-filename callback (php streams under PHP).
217 let out = io::output_buffer_create_filename_routed(filename, enc, 0);
218 unsafe { save_ctxt_new(out, options, encoding) }
219}
220
221/// `xmlSaveCtxt *xmlSaveToBuffer(xmlBuffer *buffer, const char *encoding, int options)`.
222///
223/// # SAFETY
224///
225/// - `buffer` must be a valid `_xmlBuffer`.
226#[no_mangle]
227pub unsafe extern "C" fn xmlSaveToBuffer(
228 buffer: *mut crate::abi::structs::_xmlBuffer,
229 encoding: *const c_char,
230 options: c_int,
231) -> *mut _xmlSaveCtxt {
232 let enc = unsafe { encoding_handler(encoding) };
233 let out = io::output_buffer_create_buffer(buffer, enc);
234 unsafe { save_ctxt_new(out, options, encoding) }
235}
236
237/// `xmlSaveCtxt *xmlSaveToIO(xmlOutputWriteCallback iowrite, xmlOutputCloseCallback ioclose, void *ioctx, const char *encoding, int options)`.
238///
239/// # SAFETY
240///
241/// - The callbacks must be valid function pointers or NULL.
242#[no_mangle]
243pub unsafe extern "C" fn xmlSaveToIO(
244 iowrite: Option<xmlOutputWriteCallback>,
245 ioclose: Option<xmlOutputCloseCallback>,
246 ioctx: *mut core::ffi::c_void,
247 encoding: *const c_char,
248 options: c_int,
249) -> *mut _xmlSaveCtxt {
250 let enc = unsafe { encoding_handler(encoding) };
251 let out = io::output_buffer_create_io(iowrite, ioclose, ioctx, enc);
252 unsafe { save_ctxt_new(out, options, encoding) }
253}
254
255/// Serialize `doc` into the save context's output buffer.
256///
257/// Returns the number of bytes written, or -1 on error.
258///
259/// # SAFETY
260///
261/// - `ctxt` must be a valid save context.
262/// - `doc` must be a valid document or NULL.
263#[no_mangle]
264pub unsafe extern "C" fn xmlSaveDoc(ctxt: *mut _xmlSaveCtxt, doc: *mut _xmlDoc) -> c_long {
265 unsafe { save_doc_or_tree(ctxt, doc as *mut _xmlNode) }
266}
267
268/// Serialize a node tree into the save context's output buffer.
269///
270/// Returns the number of bytes written, or -1 on error.
271///
272/// # SAFETY
273///
274/// - `ctxt` must be a valid save context.
275/// - `node` must be a valid node or NULL.
276#[no_mangle]
277pub unsafe extern "C" fn xmlSaveTree(ctxt: *mut _xmlSaveCtxt, node: *mut _xmlNode) -> c_long {
278 unsafe { save_doc_or_tree(ctxt, node) }
279}
280
281unsafe fn save_doc_or_tree(ctxt: *mut _xmlSaveCtxt, node: *mut _xmlNode) -> c_long {
282 if ctxt.is_null() || node.is_null() {
283 return -1;
284 }
285 let buf = io::buf_create(-1);
286 if buf.is_null() {
287 return -1;
288 }
289 let indent = (*ctxt).indent;
290 let format = (*ctxt).format;
291 let no_decl = (*ctxt).no_decl;
292 let no_empty = (*ctxt).no_empty;
293 let as_html = (*ctxt).as_html;
294 let encoding = (*ctxt).encoding as *const xmlChar;
295 crate::xml::tree::serialize_node_opts_enc_full(
296 node, buf, format, 0, indent, no_decl, no_empty, as_html, encoding,
297 );
298
299 let before = io::buf_length(buf);
300 let content = io::buf_content(buf);
301 let ret = if before > 0 && !content.is_null() {
302 io::output_buffer_write((*ctxt).buf, before, content as *const c_char)
303 } else {
304 0
305 };
306 io::buf_free(buf);
307 if ret < 0 {
308 -1
309 } else {
310 ret as c_long
311 }
312}
313
314/// `int xmlSaveFlush(xmlSaveCtxt *ctxt)` — flush the output buffer.
315///
316/// # SAFETY
317///
318/// - `ctxt` must be a valid save context.
319#[no_mangle]
320pub unsafe extern "C" fn xmlSaveFlush(ctxt: *mut _xmlSaveCtxt) -> c_int {
321 if ctxt.is_null() {
322 return -1;
323 }
324 io::output_buffer_flush((*ctxt).buf)
325}
326
327/// `int xmlSaveClose(xmlSaveCtxt *ctxt)` — flush, close and free the context.
328///
329/// # UPSTREAM-PARITY
330///
331/// Returns the number of bytes written (the flush result), like upstream
332/// xmlSaveClose (xmlsave.c 2.15); the underlying output buffer is closed by
333/// xmlFreeSaveCtxt.
334///
335/// # SAFETY
336///
337/// - `ctxt` must be a valid save context; it is freed by this call.
338#[no_mangle]
339pub unsafe extern "C" fn xmlSaveClose(ctxt: *mut _xmlSaveCtxt) -> c_int {
340 if ctxt.is_null() {
341 return -1;
342 }
343 let flush_ret = if (*ctxt).buf.is_null() {
344 -1
345 } else {
346 io::output_buffer_flush((*ctxt).buf)
347 };
348 // xmlFreeSaveCtxt closes the output buffer and frees the context.
349 if !(*ctxt).buf.is_null() {
350 io::output_buffer_close((*ctxt).buf);
351 }
352 if !(*ctxt).indent.is_null() {
353 libc::free((*ctxt).indent as *mut libc::c_void);
354 }
355 if !(*ctxt).encoding.is_null() {
356 xmlFreeImpl((*ctxt).encoding as *mut core::ffi::c_void);
357 }
358 libc::free(ctxt as *mut libc::c_void);
359 flush_ret
360}
361
362/// `xmlParserErrors xmlSaveFinish(xmlSaveCtxt *ctxt)` — flush, close, free;
363/// returns an xmlParserErrors code (XML_ERR_OK on success).
364///
365/// # UPSTREAM-PARITY
366///
367/// Upstream xmlSaveFinish returns `xmlOutputBufferClose(ctxt->buf)`'s error
368/// code (negated when negative), i.e. XML_ERR_OK (0) on success.
369///
370/// # SAFETY
371///
372/// - `ctxt` must be a valid save context; it is freed by this call.
373#[no_mangle]
374pub unsafe extern "C" fn xmlSaveFinish(ctxt: *mut _xmlSaveCtxt) -> c_int {
375 if ctxt.is_null() {
376 return -1;
377 }
378 let ret = if (*ctxt).buf.is_null() {
379 -1
380 } else {
381 io::output_buffer_close((*ctxt).buf)
382 };
383 if !(*ctxt).indent.is_null() {
384 libc::free((*ctxt).indent as *mut libc::c_void);
385 }
386 if !(*ctxt).encoding.is_null() {
387 xmlFreeImpl((*ctxt).encoding as *mut core::ffi::c_void);
388 }
389 libc::free(ctxt as *mut libc::c_void);
390 if ret < 0 {
391 -ret
392 } else {
393 0
394 }
395}
396
397/// `int xmlSaveSetIndentString(xmlSaveCtxt *ctxt, const char *indent)`.
398///
399/// # SAFETY
400///
401/// - `ctxt` must be a valid save context.
402/// - `indent` must be a valid NUL-terminated string or NULL (reset to
403/// default).
404#[no_mangle]
405pub unsafe extern "C" fn xmlSaveSetIndentString(
406 ctxt: *mut _xmlSaveCtxt,
407 indent: *const c_char,
408) -> c_int {
409 // UPSTREAM-PARITY: xmlSaveSetIndentString rejects NULL/empty/overlong
410 // indents (xmlsave.c 2.15: (ctxt==NULL)||(indent==NULL) -> -1,
411 // len<=0 || len>MAX_INDENT -> -1).
412 if ctxt.is_null() || indent.is_null() {
413 return -1;
414 }
415 let len = libc::strlen(indent) as usize;
416 if len == 0 || len > 60 {
417 return -1;
418 }
419 if !(*ctxt).indent.is_null() {
420 libc::free((*ctxt).indent as *mut libc::c_void);
421 (*ctxt).indent = ptr::null_mut();
422 }
423 let copy = libc::malloc(len + 1) as *mut xmlChar;
424 if copy.is_null() {
425 return -1;
426 }
427 libc::memcpy(
428 copy as *mut libc::c_void,
429 indent as *const libc::c_void,
430 len + 1,
431 );
432 (*ctxt).indent = copy;
433 0
434}
435
436/// `int xmlSaveSetEscape(xmlSaveCtxt *ctxt, xmlCharEncodingOutputFunc escape)`.
437///
438/// # SAFETY
439///
440/// - `ctxt` must be a valid save context.
441#[no_mangle]
442pub unsafe extern "C" fn xmlSaveSetEscape(
443 ctxt: *mut _xmlSaveCtxt,
444 escape: Option<xmlCharEncodingOutputFunc>,
445) -> c_int {
446 if ctxt.is_null() {
447 return -1;
448 }
449 (*ctxt).escape = escape;
450 0
451}
452
453/// `int xmlSaveSetAttrEscape(xmlSaveCtxt *ctxt, xmlCharEncodingOutputFunc escape)`.
454///
455/// # SAFETY
456///
457/// - `ctxt` must be a valid save context.
458#[no_mangle]
459pub unsafe extern "C" fn xmlSaveSetAttrEscape(
460 ctxt: *mut _xmlSaveCtxt,
461 escape: Option<xmlCharEncodingOutputFunc>,
462) -> c_int {
463 if ctxt.is_null() {
464 return -1;
465 }
466 (*ctxt).attrEscape = escape;
467 0
468}
469
470/// Wrap an existing output buffer in a save context (candidate-internal;
471/// does not close the buffer on allocation failure — upstream xmlSaveFormatFileTo
472/// semantics). `encoding` is threaded into the XML declaration like upstream
473/// xmlDocDumpInternal (buf->encoder takes precedence there; the candidate
474/// resolves the encoder at xmlSaveTo* time).
475///
476/// # SAFETY
477///
478/// - `encoding` must be NULL or a valid NUL-terminated string.
479unsafe fn save_ctxt_wrap(
480 buf: *mut _xmlOutputBuffer,
481 options: c_int,
482 encoding: *const c_char,
483) -> *mut _xmlSaveCtxt {
484 if buf.is_null() {
485 return ptr::null_mut();
486 }
487 let ctxt = libc::calloc(1, core::mem::size_of::<_xmlSaveCtxt>()) as *mut _xmlSaveCtxt;
488 if ctxt.is_null() {
489 return ptr::null_mut();
490 }
491 (*ctxt).buf = buf;
492 (*ctxt).options = options;
493 (*ctxt).format = if (options & XML_SAVE_FORMAT) != 0 {
494 1
495 } else {
496 0
497 };
498 (*ctxt).no_decl = if (options & XML_SAVE_NO_DECL) != 0 {
499 1
500 } else {
501 0
502 };
503 (*ctxt).no_empty = if (options & XML_SAVE_NO_EMPTY) != 0 {
504 1
505 } else {
506 0
507 };
508 (*ctxt).as_html = if (options & XML_SAVE_AS_HTML) != 0 {
509 1
510 } else {
511 0
512 };
513 (*ctxt).encoding = if encoding.is_null() {
514 ptr::null_mut()
515 } else {
516 crate::abi::exports_xml2::xmlStrdup(encoding as *const xmlChar)
517 };
518 ctxt
519}
520
521/// `int xmlSaveFormatFileTo(xmlOutputBufferPtr buf, xmlDocPtr cur, const char *encoding, int format)`
522/// — serialize `cur` into an existing output buffer and close it (upstream
523/// xmlsave.c).
524///
525/// # SAFETY
526///
527/// - `buf` must be a valid output buffer (closed by this call).
528/// - `cur` must be a valid document.
529#[no_mangle]
530pub unsafe extern "C" fn xmlSaveFormatFileTo(
531 buf: *mut _xmlOutputBuffer,
532 cur: *mut _xmlDoc,
533 encoding: *const c_char,
534 format: c_int,
535) -> c_int {
536 let options = if format != 0 { XML_SAVE_FORMAT } else { 0 };
537 let ctxt = unsafe { save_ctxt_wrap(buf, options, encoding) };
538 if ctxt.is_null() {
539 return -1;
540 }
541 let ret = unsafe { xmlSaveDoc(ctxt, cur) };
542 let close_ret = unsafe { xmlSaveClose(ctxt) };
543 if ret < 0 {
544 -1
545 } else {
546 close_ret
547 }
548}
549
550/// `int xmlSaveFileTo(xmlOutputBufferPtr buf, xmlDocPtr cur, const char *encoding)`
551/// — upstream xmlsave.c delegates to xmlSaveFormatFileTo(buf, cur, encoding, 0).
552///
553/// # SAFETY
554///
555/// - `buf` must be a valid output buffer (closed by this call).
556/// - `cur` must be a valid document.
557#[no_mangle]
558pub unsafe extern "C" fn xmlSaveFileTo(
559 buf: *mut _xmlOutputBuffer,
560 cur: *mut _xmlDoc,
561 encoding: *const c_char,
562) -> c_int {
563 unsafe { xmlSaveFormatFileTo(buf, cur, encoding, 0) }
564}
565
566#[cfg(test)]
567mod tests {
568 use super::*;
569 use crate::xml::tree::new_doc;
570
571 /// Build a document with a single `root` element.
572 ///
573 /// # Safety
574 ///
575 /// - The returned document is non-NULL and owns its root element; the
576 /// caller must free it with `tree::free_doc` exactly once.
577 fn doc_with_root() -> *mut _xmlDoc {
578 unsafe {
579 let doc = new_doc(c"1.0".as_ptr() as *const xmlChar);
580 let root =
581 crate::xml::tree::new_node(ptr::null_mut(), c"root".as_ptr() as *const xmlChar);
582 crate::xml::tree::doc_set_root_element(doc, root);
583 doc
584 }
585 }
586
587 /// Save a formatted doc to a buffer and compare the serialized bytes.
588 ///
589 /// # Safety
590 ///
591 /// - `doc` and `buf` are non-NULL (asserted) and valid until freed with
592 /// `tree::free_doc`/`io::buf_free`; `ctxt` is non-NULL and valid
593 /// until `xmlSaveFinish`; the buffer content/pointers are valid while
594 /// the byte slice is constructed and read.
595 #[test]
596 fn test_save_to_buffer_format_and_nodes() {
597 unsafe {
598 let doc = doc_with_root();
599 let buf = io::buf_create(-1);
600 let ctxt = xmlSaveToBuffer(buf, ptr::null(), XML_SAVE_FORMAT);
601 assert!(!ctxt.is_null());
602 assert!(xmlSaveDoc(ctxt, doc) >= 0);
603 assert_eq!(xmlSaveFinish(ctxt), 0);
604 let content = io::buf_content(buf);
605 let len = io::buf_length(buf);
606 let s = core::slice::from_raw_parts(content, len as usize);
607 let expected = "<?xml version=\"1.0\"?>\n<root/>\n";
608 assert_eq!(s, expected.as_bytes());
609 crate::xml::tree::free_doc(doc);
610 io::buf_free(buf);
611 }
612 }
613
614 /// xmlSaveFormatFileTo with an encoding name: the XML declaration
615 /// carries `encoding="..."` (upstream xmlsave.c xmlDocDumpInternal —
616 /// tree2.c's xmlSaveFormatFileEnc("-", doc, "UTF-8", 1) path; Phase-12
617 /// EXTERNAL-CONSUMERS court).
618 ///
619 /// # Safety
620 ///
621 /// - `doc` and `buf` are non-NULL (asserted) and valid until freed;
622 /// the buffer content is valid while the byte slice is read.
623 #[test]
624 fn test_save_format_file_to_encoding_decl() {
625 unsafe {
626 let doc = doc_with_root();
627 let buf = io::buf_create(-1);
628 let obuf = io::output_buffer_create_buffer(buf, ptr::null_mut());
629 assert!(!obuf.is_null());
630 assert!(xmlSaveFormatFileTo(obuf, doc, c"UTF-8".as_ptr(), 1) >= 0);
631 let content = io::buf_content(buf);
632 let len = io::buf_length(buf);
633 let s = core::slice::from_raw_parts(content, len as usize);
634 assert_eq!(s, b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root/>\n");
635 crate::xml::tree::free_doc(doc);
636 io::buf_free(buf);
637 }
638 }
639
640 /// Save a doc without an XML declaration and compare the output.
641 ///
642 /// # Safety
643 ///
644 /// - `doc` and `buf` are non-NULL (asserted) and valid until freed;
645 /// `ctxt` is valid until `xmlSaveFinish`; the buffer content is
646 /// valid while the byte slice is read.
647 #[test]
648 fn test_save_no_decl() {
649 unsafe {
650 let doc = doc_with_root();
651 let buf = io::buf_create(-1);
652 let ctxt = xmlSaveToBuffer(buf, ptr::null(), XML_SAVE_NO_DECL);
653 assert!(!ctxt.is_null());
654 xmlSaveDoc(ctxt, doc);
655 xmlSaveFinish(ctxt);
656 let content = io::buf_content(buf);
657 let len = io::buf_length(buf);
658 let s = core::slice::from_raw_parts(content, len as usize);
659 assert_eq!(s, b"<root/>\n");
660 crate::xml::tree::free_doc(doc);
661 io::buf_free(buf);
662 }
663 }
664
665 /// Phase 14.3 (dom S1): an HTML-parsed document saved with
666 /// XML_SAVE_AS_XML (PHP DOMDocument::saveXML) is dumped by the XML
667 /// serializer with the XML declaration and doc->standalone — upstream
668 /// xmlSaveDocInternal only takes the HTML branch when an HTML/XHTML save
669 /// was requested. The pre-fix html-document arm always used the HTML
670 /// serializer, so the saveXML of a loadHTML()'d document dropped the
671 /// declaration entirely (ext/dom dom005 / gh15670 / gh16535 / gh19612).
672 ///
673 /// # Safety
674 ///
675 /// - the parsed html doc and buffer are freed exactly once; content is
676 /// valid while the byte slice is read.
677 #[test]
678 fn test_save_html_doc_as_xml_includes_declaration() {
679 unsafe {
680 let doc = crate::xml::html::parse_memory(c"<html><body>x</body></html>".as_ptr(), 23);
681 assert!(!doc.is_null());
682 assert_eq!((*doc).standalone, 1);
683 let buf = io::buf_create(-1);
684 // PHP DOMDocument::saveXML passes XML_SAVE_AS_XML (1 << 5) —
685 // upstream xmlsave.h; the candidate save layer only exposes the
686 // AS_HTML bit, so pass the raw upstream option value.
687 let ctxt = xmlSaveToBuffer(buf, ptr::null(), 1 << 5);
688 assert!(!ctxt.is_null());
689 xmlSaveDoc(ctxt, doc);
690 xmlSaveFinish(ctxt);
691 let content = io::buf_content(buf);
692 let len = io::buf_length(buf);
693 let s = core::slice::from_raw_parts(content, len as usize);
694 let text = String::from_utf8_lossy(s);
695 assert!(
696 text.starts_with("<?xml version=\"1.0\" standalone=\"yes\"?>"),
697 "AS_XML save of an html doc must emit the declaration: {text:?}"
698 );
699 crate::xml::tree::free_doc(doc);
700 io::buf_free(buf);
701 }
702 }
703
704 /// Phase 14.3 (dom S1 / xmlsave parity): with NO output encoder on the
705 /// save context (ctxt->encoding == NULL), upstream xmlSaveWriteText sets
706 /// XML_ESCAPE_NON_ASCII and xmlSerializeText writes every non-ASCII
707 /// character as a hex reference (xmlSerializeHexCharRef) — `café` →
708 /// `café`, U+00A0 → ` ` — in text AND attribute content. The
709 /// pre-fix serializer passed raw UTF-8 bytes through (ext/dom dom005's
710 /// xml save of html-origin text, xslt/xmlreader non-ASCII saves).
711 ///
712 /// # Safety
713 ///
714 /// - doc/buf are freed exactly once; the byte slice is valid while read.
715 #[test]
716 fn test_save_no_encoding_escapes_non_ascii() {
717 unsafe {
718 // src: <r a="caf\xc3\xa9 \xc2\xa0">caf\xc3\xa9 \xc2\xa0 x</r>
719 let doc = crate::abi::exports_xml2::xmlReadMemory(
720 b"<r a=\"caf\xc3\xa9\">caf\xc3\xa9 \xc2\xa0 x</r>\0".as_ptr() as *const c_char,
721 27,
722 ptr::null(),
723 ptr::null(),
724 0,
725 );
726 assert!(!doc.is_null());
727 let buf = io::buf_create(-1);
728 let ctxt = xmlSaveToBuffer(buf, ptr::null(), 0);
729 assert!(!ctxt.is_null());
730 xmlSaveDoc(ctxt, doc);
731 xmlSaveFinish(ctxt);
732 let content = io::buf_content(buf);
733 let len = io::buf_length(buf);
734 let s = core::slice::from_raw_parts(content, len as usize);
735 let text = String::from_utf8_lossy(s);
736 assert!(
737 text.contains("a=\"café\""),
738 "attr content must be hex-escaped: {text:?}"
739 );
740 assert!(
741 text.contains(">café   x</r>"),
742 "text content must be hex-escaped: {text:?}"
743 );
744 crate::xml::tree::free_doc(doc);
745 io::buf_free(buf);
746 }
747 }
748
749 /// Set an indent string and verify it appears in the serialized output.
750 ///
751 /// # Safety
752 ///
753 /// - `doc`, `buf` and `ctxt` are non-NULL (asserted) and valid until
754 /// their respective frees; the indent string is a static
755 /// NUL-terminated string valid for `xmlSaveSetIndentString`; the
756 /// buffer content is valid while the byte slice is read.
757 #[test]
758 fn test_save_set_indent_string() {
759 unsafe {
760 let doc = doc_with_root();
761 let child =
762 crate::xml::tree::new_node(ptr::null_mut(), c"child".as_ptr() as *const xmlChar);
763 crate::xml::tree::add_child(crate::xml::tree::doc_get_root_element(doc), child);
764 let buf = io::buf_create(-1);
765 let ctxt = xmlSaveToBuffer(buf, ptr::null(), XML_SAVE_FORMAT);
766 assert!(!ctxt.is_null());
767 assert_eq!(
768 xmlSaveSetIndentString(ctxt, c"\t".as_ptr() as *const c_char),
769 0
770 );
771 xmlSaveDoc(ctxt, doc);
772 xmlSaveFinish(ctxt);
773 let content = io::buf_content(buf);
774 let len = io::buf_length(buf);
775 let s = core::slice::from_raw_parts(content, len as usize);
776 let expected = "<?xml version=\"1.0\"?>\n<root>\n\t<child/>\n</root>\n";
777 assert_eq!(s, expected.as_bytes());
778 crate::xml::tree::free_doc(doc);
779 io::buf_free(buf);
780 }
781 }
782
783 /// NULL and invalid arguments must be rejected without crashing.
784 ///
785 /// # Safety
786 ///
787 /// - `xmlSaveToFd`, `xmlSaveFlush`, `xmlSaveFinish`, `xmlSaveClose`,
788 /// `xmlSaveSetIndentString`, `xmlSaveSetEscape`,
789 /// `xmlSaveSetAttrEscape`, `xmlSaveDoc` and `xmlSaveTree` handle NULL
790 /// contexts/documents as documented no-ops returning an error code;
791 /// no pointer is dereferenced.
792 #[test]
793 fn test_save_close_null_and_errors() {
794 unsafe {
795 assert!(xmlSaveToFd(-1, ptr::null(), 0).is_null());
796 assert_eq!(xmlSaveFlush(ptr::null_mut()), -1);
797 assert_eq!(xmlSaveFinish(ptr::null_mut()), -1);
798 assert_eq!(xmlSaveClose(ptr::null_mut()), -1);
799 assert_eq!(xmlSaveSetIndentString(ptr::null_mut(), ptr::null()), -1);
800 assert_eq!(xmlSaveSetEscape(ptr::null_mut(), None), -1);
801 assert_eq!(xmlSaveSetAttrEscape(ptr::null_mut(), None), -1);
802 assert_eq!(xmlSaveDoc(ptr::null_mut(), ptr::null_mut()), -1);
803 assert_eq!(xmlSaveTree(ptr::null_mut(), ptr::null_mut()), -1);
804 }
805 }
806}