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