use alloc::{
borrow::{Cow, ToOwned},
format,
string::String,
vec::Vec,
};
use core::ops::Range;
#[doc(hidden)]
#[path = "lexical/storage.rs"]
pub mod storage;
use storage::{AttributeBuffer, Native, Storage};
type ScanResult<'a, S> = Result<
Option<Event<'a, <S as Storage>::Attributes<'a>>>,
Error<<S as Storage>::Label, <S as Storage>::Refusal>,
>;
#[cfg(feature = "std")]
use std::{
collections::HashSet,
io::{Error as IoError, ErrorKind, Write},
};
#[derive(Debug, thiserror::Error)]
pub enum Error<L = String, R = alloc::collections::TryReserveError> {
#[error("XML lexical error: {0}")]
Tokenizer(xmlparser::Error),
#[error("XML lexical error: {0}")]
Static(&'static str),
#[error("XML lexical error: {0}")]
Malformed(L),
#[error("XML lexical error: duplicate attribute `{0}`")]
DuplicateAttribute(L),
#[error("XML lexical storage: {0}")]
Resource(R),
}
impl<L, R> Error<L, R> {
fn tokenizer(error: xmlparser::Error) -> Self {
Self::Tokenizer(error)
}
#[doc(hidden)]
pub fn map_label<T>(self, mut map: impl FnMut(L) -> T) -> Error<T, R> {
match self {
Self::Tokenizer(error) => Error::Tokenizer(error),
Self::Static(message) => Error::Static(message),
Self::Malformed(label) => Error::Malformed(map(label)),
Self::DuplicateAttribute(label) => Error::DuplicateAttribute(map(label)),
Self::Resource(error) => Error::Resource(error),
}
}
}
impl Error {
fn malformed(message: impl Into<String>) -> Self {
Self::Malformed(message.into())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Name<'a> {
prefix: &'a str,
local: &'a str,
}
impl<'a> Name<'a> {
#[must_use]
pub fn prefix(self) -> Option<&'a str> {
(!self.prefix.is_empty()).then_some(self.prefix)
}
#[must_use]
pub const fn local(self) -> &'a str {
self.local
}
#[must_use]
pub fn is_qualified(self, value: &str) -> bool {
match self.prefix() {
Some(prefix) => {
value
.strip_prefix(prefix)
.and_then(|suffix| suffix.strip_prefix(':'))
== Some(self.local)
}
None => value == self.local,
}
}
#[must_use]
pub fn qualified(self) -> Cow<'a, str> {
self.prefix().map_or_else(
|| Cow::Borrowed(self.local),
|prefix| Cow::Owned(format!("{prefix}:{}", self.local)),
)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Attribute<'a> {
pub name: Name<'a>,
pub value: &'a str,
pub range: Range<usize>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StartTag<'a, A = Vec<Attribute<'a>>> {
pub name: Name<'a>,
pub attributes: A,
pub range: Range<usize>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Event<'a, A = Vec<Attribute<'a>>> {
Declaration {
version: &'a str,
standalone: Option<bool>,
range: Range<usize>,
},
ProcessingInstruction {
target: &'a str,
content: Option<&'a str>,
range: Range<usize>,
},
Comment {
text: &'a str,
range: Range<usize>,
},
DocType {
name: &'a str,
range: Range<usize>,
},
Start(StartTag<'a, A>),
Empty(StartTag<'a, A>),
End {
name: Name<'a>,
range: Range<usize>,
},
Text {
text: &'a str,
range: Range<usize>,
},
CData {
text: &'a str,
range: Range<usize>,
},
Reference {
name: &'a str,
range: Range<usize>,
},
}
pub struct Scanner<'a, S: Storage = Native> {
input: &'a str,
tokenizer: xmlparser::Tokenizer<'a>,
pending_text: Option<PendingText<'a>>,
pending_start: Option<PendingStart<'a, S>>,
dtd_start: Option<(usize, &'a str)>,
storage: S,
admitted: bool,
}
struct PendingStart<'a, S: Storage> {
name: Name<'a>,
attributes: S::Attributes<'a>,
start: usize,
}
struct PendingText<'a> {
text: &'a str,
range: Range<usize>,
offset: usize,
}
fn validate_unique_attributes<'a, S: Storage>(
attributes: &mut S::Attributes<'a>,
storage: S,
) -> Result<(), Error<S::Label, S::Refusal>> {
const SMALL_TAG_ATTRIBUTES: usize = 8;
if attributes.len() <= SMALL_TAG_ATTRIBUTES {
for index in 1..attributes.len() {
let name = attributes[index].name;
for attribute in &attributes[..index] {
if compare_names(attribute.name, name, storage)
.map_err(Error::Resource)?
.is_eq()
{
return Err(Error::DuplicateAttribute(
storage.qualified_name(name).map_err(Error::Resource)?,
));
}
}
}
return Ok(());
}
attributes
.sort(|left, right| compare_names(left.name, right.name, storage))
.map_err(Error::Resource)?;
for pair in attributes.windows(2) {
if compare_names(pair[0].name, pair[1].name, storage)
.map_err(Error::Resource)?
.is_eq()
{
return Err(Error::DuplicateAttribute(
storage
.qualified_name(pair[0].name)
.map_err(Error::Resource)?,
));
}
}
attributes
.sort(|left, right| Ok(left.range.start.cmp(&right.range.start)))
.map_err(Error::Resource)?;
Ok(())
}
fn compare_names<S: Storage>(
left: Name<'_>,
right: Name<'_>,
storage: S,
) -> Result<core::cmp::Ordering, S::Refusal> {
storage.admit(1)?;
storage.admit(left.prefix.len().min(right.prefix.len()))?;
let prefix = left.prefix.cmp(right.prefix);
if !prefix.is_eq() {
return Ok(prefix);
}
storage.admit(left.local.len().min(right.local.len()))?;
Ok(left.local.cmp(right.local))
}
impl<'a> Scanner<'a> {
#[must_use]
pub fn new(input: &'a str) -> Self {
Self::with_storage(input, Native)
}
}
impl<'a, S: Storage> Scanner<'a, S> {
#[doc(hidden)]
pub fn with_storage(input: &'a str, storage: S) -> Self {
Self {
input,
tokenizer: xmlparser::Tokenizer::from(input),
pending_text: None,
pending_start: None,
dtd_start: None,
storage,
admitted: false,
}
}
pub fn next_event(&mut self) -> ScanResult<'a, S> {
if !self.admitted {
self.storage
.admit(self.input.len())
.map_err(Error::Resource)?;
self.storage
.admit(self.input.len())
.map_err(Error::Resource)?;
self.storage
.admit(self.input.len())
.map_err(Error::Resource)?;
self.admitted = true;
}
if let Some(event) = self.next_text_event()? {
return Ok(Some(event));
}
loop {
let Some(token) = self.tokenizer.next() else {
if self.pending_start.is_some() {
return Err(Error::Static("unterminated element start tag"));
}
return Ok(None);
};
let token = token.map_err(Error::tokenizer)?;
use xmlparser::{ElementEnd, Token};
match &token {
Token::DtdStart { name, span, .. } => {
self.dtd_start = Some((span.range().start, name.as_str()));
continue;
}
Token::DtdEnd { span } if self.dtd_start.is_some() => {
let (start, name) = self.dtd_start.take().expect("DTD start is present");
return Ok(Some(Event::DocType {
name,
range: start..span.range().end,
}));
}
_ if self.dtd_start.is_some() => {
continue;
}
_ => {}
}
match token {
Token::Declaration {
version,
standalone,
span,
..
} => {
if version.as_str() != "1.0" {
return Err(Error::Static("unsupported XML version"));
}
return Ok(Some(Event::Declaration {
version: version.as_str(),
standalone,
range: span.range(),
}));
}
Token::ProcessingInstruction {
target,
content,
span,
} => {
return Ok(Some(Event::ProcessingInstruction {
target: target.as_str(),
content: content.map(|value| value.as_str()),
range: span.range(),
}));
}
Token::Comment { text, span } => {
return Ok(Some(Event::Comment {
text: text.as_str(),
range: span.range(),
}));
}
Token::DtdStart { .. } => unreachable!("DTD start is handled above"),
Token::EmptyDtd { name, span, .. } => {
return Ok(Some(Event::DocType {
name: name.as_str(),
range: span.range(),
}));
}
Token::DtdEnd { .. } => {
return Err(Error::Static("DOCTYPE end without a start"));
}
Token::EntityDeclaration { .. } => {}
Token::ElementStart {
prefix,
local,
span,
} => {
validate_qualified_lexeme(&self.input[span.range()])?;
if self.pending_start.is_some() {
return Err(Error::Static("nested element start tokens"));
}
self.pending_start = Some(PendingStart {
name: Name {
prefix: prefix.as_str(),
local: local.as_str(),
},
attributes: self.storage.attributes(),
start: span.range().start,
});
}
Token::Attribute {
prefix,
local,
value,
span,
} => {
validate_qualified_lexeme(&self.input[span.range()])?;
let start = self
.pending_start
.as_mut()
.ok_or(Error::Static("attribute outside a start tag"))?;
let name = Name {
prefix: prefix.as_str(),
local: local.as_str(),
};
start
.attributes
.push(Attribute {
name,
value: value.as_str(),
range: span.range(),
})
.map_err(Error::Resource)?;
}
Token::ElementEnd { end, span } => match end {
ElementEnd::Open | ElementEnd::Empty => {
let mut start = self
.pending_start
.take()
.ok_or(Error::Static("element end without a start"))?;
validate_unique_attributes(&mut start.attributes, self.storage)?;
let tag = StartTag {
name: start.name,
attributes: start.attributes,
range: start.start..span.range().end,
};
return Ok(Some(if end == ElementEnd::Open {
Event::Start(tag)
} else {
Event::Empty(tag)
}));
}
ElementEnd::Close(prefix, local) => {
validate_qualified_lexeme(&self.input[span.range()])?;
return Ok(Some(Event::End {
name: Name {
prefix: prefix.as_str(),
local: local.as_str(),
},
range: span.range(),
}));
}
},
Token::Text { text } => {
self.pending_text = Some(PendingText {
text: text.as_str(),
range: text.range(),
offset: 0,
});
if let Some(event) = self.next_text_event()? {
return Ok(Some(event));
}
}
Token::Cdata { text, span } => {
return Ok(Some(Event::CData {
text: text.as_str(),
range: span.range(),
}));
}
}
}
}
fn next_text_event(&mut self) -> ScanResult<'a, S> {
let Some(mut pending) = self.pending_text.take() else {
return Ok(None);
};
let tail = &pending.text[pending.offset..];
let Some(relative) = tail.find('&') else {
return Ok((!tail.is_empty()).then_some(Event::Text {
text: tail,
range: pending.range.start + pending.offset..pending.range.end,
}));
};
let start = pending.offset + relative;
if start > pending.offset {
let event = Event::Text {
text: &pending.text[pending.offset..start],
range: pending.range.start + pending.offset..pending.range.start + start,
};
pending.offset = start;
self.pending_text = Some(pending);
return Ok(Some(event));
}
let Some(relative_end) = pending.text[start + 1..].find(';') else {
return Err(Error::Static(
"unterminated XML reference in character data",
));
};
let end = start + 1 + relative_end;
let event = Event::Reference {
name: &pending.text[start + 1..end],
range: pending.range.start + start..pending.range.start + end + 1,
};
pending.offset = end + 1;
if pending.offset < pending.text.len() {
self.pending_text = Some(pending);
}
Ok(Some(event))
}
#[must_use]
pub const fn input(&self) -> &'a str {
self.input
}
}
fn validate_qualified_lexeme<L, R>(source: &str) -> Result<(), Error<L, R>> {
let source = source
.strip_prefix("</")
.or_else(|| source.strip_prefix('<'))
.unwrap_or(source);
let name = source
.split(|character: char| {
character.is_ascii_whitespace() || matches!(character, '=' | '/' | '>')
})
.next()
.unwrap_or_default();
let mut parts = name.split(':');
let first = parts.next().unwrap_or_default();
let second = parts.next();
if first.is_empty() || second.is_some_and(str::is_empty) || parts.next().is_some() {
return Err(Error::Static("invalid qualified XML name"));
}
Ok(())
}
#[must_use]
pub fn is_qname(value: &str) -> bool {
let mut parts = value.split(':');
let first = parts.next().unwrap_or_default();
!first.is_empty()
&& is_ncname(first)
&& parts.next().is_none_or(is_ncname)
&& parts.next().is_none()
}
fn is_ncname(value: &str) -> bool {
let mut characters = value.chars();
let Some(first) = characters.next() else {
return false;
};
is_ncname_start(first) && characters.all(is_ncname_char)
}
fn is_ncname_start(character: char) -> bool {
matches!(
character,
'A'..='Z'
| '_'
| 'a'..='z'
| '\u{C0}'..='\u{D6}'
| '\u{D8}'..='\u{F6}'
| '\u{F8}'..='\u{2FF}'
| '\u{370}'..='\u{37D}'
| '\u{37F}'..='\u{1FFF}'
| '\u{200C}'..='\u{200D}'
| '\u{2070}'..='\u{218F}'
| '\u{2C00}'..='\u{2FEF}'
| '\u{3001}'..='\u{D7FF}'
| '\u{F900}'..='\u{FDCF}'
| '\u{FDF0}'..='\u{FFFD}'
| '\u{10000}'..='\u{EFFFF}'
)
}
fn is_ncname_char(character: char) -> bool {
is_ncname_start(character)
|| matches!(
character,
'-' | '.' | '0'..='9' | '\u{B7}' | '\u{0300}'..='\u{036F}' | '\u{203F}'..='\u{2040}'
)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeclaredNamespacePrefixes(Vec<String>);
impl DeclaredNamespacePrefixes {
#[must_use]
pub fn contains(&self, prefix: &str) -> bool {
self.0.iter().any(|candidate| candidate == prefix)
}
}
pub fn declared_namespace_prefixes(opening: &str) -> Result<DeclaredNamespacePrefixes, Error> {
let standalone = format!("{} />", opening.trim_end_matches('/'));
let mut scanner = Scanner::new(&standalone);
let Some(Event::Start(tag) | Event::Empty(tag)) = scanner.next_event()? else {
return Err(Error::malformed("expected one opening element tag"));
};
Ok(DeclaredNamespacePrefixes(
tag.attributes
.iter()
.filter_map(
|attribute| match (attribute.name.prefix(), attribute.name.local()) {
(None, "xmlns") => Some(String::new()),
(Some("xmlns"), prefix) => Some(prefix.to_owned()),
_ => None,
},
)
.collect(),
))
}
pub fn decode_references(value: &str) -> Result<Cow<'_, str>, Error> {
if !value.contains('&') {
return Ok(Cow::Borrowed(value));
}
let mut output = String::with_capacity(value.len());
let mut offset = 0;
while let Some(relative) = value[offset..].find('&') {
let start = offset + relative;
output.push_str(&value[offset..start]);
let end = value[start + 1..]
.find(';')
.map(|relative| start + 1 + relative)
.ok_or_else(|| Error::malformed("unterminated XML reference"))?;
let name = &value[start + 1..end];
let character = match name {
"amp" => '&',
"apos" => '\'',
"gt" => '>',
"lt" => '<',
"quot" => '"',
value if value.starts_with('#') => decode_numeric_character_reference(&value[1..])?,
_ => {
return Err(Error::malformed(format!(
"unresolved entity reference &{name};"
)));
}
};
output.push(character);
offset = end + 1;
}
output.push_str(&value[offset..]);
Ok(Cow::Owned(output))
}
pub fn decode_numeric_character_reference(reference: &str) -> Result<char, Error> {
let (digits, radix) = reference
.strip_prefix('x')
.map_or((reference, 10), |digits| (digits, 16));
let valid_digits = if radix == 16 {
digits.bytes().all(|byte| byte.is_ascii_hexdigit())
} else {
digits.bytes().all(|byte| byte.is_ascii_digit())
};
if digits.is_empty() || !valid_digits {
return Err(Error::malformed("invalid XML character reference"));
}
u32::from_str_radix(digits, radix)
.ok()
.and_then(char::from_u32)
.filter(|character| is_xml_1_0_character(*character))
.ok_or_else(|| Error::malformed("invalid XML character reference"))
}
fn is_xml_1_0_character(character: char) -> bool {
matches!(character, '\u{9}' | '\u{A}' | '\u{D}' | '\u{20}'..='\u{D7FF}' | '\u{E000}'..='\u{FFFD}' | '\u{10000}'..='\u{10FFFF}')
}
#[must_use]
pub fn escape_text(value: &str) -> Cow<'_, str> {
escape(value, false)
}
#[must_use]
pub fn escape_attribute(value: &str) -> Cow<'_, str> {
escape(value, true)
}
fn escape(value: &str, attribute: bool) -> Cow<'_, str> {
if !value.bytes().any(|byte| {
matches!(byte, b'&' | b'<' | b'>' | b'\r')
|| (attribute && matches!(byte, b'"' | b'\n' | b'\t'))
}) {
return Cow::Borrowed(value);
}
let mut output = String::with_capacity(value.len());
for character in value.chars() {
match character {
'&' => output.push_str("&"),
'<' => output.push_str("<"),
'>' => output.push_str(">"),
'"' if attribute => output.push_str("""),
'\t' if attribute => output.push_str("	"),
'\n' if attribute => output.push_str(" "),
'\r' => output.push_str(" "),
_ => output.push(character),
}
}
Cow::Owned(output)
}
#[cfg(feature = "std")]
pub struct Writer<W> {
output: W,
frames: Vec<WriterFrame>,
}
#[cfg(feature = "std")]
struct WriterFrame {
element_name: String,
namespaces: Vec<(String, String)>,
}
#[cfg(feature = "std")]
impl<W: Write> Writer<W> {
#[must_use]
pub const fn new(output: W) -> Self {
Self {
output,
frames: Vec::new(),
}
}
pub fn start<'a>(
&mut self,
name: &str,
attributes: impl IntoIterator<Item = (&'a str, &'a str)>,
) -> std::io::Result<()> {
self.tag(name, attributes, false)
}
pub fn empty<'a>(
&mut self,
name: &str,
attributes: impl IntoIterator<Item = (&'a str, &'a str)>,
) -> std::io::Result<()> {
self.tag(name, attributes, true)
}
fn tag<'a>(
&mut self,
name: &str,
attributes: impl IntoIterator<Item = (&'a str, &'a str)>,
empty: bool,
) -> std::io::Result<()> {
validate_writer_qname(name)?;
let attributes = attributes.into_iter().collect::<Vec<_>>();
validate_writer_element_namespace(name, &attributes, &self.frames)?;
validate_writer_attributes(&attributes, &self.frames)?;
write!(self.output, "<{name}")?;
for (attribute, value) in &attributes {
write!(self.output, " {attribute}=\"{}\"", escape_attribute(value))?;
}
self.output.write_all(if empty { b"/>" } else { b">" })?;
if !empty {
self.frames.push(WriterFrame {
element_name: name.to_owned(),
namespaces: attributes
.iter()
.filter_map(|(name, uri)| namespace_declaration(name).map(|name| (name, *uri)))
.map(|(name, uri)| (name.to_owned(), uri.to_owned()))
.collect(),
});
}
Ok(())
}
pub fn end(&mut self, name: &str) -> std::io::Result<()> {
validate_writer_qname(name)?;
let frame = self.frames.last().ok_or_else(|| {
IoError::new(ErrorKind::InvalidInput, "XML end tag has no open element")
})?;
if frame.element_name != name {
return Err(IoError::new(
ErrorKind::InvalidInput,
format!(
"XML end tag `{name}` does not match open element `{}`",
frame.element_name
),
));
}
write!(self.output, "</{name}>")?;
self.frames.pop();
Ok(())
}
pub fn text(&mut self, value: &str) -> std::io::Result<()> {
validate_writer_characters(value)?;
write!(self.output, "{}", escape_text(value))
}
pub fn raw(&mut self, value: &str) -> std::io::Result<()> {
self.output.write_all(value.as_bytes())
}
#[must_use]
pub fn into_inner(self) -> W {
self.output
}
}
#[cfg(feature = "std")]
fn validate_writer_attributes(
attributes: &[(&str, &str)],
frames: &[WriterFrame],
) -> std::io::Result<()> {
const SMALL_TAG_ATTRIBUTES: usize = 8;
for (name, value) in attributes {
validate_writer_qname(name)?;
validate_writer_characters(value)?;
validate_writer_namespace_declaration(name, value)?;
}
if attributes.len() <= SMALL_TAG_ATTRIBUTES {
for index in 1..attributes.len() {
if attributes[..index]
.iter()
.any(|(name, _)| *name == attributes[index].0)
{
return Err(IoError::new(
ErrorKind::InvalidInput,
format!("duplicate XML attribute `{}`", attributes[index].0),
));
}
}
} else {
let mut names = HashSet::with_capacity(attributes.len());
if let Some((duplicate, _)) = attributes.iter().find(|(name, _)| !names.insert(*name)) {
return Err(IoError::new(
ErrorKind::InvalidInput,
format!("duplicate XML attribute `{duplicate}`"),
));
}
}
let mut expanded = HashSet::with_capacity(attributes.len());
for (name, _) in attributes {
let Some((prefix, local)) = name.split_once(':') else {
if name != &"xmlns" && !expanded.insert((None, *name)) {
return duplicate_expanded_attribute(name);
}
continue;
};
if prefix == "xmlns" {
continue;
}
let namespace = resolve_writer_prefix(prefix, attributes, frames).ok_or_else(|| {
IoError::new(
ErrorKind::InvalidInput,
format!("unbound XML namespace prefix `{prefix}`"),
)
})?;
if namespace.is_empty() {
return Err(IoError::new(
ErrorKind::InvalidInput,
format!("unbound XML namespace prefix `{prefix}`"),
));
}
if !expanded.insert((Some(namespace), local)) {
return duplicate_expanded_attribute(name);
}
}
Ok(())
}
#[cfg(feature = "std")]
fn validate_writer_element_namespace(
name: &str,
attributes: &[(&str, &str)],
frames: &[WriterFrame],
) -> std::io::Result<()> {
let Some((prefix, _)) = name.split_once(':') else {
return Ok(());
};
if prefix == "xmlns"
|| resolve_writer_prefix(prefix, attributes, frames).is_none_or(str::is_empty)
{
return Err(IoError::new(
ErrorKind::InvalidInput,
format!("unbound XML namespace prefix `{prefix}`"),
));
}
Ok(())
}
#[cfg(feature = "std")]
fn validate_writer_namespace_declaration(name: &str, uri: &str) -> std::io::Result<()> {
const XML_NAMESPACE: &str = "http://www.w3.org/XML/1998/namespace";
const XMLNS_NAMESPACE: &str = "http://www.w3.org/2000/xmlns/";
let Some(prefix) = namespace_declaration(name) else {
return Ok(());
};
let valid = if prefix == "xml" {
uri == XML_NAMESPACE
} else if prefix == "xmlns" {
false
} else {
uri != XML_NAMESPACE && uri != XMLNS_NAMESPACE && (prefix.is_empty() || !uri.is_empty())
};
if valid {
Ok(())
} else {
Err(IoError::new(
ErrorKind::InvalidInput,
format!("invalid XML namespace declaration `{name}={uri}`"),
))
}
}
#[cfg(feature = "std")]
fn namespace_declaration(name: &str) -> Option<&str> {
if name == "xmlns" {
Some("")
} else {
name.strip_prefix("xmlns:")
}
}
#[cfg(feature = "std")]
fn resolve_writer_prefix<'a>(
prefix: &str,
attributes: &'a [(&str, &str)],
frames: &'a [WriterFrame],
) -> Option<&'a str> {
if prefix == "xml" {
return Some("http://www.w3.org/XML/1998/namespace");
}
attributes
.iter()
.find_map(|(name, uri)| (namespace_declaration(name) == Some(prefix)).then_some(*uri))
.or_else(|| {
frames.iter().rev().find_map(|frame| {
frame
.namespaces
.iter()
.rev()
.find_map(|(name, uri)| (name == prefix).then_some(uri.as_str()))
})
})
}
#[cfg(feature = "std")]
fn duplicate_expanded_attribute(name: &str) -> std::io::Result<()> {
Err(IoError::new(
ErrorKind::InvalidInput,
format!("duplicate expanded XML attribute `{name}`"),
))
}
#[cfg(feature = "std")]
fn validate_writer_qname(name: &str) -> std::io::Result<()> {
if is_qname(name) {
Ok(())
} else {
Err(IoError::new(ErrorKind::InvalidInput, "invalid XML QName"))
}
}
#[cfg(feature = "std")]
fn validate_writer_characters(value: &str) -> std::io::Result<()> {
if let Some(character) = value
.chars()
.find(|character| !is_xml_1_0_character(*character))
{
Err(IoError::new(
ErrorKind::InvalidInput,
format!(
"character U+{:04X} is forbidden by XML 1.0",
u32::from(character)
),
))
} else {
Ok(())
}
}
#[cfg(all(test, feature = "std"))]
#[allow(clippy::unwrap_used)]
mod tests {
#[test]
fn xml_version_requires_digits_after_the_period() {
assert!(
super::Scanner::new("<?xml version='1.'?><r/>")
.next_event()
.is_err()
);
assert!(
super::Scanner::new("<?xml version='1.0'?><r/>")
.next_event()
.is_ok()
);
}
#[test]
fn scanner_rejects_xml_11_until_character_rules_are_version_aware() {
assert!(
super::Scanner::new("<?xml version='1.1'?><r></r>")
.next_event()
.is_err()
);
}
use super::*;
#[test]
fn scanner_groups_start_tags_and_splits_references() {
let xml = "<p xmlns='urn:test' a='x&y'>a&b<![CDATA[c]]></p>";
let mut scanner = Scanner::new(xml);
let events = std::iter::from_fn(|| scanner.next_event().transpose())
.collect::<Result<Vec<_>, _>>()
.expect("fixture must scan");
assert!(matches!(&events[0], Event::Start(tag) if tag.attributes.len() == 2));
assert!(matches!(&events[1], Event::Text { text: "a", .. }));
assert!(matches!(&events[2], Event::Reference { name: "amp", .. }));
assert!(matches!(&events[3], Event::Reference { name: "#x62", .. }));
assert!(matches!(&events[4], Event::CData { text: "c", .. }));
assert!(matches!(&events[5], Event::End { .. }));
}
#[test]
fn scanner_streams_dense_reference_runs_without_buffering_events() {
let xml = format!("<root>{}</root>", "&".repeat(4096));
let mut scanner = Scanner::new(&xml);
assert!(matches!(
scanner.next_event().expect("scan root start"),
Some(Event::Start(_))
));
assert!(matches!(
scanner.next_event().expect("scan first reference"),
Some(Event::Reference { name: "amp", .. })
));
assert_eq!(
scanner.pending_text.as_ref().map(|pending| pending.offset),
Some("&".len()),
"the scanner retains only a cursor into the source token"
);
assert_eq!(
std::iter::from_fn(|| scanner.next_event().transpose())
.collect::<Result<Vec<_>, _>>()
.expect("remaining references scan")
.len(),
4096
);
}
#[test]
fn scanner_rejects_unterminated_references_in_character_data() {
for xml in ["<root>AT&T</root>", "<root>&bad</root>"] {
let mut scanner = Scanner::new(xml);
let result = std::iter::from_fn(|| scanner.next_event().transpose())
.collect::<Result<Vec<_>, _>>();
assert!(result.is_err(), "accepted {xml}");
}
}
#[test]
fn scanner_keeps_the_internal_subset_as_one_doctype_event() {
let xml = r#"<!DOCTYPE root [<!-- <fake/> --><!ENTITY value "ok">]><root/>"#;
let mut scanner = Scanner::new(xml);
assert!(matches!(
scanner.next_event().expect("scan DTD"),
Some(Event::DocType { .. })
));
assert!(matches!(
scanner.next_event().expect("scan root"),
Some(Event::Empty(_))
));
assert!(scanner.next_event().expect("scan EOF").is_none());
}
#[test]
fn scanner_rejects_empty_and_repeated_prefix_components() {
for xml in ["<:root/>", "<root: />", "<a:b:c/>"] {
assert!(Scanner::new(xml).next_event().is_err(), "accepted {xml}");
}
}
#[test]
fn scanner_preserves_non_ascii_name_start_characters() {
let Some(Event::Empty(tag)) = Scanner::new("<\u{1680}/>")
.next_event()
.expect("valid non-ASCII name scans")
else {
panic!("empty element must produce one empty-tag event");
};
assert_eq!(tag.name.local(), "\u{1680}");
}
#[test]
fn scanner_rejects_duplicate_lexical_attributes() {
for xml in [
"<root xmlns='urn:first' xmlns='urn:second'/>",
"<root xmlns:p='urn:first' xmlns:p='urn:second'/>",
"<root value='first' value='second'/>",
] {
assert!(Scanner::new(xml).next_event().is_err(), "accepted {xml}");
}
}
#[test]
fn scanner_validates_wide_attributes_without_changing_source_order() {
let attributes = (0..4096)
.rev()
.map(|index| format!(" a{index}='{index}'"))
.collect::<String>();
let xml = format!("<root{attributes}/>");
let Some(Event::Empty(tag)) = Scanner::new(&xml)
.next_event()
.expect("wide start tag scans")
else {
panic!("wide empty element must produce one empty-tag event");
};
assert_eq!(tag.attributes.len(), 4096);
assert_eq!(tag.attributes[0].name.local(), "a4095");
assert_eq!(tag.attributes[4095].name.local(), "a0");
let unique = (0..4096)
.map(|index| format!(" a{index}='x'"))
.collect::<String>();
let duplicate = format!("<root{unique} a0='duplicate'/>");
assert!(Scanner::new(&duplicate).next_event().is_err());
}
#[test]
fn writer_escapes_text_and_attributes_by_context() {
let mut writer = Writer::new(Vec::new());
writer
.start("p", [("a", "<&\"")])
.expect("start tag must serialize");
writer.text("<&\"").expect("text must serialize");
writer.end("p").expect("end tag must serialize");
assert_eq!(
String::from_utf8(writer.into_inner()).expect("writer emits UTF-8"),
"<p a=\"<&"\"><&\"</p>"
);
}
#[test]
fn writer_preserves_normalized_whitespace_across_reparse() {
let mut writer = Writer::new(Vec::new());
writer
.empty("root", [("value", "tab\tline\nreturn\r")])
.expect("empty tag must serialize");
assert_eq!(
String::from_utf8(writer.into_inner()).expect("writer emits UTF-8"),
"<root value=\"tab	line return \"/>"
);
let mut writer = Writer::new(Vec::new());
writer.text("line\rbreak\n").expect("text must serialize");
assert_eq!(
String::from_utf8(writer.into_inner()).expect("writer emits UTF-8"),
"line break\n"
);
}
#[test]
fn writer_rejects_invalid_element_and_attribute_qnames() {
for name in ["", ":root", "root:", "a:b:c", "root><injected"] {
let mut writer = Writer::new(Vec::new());
assert!(
writer.empty(name, []).is_err(),
"accepted element name {name:?}"
);
}
let mut writer = Writer::new(Vec::new());
assert!(writer.empty("root", [("a:b:c", "value")]).is_err());
let mut writer = Writer::new(Vec::new());
assert!(writer.end("root><injected").is_err());
}
#[test]
fn writer_rejects_orphan_and_mismatched_end_tags_without_mutation() {
let mut orphan = Writer::new(Vec::new());
assert!(orphan.end("root").is_err());
assert!(orphan.into_inner().is_empty());
let mut writer = Writer::new(Vec::new());
writer.start("root", []).expect("root opens");
writer.start("child", []).expect("child opens");
assert!(writer.end("root").is_err());
writer.end("child").expect("mismatch preserves child frame");
writer.end("root").expect("root closes after child");
assert_eq!(writer.into_inner(), b"<root><child></child></root>");
}
#[test]
fn numeric_character_references_follow_the_exact_xml_grammar() {
for valid in ["65", "x41", "x10FFFF"] {
let reference = format!("&#{valid};");
assert!(decode_references(&reference).is_ok(), "rejected {valid}");
}
for malformed in ["", "x", "+65", "-1", "X41", "x+41", " 65"] {
let reference = format!("&#{malformed};");
assert!(
decode_references(&reference).is_err(),
"accepted {malformed}"
);
}
}
#[test]
fn writer_rejects_duplicate_attributes_before_emitting_markup() {
let mut writer = Writer::new(Vec::new());
assert!(
writer
.empty("root", [("id", "one"), ("id", "two")])
.is_err()
);
assert!(writer.into_inner().is_empty());
}
#[test]
fn writer_rejects_duplicate_expanded_attribute_names() {
let mut writer = Writer::new(Vec::new());
assert!(
writer
.empty(
"root",
[
("xmlns:a", "urn:shared"),
("xmlns:b", "urn:shared"),
("a:id", "one"),
("b:id", "two"),
],
)
.is_err()
);
assert!(writer.into_inner().is_empty());
let mut writer = Writer::new(Vec::new());
writer
.empty(
"root",
[
("xmlns:a", "urn:first"),
("xmlns:b", "urn:second"),
("a:id", "one"),
("b:id", "two"),
],
)
.expect("different expanded names remain legal");
}
#[test]
fn writer_rejects_invalid_element_namespace_bindings_before_output() {
for (name, attributes) in [
("p:root", Vec::new()),
("root", vec![("xmlns:xml", "urn:wrong")]),
("root", vec![("xmlns:xmlns", "urn:wrong")]),
] {
let mut writer = Writer::new(Vec::new());
assert!(writer.empty(name, attributes).is_err(), "accepted {name}");
assert!(writer.into_inner().is_empty());
}
let mut writer = Writer::new(Vec::new());
writer
.empty("p:root", [("xmlns:p", "urn:bound")])
.expect("a same-tag namespace declaration binds the element prefix");
let mut writer = Writer::new(Vec::new());
writer
.start("root", [("xmlns:p", "urn:bound")])
.expect("parent namespace declaration is valid");
writer
.empty("p:child", [])
.expect("an inherited namespace declaration binds the child prefix");
}
#[test]
fn writer_rejects_characters_forbidden_by_xml_1_0() {
let mut writer = Writer::new(Vec::new());
assert!(writer.text("before\0after").is_err());
assert!(writer.into_inner().is_empty());
let mut writer = Writer::new(Vec::new());
assert!(
writer
.empty("root", [("value", "before\u{1}after")])
.is_err()
);
}
}