1use 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#[derive(Debug, thiserror::Error)]
34pub enum Error<L = String, R = alloc::collections::TryReserveError> {
35 #[error("XML lexical error: {0}")]
37 Tokenizer(xmlparser::Error),
38 #[error("XML lexical error: {0}")]
40 Static(&'static str),
41 #[error("XML lexical error: {0}")]
43 Malformed(L),
44 #[error("XML lexical error: duplicate attribute `{0}`")]
46 DuplicateAttribute(L),
47 #[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 #[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#[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 #[must_use]
86 pub fn prefix(self) -> Option<&'a str> {
87 (!self.prefix.is_empty()).then_some(self.prefix)
88 }
89
90 #[must_use]
92 pub const fn local(self) -> &'a str {
93 self.local
94 }
95
96 #[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 #[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#[derive(Clone, Debug, PartialEq, Eq)]
122pub struct Attribute<'a> {
123 pub name: Name<'a>,
125 pub value: &'a str,
127 pub range: Range<usize>,
129}
130
131#[derive(Clone, Debug, PartialEq, Eq)]
133pub struct StartTag<'a, A = Vec<Attribute<'a>>> {
134 pub name: Name<'a>,
136 pub attributes: A,
138 pub range: Range<usize>,
140}
141
142#[derive(Clone, Debug, PartialEq, Eq)]
144pub enum Event<'a, A = Vec<Attribute<'a>>> {
145 Declaration {
147 version: &'a str,
149 standalone: Option<bool>,
151 range: Range<usize>,
153 },
154 ProcessingInstruction {
156 target: &'a str,
158 content: Option<&'a str>,
160 range: Range<usize>,
162 },
163 Comment {
165 text: &'a str,
167 range: Range<usize>,
169 },
170 DocType {
172 name: &'a str,
174 range: Range<usize>,
176 },
177 Start(StartTag<'a, A>),
179 Empty(StartTag<'a, A>),
181 End {
183 name: Name<'a>,
185 range: Range<usize>,
187 },
188 Text {
190 text: &'a str,
192 range: Range<usize>,
194 },
195 CData {
197 text: &'a str,
199 range: Range<usize>,
201 },
202 Reference {
204 name: &'a str,
206 range: Range<usize>,
208 },
209}
210
211pub 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 #[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 #[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 pub fn next_event(&mut self) -> ScanResult<'a, S> {
318 if !self.admitted {
319 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 continue;
360 }
361 _ => {}
362 }
363 match token {
364 Token::Declaration {
365 version,
366 standalone,
367 span,
368 ..
369 } => {
370 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 #[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 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#[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#[derive(Debug, Clone, PartialEq, Eq)]
623pub struct DeclaredNamespacePrefixes(Vec<String>);
624
625impl DeclaredNamespacePrefixes {
626 #[must_use]
628 pub fn contains(&self, prefix: &str) -> bool {
629 self.0.iter().any(|candidate| candidate == prefix)
630 }
631}
632
633pub 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
654pub 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
689pub 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#[must_use]
718pub fn escape_text(value: &str) -> Cow<'_, str> {
719 escape(value, false)
720}
721
722#[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("&"),
739 '<' => output.push_str("<"),
740 '>' => output.push_str(">"),
741 '"' if attribute => output.push_str("""),
742 '\t' if attribute => output.push_str("	"),
743 '\n' if attribute => output.push_str(" "),
744 '\r' => output.push_str(" "),
745 _ => output.push(character),
746 }
747 }
748 Cow::Owned(output)
749}
750
751#[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 #[must_use]
768 pub const fn new(output: W) -> Self {
769 Self {
770 output,
771 frames: Vec::new(),
772 }
773 }
774
775 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 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 pub fn end(&mut self, name: &str) -> std::io::Result<()> {
823 validate_writer_qname(name)?;
824 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 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 pub fn raw(&mut self, value: &str) -> std::io::Result<()> {
851 self.output.write_all(value.as_bytes())
852 }
853
854 #[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 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 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 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 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 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 assert!(
1070 super::Scanner::new("<?xml version='1.1'?><r></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&y'>a&b<![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 let xml = format!("<root>{}</root>", "&".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("&".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 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 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 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 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 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 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=\"<&"\"><&\"</p>"
1221 );
1222 }
1223
1224 #[test]
1225 fn writer_preserves_normalized_whitespace_across_reparse() {
1226 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	line return \"/>"
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 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 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 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 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 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}