use alloc::borrow::{Cow, ToOwned};
use alloc::boxed::Box;
use alloc::collections::BTreeSet;
use alloc::string::String;
use alloc::vec::Vec;
use crate::error::{Error, ErrorKind, Result};
use crate::limits::Limits;
use crate::tree::{Chars, Document, ExpandedName, NameId, NodeData, NodeId};
use crate::xml::is_xml_char_for;
pub(crate) use crate::xml::{
Version, is_literal_char_for, is_name_char, is_name_start,
};
#[derive(Debug, Default, Clone)]
pub(crate) struct Namespaces {
bindings: Vec<(String, String)>,
marks: Vec<usize>,
ids: Vec<u64>,
next_id: u64,
}
impl Namespaces {
fn push_scope(&mut self) {
self.marks.push(self.bindings.len());
}
pub(crate) fn pop_scope(&mut self) {
if let Some(mark) = self.marks.pop() {
self.bindings.truncate(mark);
self.ids.truncate(mark);
}
}
fn bind(&mut self, prefix: String, uri: String) {
self.next_id += 1;
self.bindings.push((prefix, uri));
self.ids.push(self.next_id);
}
fn scope_id(&self) -> u64 {
self.ids.last().copied().unwrap_or(0)
}
fn depths(&self) -> (usize, usize) {
(self.bindings.len(), self.marks.len())
}
fn truncate_to(&mut self, bindings: usize, marks: usize) {
self.bindings.truncate(bindings);
self.ids.truncate(bindings);
self.marks.truncate(marks);
}
fn resolve(&self, prefix: &str) -> Option<&str> {
self.bindings
.iter()
.rev()
.find(|(p, _)| p == prefix)
.map(|(_, u)| u.as_str())
}
}
#[derive(Default)]
struct SeenAttributes {
linear: [u32; Self::LINEAR],
len: usize,
wide: Option<BTreeSet<(Option<String>, String)>>,
}
impl SeenAttributes {
const LINEAR: usize = 16;
fn insert(&mut self, name: NameId, names: &[ExpandedName]) -> bool {
let expanded = &names[name.0 as usize];
if let Some(set) = self.wide.as_mut() {
return set.insert(Self::key(expanded));
}
let seen = &self.linear[..self.len];
if seen.iter().any(|n| names[*n as usize] == *expanded) {
return false;
}
if self.len < Self::LINEAR {
self.linear[self.len] = name.0;
self.len += 1;
} else {
self.widen(expanded, names);
}
true
}
fn widen(&mut self, latest: &ExpandedName, names: &[ExpandedName]) {
let mut set: BTreeSet<_> = self.linear[..self.len]
.iter()
.map(|n| Self::key(&names[*n as usize]))
.collect();
let _ = set.insert(Self::key(latest));
self.wide = Some(set);
}
fn key(name: &ExpandedName) -> (Option<String>, String) {
(name.namespace.clone(), name.local.clone())
}
}
fn arena_capacity(input: &str, limits: &Limits) -> usize {
const CEILING: usize = 1 << 20;
let guess = input.bytes().filter(|b| *b == b'<').count() * 2;
let cap = limits.max_nodes.map_or(CEILING, |n| n.min(CEILING));
guess.min(cap)
}
const XML_NS: &str = "http://www.w3.org/XML/1998/namespace";
const XMLNS_NS: &str = "http://www.w3.org/2000/xmlns/";
fn declared_prefix(name: &str) -> Option<&str> {
if name == "xmlns" {
Some("")
} else {
name.strip_prefix("xmlns:")
}
}
fn check_declaration(
name: &str,
prefix: &str,
value: &str,
version: Version,
) -> core::result::Result<(), ErrorKind> {
if value == XMLNS_NS || (prefix != "xml" && value == XML_NS) {
return Err(ErrorKind::ReservedNamespace);
}
if prefix.is_empty() {
return if name == "xmlns" {
Ok(())
} else {
Err(ErrorKind::ReservedNamespace)
};
}
check_prefix_declaration(prefix, value, version)
}
fn check_prefix_declaration(
prefix: &str,
value: &str,
version: Version,
) -> core::result::Result<(), ErrorKind> {
let malformed = prefix == "xmlns" || prefix.contains(':');
let xml_rebound = prefix == "xml" && value != XML_NS;
let undeclared_in_10 = value.is_empty() && version == Version::V10;
if malformed || xml_rebound || undeclared_in_10 {
return Err(ErrorKind::ReservedNamespace);
}
Ok(())
}
pub(crate) struct Parser<'a> {
pub(crate) input: &'a str,
pub(crate) bytes: &'a [u8],
pub(crate) pos: usize,
pub(crate) doc: Box<Document>,
pub(crate) ns: Namespaces,
pub(crate) depth: usize,
pub(crate) limits: Limits,
pub(crate) dtd: Option<crate::dtd::Dtd>,
pub(crate) version: Version,
pub(crate) name_index: alloc::collections::BTreeMap<String, Vec<u32>>,
pub(crate) external: &'a dyn crate::external::ExternalSource,
pub(crate) entity_budget: usize,
pub(crate) entity_depth: usize,
pub(crate) standalone: bool,
pub(crate) entity_cache: BTreeSet<(String, u64)>,
}
pub fn parse(input: &str) -> Result<Document> {
parse_with(input, Limits::default())
}
pub fn parse_bytes(input: &[u8]) -> Result<Document> {
parse_bytes_with(input, Limits::default())
}
pub fn parse_bytes_with(input: &[u8], limits: Limits) -> Result<Document> {
limits.check_input(input.len())?;
let text =
crate::encoding::decode_bounded(input, limits.max_document_bytes)?;
parse_decoded(&text, limits, &crate::external::NoExternal)
}
pub fn parse_with(input: &str, limits: Limits) -> Result<Document> {
parse_with_external(input, limits, &crate::external::NoExternal)
}
pub fn parse_with_external(
input: &str,
limits: Limits,
external: &dyn crate::external::ExternalSource,
) -> Result<Document> {
limits.check_input(input.len())?;
parse_decoded(input, limits, external)
}
fn parse_decoded(
input: &str,
limits: Limits,
external: &dyn crate::external::ExternalSource,
) -> Result<Document> {
limits.check_document(input.len(), 0)?;
#[cfg(feature = "tracing")]
let _span =
tracing::trace_span!("oxml.parse", input_len = input.len()).entered();
let version = declared_version(input)?;
let text = normalize_line_endings(input, version).into_owned();
let mut doc = parse_normalized(&text, limits, version, external)?;
let whole = text.len();
doc.input = text;
let root = doc.root();
doc.set_span(root, 0, whole);
Ok(doc)
}
pub(crate) fn prepare(
input: &str,
limits: Limits,
) -> Result<(String, Version)> {
limits.check_input(input.len())?;
limits.check_document(input.len(), 0)?;
let version = declared_version(input)?;
let text = normalize_line_endings(input, version).into_owned();
check_prolog_shape(&text)?;
check_characters(&text, version, 0)?;
Ok((text, version))
}
#[derive(Debug, Default)]
pub(crate) struct Run {
start: usize,
len: usize,
owned: Option<String>,
}
impl Run {
pub(crate) fn push_slice(&mut self, at: usize, text: &str, input: &str) {
if let Some(owned) = &mut self.owned {
owned.push_str(text);
} else if self.len == 0 {
self.start = at;
self.len = text.len();
} else if self.start + self.len == at {
self.len += text.len();
} else {
self.push_owned(text, input);
}
}
pub(crate) fn push_owned(&mut self, text: &str, input: &str) {
if let Some(owned) = &mut self.owned {
owned.push_str(text);
return;
}
let mut buffer = String::with_capacity(self.len + text.len());
buffer.push_str(
input
.get(self.start..self.start + self.len)
.unwrap_or_default(),
);
buffer.push_str(text);
self.owned = Some(buffer);
}
pub(crate) fn as_str<'a>(&'a self, input: &'a str) -> &'a str {
self.owned.as_deref().unwrap_or_else(|| {
input
.get(self.start..self.start + self.len)
.unwrap_or_default()
})
}
pub(crate) fn len(&self) -> usize {
self.owned.as_ref().map_or(self.len, String::len)
}
pub(crate) fn is_empty(&self) -> bool {
self.len() == 0
}
pub(crate) fn as_span(&self) -> Option<(usize, usize)> {
if self.owned.is_some() {
None
} else {
Some((self.start, self.len))
}
}
}
pub(crate) fn check_characters(
text: &str,
version: Version,
base: usize,
) -> Result<()> {
if let Some((offset, c)) = text
.char_indices()
.find(|(_, c)| !is_literal_char_for(*c, version))
{
return Err(Error::new(ErrorKind::IllegalCharacter(c), base + offset));
}
Ok(())
}
pub(crate) fn check_prolog_shape(input: &str) -> Result<()> {
let trimmed = input.trim_start_matches([' ', '\t', '\r', '\n']);
if trimmed.len() != input.len() && trimmed.starts_with("<?xml") {
let after = &trimmed["<?xml".len()..];
if after.starts_with([' ', '\t', '\r', '\n']) {
return Err(Error::new(ErrorKind::MalformedDeclaration, 0));
}
}
Ok(())
}
pub(crate) struct StartTag<'a> {
pub(crate) qname: &'a str,
pub(crate) raw_attrs: Vec<(&'a str, Run)>,
pub(crate) declared: Vec<(String, String)>,
pub(crate) self_closing: bool,
pub(crate) tag_start: usize,
}
fn next_reference(text: &str) -> Option<usize> {
let mut at = 0;
while at < text.len() {
let rest = &text[at..];
let amp = rest.find('&');
let cdata = rest.find("<![CDATA[");
match (amp, cdata) {
(Some(a), Some(c)) if c < a => {
let after = at + c + "<![CDATA[".len();
let end = text[after..].find("]]>")?;
at = after + end + "]]>".len();
}
(Some(a), _) => return Some(at + a),
(None, _) => return None,
}
}
None
}
fn collapse(text: &str) -> String {
let mut out = String::with_capacity(text.len());
for part in text.split(' ').filter(|p| !p.is_empty()) {
if !out.is_empty() {
out.push(' ');
}
out.push_str(part);
}
out
}
fn push_attribute_normalized(out: &mut String, text: &str) {
if !text.contains(['\n', '\t', '\r']) {
out.push_str(text);
return;
}
for c in text.chars() {
out.push(match c {
'\n' | '\t' | '\r' => ' ',
other => other,
});
}
}
pub(crate) fn normalize_line_endings(
input: &str,
version: Version,
) -> Cow<'_, str> {
let terminator = |c: char| {
c == '\r'
|| (version == Version::V11 && (c == '\u{85}' || c == '\u{2028}'))
};
if !input.chars().any(terminator) {
return Cow::Borrowed(input);
}
let mut out = String::with_capacity(input.len());
let mut chars = input.chars().peekable();
while let Some(c) = chars.next() {
match c {
'\r' => {
let paired = matches!(chars.peek(), Some('\n'))
|| (version == Version::V11
&& matches!(chars.peek(), Some('\u{85}')));
if paired {
let _ = chars.next();
}
out.push('\n');
}
'\u{85}' | '\u{2028}' if version == Version::V11 => {
out.push('\n');
}
other => out.push(other),
}
}
Cow::Owned(out)
}
fn parse_normalized(
input: &str,
limits: Limits,
version: Version,
external: &dyn crate::external::ExternalSource,
) -> Result<Document> {
limits.check_nodes(0, 1, 0)?;
check_prolog_shape(input)?;
if let Some((offset, c)) = input
.char_indices()
.find(|(_, c)| !is_literal_char_for(*c, version))
{
return Err(Error::new(ErrorKind::IllegalCharacter(c), offset));
}
let mut p = Parser {
input,
bytes: input.as_bytes(),
pos: 0,
doc: Box::new(Document::with_capacity(arena_capacity(input, &limits))),
name_index: alloc::collections::BTreeMap::new(),
external,
ns: Namespaces::default(),
depth: 0,
limits,
dtd: None,
version,
entity_budget: limits.max_entity_expansion,
entity_depth: 0,
standalone: declared_standalone(input),
entity_cache: BTreeSet::new(),
};
p.parse_document()?;
Ok(*p.doc)
}
type ResolvedAttributes = (Vec<(NameId, Chars)>, Vec<String>);
enum ChildStep {
Element,
Continue,
Done,
}
impl<'a> Parser<'a> {
fn parse_document(&mut self) -> Result<()> {
let root = self.doc.root();
let mark = self.doc.scratch_mark();
self.skip_prolog()?;
let mut seen_root = false;
loop {
self.skip_whitespace();
if self.pos >= self.bytes.len() {
break;
}
if self.peek_is(b'<') {
if self.starts_with("<!--") {
self.push_comment(root)?;
} else if self.starts_with("<?") {
self.push_pi(root)?;
} else if self.starts_with("<!DOCTYPE") {
self.late_doctype(seen_root)?;
} else if self.starts_with("</") {
let name = self.peek_end_tag_name();
return Err(Error::new(
ErrorKind::UnexpectedEndTag(name),
self.pos,
));
} else {
if seen_root {
return Err(Error::new(
ErrorKind::TrailingContent,
self.pos,
));
}
self.parse_element(root)?;
seen_root = true;
}
} else {
return Err(Error::new(ErrorKind::TrailingContent, self.pos));
}
}
if seen_root {
self.doc.finish_children(root, mark);
Ok(())
} else {
Err(Error::new(ErrorKind::NoRootElement, self.pos))
}
}
pub(crate) fn skip_prolog(&mut self) -> Result<()> {
self.skip_whitespace();
if self.starts_with("<?xml")
&& !matches!(
self.bytes.get(self.pos + 5),
Some(c) if is_name_char(char::from(*c))
)
{
let end = self.input[self.pos..].find("?>").ok_or_else(|| {
Error::new(ErrorKind::Unterminated("XML declaration"), self.pos)
})?;
let decl = &self.input[self.pos + 5..self.pos + end];
validate_xml_declaration(decl, self.pos, true)?;
self.pos += end + 2;
}
loop {
self.skip_whitespace();
if self.starts_with("<!DOCTYPE") {
self.skip_doctype()?;
} else {
break;
}
}
Ok(())
}
pub(crate) fn skip_doctype(&mut self) -> Result<()> {
let mut p = crate::dtd::DtdParser::with_limits(
self.input,
self.pos,
&self.limits,
)
.with_external(self.external, self.version);
match p.parse_doctype() {
Ok(mut dtd) => {
self.pos = p.pos;
if let Some((system, public)) = dtd.external_subset.clone()
&& let Some(content) =
self.external.fetch(&system, public.as_deref())
{
self.read_external_subset(&mut dtd, content)?;
}
self.dtd = Some(dtd);
Ok(())
}
Err((offset, reason)) => {
Err(Error::new(ErrorKind::MalformedDtd(reason), offset))
}
}
}
fn read_external_subset(
&self,
dtd: &mut crate::dtd::Dtd,
content: &str,
) -> Result<()> {
self.limits.check_document(content.len(), self.pos)?;
if self
.limits
.max_dtd_bytes
.is_some_and(|max| content.len() > max)
{
return Err(Error::new(ErrorKind::DtdTooLarge, self.pos));
}
let version = entity_version(content, self.version);
let normalized = normalize_line_endings(content, version);
check_text_decl_position(&normalized, self.pos)?;
check_text_decl(&normalized, self.version, self.pos)?;
let body = strip_text_decl(&normalized);
if let Some((_, c)) = body.char_indices().find(|(_, c)| {
!is_literal_char_for(*c, version)
|| !is_literal_char_for(*c, self.version)
}) {
return Err(Error::new(ErrorKind::IllegalCharacter(c), self.pos));
}
self.read_subset_declarations(dtd, body, version)
}
fn read_subset_declarations(
&self,
dtd: &mut crate::dtd::Dtd,
body: &str,
version: Version,
) -> Result<()> {
let mut sub = crate::dtd::DtdParser::with_limits(body, 0, &self.limits)
.with_external(self.external, version);
let before: alloc::collections::BTreeSet<String> =
dtd.general.keys().cloned().collect();
if let Err((offset, reason)) = sub.parse_external_subset(dtd) {
let _ = offset;
return Err(Error::new(ErrorKind::MalformedDtd(reason), self.pos));
}
dtd.declared_externally = dtd
.general
.keys()
.filter(|k| !before.contains(*k))
.cloned()
.collect();
dtd.incomplete = false;
Ok(())
}
fn parse_element(&mut self, parent: NodeId) -> Result<()> {
if self.depth >= self.limits.max_depth {
return Err(Error::new(ErrorKind::DepthLimitExceeded, self.pos));
}
self.depth += 1;
let result = self.parse_element_inner(parent);
self.depth -= 1;
result
}
pub(crate) fn bind_namespaces(
&mut self,
element: &str,
raw_attrs: &[(&'a str, Run)],
) -> Result<Vec<(String, String)>> {
let mut declared: Vec<(String, String)> = Vec::new();
let input = self.input;
for (name, run) in raw_attrs {
let Some(prefix) = declared_prefix(name) else {
continue;
};
if declared.iter().any(|(p, _)| p == prefix) {
return Err(Error::new(
ErrorKind::DuplicateAttribute((*name).to_owned()),
self.pos,
));
}
let tokenized = self
.dtd
.as_ref()
.is_some_and(|d| d.is_tokenized_attribute(element, name));
let collapsed;
let value = if tokenized {
collapsed = collapse(run.as_str(input));
collapsed.as_str()
} else {
run.as_str(input)
};
check_declaration(name, prefix, value, self.version)
.map_err(|kind| Error::new(kind, self.pos))?;
self.ns.bind(prefix.to_owned(), value.to_owned());
declared.push((prefix.to_owned(), value.to_owned()));
}
Ok(declared)
}
pub(crate) fn scan_start_tag(&mut self) -> Result<StartTag<'a>> {
let tag_start = self.pos;
self.pos += 1; let qname = self.parse_name()?;
self.ns.push_scope();
let raw_attrs = self.parse_attributes()?;
let declared = self.bind_namespaces(qname, &raw_attrs)?;
let self_closing = if self.starts_with("/>") {
self.pos += 2;
true
} else if self.peek_is(b'>') {
self.pos += 1;
false
} else {
self.ns.pop_scope();
return Err(Error::new(
ErrorKind::Unterminated("start tag"),
tag_start,
));
};
Ok(StartTag {
qname,
raw_attrs,
declared,
self_closing,
tag_start,
})
}
fn parse_element_inner(&mut self, parent: NodeId) -> Result<()> {
let StartTag {
qname,
raw_attrs,
declared,
self_closing,
tag_start,
} = self.scan_start_tag()?;
let (resolved, id_values) =
self.resolve_attributes(qname, raw_attrs, tag_start)?;
let name_id = self.intern_qname(qname, true, tag_start)?;
let additional = 1
+ resolved.len()
+ declared.len()
+ usize::from(parent == self.doc.root());
self.check_nodes(additional)?;
let node = self.doc.push(
NodeData::Element {
name: name_id,
attributes: (0, 0),
namespaces: (0, 0),
},
parent,
);
self.doc.set_span(node, tag_start, self.pos);
self.attach_attributes(node, resolved, id_values, declared);
if self_closing {
self.ns.pop_scope();
return Ok(());
}
self.parse_children(node, qname)?;
self.ns.pop_scope();
Ok(())
}
fn resolve_attributes(
&mut self,
qname: &str,
raw_attrs: Vec<(&'a str, Run)>,
tag_start: usize,
) -> Result<ResolvedAttributes> {
let mut resolved: Vec<(NameId, Chars)> =
Vec::with_capacity(raw_attrs.len());
let mut seen = SeenAttributes::default();
let input = self.input;
let mut id_values: Vec<String> = Vec::new();
for (raw, run) in raw_attrs {
if raw == "xmlns" || raw.starts_with("xmlns:") {
continue;
}
let an = self.intern_qname(raw, false, tag_start)?;
if !seen.insert(an, &self.doc.names) {
self.ns.pop_scope();
return Err(Error::new(
ErrorKind::DuplicateAttribute(raw.to_owned()),
tag_start,
));
}
if self
.dtd
.as_ref()
.is_some_and(|d| d.is_id_attribute(qname, raw))
{
id_values.push(run.as_str(input).to_owned());
}
let value = self.store_attribute_value(qname, raw, &run);
resolved.push((an, value));
}
Ok((resolved, id_values))
}
fn store_attribute_value(
&mut self,
qname: &str,
raw: &str,
run: &Run,
) -> Chars {
let input = self.input;
let tokenized = self
.dtd
.as_ref()
.is_some_and(|d| d.is_tokenized_attribute(qname, raw));
let collapsed = tokenized.then(|| collapse(run.as_str(input)));
match (&collapsed, run.as_span()) {
(Some(text), Some((start, len))) if text == run.as_str(input) => {
self.verbatim(start, len)
}
(Some(text), _) => self.doc.push_expanded(text),
(None, Some((start, len))) => self.verbatim(start, len),
(None, None) => self.doc.push_expanded(run.as_str(input)),
}
}
fn attach_attributes(
&mut self,
node: NodeId,
resolved: Vec<(NameId, Chars)>,
id_values: Vec<String>,
declared: Vec<(String, String)>,
) {
let start = self.doc.attr_ids.len();
for (name, value) in resolved {
let id =
self.doc.push_detached(NodeData::Attr { name, value }, node);
self.doc.attr_ids.push(id);
}
for value in id_values {
let _ = self.doc.ids.entry(value).or_insert(node);
}
let len = self.doc.attr_ids.len() - start;
let (ns_start, ns_len) = self.push_namespace_nodes(node, declared);
if let Some(NodeData::Element {
attributes,
namespaces,
..
}) = self.doc.data_mut(node)
{
*attributes = (
u32::try_from(start).unwrap_or(u32::MAX),
u32::try_from(len).unwrap_or(u32::MAX),
);
*namespaces = (
u32::try_from(ns_start).unwrap_or(u32::MAX),
u32::try_from(ns_len).unwrap_or(u32::MAX),
);
}
}
fn push_namespace_nodes(
&mut self,
node: NodeId,
declared: Vec<(String, String)>,
) -> (usize, usize) {
let start = self.doc.ns_ids.len();
if self.doc.nodes[node.index()].parent == Some(self.doc.root()) {
let prefix = self.doc.push_expanded("xml");
let uri = self
.doc
.push_expanded("http://www.w3.org/XML/1998/namespace");
let id = self
.doc
.push_detached(NodeData::Namespace { prefix, uri }, node);
self.doc.ns_ids.push(id);
}
for (prefix, uri) in declared {
let prefix = self.doc.push_expanded(&prefix);
let uri = self.doc.push_expanded(&uri);
let id = self
.doc
.push_detached(NodeData::Namespace { prefix, uri }, node);
self.doc.ns_ids.push(id);
}
(start, self.doc.ns_ids.len() - start)
}
fn late_doctype(&mut self, seen_root: bool) -> Result<()> {
if seen_root || self.dtd.is_some() {
return Err(Error::new(ErrorKind::TrailingContent, self.pos));
}
self.skip_doctype()
}
fn parse_children(&mut self, node: NodeId, open_qname: &str) -> Result<()> {
let mark = self.doc.scratch_mark();
let mut text = Run::default();
loop {
match self.child_step(node, open_qname, mark, &mut text)? {
ChildStep::Element => self.parse_element(node)?,
ChildStep::Continue => {}
ChildStep::Done => return Ok(()),
}
}
}
fn child_step(
&mut self,
node: NodeId,
open_qname: &str,
mark: usize,
text: &mut Run,
) -> Result<ChildStep> {
if self.read_text_piece(text)? {
return Ok(ChildStep::Continue);
}
self.flush_text(text, node)?;
if self.starts_with("</") {
self.parse_end_tag(node, open_qname, mark)?;
return Ok(ChildStep::Done);
}
Ok(if self.push_misc(node)? {
ChildStep::Continue
} else {
ChildStep::Element
})
}
fn read_text_piece(&mut self, text: &mut Run) -> Result<bool> {
if self.pos >= self.bytes.len() {
return Err(Error::new(ErrorKind::UnexpectedEof, self.pos));
}
if !self.peek_is(b'<') {
self.parse_text_run(text)?;
return Ok(true);
}
if self.starts_with("<![CDATA[") {
self.parse_cdata(text)?;
return Ok(true);
}
Ok(false)
}
fn push_misc(&mut self, node: NodeId) -> Result<bool> {
if self.starts_with("<!--") {
self.push_comment(node)?;
return Ok(true);
}
if self.starts_with("<?") {
self.push_pi(node)?;
return Ok(true);
}
Ok(false)
}
fn parse_end_tag(
&mut self,
node: NodeId,
open_qname: &str,
mark: usize,
) -> Result<()> {
let start = self.pos;
self.pos += 2;
let close = self.parse_name()?;
self.skip_whitespace();
if !self.peek_is(b'>') {
return Err(Error::new(ErrorKind::Unterminated("end tag"), start));
}
self.pos += 1;
if close != open_qname {
return Err(Error::new(
ErrorKind::MismatchedEndTag {
expected: open_qname.to_owned(),
found: close.to_owned(),
},
start,
));
}
self.doc.finish_children(node, mark);
self.doc.set_span_end(node, self.pos);
Ok(())
}
fn push_comment(&mut self, parent: NodeId) -> Result<()> {
self.check_nodes(1)?;
let at = self.pos;
let c = self.parse_comment()?;
let id = self.doc.push(NodeData::Comment(c), parent);
self.doc.set_span(id, at, self.pos);
Ok(())
}
fn push_pi(&mut self, parent: NodeId) -> Result<()> {
self.check_nodes(1)?;
let at = self.pos;
let (target, data) = self.parse_pi()?;
let id = self
.doc
.push(NodeData::ProcessingInstruction { target, data }, parent);
self.doc.set_span(id, at, self.pos);
Ok(())
}
fn flush_text(&mut self, text: &mut Run, node: NodeId) -> Result<()> {
if !text.is_empty() {
self.check_nodes(1)?;
let run = core::mem::take(text);
if self.limits.max_text_length.is_some_and(|m| run.len() > m) {
return Err(Error::new(ErrorKind::TextTooLong, self.pos));
}
let chars = if let Some((start, len)) = run.as_span() {
self.verbatim(start, len)
} else {
let text = run.as_str(self.input);
self.doc.push_expanded(text)
};
let id = self.doc.push(NodeData::Text(chars), node);
self.doc.set_span(id, run.start, self.pos);
}
Ok(())
}
fn check_nodes(&self, additional: usize) -> Result<()> {
self.limits
.check_nodes(self.doc.len(), additional, self.pos)
}
#[inline]
const fn has_byte(v: u64, b: u8) -> bool {
let splat = u64::from_ne_bytes([b; 8]);
let x = v ^ splat;
(x.wrapping_sub(0x0101_0101_0101_0101) & !x & 0x8080_8080_8080_8080)
!= 0
}
#[inline]
const fn has_text_delimiter(v: u64) -> bool {
Self::has_byte(v, b'<') || Self::has_byte(v, b'&')
}
#[inline]
const fn has_attr_delimiter(v: u64, quote: u8) -> bool {
Self::has_byte(v, quote)
|| Self::has_byte(v, b'&')
|| Self::has_byte(v, b'<')
}
pub(crate) fn parse_text_run(&mut self, out: &mut Run) -> Result<()> {
while self.pos < self.bytes.len() {
if self.starts_with("]]>") {
return Err(Error::new(ErrorKind::IllegalCdataEnd, self.pos));
}
match self.bytes[self.pos] {
b'<' => break,
b'&' => self.text_reference(out)?,
_ => self.text_chars(out)?,
}
}
Ok(())
}
fn reference_len(&self, stop: u8) -> Option<usize> {
let rest = &self.bytes[self.pos..];
rest.iter()
.position(|&b| matches!(b, b';' | b'<') || b == stop)
.filter(|&i| rest[i] == b';')
}
fn text_reference(&mut self, out: &mut Run) -> Result<()> {
let s = self.pos;
self.pos += 1;
let end = self
.reference_len(b'<')
.ok_or_else(|| Error::new(ErrorKind::Unterminated("entity"), s))?;
let ent = &self.input[self.pos..self.pos + end];
let expanded = self.expand_entity(ent, s, false)?;
out.push_owned(&expanded, self.input);
self.pos += end + 1;
Ok(())
}
fn text_chars(&mut self, out: &mut Run) -> Result<()> {
let start = self.pos;
while self.pos + 8 <= self.bytes.len() {
let Ok(chunk) = self.bytes[self.pos..self.pos + 8].try_into()
else {
break;
};
let v = u64::from_ne_bytes(chunk);
if Self::has_text_delimiter(v) {
break;
}
self.pos += 8;
}
while self.pos < self.bytes.len()
&& self.bytes[self.pos] != b'<'
&& self.bytes[self.pos] != b'&'
{
self.pos += 1;
}
let run = &self.input[start..self.pos];
if let Some(at) = run.find("]]>") {
return Err(Error::new(ErrorKind::IllegalCdataEnd, start + at));
}
out.push_slice(start, run, self.input);
Ok(())
}
pub(crate) fn parse_cdata(&mut self, out: &mut Run) -> Result<()> {
self.pos += "<![CDATA[".len();
let end = self.input[self.pos..].find("]]>").ok_or_else(|| {
Error::new(ErrorKind::Unterminated("CDATA"), self.pos)
})?;
let body = &self.input[self.pos..self.pos + end];
out.push_slice(self.pos, body, self.input);
self.pos += end + 3;
Ok(())
}
fn replacement_text(&self, text: &str, offset: usize) -> Result<String> {
let mut out = String::with_capacity(text.len());
let mut rest = text;
while let Some(amp) = rest.find('&') {
out.push_str(&rest[..amp]);
let tail = &rest[amp..];
let Some(semi) = tail.find(';') else {
out.push_str(tail);
return Ok(out);
};
let name = &tail[1..semi];
if let Some(rest_name) = name.strip_prefix('#') {
let _ = rest_name;
match decode_predefined(name) {
Some(direct) => {
if let Some(bad) = direct
.chars()
.find(|c| !is_xml_char_for(*c, self.version))
{
return Err(Error::new(
ErrorKind::IllegalCharacter(bad),
offset,
));
}
out.push_str(&direct);
}
None => out.push_str(&tail[..=semi]),
}
} else {
out.push_str(&tail[..=semi]);
}
rest = &tail[semi + 1..];
}
out.push_str(rest);
Ok(out)
}
fn check_entity_in_attribute(text: &str, offset: usize) -> Result<()> {
let bytes = text.as_bytes();
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'<' => {
return Err(Error::new(
ErrorKind::IllegalCharacter('<'),
offset,
));
}
b'&' => {
let rest = &text[i + 1..];
let Some(semi) = rest.find(';') else {
return Err(Error::new(
ErrorKind::Unterminated("entity"),
offset,
));
};
if semi == 0 {
return Err(Error::new(ErrorKind::InvalidName, offset));
}
i += semi + 2;
}
_ => i += 1,
}
}
Ok(())
}
fn check_entity_as_content(
&mut self,
text: &str,
offset: usize,
) -> Result<()> {
if self.entity_depth >= self.limits.max_entity_depth {
return Err(Error::new(ErrorKind::EntityLimitExceeded, offset));
}
let (bindings_before, marks_before) = self.ns.depths();
let mut sub = Parser {
input: text,
bytes: text.as_bytes(),
pos: 0,
doc: Box::new(Document::with_capacity(0)),
ns: core::mem::take(&mut self.ns),
depth: self.depth,
limits: self.limits,
dtd: self.dtd.take(),
version: self.version,
name_index: alloc::collections::BTreeMap::new(),
external: self.external,
entity_budget: self.entity_budget,
entity_depth: self.entity_depth + 1,
standalone: self.standalone,
entity_cache: core::mem::take(&mut self.entity_cache),
};
let result = sub.parse_entity_body();
self.ns = sub.ns;
self.ns.truncate_to(bindings_before, marks_before);
self.dtd = sub.dtd;
self.entity_cache = sub.entity_cache;
self.entity_budget = sub.entity_budget;
result.map_err(|e| Error::new(e.kind, offset))
}
fn parse_entity_body(&mut self) -> Result<()> {
let root = self.doc.root();
let mark = self.doc.scratch_mark();
while self.pos < self.bytes.len() {
if !self.peek_is(b'<') {
let mut run = Run::default();
self.parse_text_run(&mut run)?;
continue;
}
if self.starts_with("</") {
let name = self.peek_end_tag_name();
return Err(Error::new(
ErrorKind::UnexpectedEndTag(name),
self.pos,
));
}
if self.starts_with("<!--") {
let _ = self.parse_comment()?;
} else if self.starts_with("<![CDATA[") {
let mut run = Run::default();
self.parse_cdata(&mut run)?;
} else if self.starts_with("<?") {
let _ = self.parse_pi()?;
} else {
self.parse_element(root)?;
}
}
self.doc.finish_children(root, mark);
Ok(())
}
pub(crate) fn owned(&self, c: Chars) -> &str {
match c {
Chars::Span(start, len) => {
let (start, len) = (start as usize, len as usize);
self.input.get(start..start + len).unwrap_or_default()
}
Chars::Expanded(i) => self
.doc
.expanded
.get(i as usize)
.map_or("", alloc::string::String::as_str),
}
}
pub(crate) fn verbatim(&mut self, start: usize, len: usize) -> Chars {
if let (Ok(s), Ok(l)) = (u32::try_from(start), u32::try_from(len)) {
return Chars::Span(s, l);
}
let text = &self.input[start..start + len];
self.doc.push_expanded(text)
}
pub(crate) fn parse_comment(&mut self) -> Result<Chars> {
let start = self.pos;
self.pos += "<!--".len();
let end = self.input[self.pos..].find("-->").ok_or_else(|| {
Error::new(ErrorKind::Unterminated("comment"), start)
})?;
let body = &self.input[self.pos..self.pos + end];
if body.contains("--") || body.ends_with('-') {
return Err(Error::new(ErrorKind::MalformedComment, start));
}
let at = self.pos;
let len = body.len();
self.pos += end + 3;
Ok(self.verbatim(at, len))
}
pub(crate) fn parse_pi(&mut self) -> Result<(Chars, Chars)> {
let start = self.pos;
self.pos += 2; let target_at = self.pos;
let target = self.parse_name()?;
let target_len = target.len();
if target.eq_ignore_ascii_case("xml") {
return Err(Error::new(ErrorKind::ReservedPiTarget, self.pos));
}
if target.contains(':') {
return Err(Error::new(ErrorKind::InvalidName, self.pos));
}
if !matches!(
self.bytes.get(self.pos),
Some(b' ' | b'\t' | b'\r' | b'\n')
) && !self.starts_with("?>")
{
return Err(Error::new(ErrorKind::InvalidName, self.pos));
}
let end = self.input[self.pos..].find("?>").ok_or_else(|| {
Error::new(ErrorKind::Unterminated("processing instruction"), start)
})?;
let raw = &self.input[self.pos..self.pos + end];
let lead = raw.len() - raw.trim_start().len();
let data_at = self.pos + lead;
let data_len = raw.trim().len();
self.pos += end + 2;
let target = self.verbatim(target_at, target_len);
let data = self.verbatim(data_at, data_len);
Ok((target, data))
}
fn parse_attributes(&mut self) -> Result<Vec<(&'a str, Run)>> {
let mut out = Vec::new();
loop {
self.skip_whitespace();
if self.pos >= self.bytes.len() {
return Err(Error::new(ErrorKind::UnexpectedEof, self.pos));
}
let b = self.bytes[self.pos];
if b == b'>' || b == b'/' {
return Ok(out);
}
let name = self.parse_name()?;
self.skip_whitespace();
if !self.peek_is(b'=') {
return Err(Error::new(
ErrorKind::UnquotedAttributeValue,
self.pos,
));
}
self.pos += 1;
self.skip_whitespace();
let value = self.parse_attribute_value()?;
if !matches!(
self.bytes.get(self.pos),
None | Some(b' ' | b'\t' | b'\r' | b'\n' | b'>' | b'/')
) {
return Err(Error::new(
ErrorKind::UnquotedAttributeValue,
self.pos,
));
}
if value.len() > self.limits.max_attribute_size {
return Err(Error::new(ErrorKind::AttributeTooLarge, self.pos));
}
if out.len() >= self.limits.max_attributes_per_element {
return Err(Error::new(ErrorKind::TooManyAttributes, self.pos));
}
out.push((name, value));
}
}
fn parse_attribute_value(&mut self) -> Result<Run> {
let start = self.pos;
let quote = self.attribute_quote()?;
let mut out = Run::default();
loop {
match self.bytes.get(self.pos).copied() {
None => {
return Err(Error::new(
ErrorKind::Unterminated("attribute value"),
start,
));
}
Some(b) if b == quote => {
self.pos += 1;
return Ok(out);
}
Some(b'<') => {
return Err(Error::new(
ErrorKind::IllegalCharacter('<'),
self.pos,
));
}
Some(b'&') => self.value_reference(quote, &mut out)?,
Some(_) => self.value_chars(quote, &mut out),
}
}
}
fn attribute_quote(&mut self) -> Result<u8> {
match self.bytes.get(self.pos).copied() {
None => Err(Error::new(ErrorKind::UnexpectedEof, self.pos)),
Some(quote @ (b'"' | b'\'')) => {
self.pos += 1;
Ok(quote)
}
Some(_) => {
Err(Error::new(ErrorKind::UnquotedAttributeValue, self.pos))
}
}
}
fn value_reference(&mut self, quote: u8, out: &mut Run) -> Result<()> {
let s = self.pos;
self.pos += 1;
let end = self
.reference_len(quote)
.ok_or_else(|| Error::new(ErrorKind::Unterminated("entity"), s))?;
let ent = &self.input[self.pos..self.pos + end];
let expanded = self.expand_entity(ent, s, true)?;
out.push_owned(&expanded, self.input);
self.pos += end + 1;
Ok(())
}
fn value_chars(&mut self, quote: u8, out: &mut Run) {
let s = self.pos;
while self.pos + 8 <= self.bytes.len() {
let Ok(chunk) = self.bytes[self.pos..self.pos + 8].try_into()
else {
break;
};
let v = u64::from_ne_bytes(chunk);
if Self::has_attr_delimiter(v, quote) {
break;
}
self.pos += 8;
}
while self.pos < self.bytes.len()
&& self.bytes[self.pos] != quote
&& self.bytes[self.pos] != b'&'
&& self.bytes[self.pos] != b'<'
{
self.pos += 1;
}
let raw = &self.input[s..self.pos];
if raw.bytes().any(|b| matches!(b, b'\t' | b'\n' | b'\r')) {
let mut normalized = String::new();
push_attribute_normalized(&mut normalized, raw);
out.push_owned(&normalized, self.input);
} else {
out.push_slice(s, raw, self.input);
}
}
pub(crate) fn parse_name(&mut self) -> Result<&'a str> {
let name = self.parse_name_unchecked()?;
if name.len() > self.limits.max_name_length {
return Err(Error::new(ErrorKind::NameTooLong, self.pos));
}
Ok(name)
}
fn parse_name_unchecked(&mut self) -> Result<&'a str> {
let input: &'a str = self.input;
let start = self.pos;
let rest = &input[self.pos..];
let mut chars = rest.char_indices();
match chars.next() {
Some((_, c)) if self.is_name_start(c) => {}
_ => {
return Err(Error::new(ErrorKind::InvalidName, start));
}
}
let mut end = rest.len();
for (i, c) in rest.char_indices() {
if !self.is_name_char(c) {
end = i;
break;
}
}
self.pos = start + end;
Ok(&rest[..end])
}
fn resolve_parts<'q>(
&'q self,
qname: &'q str,
is_element: bool,
offset: usize,
) -> Result<(&'q str, Option<&'q str>)> {
match qname.split_once(':') {
Some((prefix, local)) => {
if prefix == "xml" {
return Ok((
local,
Some("http://www.w3.org/XML/1998/namespace"),
));
}
let uri = self.ns.resolve(prefix).ok_or_else(|| {
Error::new(
ErrorKind::UnboundPrefix(prefix.to_owned()),
offset,
)
})?;
Ok((local, Some(uri)))
}
None if is_element => match self.ns.resolve("") {
Some(uri) if !uri.is_empty() => Ok((qname, Some(uri))),
_ => Ok((qname, None)),
},
None => Ok((qname, None)),
}
}
pub(crate) fn intern_qname(
&mut self,
qname: &str,
is_element: bool,
offset: usize,
) -> Result<crate::tree::NameId> {
let prefix = qname.split_once(':').map(|(p, _)| p);
{
let (local, namespace) =
self.resolve_parts(qname, is_element, offset)?;
if let Some(candidates) = self.name_index.get(local) {
for &id in candidates {
if self.doc.names[id as usize].namespace.as_deref()
== namespace
&& self.doc.name_prefixes[id as usize].as_deref()
== prefix
{
return Ok(crate::tree::NameId(id));
}
}
}
}
let name = self.expand(qname, is_element, offset)?;
Ok(self.intern(&name, prefix))
}
fn expand(
&self,
qname: &str,
is_element: bool,
offset: usize,
) -> Result<ExpandedName> {
match qname.split_once(':') {
Some((prefix, local)) => {
if prefix == "xml" {
return Ok(ExpandedName::qualified(
"http://www.w3.org/XML/1998/namespace",
local,
));
}
let uri = self.ns.resolve(prefix).ok_or_else(|| {
Error::new(
ErrorKind::UnboundPrefix(prefix.to_owned()),
offset,
)
})?;
Ok(ExpandedName::qualified(uri, local))
}
None => {
if is_element {
match self.ns.resolve("") {
Some(uri) if !uri.is_empty() => {
Ok(ExpandedName::qualified(uri, qname))
}
_ => Ok(ExpandedName::local(qname)),
}
} else {
Ok(ExpandedName::local(qname))
}
}
}
}
pub(crate) fn peek_end_tag_name(&self) -> String {
self.input[self.pos + 2..]
.split(['>', ' ', '\t', '\n', '\r'])
.next()
.unwrap_or_default()
.to_owned()
}
pub(crate) fn starts_with(&self, s: &str) -> bool {
self.input[self.pos..].starts_with(s)
}
pub(crate) fn peek_is(&self, b: u8) -> bool {
self.bytes.get(self.pos) == Some(&b)
}
pub(crate) fn skip_whitespace(&mut self) {
while self.pos < self.bytes.len()
&& matches!(self.bytes[self.pos], b' ' | b'\t' | b'\r' | b'\n')
{
self.pos += 1;
}
}
fn is_name_start(&self, c: char) -> bool {
match self.limits.edition {
crate::Edition::Fourth => crate::names4e::is_name_start_4e(c),
_ => is_name_start(c),
}
}
fn is_name_char(&self, c: char) -> bool {
match self.limits.edition {
crate::Edition::Fourth => crate::names4e::is_name_char_4e(c),
_ => is_name_char(c),
}
}
fn expand_entity(
&mut self,
ent: &str,
offset: usize,
in_attribute: bool,
) -> Result<String> {
if let Some(direct) = self.predefined_reference(ent, offset)? {
return Ok(direct);
}
let dtd = self.entity_declarations(ent, offset)?;
match dtd.entity(ent) {
Some(crate::dtd::EntityValue::Internal(text)) => {
let text = text.clone();
self.expand_internal_entity(ent, &text, offset, in_attribute)
}
Some(crate::dtd::EntityValue::External { system, public }) => {
let (system, public) = (system.clone(), public.clone());
self.expand_external_entity(
ent,
&system,
public.as_deref(),
offset,
in_attribute,
)
}
Some(crate::dtd::EntityValue::Unparsed) => Err(Error::new(
ErrorKind::ForbiddenEntityReference(ent.to_owned()),
offset,
)),
None => self.unresolved_entity(ent, offset, dtd.incomplete),
}
}
fn entity_declarations(
&self,
ent: &str,
offset: usize,
) -> Result<&crate::dtd::Dtd> {
let dtd = self.dtd.as_ref().ok_or_else(|| {
Error::new(ErrorKind::UnknownEntity(ent.to_owned()), offset)
})?;
if self.standalone && dtd.declared_externally.contains(ent) {
return Err(Error::new(
ErrorKind::ForbiddenEntityReference(ent.to_owned()),
offset,
));
}
Ok(dtd)
}
fn unresolved_entity(
&self,
ent: &str,
offset: usize,
incomplete: bool,
) -> Result<String> {
if incomplete && !self.standalone {
return Ok(String::new());
}
Err(Error::new(ErrorKind::UnknownEntity(ent.to_owned()), offset))
}
fn predefined_reference(
&self,
ent: &str,
offset: usize,
) -> Result<Option<String>> {
let Some(direct) = decode_predefined(ent) else {
return Ok(None);
};
if let Some(bad) =
direct.chars().find(|c| !is_xml_char_for(*c, self.version))
{
return Err(Error::new(ErrorKind::IllegalCharacter(bad), offset));
}
Ok(Some(direct))
}
fn expand_internal_entity(
&mut self,
ent: &str,
text: &str,
offset: usize,
in_attribute: bool,
) -> Result<String> {
let replacement = self.replacement_text(text, offset)?;
if in_attribute {
Self::check_entity_in_attribute(&replacement, offset)?;
} else {
let key = (ent.to_owned(), self.ns.scope_id());
if !self.entity_cache.contains(&key) {
self.check_entity_as_content(&replacement, offset)?;
let _ = self.entity_cache.insert(key);
}
}
let mut budget = self.entity_budget;
let out = self.expand_text(text, offset, 1, &mut budget, in_attribute);
self.entity_budget = budget;
out
}
fn expand_external_entity(
&mut self,
ent: &str,
system: &str,
public: Option<&str>,
offset: usize,
in_attribute: bool,
) -> Result<String> {
if in_attribute {
return Err(Error::new(
ErrorKind::ForbiddenEntityReference(ent.to_owned()),
offset,
));
}
match self.external.fetch(system, public) {
Some(content) => self.expand_external_content(content, offset),
None => Ok(String::new()),
}
}
fn expand_external_content(
&mut self,
content: &str,
offset: usize,
) -> Result<String> {
self.limits.check_document(content.len(), offset)?;
let entity_version = entity_version(content, self.version);
let normalized = normalize_line_endings(content, entity_version);
let content: &str = &normalized;
check_text_decl_position(content, offset)?;
check_text_decl(content, self.version, offset)?;
let body = strip_text_decl(content);
if let Some((at, c)) = body
.char_indices()
.find(|(_, c)| !is_literal_char_for(*c, self.version))
{
let _ = at;
return Err(Error::new(ErrorKind::IllegalCharacter(c), offset));
}
self.check_entity_as_content(body, offset)?;
let mut budget = self.entity_budget;
let out = self.expand_text(body, offset, 1, &mut budget, false);
self.entity_budget = budget;
out
}
fn intern(
&mut self,
name: &ExpandedName,
prefix: Option<&str>,
) -> crate::tree::NameId {
if let Some(candidates) = self.name_index.get(name.local.as_str()) {
for &id in candidates {
if self.doc.names[id as usize].namespace == name.namespace
&& self.doc.name_prefixes[id as usize].as_deref() == prefix
{
return crate::tree::NameId(id);
}
}
}
let id = u32::try_from(self.doc.names.len()).unwrap_or(u32::MAX);
self.doc.names.push(name.clone());
self.doc.name_prefixes.push(prefix.map(str::to_owned));
self.name_index
.entry(name.local.clone())
.or_default()
.push(id);
crate::tree::NameId(id)
}
fn push_run(
out: &mut String,
text: &str,
offset: usize,
budget: &mut usize,
in_attribute: bool,
) -> Result<()> {
if in_attribute && text.contains('<') {
return Err(Error::new(ErrorKind::IllegalCharacter('<'), offset));
}
if in_attribute && text.contains(['\n', '\t', '\r']) {
let mut buf = String::with_capacity(text.len());
push_attribute_normalized(&mut buf, text);
return Self::push_bounded(out, &buf, offset, budget);
}
Self::push_bounded(out, text, offset, budget)
}
fn expand_text(
&mut self,
text: &str,
offset: usize,
depth: usize,
budget: &mut usize,
in_attribute: bool,
) -> Result<String> {
if depth > self.limits.max_entity_depth {
return Err(Error::new(ErrorKind::EntityLimitExceeded, offset));
}
let mut out = String::new();
let mut rest = text;
while let Some(amp) = next_reference(rest) {
let (before, tail) = rest.split_at(amp);
Self::push_run(&mut out, before, offset, budget, in_attribute)?;
let Some(semi) = tail.find(';') else {
return Err(Error::new(
ErrorKind::UnknownEntity(tail.to_owned()),
offset,
));
};
let name = &tail[1..semi];
rest = &tail[semi + 1..];
if let Some(direct) = decode_predefined(name) {
Self::push_bounded(&mut out, &direct, offset, budget)?;
continue;
}
let inner = match self.dtd.as_ref().and_then(|d| d.entity(name)) {
Some(crate::dtd::EntityValue::Internal(t)) => t.clone(),
Some(crate::dtd::EntityValue::External { .. })
if in_attribute =>
{
return Err(Error::new(
ErrorKind::ForbiddenEntityReference(name.to_owned()),
offset,
));
}
Some(crate::dtd::EntityValue::Unparsed) => {
return Err(Error::new(
ErrorKind::ForbiddenEntityReference(name.to_owned()),
offset,
));
}
None if self.standalone
|| !self.dtd.as_ref().is_some_and(|d| d.incomplete) =>
{
return Err(Error::new(
ErrorKind::UnknownEntity(name.to_owned()),
offset,
));
}
_ => continue,
};
let expanded = self.expand_text(
&inner,
offset,
depth + 1,
budget,
in_attribute,
)?;
Self::push_bounded(&mut out, &expanded, offset, budget)?;
}
Self::push_run(&mut out, rest, offset, budget, in_attribute)?;
Ok(out)
}
fn push_bounded(
out: &mut String,
text: &str,
offset: usize,
budget: &mut usize,
) -> Result<()> {
if *budget < text.len() {
return Err(Error::new(ErrorKind::EntityLimitExceeded, offset));
}
*budget -= text.len();
out.push_str(text);
Ok(())
}
}
fn decode_predefined(ent: &str) -> Option<String> {
let out = match ent {
"lt" => "<".to_owned(),
"gt" => ">".to_owned(),
"amp" => "&".to_owned(),
"apos" => "'".to_owned(),
"quot" => "\"".to_owned(),
_ => {
let cp = match ent.strip_prefix("#x") {
Some(hex) => u32::from_str_radix(hex, 16).ok()?,
None => ent.strip_prefix('#')?.parse::<u32>().ok()?,
};
let ch = char::from_u32(cp)?;
let mut s = String::new();
s.push(ch);
s
}
};
Some(out)
}
fn is_legal_version(value: &str) -> bool {
value.strip_prefix("1.").is_some_and(|digits| {
!digits.is_empty() && digits.bytes().all(|b| b.is_ascii_digit())
})
}
pub(crate) fn entity_version(content: &str, document: Version) -> Version {
let Some(rest) = content.strip_prefix("<?xml") else {
return document;
};
if !rest.starts_with([' ', '\t', '\r', '\n']) {
return document;
}
match declared_version(content) {
Ok(Version::V10) if !rest.contains("version") => document,
Ok(version) => version,
Err(_) => document,
}
}
pub(crate) fn check_text_decl_position(
content: &str,
offset: usize,
) -> Result<()> {
let mut at = 0;
while let Some(i) = content[at..].find("<?") {
let start = at + i;
let after = &content[start + 2..];
let end = after
.find(|c: char| c.is_whitespace() || c == '?')
.unwrap_or(after.len());
let target = &after[..end];
if target.eq_ignore_ascii_case("xml") && (start != 0 || target != "xml")
{
return Err(Error::new(ErrorKind::ReservedPiTarget, offset));
}
at = start + 2;
}
Ok(())
}
pub(crate) fn strip_text_decl(content: &str) -> &str {
let Some(rest) = content.strip_prefix("<?xml") else {
return content;
};
if !rest.starts_with([' ', '\t', '\r', '\n']) {
return content;
}
match rest.find("?>") {
Some(end) => &rest[end + 2..],
None => content,
}
}
fn trim_xml_start(s: &str) -> &str {
s.trim_start_matches([' ', '\t', '\r', '\n'])
}
pub(crate) fn check_text_decl(
content: &str,
document_version: Version,
offset: usize,
) -> Result<()> {
let Some(rest) = content.strip_prefix("<?xml") else {
return Ok(());
};
if !rest.starts_with([' ', '\t', '\r', '\n']) {
return Ok(());
}
let Some(end) = rest.find("?>") else {
return Err(Error::new(ErrorKind::MalformedDeclaration, offset));
};
let decl = &rest[..end];
validate_xml_declaration(decl, offset, false)?;
let field = |name: &str| -> Option<&str> {
let at = decl.find(name)?;
let after = trim_xml_start(&decl[at + name.len()..]);
let after = trim_xml_start(after.strip_prefix('=')?);
let quote = after.chars().next()?;
let body = after.get(1..)?;
let close = body.find(quote)?;
body.get(..close)
};
if field("encoding").is_none() {
return Err(Error::new(ErrorKind::MalformedDeclaration, offset));
}
if field("standalone").is_some() {
return Err(Error::new(ErrorKind::MalformedDeclaration, offset));
}
if let Some(version) = field("version") {
let entity_is_11 = version == "1.1";
if entity_is_11 && document_version == Version::V10 {
return Err(Error::new(ErrorKind::UnsupportedVersion, offset));
}
}
Ok(())
}
fn validate_xml_declaration(
decl: &str,
offset: usize,
require_version: bool,
) -> Result<()> {
let mut rest = decl;
let mut seen: Vec<&str> = Vec::new();
loop {
let trimmed = trim_xml_start(rest);
if trimmed.is_empty() {
break;
}
if trimmed.len() == rest.len() {
return Err(Error::new(ErrorKind::MalformedDeclaration, offset));
}
rest = trimmed;
let name_end = rest
.find(|c: char| !c.is_ascii_alphabetic())
.unwrap_or(rest.len());
let name = &rest[..name_end];
if !matches!(name, "version" | "encoding" | "standalone") {
return Err(Error::new(ErrorKind::MalformedDeclaration, offset));
}
if seen.contains(&name) {
return Err(Error::new(ErrorKind::MalformedDeclaration, offset));
}
let rank = |n: &str| match n {
"version" => 0u8,
"encoding" => 1,
_ => 2,
};
if seen.last().is_some_and(|prev| rank(name) <= rank(prev)) {
return Err(Error::new(ErrorKind::MalformedDeclaration, offset));
}
seen.push(name);
let after = trim_xml_start(&rest[name_end..]);
let Some(after) = after.strip_prefix('=') else {
return Err(Error::new(ErrorKind::MalformedDeclaration, offset));
};
let after = trim_xml_start(after);
let Some(quote) =
after.chars().next().filter(|c| *c == '"' || *c == '\'')
else {
return Err(Error::new(ErrorKind::MalformedDeclaration, offset));
};
let body = &after[1..];
let Some(close) = body.find(quote) else {
return Err(Error::new(ErrorKind::MalformedDeclaration, offset));
};
let value = &body[..close];
match name {
"version" if !is_legal_version(value) => {
return Err(Error::new(
ErrorKind::MalformedDeclaration,
offset,
));
}
"standalone" if !matches!(value, "yes" | "no") => {
return Err(Error::new(
ErrorKind::MalformedDeclaration,
offset,
));
}
"encoding" if !crate::encoding::is_legal_encoding_name(value) => {
return Err(Error::new(
ErrorKind::MalformedDeclaration,
offset,
));
}
_ => {}
}
rest = &body[close + 1..];
}
if require_version && seen.first() != Some(&"version") {
return Err(Error::new(ErrorKind::MalformedDeclaration, offset));
}
Ok(())
}
pub(crate) fn declared_standalone(input: &str) -> bool {
let Some(decl) = xml_declaration(input) else {
return false;
};
let Some(at) = decl.find("standalone") else {
return false;
};
let after = trim_xml_start(&decl[at + "standalone".len()..]);
let Some(after) = after.strip_prefix('=') else {
return false;
};
let after = trim_xml_start(after);
let Some(quote) = after.chars().next() else {
return false;
};
if quote != '"' && quote != '\'' {
return false;
}
let body = &after[1..];
body.find(quote)
.is_some_and(|close| &body[..close] == "yes")
}
pub(crate) fn xml_declaration(input: &str) -> Option<&str> {
let rest = input.strip_prefix("<?xml")?;
if !rest.starts_with([' ', '\t', '\r', '\n']) {
return None;
}
let end = rest.find("?>")?;
Some(&rest[..end])
}
pub(crate) fn declared_version(input: &str) -> Result<Version> {
let Some(decl) = xml_declaration(input) else {
return Ok(Version::V10);
};
let Some(at) = decl.find("version") else {
return Ok(Version::V10);
};
let after = decl[at + "version".len()..].trim_start();
let Some(after) = after.strip_prefix('=') else {
return Ok(Version::V10);
};
let after = after.trim_start();
let Some(quote) = after.chars().next() else {
return Ok(Version::V10);
};
if quote != '"' && quote != '\'' {
return Ok(Version::V10);
}
let body = &after[1..];
let Some(close) = body.find(quote) else {
return Ok(Version::V10);
};
match &body[..close] {
"1.0" => Ok(Version::V10),
"1.1" => Ok(Version::V11),
v if v.starts_with("1.") => Ok(Version::V10),
_ => Err(Error::new(ErrorKind::UnsupportedVersion, 0)),
}
}