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