Skip to main content

serde_xml/
writer.rs

1//! Low-level XML writer.
2//!
3//! This module provides a fast XML writer that produces well-formed XML output.
4
5use crate::escape::{entity_for, find_special};
6use std::io::{self, Write};
7
8/// An XML writer that produces well-formed XML output.
9///
10/// The writer performs many small writes, so unbuffered sinks such as files
11/// or sockets should be wrapped in [`std::io::BufWriter`] to avoid a syscall
12/// per write.
13///
14/// Element and attribute names as well as comment, CDATA, and processing
15/// instruction contents are written as provided, without validation or
16/// escaping — callers must ensure names are valid XML Names and that bodies
17/// do not contain their closing delimiters (`-->`, `]]>`).
18pub struct XmlWriter<W: Write> {
19    writer: W,
20    /// Stack of open element names.
21    element_stack: Vec<Box<str>>,
22    /// Whether we're currently in an element tag (before the closing >).
23    in_tag: bool,
24    /// Indentation settings.
25    indent: Option<IndentConfig>,
26    /// Current indentation level.
27    level: usize,
28    /// Whether the last write was a start element (for formatting).
29    last_was_start: bool,
30}
31
32/// Indentation configuration.
33#[derive(Clone)]
34pub struct IndentConfig {
35    /// Characters to use for each level of indentation.
36    pub indent_str: String,
37    /// Whether to add a newline before each element.
38    pub newlines: bool,
39}
40
41impl Default for IndentConfig {
42    fn default() -> Self {
43        Self {
44            indent_str: "  ".to_string(),
45            newlines: true,
46        }
47    }
48}
49
50impl<W: Write> XmlWriter<W> {
51    /// Creates a new XML writer.
52    #[inline]
53    pub fn new(writer: W) -> Self {
54        Self {
55            writer,
56            element_stack: Vec::new(),
57            in_tag: false,
58            indent: None,
59            level: 0,
60            last_was_start: false,
61        }
62    }
63
64    /// Creates a new XML writer with indentation.
65    #[inline]
66    pub fn with_indent(writer: W, indent: IndentConfig) -> Self {
67        Self {
68            writer,
69            element_stack: Vec::new(),
70            in_tag: false,
71            indent: Some(indent),
72            level: 0,
73            last_was_start: false,
74        }
75    }
76
77    /// Returns the inner writer.
78    #[inline]
79    pub fn into_inner(self) -> W {
80        self.writer
81    }
82
83    /// Returns the current nesting depth.
84    #[inline]
85    pub fn depth(&self) -> usize {
86        self.element_stack.len()
87    }
88
89    /// Writes the XML declaration.
90    pub fn write_declaration(&mut self, version: &str, encoding: Option<&str>) -> io::Result<()> {
91        self.close_tag_if_open()?;
92        self.writer.write_all(b"<?xml version=\"")?;
93        self.writer.write_all(version.as_bytes())?;
94        self.writer.write_all(b"\"")?;
95        if let Some(enc) = encoding {
96            self.writer.write_all(b" encoding=\"")?;
97            self.writer.write_all(enc.as_bytes())?;
98            self.writer.write_all(b"\"")?;
99        }
100        self.writer.write_all(b"?>")
101    }
102
103    /// Starts an element.
104    pub fn start_element(&mut self, name: &str) -> io::Result<()> {
105        self.close_tag_if_open()?;
106        self.write_indent()?;
107        self.writer.write_all(b"<")?;
108        self.writer.write_all(name.as_bytes())?;
109        self.element_stack.push(Box::from(name));
110        self.in_tag = true;
111        self.last_was_start = true;
112        self.level += 1;
113        Ok(())
114    }
115
116    /// Writes an attribute for the current element.
117    pub fn write_attribute(&mut self, name: &str, value: &str) -> io::Result<()> {
118        if !self.in_tag {
119            return Err(io::Error::new(
120                io::ErrorKind::InvalidInput,
121                "cannot write attribute outside of element tag",
122            ));
123        }
124        self.writer.write_all(b" ")?;
125        self.writer.write_all(name.as_bytes())?;
126        self.writer.write_all(b"=\"")?;
127        self.write_escaped(value)?;
128        self.writer.write_all(b"\"")
129    }
130
131    /// Ends the current element.
132    pub fn end_element(&mut self) -> io::Result<()> {
133        self.level = self.level.saturating_sub(1);
134
135        if let Some(name) = self.element_stack.pop() {
136            if self.in_tag {
137                // Self-closing tag
138                self.writer.write_all(b"/>")?;
139                self.in_tag = false;
140            } else {
141                if !self.last_was_start {
142                    self.write_indent()?;
143                }
144                self.writer.write_all(b"</")?;
145                self.writer.write_all(name.as_bytes())?;
146                self.writer.write_all(b">")?;
147            }
148            self.last_was_start = false;
149            Ok(())
150        } else {
151            Err(io::Error::new(
152                io::ErrorKind::InvalidInput,
153                "no element to close",
154            ))
155        }
156    }
157
158    /// Writes text content.
159    pub fn write_text(&mut self, text: &str) -> io::Result<()> {
160        self.close_tag_if_open()?;
161        self.write_escaped(text)?;
162        self.last_was_start = false;
163        Ok(())
164    }
165
166    /// Writes a CDATA section.
167    pub fn write_cdata(&mut self, data: &str) -> io::Result<()> {
168        self.close_tag_if_open()?;
169        self.writer.write_all(b"<![CDATA[")?;
170        self.writer.write_all(data.as_bytes())?;
171        self.writer.write_all(b"]]>")
172    }
173
174    /// Writes a comment.
175    pub fn write_comment(&mut self, comment: &str) -> io::Result<()> {
176        self.close_tag_if_open()?;
177        self.write_indent()?;
178        self.writer.write_all(b"<!-- ")?;
179        self.writer.write_all(comment.as_bytes())?;
180        self.writer.write_all(b" -->")
181    }
182
183    /// Writes a processing instruction.
184    pub fn write_pi(&mut self, target: &str, data: Option<&str>) -> io::Result<()> {
185        self.close_tag_if_open()?;
186        self.write_indent()?;
187        self.writer.write_all(b"<?")?;
188        self.writer.write_all(target.as_bytes())?;
189        if let Some(d) = data {
190            self.writer.write_all(b" ")?;
191            self.writer.write_all(d.as_bytes())?;
192        }
193        self.writer.write_all(b"?>")
194    }
195
196    /// Writes a complete element with text content.
197    pub fn write_element(&mut self, name: &str, content: &str) -> io::Result<()> {
198        self.start_element(name)?;
199        self.write_text(content)?;
200        self.end_element()
201    }
202
203    /// Writes an empty element.
204    pub fn write_empty_element(&mut self, name: &str) -> io::Result<()> {
205        self.close_tag_if_open()?;
206        self.write_indent()?;
207        self.writer.write_all(b"<")?;
208        self.writer.write_all(name.as_bytes())?;
209        self.writer.write_all(b"/>")?;
210        self.last_was_start = false;
211        Ok(())
212    }
213
214    /// Closes the opening tag if one is open.
215    fn close_tag_if_open(&mut self) -> io::Result<()> {
216        if self.in_tag {
217            self.writer.write_all(b">")?;
218            self.in_tag = false;
219        }
220        Ok(())
221    }
222
223    /// Writes indentation if configured.
224    fn write_indent(&mut self) -> io::Result<()> {
225        if let Some(ref indent) = self.indent {
226            if indent.newlines && self.level > 0 {
227                self.writer.write_all(b"\n")?;
228            }
229            for _ in 0..self.level.saturating_sub(1) {
230                self.writer.write_all(indent.indent_str.as_bytes())?;
231            }
232        }
233        Ok(())
234    }
235
236    /// Writes escaped text directly to the sink, without an intermediate buffer.
237    ///
238    /// Clean spans between special characters are written as-is; each special
239    /// character is replaced by its entity. Shares `find_special` and
240    /// `entity_for` with `crate::escape`.
241    fn write_escaped(&mut self, s: &str) -> io::Result<()> {
242        let bytes = s.as_bytes();
243        let mut start = 0;
244
245        while let Some(offset) = find_special(&bytes[start..]) {
246            let i = start + offset;
247            let entity = entity_for(bytes[i]);
248
249            // Batch write non-escaped bytes
250            if start < i {
251                self.writer.write_all(&bytes[start..i])?;
252            }
253            self.writer.write_all(entity.as_bytes())?;
254            start = i + 1;
255        }
256
257        // Write remaining
258        if start < bytes.len() {
259            self.writer.write_all(&bytes[start..])?;
260        }
261        Ok(())
262    }
263
264    /// Flushes the writer.
265    pub fn flush(&mut self) -> io::Result<()> {
266        self.writer.flush()
267    }
268}
269
270#[cfg(test)]
271mod tests {
272    use super::*;
273
274    fn write_to_string<F>(f: F) -> String
275    where
276        F: FnOnce(&mut XmlWriter<Vec<u8>>) -> io::Result<()>,
277    {
278        let mut writer = XmlWriter::new(Vec::new());
279        f(&mut writer).unwrap();
280        String::from_utf8(writer.into_inner()).unwrap()
281    }
282
283    #[test]
284    fn test_simple_element() {
285        let result = write_to_string(|w| {
286            w.start_element("root")?;
287            w.end_element()
288        });
289        assert_eq!(result, "<root/>");
290    }
291
292    #[test]
293    fn test_element_with_text() {
294        let result = write_to_string(|w| {
295            w.start_element("root")?;
296            w.write_text("Hello")?;
297            w.end_element()
298        });
299        assert_eq!(result, "<root>Hello</root>");
300    }
301
302    #[test]
303    fn test_element_with_attributes() {
304        let result = write_to_string(|w| {
305            w.start_element("root")?;
306            w.write_attribute("id", "1")?;
307            w.write_attribute("name", "test")?;
308            w.end_element()
309        });
310        assert_eq!(result, r#"<root id="1" name="test"/>"#);
311    }
312
313    #[test]
314    fn test_nested_elements() {
315        let result = write_to_string(|w| {
316            w.start_element("root")?;
317            w.start_element("child")?;
318            w.write_text("content")?;
319            w.end_element()?;
320            w.end_element()
321        });
322        assert_eq!(result, "<root><child>content</child></root>");
323    }
324
325    #[test]
326    fn test_escaped_content() {
327        let result = write_to_string(|w| {
328            w.start_element("root")?;
329            w.write_text("<>&\"\'")?;
330            w.end_element()
331        });
332        assert_eq!(result, "<root>&lt;&gt;&amp;&quot;&apos;</root>");
333    }
334
335    #[test]
336    fn test_escaped_attribute() {
337        let result = write_to_string(|w| {
338            w.start_element("root")?;
339            w.write_attribute("attr", "value with \"quotes\"")?;
340            w.end_element()
341        });
342        assert_eq!(result, r#"<root attr="value with &quot;quotes&quot;"/>"#);
343    }
344
345    #[test]
346    fn test_xml_declaration() {
347        let result = write_to_string(|w| {
348            w.write_declaration("1.0", Some("UTF-8"))?;
349            w.start_element("root")?;
350            w.end_element()
351        });
352        assert_eq!(result, r#"<?xml version="1.0" encoding="UTF-8"?><root/>"#);
353    }
354
355    #[test]
356    fn test_comment() {
357        let result = write_to_string(|w| {
358            w.start_element("root")?;
359            w.write_comment("This is a comment")?;
360            w.end_element()
361        });
362        assert!(result.contains("<!-- This is a comment -->"));
363    }
364
365    #[test]
366    fn test_cdata() {
367        let result = write_to_string(|w| {
368            w.start_element("root")?;
369            w.write_cdata("<special>content</special>")?;
370            w.end_element()
371        });
372        assert_eq!(result, "<root><![CDATA[<special>content</special>]]></root>");
373    }
374
375    #[test]
376    fn test_empty_element() {
377        let result = write_to_string(|w| {
378            w.write_empty_element("br")
379        });
380        assert_eq!(result, "<br/>");
381    }
382
383    #[test]
384    fn test_write_element_shorthand() {
385        let result = write_to_string(|w| {
386            w.write_element("name", "John")
387        });
388        assert_eq!(result, "<name>John</name>");
389    }
390
391    #[test]
392    fn test_depth() {
393        let mut writer = XmlWriter::new(Vec::new());
394        assert_eq!(writer.depth(), 0);
395
396        writer.start_element("a").unwrap();
397        assert_eq!(writer.depth(), 1);
398
399        writer.start_element("b").unwrap();
400        assert_eq!(writer.depth(), 2);
401
402        writer.end_element().unwrap();
403        assert_eq!(writer.depth(), 1);
404
405        writer.end_element().unwrap();
406        assert_eq!(writer.depth(), 0);
407    }
408
409    #[test]
410    fn test_processing_instruction() {
411        let result = write_to_string(|w| {
412            w.write_pi("xml-stylesheet", Some("type=\"text/xsl\" href=\"style.xsl\""))
413        });
414        assert_eq!(result, r#"<?xml-stylesheet type="text/xsl" href="style.xsl"?>"#);
415    }
416
417    #[test]
418    fn test_indented_output() {
419        let mut writer = XmlWriter::with_indent(Vec::new(), IndentConfig::default());
420        writer.start_element("root").unwrap();
421        writer.start_element("child").unwrap();
422        writer.write_text("text").unwrap();
423        writer.end_element().unwrap();
424        writer.end_element().unwrap();
425
426        let result = String::from_utf8(writer.into_inner()).unwrap();
427        assert!(result.contains("\n"));
428    }
429}