Skip to main content

xml_sec/xml_input/
lexical.rs

1//! Zero-copy lexical XML events and deterministic XML serialization.
2//!
3//! This module is the parser-neutral boundary used before semantic DOM
4//! construction. The public event model intentionally does not expose the
5//! implementation tokenizer, so consumers can share source ranges, escaping,
6//! and serialization without inheriting a parser's tree semantics.
7
8use alloc::{
9    borrow::{Cow, ToOwned},
10    format,
11    string::String,
12    vec::Vec,
13};
14use core::ops::Range;
15
16#[doc(hidden)]
17#[path = "lexical/storage.rs"]
18pub mod storage;
19use storage::{AttributeBuffer, Native, Storage};
20
21type ScanResult<'a, S> = Result<
22    Option<Event<'a, <S as Storage>::Attributes<'a>>>,
23    Error<<S as Storage>::Label, <S as Storage>::Refusal>,
24>;
25
26#[cfg(feature = "std")]
27use std::{
28    collections::HashSet,
29    io::{Error as IoError, ErrorKind, Write},
30};
31
32/// A lexical XML failure with a source position.
33#[derive(Debug, thiserror::Error)]
34pub enum Error<L = String, R = alloc::collections::TryReserveError> {
35    /// Original fixed-size tokenizer failure; formatting is deferred.
36    #[error("XML lexical error: {0}")]
37    Tokenizer(xmlparser::Error),
38    /// Static grammar diagnostic.
39    #[error("XML lexical error: {0}")]
40    Static(&'static str),
41    /// An owned diagnostic produced by a materializing helper.
42    #[error("XML lexical error: {0}")]
43    Malformed(L),
44    /// The duplicated lexical QName, retained by the selected storage.
45    #[error("XML lexical error: duplicate attribute `{0}`")]
46    DuplicateAttribute(L),
47    /// Storage or current-work refusal, never a syntax rejection.
48    #[error("XML lexical storage: {0}")]
49    Resource(R),
50}
51
52impl<L, R> Error<L, R> {
53    fn tokenizer(error: xmlparser::Error) -> Self {
54        Self::Tokenizer(error)
55    }
56
57    /// Move completed labels without formatting or changing the failure kind.
58    #[doc(hidden)]
59    pub fn map_label<T>(self, mut map: impl FnMut(L) -> T) -> Error<T, R> {
60        match self {
61            Self::Tokenizer(error) => Error::Tokenizer(error),
62            Self::Static(message) => Error::Static(message),
63            Self::Malformed(label) => Error::Malformed(map(label)),
64            Self::DuplicateAttribute(label) => Error::DuplicateAttribute(map(label)),
65            Self::Resource(error) => Error::Resource(error),
66        }
67    }
68}
69
70impl Error {
71    fn malformed(message: impl Into<String>) -> Self {
72        Self::Malformed(message.into())
73    }
74}
75
76/// A borrowed qualified XML name.
77#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
78pub struct Name<'a> {
79    prefix: &'a str,
80    local: &'a str,
81}
82
83impl<'a> Name<'a> {
84    /// Namespace prefix, or `None` for an unprefixed name.
85    #[must_use]
86    pub fn prefix(self) -> Option<&'a str> {
87        (!self.prefix.is_empty()).then_some(self.prefix)
88    }
89
90    /// Local component of the name.
91    #[must_use]
92    pub const fn local(self) -> &'a str {
93        self.local
94    }
95
96    /// Compare the lexical qualified name without allocating.
97    #[must_use]
98    pub fn is_qualified(self, value: &str) -> bool {
99        match self.prefix() {
100            Some(prefix) => {
101                value
102                    .strip_prefix(prefix)
103                    .and_then(|suffix| suffix.strip_prefix(':'))
104                    == Some(self.local)
105            }
106            None => value == self.local,
107        }
108    }
109
110    /// Materialize the lexical qualified name.
111    #[must_use]
112    pub fn qualified(self) -> Cow<'a, str> {
113        self.prefix().map_or_else(
114            || Cow::Borrowed(self.local),
115            |prefix| Cow::Owned(format!("{prefix}:{}", self.local)),
116        )
117    }
118}
119
120/// A borrowed attribute from one start tag.
121#[derive(Clone, Debug, PartialEq, Eq)]
122pub struct Attribute<'a> {
123    /// Lexical attribute name.
124    pub name: Name<'a>,
125    /// Raw value before entity/reference expansion.
126    pub value: &'a str,
127    /// Complete source range of the attribute.
128    pub range: Range<usize>,
129}
130
131/// A complete start or empty-element tag.
132#[derive(Clone, Debug, PartialEq, Eq)]
133pub struct StartTag<'a, A = Vec<Attribute<'a>>> {
134    /// Lexical element name.
135    pub name: Name<'a>,
136    /// Attributes in source order.
137    pub attributes: A,
138    /// Complete source range including `<` and `>`.
139    pub range: Range<usize>,
140}
141
142/// One lexical XML event.
143#[derive(Clone, Debug, PartialEq, Eq)]
144pub enum Event<'a, A = Vec<Attribute<'a>>> {
145    /// XML declaration.
146    Declaration {
147        /// Declared XML version.
148        version: &'a str,
149        /// Standalone declaration, if supplied; significant for XML entity constraints.
150        standalone: Option<bool>,
151        /// Complete declaration range.
152        range: Range<usize>,
153    },
154    /// Processing instruction.
155    ProcessingInstruction {
156        /// PI target.
157        target: &'a str,
158        /// Optional PI value without separator whitespace.
159        content: Option<&'a str>,
160        /// Complete source range.
161        range: Range<usize>,
162    },
163    /// XML comment.
164    Comment {
165        /// Comment content.
166        text: &'a str,
167        /// Complete source range.
168        range: Range<usize>,
169    },
170    /// Complete document type declaration.
171    DocType {
172        /// Declared document element lexical qualified name.
173        name: &'a str,
174        /// Complete source range.
175        range: Range<usize>,
176    },
177    /// Opening element tag.
178    Start(StartTag<'a, A>),
179    /// Empty-element tag.
180    Empty(StartTag<'a, A>),
181    /// Closing element tag.
182    End {
183        /// Lexical closing name.
184        name: Name<'a>,
185        /// Complete source range.
186        range: Range<usize>,
187    },
188    /// Character data without general references.
189    Text {
190        /// Raw text.
191        text: &'a str,
192        /// Source range equal to the text.
193        range: Range<usize>,
194    },
195    /// CDATA content.
196    CData {
197        /// Unwrapped CDATA content.
198        text: &'a str,
199        /// Complete source range including delimiters.
200        range: Range<usize>,
201    },
202    /// A character or entity reference from character data.
203    Reference {
204        /// Reference body without `&` and `;`.
205        name: &'a str,
206        /// Complete reference range.
207        range: Range<usize>,
208    },
209}
210
211/// Pull-based, zero-copy lexical scanner.
212pub struct Scanner<'a, S: Storage = Native> {
213    input: &'a str,
214    tokenizer: xmlparser::Tokenizer<'a>,
215    pending_text: Option<PendingText<'a>>,
216    pending_start: Option<PendingStart<'a, S>>,
217    dtd_start: Option<(usize, &'a str)>,
218    storage: S,
219    admitted: bool,
220}
221
222struct PendingStart<'a, S: Storage> {
223    name: Name<'a>,
224    attributes: S::Attributes<'a>,
225    start: usize,
226}
227
228struct PendingText<'a> {
229    text: &'a str,
230    range: Range<usize>,
231    offset: usize,
232}
233
234fn validate_unique_attributes<'a, S: Storage>(
235    attributes: &mut S::Attributes<'a>,
236    storage: S,
237) -> Result<(), Error<S::Label, S::Refusal>> {
238    const SMALL_TAG_ATTRIBUTES: usize = 8;
239
240    if attributes.len() <= SMALL_TAG_ATTRIBUTES {
241        for index in 1..attributes.len() {
242            let name = attributes[index].name;
243            for attribute in &attributes[..index] {
244                if compare_names(attribute.name, name, storage)
245                    .map_err(Error::Resource)?
246                    .is_eq()
247                {
248                    return Err(Error::DuplicateAttribute(
249                        storage.qualified_name(name).map_err(Error::Resource)?,
250                    ));
251                }
252            }
253        }
254        return Ok(());
255    }
256
257    attributes
258        .sort(|left, right| compare_names(left.name, right.name, storage))
259        .map_err(Error::Resource)?;
260    for pair in attributes.windows(2) {
261        if compare_names(pair[0].name, pair[1].name, storage)
262            .map_err(Error::Resource)?
263            .is_eq()
264        {
265            return Err(Error::DuplicateAttribute(
266                storage
267                    .qualified_name(pair[0].name)
268                    .map_err(Error::Resource)?,
269            ));
270        }
271    }
272    attributes
273        .sort(|left, right| Ok(left.range.start.cmp(&right.range.start)))
274        .map_err(Error::Resource)?;
275    Ok(())
276}
277
278fn compare_names<S: Storage>(
279    left: Name<'_>,
280    right: Name<'_>,
281    storage: S,
282) -> Result<core::cmp::Ordering, S::Refusal> {
283    storage.admit(1)?;
284    storage.admit(left.prefix.len().min(right.prefix.len()))?;
285    let prefix = left.prefix.cmp(right.prefix);
286    if !prefix.is_eq() {
287        return Ok(prefix);
288    }
289    storage.admit(left.local.len().min(right.local.len()))?;
290    Ok(left.local.cmp(right.local))
291}
292
293impl<'a> Scanner<'a> {
294    /// Scan a complete XML document.
295    #[must_use]
296    pub fn new(input: &'a str) -> Self {
297        Self::with_storage(input, Native)
298    }
299}
300
301impl<'a, S: Storage> Scanner<'a, S> {
302    /// Select the allocator/work specialization without changing lexical rules.
303    #[doc(hidden)]
304    pub fn with_storage(input: &'a str, storage: S) -> Self {
305        Self {
306            input,
307            tokenizer: xmlparser::Tokenizer::from(input),
308            pending_text: None,
309            pending_start: None,
310            dtd_start: None,
311            storage,
312            admitted: false,
313        }
314    }
315
316    /// Return the next event, or `None` at end of input.
317    pub fn next_event(&mut self) -> ScanResult<'a, S> {
318        if !self.admitted {
319            // Tokenization, QName validation and reference splitting each
320            // walk at most this actual source window. Sorting is separate.
321            self.storage
322                .admit(self.input.len())
323                .map_err(Error::Resource)?;
324            self.storage
325                .admit(self.input.len())
326                .map_err(Error::Resource)?;
327            self.storage
328                .admit(self.input.len())
329                .map_err(Error::Resource)?;
330            self.admitted = true;
331        }
332        if let Some(event) = self.next_text_event()? {
333            return Ok(Some(event));
334        }
335        loop {
336            let Some(token) = self.tokenizer.next() else {
337                if self.pending_start.is_some() {
338                    return Err(Error::Static("unterminated element start tag"));
339                }
340                return Ok(None);
341            };
342            let token = token.map_err(Error::tokenizer)?;
343            use xmlparser::{ElementEnd, Token};
344            match &token {
345                Token::DtdStart { name, span, .. } => {
346                    self.dtd_start = Some((span.range().start, name.as_str()));
347                    continue;
348                }
349                Token::DtdEnd { span } if self.dtd_start.is_some() => {
350                    let (start, name) = self.dtd_start.take().expect("DTD start is present");
351                    return Ok(Some(Event::DocType {
352                        name,
353                        range: start..span.range().end,
354                    }));
355                }
356                _ if self.dtd_start.is_some() => {
357                    // Internal-subset tokens belong to the DocType event and
358                    // must never be projected as document-tree nodes.
359                    continue;
360                }
361                _ => {}
362            }
363            match token {
364                Token::Declaration {
365                    version,
366                    standalone,
367                    span,
368                    ..
369                } => {
370                    // XML 1.0 Fifth Edition section 2.8 allows later 1.x declarations only when
371                    // the processor implements that version's complete syntax and character
372                    // model. This scanner currently implements XML 1.0, so accepting XML 1.1
373                    // here would silently apply the wrong character rules downstream.
374                    // https://www.w3.org/TR/xml/#sec-prolog-dtd
375                    if version.as_str() != "1.0" {
376                        return Err(Error::Static("unsupported XML version"));
377                    }
378                    return Ok(Some(Event::Declaration {
379                        version: version.as_str(),
380                        standalone,
381                        range: span.range(),
382                    }));
383                }
384                Token::ProcessingInstruction {
385                    target,
386                    content,
387                    span,
388                } => {
389                    return Ok(Some(Event::ProcessingInstruction {
390                        target: target.as_str(),
391                        content: content.map(|value| value.as_str()),
392                        range: span.range(),
393                    }));
394                }
395                Token::Comment { text, span } => {
396                    return Ok(Some(Event::Comment {
397                        text: text.as_str(),
398                        range: span.range(),
399                    }));
400                }
401                Token::DtdStart { .. } => unreachable!("DTD start is handled above"),
402                Token::EmptyDtd { name, span, .. } => {
403                    return Ok(Some(Event::DocType {
404                        name: name.as_str(),
405                        range: span.range(),
406                    }));
407                }
408                Token::DtdEnd { .. } => {
409                    return Err(Error::Static("DOCTYPE end without a start"));
410                }
411                Token::EntityDeclaration { .. } => {}
412                Token::ElementStart {
413                    prefix,
414                    local,
415                    span,
416                } => {
417                    validate_qualified_lexeme(&self.input[span.range()])?;
418                    if self.pending_start.is_some() {
419                        return Err(Error::Static("nested element start tokens"));
420                    }
421                    self.pending_start = Some(PendingStart {
422                        name: Name {
423                            prefix: prefix.as_str(),
424                            local: local.as_str(),
425                        },
426                        attributes: self.storage.attributes(),
427                        start: span.range().start,
428                    });
429                }
430                Token::Attribute {
431                    prefix,
432                    local,
433                    value,
434                    span,
435                } => {
436                    validate_qualified_lexeme(&self.input[span.range()])?;
437                    let start = self
438                        .pending_start
439                        .as_mut()
440                        .ok_or(Error::Static("attribute outside a start tag"))?;
441                    let name = Name {
442                        prefix: prefix.as_str(),
443                        local: local.as_str(),
444                    };
445                    start
446                        .attributes
447                        .push(Attribute {
448                            name,
449                            value: value.as_str(),
450                            range: span.range(),
451                        })
452                        .map_err(Error::Resource)?;
453                }
454                Token::ElementEnd { end, span } => match end {
455                    ElementEnd::Open | ElementEnd::Empty => {
456                        let mut start = self
457                            .pending_start
458                            .take()
459                            .ok_or(Error::Static("element end without a start"))?;
460                        validate_unique_attributes(&mut start.attributes, self.storage)?;
461                        let tag = StartTag {
462                            name: start.name,
463                            attributes: start.attributes,
464                            range: start.start..span.range().end,
465                        };
466                        return Ok(Some(if end == ElementEnd::Open {
467                            Event::Start(tag)
468                        } else {
469                            Event::Empty(tag)
470                        }));
471                    }
472                    ElementEnd::Close(prefix, local) => {
473                        validate_qualified_lexeme(&self.input[span.range()])?;
474                        return Ok(Some(Event::End {
475                            name: Name {
476                                prefix: prefix.as_str(),
477                                local: local.as_str(),
478                            },
479                            range: span.range(),
480                        }));
481                    }
482                },
483                Token::Text { text } => {
484                    self.pending_text = Some(PendingText {
485                        text: text.as_str(),
486                        range: text.range(),
487                        offset: 0,
488                    });
489                    if let Some(event) = self.next_text_event()? {
490                        return Ok(Some(event));
491                    }
492                }
493                Token::Cdata { text, span } => {
494                    return Ok(Some(Event::CData {
495                        text: text.as_str(),
496                        range: span.range(),
497                    }));
498                }
499            }
500        }
501    }
502
503    fn next_text_event(&mut self) -> ScanResult<'a, S> {
504        let Some(mut pending) = self.pending_text.take() else {
505            return Ok(None);
506        };
507        let tail = &pending.text[pending.offset..];
508        let Some(relative) = tail.find('&') else {
509            return Ok((!tail.is_empty()).then_some(Event::Text {
510                text: tail,
511                range: pending.range.start + pending.offset..pending.range.end,
512            }));
513        };
514        let start = pending.offset + relative;
515        if start > pending.offset {
516            let event = Event::Text {
517                text: &pending.text[pending.offset..start],
518                range: pending.range.start + pending.offset..pending.range.start + start,
519            };
520            pending.offset = start;
521            self.pending_text = Some(pending);
522            return Ok(Some(event));
523        }
524        let Some(relative_end) = pending.text[start + 1..].find(';') else {
525            return Err(Error::Static(
526                "unterminated XML reference in character data",
527            ));
528        };
529        let end = start + 1 + relative_end;
530        let event = Event::Reference {
531            name: &pending.text[start + 1..end],
532            range: pending.range.start + start..pending.range.start + end + 1,
533        };
534        pending.offset = end + 1;
535        if pending.offset < pending.text.len() {
536            self.pending_text = Some(pending);
537        }
538        Ok(Some(event))
539    }
540
541    /// Original scanner input.
542    #[must_use]
543    pub const fn input(&self) -> &'a str {
544        self.input
545    }
546}
547
548fn validate_qualified_lexeme<L, R>(source: &str) -> Result<(), Error<L, R>> {
549    // XML 1.0 productions [40] and [42] place Name immediately after `<` or `</`; applying
550    // Unicode whitespace trimming here would erase legal non-ASCII NameStartChar values.
551    // https://www.w3.org/TR/xml/#NT-STag
552    let source = source
553        .strip_prefix("</")
554        .or_else(|| source.strip_prefix('<'))
555        .unwrap_or(source);
556    let name = source
557        .split(|character: char| {
558            character.is_ascii_whitespace() || matches!(character, '=' | '/' | '>')
559        })
560        .next()
561        .unwrap_or_default();
562    let mut parts = name.split(':');
563    let first = parts.next().unwrap_or_default();
564    let second = parts.next();
565    if first.is_empty() || second.is_some_and(str::is_empty) || parts.next().is_some() {
566        return Err(Error::Static("invalid qualified XML name"));
567    }
568    Ok(())
569}
570
571/// Return whether `value` is an XML Namespaces 1.0 `QName`.
572#[must_use]
573pub fn is_qname(value: &str) -> bool {
574    let mut parts = value.split(':');
575    let first = parts.next().unwrap_or_default();
576    !first.is_empty()
577        && is_ncname(first)
578        && parts.next().is_none_or(is_ncname)
579        && parts.next().is_none()
580}
581
582fn is_ncname(value: &str) -> bool {
583    let mut characters = value.chars();
584    let Some(first) = characters.next() else {
585        return false;
586    };
587    is_ncname_start(first) && characters.all(is_ncname_char)
588}
589
590fn is_ncname_start(character: char) -> bool {
591    matches!(
592        character,
593        'A'..='Z'
594            | '_'
595            | 'a'..='z'
596            | '\u{C0}'..='\u{D6}'
597            | '\u{D8}'..='\u{F6}'
598            | '\u{F8}'..='\u{2FF}'
599            | '\u{370}'..='\u{37D}'
600            | '\u{37F}'..='\u{1FFF}'
601            | '\u{200C}'..='\u{200D}'
602            | '\u{2070}'..='\u{218F}'
603            | '\u{2C00}'..='\u{2FEF}'
604            | '\u{3001}'..='\u{D7FF}'
605            | '\u{F900}'..='\u{FDCF}'
606            | '\u{FDF0}'..='\u{FFFD}'
607            | '\u{10000}'..='\u{EFFFF}'
608    )
609}
610
611fn is_ncname_char(character: char) -> bool {
612    is_ncname_start(character)
613        || matches!(
614            character,
615            '-' | '.' | '0'..='9' | '\u{B7}' | '\u{0300}'..='\u{036F}' | '\u{203F}'..='\u{2040}'
616        )
617}
618
619/// Namespace prefixes declared directly by one lexical opening tag.
620///
621/// The default namespace is represented by an empty string.
622#[derive(Debug, Clone, PartialEq, Eq)]
623pub struct DeclaredNamespacePrefixes(Vec<String>);
624
625impl DeclaredNamespacePrefixes {
626    /// Return whether the opening tag declares `prefix` directly.
627    #[must_use]
628    pub fn contains(&self, prefix: &str) -> bool {
629        self.0.iter().any(|candidate| candidate == prefix)
630    }
631}
632
633/// Parse the namespace prefixes declared directly by one lexical opening tag.
634pub fn declared_namespace_prefixes(opening: &str) -> Result<DeclaredNamespacePrefixes, Error> {
635    let standalone = format!("{} />", opening.trim_end_matches('/'));
636    let mut scanner = Scanner::new(&standalone);
637    let Some(Event::Start(tag) | Event::Empty(tag)) = scanner.next_event()? else {
638        return Err(Error::malformed("expected one opening element tag"));
639    };
640    Ok(DeclaredNamespacePrefixes(
641        tag.attributes
642            .iter()
643            .filter_map(
644                |attribute| match (attribute.name.prefix(), attribute.name.local()) {
645                    (None, "xmlns") => Some(String::new()),
646                    (Some("xmlns"), prefix) => Some(prefix.to_owned()),
647                    _ => None,
648                },
649            )
650            .collect(),
651    ))
652}
653
654/// Expand the five predefined entities and XML character references.
655pub fn decode_references(value: &str) -> Result<Cow<'_, str>, Error> {
656    if !value.contains('&') {
657        return Ok(Cow::Borrowed(value));
658    }
659    let mut output = String::with_capacity(value.len());
660    let mut offset = 0;
661    while let Some(relative) = value[offset..].find('&') {
662        let start = offset + relative;
663        output.push_str(&value[offset..start]);
664        let end = value[start + 1..]
665            .find(';')
666            .map(|relative| start + 1 + relative)
667            .ok_or_else(|| Error::malformed("unterminated XML reference"))?;
668        let name = &value[start + 1..end];
669        let character = match name {
670            "amp" => '&',
671            "apos" => '\'',
672            "gt" => '>',
673            "lt" => '<',
674            "quot" => '"',
675            value if value.starts_with('#') => decode_numeric_character_reference(&value[1..])?,
676            _ => {
677                return Err(Error::malformed(format!(
678                    "unresolved entity reference &{name};"
679                )));
680            }
681        };
682        output.push(character);
683        offset = end + 1;
684    }
685    output.push_str(&value[offset..]);
686    Ok(Cow::Owned(output))
687}
688
689/// Decode the portion of an XML numeric character reference after `&#`.
690///
691/// XML 1.0 section 4.1 permits exactly decimal digits or a lowercase `x` followed by
692/// hexadecimal digits: https://www.w3.org/TR/xml/#sec-references
693pub fn decode_numeric_character_reference(reference: &str) -> Result<char, Error> {
694    let (digits, radix) = reference
695        .strip_prefix('x')
696        .map_or((reference, 10), |digits| (digits, 16));
697    let valid_digits = if radix == 16 {
698        digits.bytes().all(|byte| byte.is_ascii_hexdigit())
699    } else {
700        digits.bytes().all(|byte| byte.is_ascii_digit())
701    };
702    if digits.is_empty() || !valid_digits {
703        return Err(Error::malformed("invalid XML character reference"));
704    }
705    u32::from_str_radix(digits, radix)
706        .ok()
707        .and_then(char::from_u32)
708        .filter(|character| is_xml_1_0_character(*character))
709        .ok_or_else(|| Error::malformed("invalid XML character reference"))
710}
711
712fn is_xml_1_0_character(character: char) -> bool {
713    matches!(character, '\u{9}' | '\u{A}' | '\u{D}' | '\u{20}'..='\u{D7FF}' | '\u{E000}'..='\u{FFFD}' | '\u{10000}'..='\u{10FFFF}')
714}
715
716/// Escape XML character data.
717#[must_use]
718pub fn escape_text(value: &str) -> Cow<'_, str> {
719    escape(value, false)
720}
721
722/// Escape a double-quoted XML attribute value.
723#[must_use]
724pub fn escape_attribute(value: &str) -> Cow<'_, str> {
725    escape(value, true)
726}
727
728fn escape(value: &str, attribute: bool) -> Cow<'_, str> {
729    if !value.bytes().any(|byte| {
730        matches!(byte, b'&' | b'<' | b'>' | b'\r')
731            || (attribute && matches!(byte, b'"' | b'\n' | b'\t'))
732    }) {
733        return Cow::Borrowed(value);
734    }
735    let mut output = String::with_capacity(value.len());
736    for character in value.chars() {
737        match character {
738            '&' => output.push_str("&amp;"),
739            '<' => output.push_str("&lt;"),
740            '>' => output.push_str("&gt;"),
741            '"' if attribute => output.push_str("&quot;"),
742            '\t' if attribute => output.push_str("&#9;"),
743            '\n' if attribute => output.push_str("&#10;"),
744            '\r' => output.push_str("&#13;"),
745            _ => output.push(character),
746        }
747    }
748    Cow::Owned(output)
749}
750
751/// Deterministic UTF-8 XML writer for generated markup.
752#[cfg(feature = "std")]
753pub struct Writer<W> {
754    output: W,
755    frames: Vec<WriterFrame>,
756}
757
758#[cfg(feature = "std")]
759struct WriterFrame {
760    element_name: String,
761    namespaces: Vec<(String, String)>,
762}
763
764#[cfg(feature = "std")]
765impl<W: Write> Writer<W> {
766    /// Wrap an output sink.
767    #[must_use]
768    pub const fn new(output: W) -> Self {
769        Self {
770            output,
771            frames: Vec::new(),
772        }
773    }
774
775    /// Write an opening tag and escaped attributes.
776    pub fn start<'a>(
777        &mut self,
778        name: &str,
779        attributes: impl IntoIterator<Item = (&'a str, &'a str)>,
780    ) -> std::io::Result<()> {
781        self.tag(name, attributes, false)
782    }
783
784    /// Write an empty-element tag and escaped attributes.
785    pub fn empty<'a>(
786        &mut self,
787        name: &str,
788        attributes: impl IntoIterator<Item = (&'a str, &'a str)>,
789    ) -> std::io::Result<()> {
790        self.tag(name, attributes, true)
791    }
792
793    fn tag<'a>(
794        &mut self,
795        name: &str,
796        attributes: impl IntoIterator<Item = (&'a str, &'a str)>,
797        empty: bool,
798    ) -> std::io::Result<()> {
799        validate_writer_qname(name)?;
800        let attributes = attributes.into_iter().collect::<Vec<_>>();
801        validate_writer_element_namespace(name, &attributes, &self.frames)?;
802        validate_writer_attributes(&attributes, &self.frames)?;
803        write!(self.output, "<{name}")?;
804        for (attribute, value) in &attributes {
805            write!(self.output, " {attribute}=\"{}\"", escape_attribute(value))?;
806        }
807        self.output.write_all(if empty { b"/>" } else { b">" })?;
808        if !empty {
809            self.frames.push(WriterFrame {
810                element_name: name.to_owned(),
811                namespaces: attributes
812                    .iter()
813                    .filter_map(|(name, uri)| namespace_declaration(name).map(|name| (name, *uri)))
814                    .map(|(name, uri)| (name.to_owned(), uri.to_owned()))
815                    .collect(),
816            });
817        }
818        Ok(())
819    }
820
821    /// Write a closing tag.
822    pub fn end(&mut self, name: &str) -> std::io::Result<()> {
823        validate_writer_qname(name)?;
824        // XML 1.0 section 3.1 requires the end-tag Name to match the corresponding start-tag:
825        // https://www.w3.org/TR/xml/#sec-starttags
826        let frame = self.frames.last().ok_or_else(|| {
827            IoError::new(ErrorKind::InvalidInput, "XML end tag has no open element")
828        })?;
829        if frame.element_name != name {
830            return Err(IoError::new(
831                ErrorKind::InvalidInput,
832                format!(
833                    "XML end tag `{name}` does not match open element `{}`",
834                    frame.element_name
835                ),
836            ));
837        }
838        write!(self.output, "</{name}>")?;
839        self.frames.pop();
840        Ok(())
841    }
842
843    /// Write escaped character data.
844    pub fn text(&mut self, value: &str) -> std::io::Result<()> {
845        validate_writer_characters(value)?;
846        write!(self.output, "{}", escape_text(value))
847    }
848
849    /// Write caller-validated XML markup unchanged.
850    pub fn raw(&mut self, value: &str) -> std::io::Result<()> {
851        self.output.write_all(value.as_bytes())
852    }
853
854    /// Return the wrapped sink.
855    #[must_use]
856    pub fn into_inner(self) -> W {
857        self.output
858    }
859}
860
861#[cfg(feature = "std")]
862fn validate_writer_attributes(
863    attributes: &[(&str, &str)],
864    frames: &[WriterFrame],
865) -> std::io::Result<()> {
866    const SMALL_TAG_ATTRIBUTES: usize = 8;
867
868    for (name, value) in attributes {
869        validate_writer_qname(name)?;
870        validate_writer_characters(value)?;
871        validate_writer_namespace_declaration(name, value)?;
872    }
873    if attributes.len() <= SMALL_TAG_ATTRIBUTES {
874        for index in 1..attributes.len() {
875            if attributes[..index]
876                .iter()
877                .any(|(name, _)| *name == attributes[index].0)
878            {
879                return Err(IoError::new(
880                    ErrorKind::InvalidInput,
881                    format!("duplicate XML attribute `{}`", attributes[index].0),
882                ));
883            }
884        }
885    } else {
886        let mut names = HashSet::with_capacity(attributes.len());
887        if let Some((duplicate, _)) = attributes.iter().find(|(name, _)| !names.insert(*name)) {
888            return Err(IoError::new(
889                ErrorKind::InvalidInput,
890                format!("duplicate XML attribute `{duplicate}`"),
891            ));
892        }
893    }
894
895    let mut expanded = HashSet::with_capacity(attributes.len());
896    for (name, _) in attributes {
897        let Some((prefix, local)) = name.split_once(':') else {
898            if name != &"xmlns" && !expanded.insert((None, *name)) {
899                return duplicate_expanded_attribute(name);
900            }
901            continue;
902        };
903        if prefix == "xmlns" {
904            continue;
905        }
906        let namespace = resolve_writer_prefix(prefix, attributes, frames).ok_or_else(|| {
907            IoError::new(
908                ErrorKind::InvalidInput,
909                format!("unbound XML namespace prefix `{prefix}`"),
910            )
911        })?;
912        if namespace.is_empty() {
913            return Err(IoError::new(
914                ErrorKind::InvalidInput,
915                format!("unbound XML namespace prefix `{prefix}`"),
916            ));
917        }
918        if !expanded.insert((Some(namespace), local)) {
919            return duplicate_expanded_attribute(name);
920        }
921    }
922    Ok(())
923}
924
925#[cfg(feature = "std")]
926fn validate_writer_element_namespace(
927    name: &str,
928    attributes: &[(&str, &str)],
929    frames: &[WriterFrame],
930) -> std::io::Result<()> {
931    let Some((prefix, _)) = name.split_once(':') else {
932        return Ok(());
933    };
934    // Namespaces in XML 1.0 sections 2.2 and 5 require a non-empty namespace binding for every
935    // prefixed element name.
936    // https://www.w3.org/TR/xml-names/#iri-use https://www.w3.org/TR/xml-names/#ns-using
937    if prefix == "xmlns"
938        || resolve_writer_prefix(prefix, attributes, frames).is_none_or(str::is_empty)
939    {
940        return Err(IoError::new(
941            ErrorKind::InvalidInput,
942            format!("unbound XML namespace prefix `{prefix}`"),
943        ));
944    }
945    Ok(())
946}
947
948#[cfg(feature = "std")]
949fn validate_writer_namespace_declaration(name: &str, uri: &str) -> std::io::Result<()> {
950    const XML_NAMESPACE: &str = "http://www.w3.org/XML/1998/namespace";
951    const XMLNS_NAMESPACE: &str = "http://www.w3.org/2000/xmlns/";
952
953    let Some(prefix) = namespace_declaration(name) else {
954        return Ok(());
955    };
956    // Namespaces in XML 1.0 section 3 reserves both namespace names and their prefixes. Only the
957    // fixed xml -> XML namespace binding is legal; xmlns itself cannot be declared.
958    // https://www.w3.org/TR/xml-names/#ns-decl
959    let valid = if prefix == "xml" {
960        uri == XML_NAMESPACE
961    } else if prefix == "xmlns" {
962        false
963    } else {
964        uri != XML_NAMESPACE && uri != XMLNS_NAMESPACE && (prefix.is_empty() || !uri.is_empty())
965    };
966    if valid {
967        Ok(())
968    } else {
969        Err(IoError::new(
970            ErrorKind::InvalidInput,
971            format!("invalid XML namespace declaration `{name}={uri}`"),
972        ))
973    }
974}
975
976#[cfg(feature = "std")]
977fn namespace_declaration(name: &str) -> Option<&str> {
978    if name == "xmlns" {
979        Some("")
980    } else {
981        name.strip_prefix("xmlns:")
982    }
983}
984
985#[cfg(feature = "std")]
986fn resolve_writer_prefix<'a>(
987    prefix: &str,
988    attributes: &'a [(&str, &str)],
989    frames: &'a [WriterFrame],
990) -> Option<&'a str> {
991    if prefix == "xml" {
992        return Some("http://www.w3.org/XML/1998/namespace");
993    }
994    attributes
995        .iter()
996        .find_map(|(name, uri)| (namespace_declaration(name) == Some(prefix)).then_some(*uri))
997        .or_else(|| {
998            frames.iter().rev().find_map(|frame| {
999                frame
1000                    .namespaces
1001                    .iter()
1002                    .rev()
1003                    .find_map(|(name, uri)| (name == prefix).then_some(uri.as_str()))
1004            })
1005        })
1006}
1007
1008#[cfg(feature = "std")]
1009fn duplicate_expanded_attribute(name: &str) -> std::io::Result<()> {
1010    Err(IoError::new(
1011        ErrorKind::InvalidInput,
1012        format!("duplicate expanded XML attribute `{name}`"),
1013    ))
1014}
1015
1016#[cfg(feature = "std")]
1017fn validate_writer_qname(name: &str) -> std::io::Result<()> {
1018    // Namespaces in XML 1.0 section 3 production [6] permits exactly one optional prefix.
1019    // https://www.w3.org/TR/xml-names/#NT-QName
1020    if is_qname(name) {
1021        Ok(())
1022    } else {
1023        Err(IoError::new(ErrorKind::InvalidInput, "invalid XML QName"))
1024    }
1025}
1026
1027#[cfg(feature = "std")]
1028fn validate_writer_characters(value: &str) -> std::io::Result<()> {
1029    // XML 1.0 section 2.2 production [2] is the character repertoire for parsed entities;
1030    // escaping markup delimiters cannot make a forbidden control character legal.
1031    // https://www.w3.org/TR/xml/#charsets
1032    if let Some(character) = value
1033        .chars()
1034        .find(|character| !is_xml_1_0_character(*character))
1035    {
1036        Err(IoError::new(
1037            ErrorKind::InvalidInput,
1038            format!(
1039                "character U+{:04X} is forbidden by XML 1.0",
1040                u32::from(character)
1041            ),
1042        ))
1043    } else {
1044        Ok(())
1045    }
1046}
1047
1048#[cfg(all(test, feature = "std"))]
1049#[allow(clippy::unwrap_used)]
1050mod tests {
1051    #[test]
1052    fn xml_version_requires_digits_after_the_period() {
1053        // The scanner implements the XML 1.0 character and grammar contract only.
1054        assert!(
1055            super::Scanner::new("<?xml version='1.'?><r/>")
1056                .next_event()
1057                .is_err()
1058        );
1059        assert!(
1060            super::Scanner::new("<?xml version='1.0'?><r/>")
1061                .next_event()
1062                .is_ok()
1063        );
1064    }
1065
1066    #[test]
1067    fn scanner_rejects_xml_11_until_character_rules_are_version_aware() {
1068        // Accepting an XML version means applying that version's complete character model.
1069        assert!(
1070            super::Scanner::new("<?xml version='1.1'?><r>&#x1;</r>")
1071                .next_event()
1072                .is_err()
1073        );
1074    }
1075    use super::*;
1076
1077    #[test]
1078    fn scanner_groups_start_tags_and_splits_references() {
1079        let xml = "<p xmlns='urn:test' a='x&amp;y'>a&amp;&#x62;<![CDATA[c]]></p>";
1080        let mut scanner = Scanner::new(xml);
1081        let events = std::iter::from_fn(|| scanner.next_event().transpose())
1082            .collect::<Result<Vec<_>, _>>()
1083            .expect("fixture must scan");
1084        assert!(matches!(&events[0], Event::Start(tag) if tag.attributes.len() == 2));
1085        assert!(matches!(&events[1], Event::Text { text: "a", .. }));
1086        assert!(matches!(&events[2], Event::Reference { name: "amp", .. }));
1087        assert!(matches!(&events[3], Event::Reference { name: "#x62", .. }));
1088        assert!(matches!(&events[4], Event::CData { text: "c", .. }));
1089        assert!(matches!(&events[5], Event::End { .. }));
1090    }
1091
1092    #[test]
1093    fn scanner_streams_dense_reference_runs_without_buffering_events() {
1094        // One borrowed lexical event must be produced per pull. A source-sized reference run
1095        // cannot amplify into an event queue before the consumer sees its first reference.
1096        let xml = format!("<root>{}</root>", "&amp;".repeat(4096));
1097        let mut scanner = Scanner::new(&xml);
1098        assert!(matches!(
1099            scanner.next_event().expect("scan root start"),
1100            Some(Event::Start(_))
1101        ));
1102        assert!(matches!(
1103            scanner.next_event().expect("scan first reference"),
1104            Some(Event::Reference { name: "amp", .. })
1105        ));
1106        assert_eq!(
1107            scanner.pending_text.as_ref().map(|pending| pending.offset),
1108            Some("&amp;".len()),
1109            "the scanner retains only a cursor into the source token"
1110        );
1111        assert_eq!(
1112            std::iter::from_fn(|| scanner.next_event().transpose())
1113                .collect::<Result<Vec<_>, _>>()
1114                .expect("remaining references scan")
1115                .len(),
1116            4096
1117        );
1118    }
1119
1120    #[test]
1121    fn scanner_rejects_unterminated_references_in_character_data() {
1122        // XML 1.0 productions [66]-[68] require every reference opened by `&` to end with `;`.
1123        // https://www.w3.org/TR/xml/#NT-Reference
1124        for xml in ["<root>AT&T</root>", "<root>&bad</root>"] {
1125            let mut scanner = Scanner::new(xml);
1126            let result = std::iter::from_fn(|| scanner.next_event().transpose())
1127                .collect::<Result<Vec<_>, _>>();
1128            assert!(result.is_err(), "accepted {xml}");
1129        }
1130    }
1131
1132    #[test]
1133    fn scanner_keeps_the_internal_subset_as_one_doctype_event() {
1134        // DTD comments and declarations are not document-tree nodes. Keeping
1135        // them inside one event prevents semantic sidecars from diverging.
1136        let xml = r#"<!DOCTYPE root [<!-- <fake/> --><!ENTITY value "ok">]><root/>"#;
1137        let mut scanner = Scanner::new(xml);
1138        assert!(matches!(
1139            scanner.next_event().expect("scan DTD"),
1140            Some(Event::DocType { .. })
1141        ));
1142        assert!(matches!(
1143            scanner.next_event().expect("scan root"),
1144            Some(Event::Empty(_))
1145        ));
1146        assert!(scanner.next_event().expect("scan EOF").is_none());
1147    }
1148
1149    #[test]
1150    fn scanner_rejects_empty_and_repeated_prefix_components() {
1151        // The tokenizer exposes split names but accepts these malformed QName
1152        // spellings, so the shared lexical contract must reject them itself.
1153        for xml in ["<:root/>", "<root: />", "<a:b:c/>"] {
1154            assert!(Scanner::new(xml).next_event().is_err(), "accepted {xml}");
1155        }
1156    }
1157
1158    #[test]
1159    fn scanner_preserves_non_ascii_name_start_characters() {
1160        // XML 1.0 production [4] admits U+1680 as NameStartChar; it is not markup whitespace.
1161        // https://www.w3.org/TR/xml/#NT-NameStartChar
1162        let Some(Event::Empty(tag)) = Scanner::new("<\u{1680}/>")
1163            .next_event()
1164            .expect("valid non-ASCII name scans")
1165        else {
1166            panic!("empty element must produce one empty-tag event");
1167        };
1168        assert_eq!(tag.name.local(), "\u{1680}");
1169    }
1170
1171    #[test]
1172    fn scanner_rejects_duplicate_lexical_attributes() {
1173        // Namespace declarations are attributes under XML Namespaces 1.0 and
1174        // may not be repeated even though they do not enter the XPath axis.
1175        for xml in [
1176            "<root xmlns='urn:first' xmlns='urn:second'/>",
1177            "<root xmlns:p='urn:first' xmlns:p='urn:second'/>",
1178            "<root value='first' value='second'/>",
1179        ] {
1180            assert!(Scanner::new(xml).next_event().is_err(), "accepted {xml}");
1181        }
1182    }
1183
1184    #[test]
1185    fn scanner_validates_wide_attributes_without_changing_source_order() {
1186        // Wide tags use the allocation-free sorted duplicate check. Restoring source order is part
1187        // of the public lexical event contract and keeps downstream namespace processing stable.
1188        let attributes = (0..4096)
1189            .rev()
1190            .map(|index| format!(" a{index}='{index}'"))
1191            .collect::<String>();
1192        let xml = format!("<root{attributes}/>");
1193        let Some(Event::Empty(tag)) = Scanner::new(&xml)
1194            .next_event()
1195            .expect("wide start tag scans")
1196        else {
1197            panic!("wide empty element must produce one empty-tag event");
1198        };
1199        assert_eq!(tag.attributes.len(), 4096);
1200        assert_eq!(tag.attributes[0].name.local(), "a4095");
1201        assert_eq!(tag.attributes[4095].name.local(), "a0");
1202
1203        let unique = (0..4096)
1204            .map(|index| format!(" a{index}='x'"))
1205            .collect::<String>();
1206        let duplicate = format!("<root{unique} a0='duplicate'/>");
1207        assert!(Scanner::new(&duplicate).next_event().is_err());
1208    }
1209
1210    #[test]
1211    fn writer_escapes_text_and_attributes_by_context() {
1212        let mut writer = Writer::new(Vec::new());
1213        writer
1214            .start("p", [("a", "<&\"")])
1215            .expect("start tag must serialize");
1216        writer.text("<&\"").expect("text must serialize");
1217        writer.end("p").expect("end tag must serialize");
1218        assert_eq!(
1219            String::from_utf8(writer.into_inner()).expect("writer emits UTF-8"),
1220            "<p a=\"&lt;&amp;&quot;\">&lt;&amp;\"</p>"
1221        );
1222    }
1223
1224    #[test]
1225    fn writer_preserves_normalized_whitespace_across_reparse() {
1226        // Literal XML whitespace is normalized differently in character data
1227        // and attributes, so numeric references preserve the semantic value.
1228        let mut writer = Writer::new(Vec::new());
1229        writer
1230            .empty("root", [("value", "tab\tline\nreturn\r")])
1231            .expect("empty tag must serialize");
1232        assert_eq!(
1233            String::from_utf8(writer.into_inner()).expect("writer emits UTF-8"),
1234            "<root value=\"tab&#9;line&#10;return&#13;\"/>"
1235        );
1236
1237        let mut writer = Writer::new(Vec::new());
1238        writer.text("line\rbreak\n").expect("text must serialize");
1239        assert_eq!(
1240            String::from_utf8(writer.into_inner()).expect("writer emits UTF-8"),
1241            "line&#13;break\n"
1242        );
1243    }
1244
1245    #[test]
1246    fn writer_rejects_invalid_element_and_attribute_qnames() {
1247        for name in ["", ":root", "root:", "a:b:c", "root><injected"] {
1248            let mut writer = Writer::new(Vec::new());
1249            assert!(
1250                writer.empty(name, []).is_err(),
1251                "accepted element name {name:?}"
1252            );
1253        }
1254
1255        let mut writer = Writer::new(Vec::new());
1256        assert!(writer.empty("root", [("a:b:c", "value")]).is_err());
1257        let mut writer = Writer::new(Vec::new());
1258        assert!(writer.end("root><injected").is_err());
1259    }
1260
1261    #[test]
1262    fn writer_rejects_orphan_and_mismatched_end_tags_without_mutation() {
1263        // XML 1.0 section 3.1 requires every end-tag to match its open start-tag. Rejection must
1264        // happen before output or stack mutation so the caller can still close the open element.
1265        let mut orphan = Writer::new(Vec::new());
1266        assert!(orphan.end("root").is_err());
1267        assert!(orphan.into_inner().is_empty());
1268
1269        let mut writer = Writer::new(Vec::new());
1270        writer.start("root", []).expect("root opens");
1271        writer.start("child", []).expect("child opens");
1272        assert!(writer.end("root").is_err());
1273        writer.end("child").expect("mismatch preserves child frame");
1274        writer.end("root").expect("root closes after child");
1275        assert_eq!(writer.into_inner(), b"<root><child></child></root>");
1276    }
1277
1278    #[test]
1279    fn numeric_character_references_follow_the_exact_xml_grammar() {
1280        for valid in ["65", "x41", "x10FFFF"] {
1281            let reference = format!("&#{valid};");
1282            assert!(decode_references(&reference).is_ok(), "rejected {valid}");
1283        }
1284        for malformed in ["", "x", "+65", "-1", "X41", "x+41", " 65"] {
1285            let reference = format!("&#{malformed};");
1286            assert!(
1287                decode_references(&reference).is_err(),
1288                "accepted {malformed}"
1289            );
1290        }
1291    }
1292
1293    #[test]
1294    fn writer_rejects_duplicate_attributes_before_emitting_markup() {
1295        // XML 1.0 section 3.1 forbids an attribute name from appearing more than once in the
1296        // same start-tag: https://www.w3.org/TR/xml/#sec-starttags
1297        let mut writer = Writer::new(Vec::new());
1298        assert!(
1299            writer
1300                .empty("root", [("id", "one"), ("id", "two")])
1301                .is_err()
1302        );
1303        assert!(writer.into_inner().is_empty());
1304    }
1305
1306    #[test]
1307    fn writer_rejects_duplicate_expanded_attribute_names() {
1308        // Namespaces in XML 1.0 section 6.3 makes expanded names, not lexical prefixes, unique.
1309        // https://www.w3.org/TR/xml-names/#uniqAttrs
1310        let mut writer = Writer::new(Vec::new());
1311        assert!(
1312            writer
1313                .empty(
1314                    "root",
1315                    [
1316                        ("xmlns:a", "urn:shared"),
1317                        ("xmlns:b", "urn:shared"),
1318                        ("a:id", "one"),
1319                        ("b:id", "two"),
1320                    ],
1321                )
1322                .is_err()
1323        );
1324        assert!(writer.into_inner().is_empty());
1325
1326        let mut writer = Writer::new(Vec::new());
1327        writer
1328            .empty(
1329                "root",
1330                [
1331                    ("xmlns:a", "urn:first"),
1332                    ("xmlns:b", "urn:second"),
1333                    ("a:id", "one"),
1334                    ("b:id", "two"),
1335                ],
1336            )
1337            .expect("different expanded names remain legal");
1338    }
1339
1340    #[test]
1341    fn writer_rejects_invalid_element_namespace_bindings_before_output() {
1342        // Namespaces in XML 1.0 sections 3 and 5 reserve xml/xmlns and require every other
1343        // element prefix to be declared: https://www.w3.org/TR/xml-names/#ns-decl and
1344        // https://www.w3.org/TR/xml-names/#ns-using
1345        for (name, attributes) in [
1346            ("p:root", Vec::new()),
1347            ("root", vec![("xmlns:xml", "urn:wrong")]),
1348            ("root", vec![("xmlns:xmlns", "urn:wrong")]),
1349        ] {
1350            let mut writer = Writer::new(Vec::new());
1351            assert!(writer.empty(name, attributes).is_err(), "accepted {name}");
1352            assert!(writer.into_inner().is_empty());
1353        }
1354
1355        let mut writer = Writer::new(Vec::new());
1356        writer
1357            .empty("p:root", [("xmlns:p", "urn:bound")])
1358            .expect("a same-tag namespace declaration binds the element prefix");
1359
1360        let mut writer = Writer::new(Vec::new());
1361        writer
1362            .start("root", [("xmlns:p", "urn:bound")])
1363            .expect("parent namespace declaration is valid");
1364        writer
1365            .empty("p:child", [])
1366            .expect("an inherited namespace declaration binds the child prefix");
1367    }
1368
1369    #[test]
1370    fn writer_rejects_characters_forbidden_by_xml_1_0() {
1371        let mut writer = Writer::new(Vec::new());
1372        assert!(writer.text("before\0after").is_err());
1373        assert!(writer.into_inner().is_empty());
1374
1375        let mut writer = Writer::new(Vec::new());
1376        assert!(
1377            writer
1378                .empty("root", [("value", "before\u{1}after")])
1379                .is_err()
1380        );
1381    }
1382}