Skip to main content

aster_forge_xml/
writer.rs

1//! Bounded, namespace-aware streaming XML writer.
2
3use std::io::{self, Write};
4
5use quick_xml::events::{BytesCData, BytesDecl, BytesEnd, BytesPI, BytesStart, BytesText, Event};
6use quick_xml::writer::Writer;
7
8use crate::{Error, ValidatedXml, XmlSafetyError};
9
10const DEFAULT_MAX_OUTPUT_BYTES: usize = 64 * 1024 * 1024;
11const DEFAULT_MAX_DEPTH: usize = 128;
12const DEFAULT_MAX_ATTRIBUTES_PER_ELEMENT: usize = 1_024;
13const XML_NAMESPACE_URI: &str = "http://www.w3.org/XML/1998/namespace";
14const XMLNS_NAMESPACE_URI: &str = "http://www.w3.org/2000/xmlns/";
15
16/// Finite output limits and document options for [`XmlStreamWriter`].
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub struct XmlWriteOptions {
19    pub max_output_bytes: usize,
20    pub max_depth: usize,
21    pub max_attributes_per_element: usize,
22    pub write_document_declaration: bool,
23}
24
25impl XmlWriteOptions {
26    pub const fn new() -> Self {
27        Self {
28            max_output_bytes: DEFAULT_MAX_OUTPUT_BYTES,
29            max_depth: DEFAULT_MAX_DEPTH,
30            max_attributes_per_element: DEFAULT_MAX_ATTRIBUTES_PER_ELEMENT,
31            write_document_declaration: false,
32        }
33    }
34
35    pub const fn max_output_bytes(mut self, value: usize) -> Self {
36        self.max_output_bytes = value;
37        self
38    }
39
40    pub const fn max_depth(mut self, value: usize) -> Self {
41        self.max_depth = value;
42        self
43    }
44
45    pub const fn max_attributes_per_element(mut self, value: usize) -> Self {
46        self.max_attributes_per_element = value;
47        self
48    }
49
50    pub const fn write_document_declaration(mut self, value: bool) -> Self {
51        self.write_document_declaration = value;
52        self
53    }
54
55    fn validate(self) -> Result<(), Error> {
56        if self.max_output_bytes == 0 || self.max_depth == 0 || self.max_attributes_per_element == 0
57        {
58            Err(XmlSafetyError::InvalidPolicy.into())
59        } else {
60            Ok(())
61        }
62    }
63}
64
65impl Default for XmlWriteOptions {
66    fn default() -> Self {
67        Self::new()
68    }
69}
70
71/// One attribute passed to a streaming element write.
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub struct XmlWriteAttribute<'a> {
74    pub name: &'a str,
75    pub value: &'a str,
76}
77
78impl<'a> XmlWriteAttribute<'a> {
79    pub const fn new(name: &'a str, value: &'a str) -> Self {
80        Self { name, value }
81    }
82}
83
84impl<'a> From<(&'a str, &'a str)> for XmlWriteAttribute<'a> {
85    fn from((name, value): (&'a str, &'a str)) -> Self {
86        Self { name, value }
87    }
88}
89
90struct NamespaceBinding {
91    prefix: String,
92    uri: Option<String>,
93}
94
95/// Direct XML event writer with bounded output and namespace/state validation.
96pub struct XmlStreamWriter<W: Write> {
97    writer: Writer<LimitedWriter<W>>,
98    options: XmlWriteOptions,
99    depth: usize,
100    root_seen: bool,
101    root_complete: bool,
102    open_names: Vec<String>,
103    binding_starts: Vec<usize>,
104    bindings: Vec<NamespaceBinding>,
105    instruction: String,
106}
107
108impl<W: Write> XmlStreamWriter<W> {
109    pub fn new(inner: W) -> Result<Self, Error> {
110        Self::with_options(inner, XmlWriteOptions::default())
111    }
112
113    pub fn with_options(inner: W, options: XmlWriteOptions) -> Result<Self, Error> {
114        options.validate()?;
115        let mut output = Self {
116            writer: Writer::new(LimitedWriter::new(inner, options.max_output_bytes)),
117            options,
118            depth: 0,
119            root_seen: false,
120            root_complete: false,
121            open_names: Vec::new(),
122            binding_starts: Vec::new(),
123            bindings: Vec::new(),
124            instruction: String::new(),
125        };
126        if options.write_document_declaration {
127            output.write_event(Event::Decl(BytesDecl::new("1.0", Some("UTF-8"), None)))?;
128        }
129        Ok(output)
130    }
131
132    pub fn start_element<'a, I, A>(&mut self, name: &str, attributes: I) -> Result<(), Error>
133    where
134        I: IntoIterator<Item = A>,
135        A: Into<XmlWriteAttribute<'a>>,
136    {
137        self.write_element(name, attributes, false)
138    }
139
140    pub fn empty_element<'a, I, A>(&mut self, name: &str, attributes: I) -> Result<(), Error>
141    where
142        I: IntoIterator<Item = A>,
143        A: Into<XmlWriteAttribute<'a>>,
144    {
145        self.write_element(name, attributes, true)
146    }
147
148    pub fn start(&mut self, name: &str) -> Result<(), Error> {
149        self.start_element(name, std::iter::empty::<XmlWriteAttribute<'_>>())
150    }
151
152    pub fn empty(&mut self, name: &str) -> Result<(), Error> {
153        self.empty_element(name, std::iter::empty::<XmlWriteAttribute<'_>>())
154    }
155
156    pub fn end_element(&mut self) -> Result<(), Error> {
157        if self.depth == 0 {
158            return Err(Error::InvalidData(
159                "end_element called without an open element".into(),
160            ));
161        }
162        let index = self.depth - 1;
163        let name = self.open_names[index].as_str();
164        let result = self.writer.write_event(Event::End(BytesEnd::new(name)));
165        if let Err(error) = result {
166            return self.map_io_error(error);
167        }
168        self.depth = index;
169        let binding_start = self.binding_starts[index];
170        self.bindings.truncate(binding_start);
171        if self.depth == 0 {
172            self.root_complete = true;
173        }
174        Ok(())
175    }
176
177    pub fn text(&mut self, value: &str) -> Result<(), Error> {
178        validate_xml_text(value)?;
179        if self.depth == 0 && !value.chars().all(char::is_whitespace) {
180            return Err(Error::InvalidData(
181                "text cannot appear outside the root".into(),
182            ));
183        }
184        self.write_event(Event::Text(BytesText::new(value)))
185    }
186
187    pub fn cdata(&mut self, value: &str) -> Result<(), Error> {
188        validate_xml_text(value)?;
189        if self.depth == 0 {
190            return Err(Error::InvalidData(
191                "CDATA cannot appear outside the root".into(),
192            ));
193        }
194        if value.contains("]]>") {
195            return Err(Error::InvalidData("CDATA contains `]]>`".into()));
196        }
197        self.write_event(Event::CData(BytesCData::new(value)))
198    }
199
200    pub fn comment(&mut self, value: &str) -> Result<(), Error> {
201        validate_xml_text(value)?;
202        if value.contains("--") || value.ends_with('-') {
203            return Err(Error::InvalidData(
204                "comment contains an XML-forbidden hyphen sequence".into(),
205            ));
206        }
207        self.write_event(Event::Comment(BytesText::from_escaped(value)))
208    }
209
210    pub fn processing_instruction(
211        &mut self,
212        target: &str,
213        content: Option<&str>,
214    ) -> Result<(), Error> {
215        validate_name(target, false)?;
216        if target.eq_ignore_ascii_case("xml") {
217            return Err(Error::InvalidData(
218                "processing-instruction target cannot be `xml`".into(),
219            ));
220        }
221        self.instruction.clear();
222        self.instruction.push_str(target);
223        if let Some(content) = content {
224            validate_xml_text(content)?;
225            if content.contains("?>") {
226                return Err(Error::InvalidData(
227                    "processing instruction contains `?>`".into(),
228                ));
229            }
230            if !content.is_empty() {
231                self.instruction.push(' ');
232                self.instruction.push_str(content);
233            }
234        }
235        let instruction = BytesPI::new(self.instruction.as_str());
236        let result = self.writer.write_event(Event::PI(instruction));
237        match result {
238            Ok(()) => Ok(()),
239            Err(error) => self.map_io_error(error),
240        }
241    }
242
243    /// Embeds one already validated, self-contained XML root below the current element.
244    pub fn validated_subtree(&mut self, subtree: &ValidatedXml) -> Result<(), Error> {
245        if self.depth == 0 {
246            return Err(Error::InvalidData(
247                "validated subtree requires an open parent element".into(),
248            ));
249        }
250        let caller_has_default_namespace = self.resolve_namespace("").is_some();
251        for element in subtree.document().root().descendants() {
252            if caller_has_default_namespace
253                && element.prefix().is_none()
254                && element.namespace().is_none()
255            {
256                return Err(Error::InvalidData(
257                    "unprefixed validated subtree element would inherit the writer default namespace"
258                        .into(),
259                ));
260            }
261            if element.attributes().count() > self.options.max_attributes_per_element {
262                return Err(XmlSafetyError::TooManyAttributes.into());
263            }
264            let mut relative_depth = 1usize;
265            let mut parent = element.parent();
266            while let Some(element) = parent {
267                relative_depth = relative_depth
268                    .checked_add(1)
269                    .ok_or(XmlSafetyError::TooDeep)?;
270                parent = element.parent();
271            }
272            let combined_depth = self
273                .depth
274                .checked_add(relative_depth)
275                .ok_or(XmlSafetyError::TooDeep)?;
276            if combined_depth > self.options.max_depth {
277                return Err(XmlSafetyError::TooDeep.into());
278            }
279        }
280        self.write_raw(subtree.document().root().raw_xml())
281    }
282
283    pub fn written_bytes(&self) -> usize {
284        self.writer.get_ref().written
285    }
286
287    pub fn get_ref(&self) -> &W {
288        &self.writer.get_ref().inner
289    }
290
291    pub fn finish(mut self) -> Result<W, Error> {
292        if self.depth != 0 {
293            return Err(Error::InvalidData(
294                "XML document has unclosed elements".into(),
295            ));
296        }
297        if !self.root_seen || !self.root_complete {
298            return Err(Error::InvalidData(
299                "XML document has no complete root".into(),
300            ));
301        }
302        if let Err(error) = self.writer.get_mut().flush() {
303            if self.writer.get_ref().exceeded {
304                return Err(XmlSafetyError::OutputTooLarge.into());
305            }
306            return Err(Error::Io(error));
307        }
308        Ok(self.writer.into_inner().inner)
309    }
310
311    fn write_element<'a, I, A>(
312        &mut self,
313        name: &str,
314        attributes: I,
315        empty: bool,
316    ) -> Result<(), Error>
317    where
318        I: IntoIterator<Item = A>,
319        A: Into<XmlWriteAttribute<'a>>,
320    {
321        if self.depth == 0 && self.root_complete {
322            return Err(Error::InvalidData(
323                "XML document cannot contain multiple roots".into(),
324            ));
325        }
326        let next_depth = self.depth.checked_add(1).ok_or(XmlSafetyError::TooDeep)?;
327        if next_depth > self.options.max_depth {
328            return Err(XmlSafetyError::TooDeep.into());
329        }
330        validate_name(name, true)?;
331
332        let binding_start = self.bindings.len();
333        let mut start = BytesStart::new(name);
334        let result = (|| {
335            let mut attribute_count = 0usize;
336            for attribute in attributes {
337                let attribute = attribute.into();
338                attribute_count = attribute_count
339                    .checked_add(1)
340                    .ok_or(XmlSafetyError::TooManyAttributes)?;
341                if attribute_count > self.options.max_attributes_per_element {
342                    return Err(XmlSafetyError::TooManyAttributes.into());
343                }
344                validate_name(attribute.name, true)?;
345                validate_xml_text(attribute.value)?;
346                self.record_namespace_binding(attribute)?;
347                start.push_attribute((attribute.name, attribute.value));
348            }
349            for attribute in start.attributes() {
350                let attribute = attribute.map_err(|error| Error::InvalidData(error.to_string()))?;
351                let attribute_name = std::str::from_utf8(attribute.key.as_ref())
352                    .map_err(|_| Error::InvalidData("attribute name is not UTF-8".into()))?;
353                self.validate_attribute_namespace(attribute_name)?;
354            }
355            for (index, left) in start.attributes().enumerate() {
356                let left = left.map_err(|error| Error::InvalidData(error.to_string()))?;
357                let left_name = std::str::from_utf8(left.key.as_ref())
358                    .map_err(|_| Error::InvalidData("attribute name is not UTF-8".into()))?;
359                for right in start.attributes().skip(index + 1) {
360                    let right = right.map_err(|error| Error::InvalidData(error.to_string()))?;
361                    let right_name = std::str::from_utf8(right.key.as_ref())
362                        .map_err(|_| Error::InvalidData("attribute name is not UTF-8".into()))?;
363                    if self.attributes_share_expanded_name(left_name, right_name) {
364                        return Err(Error::InvalidData(format!(
365                            "attributes `{left_name}` and `{right_name}` have the same expanded name"
366                        )));
367                    }
368                }
369            }
370            self.validate_element_namespace(name)
371        })();
372        if let Err(error) = result {
373            self.bindings.truncate(binding_start);
374            return Err(error);
375        }
376
377        let event = if empty {
378            Event::Empty(start)
379        } else {
380            Event::Start(start)
381        };
382        if let Err(error) = self.write_event(event) {
383            self.bindings.truncate(binding_start);
384            return Err(error);
385        }
386        self.root_seen = true;
387        if empty {
388            self.bindings.truncate(binding_start);
389            if self.depth == 0 {
390                self.root_complete = true;
391            }
392        } else {
393            if self.open_names.len() == self.depth {
394                self.open_names.push(String::new());
395                self.binding_starts.push(0);
396            }
397            self.open_names[self.depth].clear();
398            self.open_names[self.depth].push_str(name);
399            self.binding_starts[self.depth] = binding_start;
400            self.depth = next_depth;
401        }
402        Ok(())
403    }
404
405    fn record_namespace_binding(&mut self, attribute: XmlWriteAttribute<'_>) -> Result<(), Error> {
406        let Some(prefix) = namespace_declaration_prefix(attribute.name) else {
407            return Ok(());
408        };
409        validate_namespace_binding(prefix, attribute.value)?;
410        self.bindings.push(NamespaceBinding {
411            prefix: prefix.to_owned(),
412            uri: (!attribute.value.is_empty()).then(|| attribute.value.to_owned()),
413        });
414        Ok(())
415    }
416
417    fn validate_element_namespace(&self, qualified_name: &str) -> Result<(), Error> {
418        if let Some((prefix, _)) = qualified_name.split_once(':')
419            && self.resolve_namespace(prefix).is_none()
420        {
421            return Err(Error::InvalidData(format!(
422                "element prefix `{prefix}` has no namespace binding"
423            )));
424        }
425        Ok(())
426    }
427
428    fn validate_attribute_namespace(&self, qualified_name: &str) -> Result<(), Error> {
429        if namespace_declaration_prefix(qualified_name).is_some() {
430            return Ok(());
431        }
432        if let Some((prefix, _)) = qualified_name.split_once(':')
433            && prefix != "xml"
434            && self.resolve_namespace(prefix).is_none()
435        {
436            return Err(Error::InvalidData(format!(
437                "attribute prefix `{prefix}` has no namespace binding"
438            )));
439        }
440        Ok(())
441    }
442
443    fn attributes_share_expanded_name(&self, left: &str, right: &str) -> bool {
444        match (
445            namespace_declaration_prefix(left),
446            namespace_declaration_prefix(right),
447        ) {
448            (Some(left_prefix), Some(right_prefix)) => return left_prefix == right_prefix,
449            (Some(_), None) | (None, Some(_)) => return false,
450            (None, None) => {}
451        }
452        let (left_prefix, left_local) = left.split_once(':').unwrap_or(("", left));
453        let (right_prefix, right_local) = right.split_once(':').unwrap_or(("", right));
454        if left_local != right_local {
455            return false;
456        }
457        let left_namespace = (!left_prefix.is_empty())
458            .then(|| self.resolve_namespace(left_prefix))
459            .flatten();
460        let right_namespace = (!right_prefix.is_empty())
461            .then(|| self.resolve_namespace(right_prefix))
462            .flatten();
463        left_namespace == right_namespace
464    }
465
466    fn resolve_namespace(&self, prefix: &str) -> Option<&str> {
467        if prefix == "xml" {
468            return Some(XML_NAMESPACE_URI);
469        }
470        self.bindings
471            .iter()
472            .rev()
473            .find(|binding| binding.prefix == prefix)
474            .and_then(|binding| binding.uri.as_deref())
475    }
476
477    fn write_event(&mut self, event: Event<'_>) -> Result<(), Error> {
478        match self.writer.write_event(event) {
479            Ok(()) => Ok(()),
480            Err(error) => self.map_io_error(error),
481        }
482    }
483
484    fn write_raw(&mut self, bytes: &[u8]) -> Result<(), Error> {
485        match self.writer.get_mut().write_all(bytes) {
486            Ok(()) => Ok(()),
487            Err(error) => self.map_io_error(error),
488        }
489    }
490
491    fn map_io_error(&self, error: io::Error) -> Result<(), Error> {
492        if self.writer.get_ref().exceeded {
493            Err(XmlSafetyError::OutputTooLarge.into())
494        } else {
495            Err(Error::Io(error))
496        }
497    }
498}
499
500fn namespace_declaration_prefix(name: &str) -> Option<&str> {
501    if name == "xmlns" {
502        Some("")
503    } else {
504        name.strip_prefix("xmlns:")
505    }
506}
507
508fn validate_namespace_binding(prefix: &str, uri: &str) -> Result<(), Error> {
509    if prefix == "xmlns"
510        || uri == XMLNS_NAMESPACE_URI
511        || (prefix == "xml" && uri != XML_NAMESPACE_URI)
512        || (prefix != "xml" && uri == XML_NAMESPACE_URI)
513        || (!prefix.is_empty() && uri.is_empty())
514    {
515        Err(Error::InvalidData("invalid namespace binding".into()))
516    } else {
517        Ok(())
518    }
519}
520
521fn validate_name(name: &str, qualified: bool) -> Result<(), Error> {
522    if name.is_empty() || (!qualified && name.contains(':')) || name.matches(':').count() > 1 {
523        return Err(Error::InvalidData(format!("invalid XML name `{name}`")));
524    }
525    for part in name.split(':') {
526        let mut characters = part.chars();
527        if !characters.next().is_some_and(is_name_start) || !characters.all(is_name_char) {
528            return Err(Error::InvalidData(format!("invalid XML name `{name}`")));
529        }
530    }
531    Ok(())
532}
533
534fn is_name_start(character: char) -> bool {
535    matches!(
536        character,
537        'A'..='Z'
538            | '_'
539            | 'a'..='z'
540            | '\u{00C0}'..='\u{00D6}'
541            | '\u{00D8}'..='\u{00F6}'
542            | '\u{00F8}'..='\u{02FF}'
543            | '\u{0370}'..='\u{037D}'
544            | '\u{037F}'..='\u{1FFF}'
545            | '\u{200C}'..='\u{200D}'
546            | '\u{2070}'..='\u{218F}'
547            | '\u{2C00}'..='\u{2FEF}'
548            | '\u{3001}'..='\u{D7FF}'
549            | '\u{F900}'..='\u{FDCF}'
550            | '\u{FDF0}'..='\u{FFFD}'
551            | '\u{10000}'..='\u{EFFFF}'
552    )
553}
554
555fn is_name_char(character: char) -> bool {
556    is_name_start(character)
557        || character.is_ascii_digit()
558        || matches!(character, '-' | '.' | '\u{B7}')
559        || ('\u{300}'..='\u{36F}').contains(&character)
560        || ('\u{203F}'..='\u{2040}').contains(&character)
561}
562
563fn validate_xml_text(value: &str) -> Result<(), Error> {
564    if value.chars().all(|character| {
565        matches!(character, '\u{9}' | '\u{A}' | '\u{D}')
566            || ('\u{20}'..='\u{D7FF}').contains(&character)
567            || ('\u{E000}'..='\u{FFFD}').contains(&character)
568            || ('\u{10000}'..='\u{10FFFF}').contains(&character)
569    }) {
570        Ok(())
571    } else {
572        Err(Error::InvalidData(
573            "value contains a character forbidden by XML 1.0".into(),
574        ))
575    }
576}
577
578struct LimitedWriter<W> {
579    inner: W,
580    max_bytes: usize,
581    written: usize,
582    exceeded: bool,
583}
584
585impl<W> LimitedWriter<W> {
586    fn new(inner: W, max_bytes: usize) -> Self {
587        Self {
588            inner,
589            max_bytes,
590            written: 0,
591            exceeded: false,
592        }
593    }
594}
595
596impl<W: Write> Write for LimitedWriter<W> {
597    fn write(&mut self, buffer: &[u8]) -> io::Result<usize> {
598        let Some(new_written) = self.written.checked_add(buffer.len()) else {
599            self.exceeded = true;
600            return Err(io::Error::other("XML output exceeds byte limit"));
601        };
602        if new_written > self.max_bytes {
603            self.exceeded = true;
604            return Err(io::Error::other("XML output exceeds byte limit"));
605        }
606        self.inner.write_all(buffer)?;
607        self.written = new_written;
608        Ok(buffer.len())
609    }
610
611    fn flush(&mut self) -> io::Result<()> {
612        self.inner.flush()
613    }
614}