Skip to main content

libxml_rs/xslt/serialization/
mod.rs

1//! XSLT result serialization (§33, §85 Phase 8).
2//!
3//! Serializes the result tree to a file, fd, or string, honoring the
4//! stylesheet's `<xsl:output>` settings (method, encoding, indent,
5//! omit-xml-declaration, doctype, media-type).
6//!
7//! # UPSTREAM-PARITY
8//!
9//! This module is a faithful port of libxslt 1.1.45's `xsltSaveResultTo`
10//! family (xsltutils.c). The XML output path is:
11//!
12//! 1. The XML declaration is written explicitly, with the version taken
13//!    from the result document (defaulting to `1.0`), the encoding taken
14//!    from the stylesheet's `<xsl:output encoding>` (falling back to the
15//!    result document's encoding, then its charset), and `standalone`
16//!    only when the stylesheet sets it. The declaration always ends with a
17//!    newline.
18//! 2. Each top-level child is serialized independently with
19//!    `xmlNodeDumpOutput`-semantics (`level = 0`, `format = (indent == 1)`),
20//!    i.e. no separators are inserted between top-level elements.
21//! 3. A newline is written after a top-level child when indentation is
22//!    enabled *and* the child is a DTD node, or a comment that is not the
23//!    last child; a final newline is written after the last child.
24//!
25//! Note the C quirks preserved here: `indent == -1` (the default when no
26//! `indent` attribute is present) is truthy for the newline logic while the
27//! *formatting* of element content only happens when `indent == 1`.
28//!
29//! ```text
30//! UPSTREAM-PARITY: xsltutils.c, xsltSaveResultTo (v1.1.45)
31//! ```
32
33use crate::abi::allocator::xmlFreeImpl;
34use crate::abi::structs::*;
35use crate::abi::types::xmlElementType::*;
36use crate::abi::types::*;
37use crate::xml::encoding;
38use std::ffi::c_void;
39use std::os::raw::{c_char, c_int};
40use std::ptr;
41
42/// The result document with only a DTD child (or no children at all) is
43/// treated as empty by upstream `xsltSaveResultTo`.
44unsafe fn result_is_empty(result: *mut _xmlDoc) -> bool {
45    if result.is_null() {
46        return true;
47    }
48    let children = (*result).children;
49    if children.is_null() {
50        return true;
51    }
52    if (*children).type_ == XML_DTD_NODE as c_int && (*children).next.is_null() {
53        return true;
54    }
55    false
56}
57
58/// Walk the import chain like upstream `xsltNextImport`: the current
59/// stylesheet, then the last of its imports, and so on.
60unsafe fn next_import(style: *mut _xsltStylesheet) -> *mut _xsltStylesheet {
61    if style.is_null() || (*style).imports.is_null() {
62        return ptr::null_mut();
63    }
64    let mut imp = (*style).imports;
65    while !(*imp).next.is_null() {
66        imp = (*imp).next;
67    }
68    imp
69}
70
71/// `XSLT_GET_IMPORT_PTR`: the first non-NULL value in the import chain.
72unsafe fn import_chain_str(
73    style: *mut _xsltStylesheet,
74    get: fn(&_xsltStylesheet) -> *const xmlChar,
75) -> *const xmlChar {
76    let mut s = style;
77    while !s.is_null() {
78        let v = get(unsafe { &*s });
79        if !v.is_null() {
80            return v;
81        }
82        s = next_import(s);
83    }
84    ptr::null()
85}
86
87/// `XSLT_GET_IMPORT_INT`: the first value != -1 in the import chain.
88unsafe fn import_chain_int(
89    style: *mut _xsltStylesheet,
90    get: fn(&_xsltStylesheet) -> c_int,
91) -> c_int {
92    let mut s = style;
93    while !s.is_null() {
94        let v = get(unsafe { &*s });
95        if v != -1 {
96            return v;
97        }
98        s = next_import(s);
99    }
100    -1
101}
102
103/// Case-insensitive string comparison against a byte literal.
104unsafe fn cstr_eq_ignore_case(s: *const xmlChar, lit: &[u8]) -> bool {
105    if s.is_null() {
106        return lit.is_empty();
107    }
108    let mut i = 0usize;
109    while i < lit.len() {
110        let c = unsafe { *s.add(i) };
111        let l = lit[i];
112        if c.to_ascii_lowercase() != l.to_ascii_lowercase() {
113            return false;
114        }
115        i += 1;
116    }
117    unsafe { *s.add(i) == 0 }
118}
119
120/// Serialize a result document into a UTF-8 byte vector, mirroring
121/// upstream `xsltSaveResultTo` (xsltutils.c 1.1.45).
122///
123/// Returns `Ok(bytes)` on success and `Err(-1)` on error (matching the
124/// upstream return convention).
125///
126/// # SAFETY
127///
128/// - `result` must be a valid document.
129/// - `style` must be a valid stylesheet.
130pub(crate) unsafe fn save_result_to_vec(
131    result: *mut _xmlDoc,
132    style: *mut _xsltStylesheet,
133) -> Result<Vec<u8>, c_int> {
134    if result.is_null() || style.is_null() {
135        return Err(-1);
136    }
137    if result_is_empty(result) {
138        return Ok(Vec::new());
139    }
140
141    // Unknown output method guard (upstream checks the *direct* fields).
142    if !(*style).methodURI.is_null()
143        && ((*style).method.is_null() || !cstr_eq_ignore_case((*style).method, b"xhtml"))
144    {
145        eprintln!("xsltSaveResultTo : unknown output method");
146        return Err(-1);
147    }
148
149    let method = import_chain_str(style, |st| st.method);
150    let encoding = import_chain_str(style, |st| st.encoding);
151    let indent = import_chain_int(style, |st| st.indent);
152
153    let method: Option<Vec<u8>> = if !method.is_null() {
154        Some(
155            crate::abi::versioning::c_str_to_bytes(method as *const c_char)
156                .unwrap_or(b"")
157                .to_vec(),
158        )
159    } else if (*result).type_ == XML_HTML_DOCUMENT_NODE as c_int {
160        Some(b"html".to_vec())
161    } else {
162        None
163    };
164
165    let mut out: Vec<u8> = Vec::new();
166
167    match method.as_deref() {
168        Some(b"html") => {
169            // htmlDocContentDumpFormatOutput equivalent. The upstream code
170            // defaults indent to 1 for HTML output and inserts a meta
171            // charset element; see RESIDUAL R-HTML-OUTPUT.
172            let fmt = if indent != 0 { 1 } else { 0 };
173            // UPSTREAM-PARITY: xsltSaveResultTo calls htmlSetMetaEncoding
174            // with the stylesheet encoding (defaulting to "UTF-8") before
175            // dumping; the HTML serializer reads the document's encoding to
176            // emit the <meta charset> element.
177            if (*result).encoding.is_null() {
178                let enc: *const xmlChar = if !encoding.is_null() {
179                    encoding
180                } else {
181                    b"UTF-8\0".as_ptr() as *const xmlChar
182                };
183                (*result).encoding =
184                    crate::abi::allocator::xmlMemStrdupImpl(enc as *const c_char) as *mut xmlChar;
185            }
186            let buf = crate::xml::io::buf_create(-1);
187            if buf.is_null() {
188                return Err(-1);
189            }
190            crate::xml::html::serialize_node(result as *mut _xmlNode, buf, fmt, 0);
191            let len = crate::xml::io::buf_length(buf);
192            let content = crate::xml::io::buf_content(buf);
193            if len > 0 && !content.is_null() {
194                out.extend_from_slice(core::slice::from_raw_parts(content, len as usize));
195            }
196            crate::xml::io::buf_free(buf);
197        }
198        Some(b"xhtml") => {
199            // Upstream uses the HTML serializer's non-formatting mode.
200            let buf = crate::xml::io::buf_create(-1);
201            if buf.is_null() {
202                return Err(-1);
203            }
204            crate::xml::html::serialize_node(result as *mut _xmlNode, buf, 0, 0);
205            let len = crate::xml::io::buf_length(buf);
206            let content = crate::xml::io::buf_content(buf);
207            if len > 0 && !content.is_null() {
208                out.extend_from_slice(core::slice::from_raw_parts(content, len as usize));
209            }
210            crate::xml::io::buf_free(buf);
211        }
212        Some(b"text") => {
213            // Text output: the concatenation of every text node in document
214            // order, written raw (no escaping, no trailing newline).
215            let mut cur = (*result).children;
216            while !cur.is_null() {
217                if (*cur).type_ == XML_TEXT_NODE as c_int && !(*cur).content.is_null() {
218                    let len = crate::xml::tree::xml_strlen((*cur).content);
219                    out.extend_from_slice(core::slice::from_raw_parts(
220                        (*cur).content,
221                        len as usize,
222                    ));
223                }
224                if !(*cur).children.is_null() {
225                    let ct = (*(*cur).children).type_;
226                    if ct != XML_ENTITY_DECL as c_int
227                        && ct != XML_ENTITY_REF_NODE as c_int
228                        && ct != XML_ENTITY_NODE as c_int
229                    {
230                        cur = (*cur).children;
231                        continue;
232                    }
233                }
234                if !(*cur).next.is_null() {
235                    cur = (*cur).next;
236                    continue;
237                }
238                loop {
239                    cur = (*cur).parent;
240                    if cur.is_null() {
241                        break;
242                    }
243                    if cur == style as *mut _xmlNode {
244                        cur = ptr::null_mut();
245                        break;
246                    }
247                    if !(*cur).next.is_null() {
248                        cur = (*cur).next;
249                        break;
250                    }
251                }
252            }
253        }
254        _ => {
255            // XML output (the default).
256            let omit = import_chain_int(style, |st| st.omitXmlDeclaration);
257            let standalone = import_chain_int(style, |st| st.standalone);
258
259            if omit != 1 {
260                out.extend_from_slice(b"<?xml version=\"");
261                if !(*result).version.is_null() {
262                    if let Some(v) =
263                        crate::abi::versioning::c_str_to_bytes((*result).version as *const c_char)
264                    {
265                        out.extend_from_slice(v);
266                    }
267                } else {
268                    out.extend_from_slice(b"1.0");
269                }
270                out.push(b'"');
271                let mut enc = encoding;
272                if enc.is_null() && !(*result).encoding.is_null() {
273                    enc = (*result).encoding;
274                }
275                if !enc.is_null() {
276                    out.extend_from_slice(b" encoding=\"");
277                    if let Some(e) = crate::abi::versioning::c_str_to_bytes(enc as *const c_char) {
278                        out.extend_from_slice(e);
279                    }
280                    out.push(b'"');
281                }
282                match standalone {
283                    0 => out.extend_from_slice(b" standalone=\"no\""),
284                    1 => out.extend_from_slice(b" standalone=\"yes\""),
285                    _ => {}
286                }
287                out.extend_from_slice(b"?>\n");
288            }
289
290            if !(*result).children.is_null() {
291                let mut child = (*result).children;
292                while !child.is_null() {
293                    append_serialized_node(&mut out, child, if indent == 1 { 1 } else { 0 });
294                    if indent != 0 {
295                        let ct = (*child).type_;
296                        if ct == XML_DTD_NODE as c_int
297                            || (ct == XML_COMMENT_NODE as c_int && !(*child).next.is_null())
298                        {
299                            out.push(b'\n');
300                        }
301                    }
302                    child = (*child).next;
303                }
304                if indent != 0 {
305                    out.push(b'\n');
306                }
307            }
308        }
309    }
310
311    Ok(out)
312}
313
314/// Helper used by `save_result_to_vec`: serialize one top-level child into a
315/// buffer and append it to `out`.
316///
317/// # SAFETY
318///
319/// - `node` must be a valid node.
320unsafe fn append_serialized_node(out: &mut Vec<u8>, node: *mut _xmlNode, format: c_int) {
321    let buf = crate::xml::io::buf_create(-1);
322    if buf.is_null() {
323        return;
324    }
325    crate::xml::tree::serialize_node(node, buf, format, 0);
326    let len = crate::xml::io::buf_length(buf);
327    let content = crate::xml::io::buf_content(buf);
328    if len > 0 && !content.is_null() {
329        out.extend_from_slice(core::slice::from_raw_parts(content, len as usize));
330    }
331    crate::xml::io::buf_free(buf);
332}
333
334/// Save a result document to a buffer, honoring output settings.
335///
336/// Returns a newly allocated string in `doc_txt_ptr` and its length in
337/// `doc_txt_len`. Returns 0 on success, -1 on error.
338///
339/// # UPSTREAM-PARITY
340///
341/// Mirrors `xsltSaveResultToString` (xsltutils.c 1.1.45): the output is the
342/// serialized document converted to the stylesheet's output encoding
343/// (UTF-8 when no encoding or a UTF-8 encoding is selected). Note that
344/// upstream does not validate `style` here; `xsltSaveResultTo` fails for a
345/// NULL stylesheet and yields an empty string.
346///
347/// # SAFETY
348///
349/// - `doc_txt_ptr` and `doc_txt_len` must be valid non-null pointers.
350/// - `result` must be a valid document.
351/// - `style` must be a valid stylesheet (or NULL for empty output).
352#[no_mangle]
353pub unsafe extern "C" fn xsltSaveResultToString(
354    doc_txt_ptr: *mut *mut xmlChar,
355    doc_txt_len: *mut c_int,
356    result: *mut _xmlDoc,
357    style: *mut _xsltStylesheet,
358) -> c_int {
359    if doc_txt_ptr.is_null() || doc_txt_len.is_null() || result.is_null() {
360        return -1;
361    }
362    *doc_txt_ptr = ptr::null_mut();
363    *doc_txt_len = 0;
364
365    if (*result).children.is_null() {
366        return 0;
367    }
368
369    let bytes = match save_result_to_vec(result, style) {
370        Ok(b) => b,
371        Err(_) => {
372            // Upstream produces an empty string when the save fails.
373            let empty = crate::abi::allocator::xmlMallocImpl(1) as *mut xmlChar;
374            if empty.is_null() {
375                return -1;
376            }
377            *empty = 0;
378            *doc_txt_ptr = empty;
379            *doc_txt_len = 0;
380            return 0;
381        }
382    };
383
384    // Convert to the output encoding when it is not UTF-8.
385    let encoding = if !style.is_null() {
386        import_chain_str(style, |st| st.encoding)
387    } else {
388        ptr::null()
389    };
390    let converted: Vec<u8> = if !encoding.is_null()
391        && !cstr_eq_ignore_case(encoding, b"UTF-8")
392        && !cstr_eq_ignore_case(encoding, b"UTF8")
393    {
394        let enc = crate::abi::versioning::c_str_to_bytes(encoding as *const c_char).unwrap_or(b"");
395        let enc_lower = enc.to_ascii_lowercase();
396        if enc_lower.as_slice() == b"iso-8859-1"
397            || enc_lower.as_slice() == b"latin1"
398            || enc_lower.as_slice() == b"latin-1"
399        {
400            match encoding::utf8_to_latin1(&bytes) {
401                Ok(c) => c,
402                Err(_) => bytes,
403            }
404        } else {
405            // RESIDUAL R-ENCODING-CONVERSION: encodings other than UTF-8 and
406            // ISO-8859-1 are emitted as UTF-8 for now.
407            bytes
408        }
409    } else {
410        bytes
411    };
412
413    let out = crate::abi::allocator::xmlMallocImpl(converted.len() + 1) as *mut xmlChar;
414    if out.is_null() {
415        return -1;
416    }
417    if !converted.is_empty() {
418        core::ptr::copy_nonoverlapping(converted.as_ptr(), out, converted.len());
419    }
420    *out.add(converted.len()) = 0;
421    *doc_txt_ptr = out;
422    *doc_txt_len = converted.len() as c_int;
423    0
424}
425
426/// Save a result document to a file (FILE*).
427///
428/// # SAFETY
429///
430/// - `output` must be a valid FILE*.
431/// - `result` must be a valid document.
432#[no_mangle]
433pub unsafe extern "C" fn xsltSaveResultToFile(
434    output: *mut c_void,
435    result: *mut _xmlDoc,
436    style: *mut _xsltStylesheet,
437) -> c_int {
438    if output.is_null() || result.is_null() || style.is_null() {
439        return -1;
440    }
441    if result_is_empty(result) {
442        return 0;
443    }
444    let mut txt: *mut xmlChar = ptr::null_mut();
445    let mut len: c_int = 0;
446    let ret = xsltSaveResultToString(&mut txt, &mut len, result, style);
447    if ret != 0 {
448        return -1;
449    }
450    let written = libc::fwrite(
451        txt as *const libc::c_void,
452        1,
453        len as usize,
454        output as *mut libc::FILE,
455    );
456    xmlFreeImpl(txt as *mut c_void);
457    written as c_int
458}
459
460/// Save a result document to a filename or URL.
461///
462/// # UPSTREAM-PARITY
463///
464/// Mirrors `xsltSaveResultToFilename` (xsltutils.c 1.1.45): opens the file
465/// (compression is not supported by the Rust artifacts yet; see
466/// RESIDUAL R-COMPRESSION), serializes via `xsltSaveResultTo`, closes it and
467/// returns the number of bytes written.
468///
469/// # SAFETY
470///
471/// - `URL` must be a valid NUL-terminated path.
472/// - `result` must be a valid document.
473#[no_mangle]
474pub unsafe extern "C" fn xsltSaveResultToFilename(
475    URL: *const c_char,
476    result: *mut _xmlDoc,
477    style: *mut _xsltStylesheet,
478    compression: c_int,
479) -> c_int {
480    let _ = compression;
481    if URL.is_null() || result.is_null() || style.is_null() {
482        return -1;
483    }
484    if result_is_empty(result) {
485        return 0;
486    }
487    let file = libc::fopen(URL, b"wb\0".as_ptr() as *const c_char);
488    if file.is_null() {
489        return -1;
490    }
491    let ret = xsltSaveResultToFile(file as *mut c_void, result, style);
492    libc::fclose(file);
493    ret
494}
495
496/// Save a result document to a file descriptor.
497///
498/// # SAFETY
499///
500/// - `result` must be a valid document.
501#[no_mangle]
502pub unsafe extern "C" fn xsltSaveResultToFd(
503    fd: c_int,
504    result: *mut _xmlDoc,
505    style: *mut _xsltStylesheet,
506) -> c_int {
507    if fd < 0 || result.is_null() || style.is_null() {
508        return -1;
509    }
510    if result_is_empty(result) {
511        return 0;
512    }
513    let mut txt: *mut xmlChar = ptr::null_mut();
514    let mut len: c_int = 0;
515    let ret = xsltSaveResultToString(&mut txt, &mut len, result, style);
516    if ret != 0 {
517        return -1;
518    }
519    let bytes = core::slice::from_raw_parts(txt, len as usize);
520    let mut written = 0usize;
521    while written < bytes.len() {
522        let n = libc::write(
523            fd,
524            bytes[written..].as_ptr() as *const libc::c_void,
525            bytes.len() - written,
526        );
527        if n < 0 {
528            break;
529        }
530        written += n as usize;
531    }
532    xmlFreeImpl(txt as *mut c_void);
533    written as c_int
534}
535
536#[cfg(test)]
537mod tests {
538    use super::*;
539    use crate::xml::tree::*;
540    use core::ptr;
541
542    #[test]
543    fn test_save_result_null() {
544        unsafe {
545            assert_eq!(
546                xsltSaveResultToString(
547                    ptr::null_mut(),
548                    ptr::null_mut(),
549                    ptr::null_mut(),
550                    ptr::null_mut()
551                ),
552                -1
553            );
554        }
555    }
556
557    #[test]
558    fn test_save_result_to_string() {
559        unsafe {
560            let doc = new_doc(b"1.0\0".as_ptr() as *const xmlChar);
561            let root = new_node(ptr::null_mut(), b"root\0".as_ptr() as *const xmlChar);
562            doc_set_root_element(doc, root);
563            let text = new_text(b"hello\0".as_ptr() as *const xmlChar);
564            add_child(root, text);
565            let style = crate::xslt::stylesheet::xsltStylesheetCreate();
566            let mut txt: *mut xmlChar = ptr::null_mut();
567            let mut len: c_int = 0;
568            let ret = xsltSaveResultToString(&mut txt, &mut len, doc, style);
569            assert_eq!(ret, 0);
570            assert!(!txt.is_null());
571            assert!(len > 0);
572            // UPSTREAM-PARITY: no encoding in the declaration (result doc has
573            // none), newline after the declaration and after the root child.
574            let bytes = core::slice::from_raw_parts(txt, len as usize);
575            assert_eq!(bytes, b"<?xml version=\"1.0\"?>\n<root>hello</root>\n");
576            xmlFreeImpl(txt as *mut c_void);
577            crate::xslt::stylesheet::xsltFreeStylesheet(style);
578            free_doc(doc);
579        }
580    }
581
582    #[test]
583    fn test_save_result_indent_quirk() {
584        // UPSTREAM-PARITY: `indent == -1` (unset) writes the trailing
585        // newline while `indent == 0` (indent="no") does not.
586        unsafe {
587            let doc = new_doc(b"1.0\0".as_ptr() as *const xmlChar);
588            let root = new_node(ptr::null_mut(), b"root\0".as_ptr() as *const xmlChar);
589            doc_set_root_element(doc, root);
590            let style = crate::xslt::stylesheet::xsltStylesheetCreate();
591            (*style).indent = 0;
592            let mut txt: *mut xmlChar = ptr::null_mut();
593            let mut len: c_int = 0;
594            assert_eq!(xsltSaveResultToString(&mut txt, &mut len, doc, style), 0);
595            let bytes = core::slice::from_raw_parts(txt, len as usize);
596            assert_eq!(bytes, b"<?xml version=\"1.0\"?>\n<root/>");
597            xmlFreeImpl(txt as *mut c_void);
598            crate::xslt::stylesheet::xsltFreeStylesheet(style);
599            free_doc(doc);
600        }
601    }
602
603    #[test]
604    fn test_save_result_text_method() {
605        unsafe {
606            let doc = new_doc(b"1.0\0".as_ptr() as *const xmlChar);
607            let root = new_node(ptr::null_mut(), b"root\0".as_ptr() as *const xmlChar);
608            doc_set_root_element(doc, root);
609            let text = new_text(b"a\0".as_ptr() as *const xmlChar);
610            add_child(root, text);
611            let style = crate::xslt::stylesheet::xsltStylesheetCreate();
612            let method = libc::malloc(5) as *mut xmlChar;
613            core::ptr::copy_nonoverlapping(b"text\0".as_ptr(), method, 5);
614            (*style).method = method;
615            let mut txt: *mut xmlChar = ptr::null_mut();
616            let mut len: c_int = 0;
617            assert_eq!(xsltSaveResultToString(&mut txt, &mut len, doc, style), 0);
618            let bytes = core::slice::from_raw_parts(txt, len as usize);
619            assert_eq!(bytes, b"a");
620            xmlFreeImpl(txt as *mut c_void);
621            crate::xslt::stylesheet::xsltFreeStylesheet(style);
622            free_doc(doc);
623        }
624    }
625}