use alloc::borrow::{Cow, ToOwned};
use alloc::string::String;
use alloc::vec::Vec;
use crate::error::{Error, ErrorKind, Result};
use crate::limits::Limits;
use crate::tree::{Attribute, Document, ExpandedName, NodeId, NodeKind};
#[derive(Debug, Default)]
struct Namespaces {
bindings: Vec<(String, String)>,
marks: Vec<usize>,
}
impl Namespaces {
fn push_scope(&mut self) {
self.marks.push(self.bindings.len());
}
fn pop_scope(&mut self) {
if let Some(mark) = self.marks.pop() {
self.bindings.truncate(mark);
}
}
fn bind(&mut self, prefix: String, uri: String) {
self.bindings.push((prefix, uri));
}
fn resolve(&self, prefix: &str) -> Option<&str> {
self.bindings
.iter()
.rev()
.find(|(p, _)| p == prefix)
.map(|(_, u)| u.as_str())
}
}
struct Parser<'a> {
input: &'a str,
bytes: &'a [u8],
pos: usize,
doc: Document,
ns: Namespaces,
depth: usize,
limits: Limits,
dtd: Option<crate::dtd::Dtd>,
version: Version,
name_index: alloc::collections::BTreeMap<String, Vec<u32>>,
external: &'a dyn crate::external::ExternalSource,
entity_budget: usize,
}
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> {
let text = crate::encoding::decode(input)?;
parse_with(&text, limits)
}
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> {
let version = declared_version(input)?;
match normalize_line_endings(input, version) {
Cow::Borrowed(text) => {
parse_normalized(text, limits, version, external)
}
Cow::Owned(text) => parse_normalized(&text, limits, version, external),
}
}
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 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,
});
}
}
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> {
let trimmed = input.trim_start();
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));
}
}
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: Document::with_capacity(
input.bytes().filter(|b| *b == b'<').count() * 2,
),
name_index: alloc::collections::BTreeMap::new(),
external,
ns: Namespaces::default(),
depth: 0,
limits,
dtd: None,
version,
entity_budget: limits.max_entity_expansion,
};
p.parse_document()?;
Ok(p.doc)
}
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("<!--") {
let c = self.parse_comment()?;
let _ = self.doc.push(NodeKind::Comment(c), root);
} else if self.starts_with("<?") {
let (t, d) = self.parse_pi()?;
let _ = self.doc.push(
NodeKind::ProcessingInstruction { target: t, data: d },
root,
);
} else if self.starts_with("<!DOCTYPE") {
if seen_root || self.dtd.is_some() {
return Err(Error::new(
ErrorKind::TrailingContent,
self.pos,
));
}
self.skip_doctype()?;
} 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))
}
}
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(())
}
fn skip_doctype(&mut self) -> Result<()> {
let mut p = crate::dtd::DtdParser::new(
self.input,
self.pos,
self.limits.edition,
);
match p.parse_doctype() {
Ok(mut dtd) => {
self.pos = p.pos;
if let Some((system, public)) = dtd.external_subset.clone() {
if let Some(content) =
self.external.fetch(&system, public.as_deref())
{
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))
{
return Err(Error::new(
ErrorKind::IllegalCharacter(c),
self.pos,
));
}
let mut sub = crate::dtd::DtdParser::new(
body,
0,
self.limits.edition,
);
if let Err((offset, reason)) =
sub.parse_external_subset(&mut dtd)
{
let _ = offset;
return Err(Error::new(
ErrorKind::MalformedDtd(reason),
self.pos,
));
}
dtd.incomplete = false;
}
}
self.dtd = Some(dtd);
Ok(())
}
Err((offset, reason)) => {
Err(Error::new(ErrorKind::MalformedDtd(reason), offset))
}
}
}
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
}
fn bind_namespaces(
&mut self,
raw_attrs: &[(&'a str, String)],
) -> Result<()> {
for (name, value) in raw_attrs {
if let Some(prefix) = name.strip_prefix("xmlns:") {
const XML_NS: &str = "http://www.w3.org/XML/1998/namespace";
const XMLNS_NS: &str = "http://www.w3.org/2000/xmlns/";
if prefix == "xmlns"
|| (prefix == "xml" && value != XML_NS)
|| (prefix != "xml" && value == XML_NS)
|| value == XMLNS_NS
{
return Err(Error::new(
ErrorKind::ReservedNamespace,
self.pos,
));
}
if prefix.is_empty() || prefix.contains(':') {
return Err(Error::new(
ErrorKind::ReservedNamespace,
self.pos,
));
}
if value.is_empty() && self.version == Version::V10 {
return Err(Error::new(
ErrorKind::ReservedNamespace,
self.pos,
));
}
self.ns.bind(prefix.to_owned(), value.clone());
} else if *name == "xmlns" {
const XML_NS: &str = "http://www.w3.org/XML/1998/namespace";
const XMLNS_NS: &str = "http://www.w3.org/2000/xmlns/";
if value == XML_NS || value == XMLNS_NS {
return Err(Error::new(
ErrorKind::ReservedNamespace,
self.pos,
));
}
self.ns.bind(String::new(), value.clone());
}
}
Ok(())
}
fn parse_element_inner(&mut self, parent: NodeId) -> Result<()> {
let tag_start = self.pos;
self.pos += 1; let qname = self.parse_name()?;
self.ns.push_scope();
let raw_attrs = self.parse_attributes()?;
self.bind_namespaces(&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,
));
};
let mut resolved: Vec<Attribute> = Vec::with_capacity(raw_attrs.len());
for (raw, value) in raw_attrs {
if raw == "xmlns" || raw.starts_with("xmlns:") {
continue;
}
let an = self.intern_qname(raw, false, tag_start)?;
if resolved.iter().any(|a| a.name == an) {
self.ns.pop_scope();
return Err(Error::new(
ErrorKind::DuplicateAttribute(raw.to_owned()),
tag_start,
));
}
resolved.push(Attribute { name: an, value });
}
let name_id = self.intern_qname(qname, true, tag_start)?;
let node = self.doc.push(
NodeKind::Element {
name: name_id,
attributes: (0, 0),
},
parent,
);
let start = self.doc.attr_ids.len();
for at in resolved {
let id = self.doc.push_detached(NodeKind::Attr(at), node);
self.doc.attr_ids.push(id);
}
let len = self.doc.attr_ids.len() - start;
if let Some(NodeKind::Element { attributes, .. }) =
self.doc.kind_mut(node)
{
*attributes = (
u32::try_from(start).unwrap_or(u32::MAX),
u32::try_from(len).unwrap_or(u32::MAX),
);
}
if self_closing {
self.ns.pop_scope();
return Ok(());
}
self.parse_children(node, qname)?;
self.ns.pop_scope();
Ok(())
}
fn parse_children(&mut self, node: NodeId, open_qname: &str) -> Result<()> {
let mark = self.doc.scratch_mark();
let mut text = String::new();
loop {
if self.limits.max_nodes.is_some_and(|m| self.doc.len() > m) {
return Err(Error::new(ErrorKind::TooManyNodes, self.pos));
}
if self.pos >= self.bytes.len() {
return Err(Error::new(ErrorKind::UnexpectedEof, self.pos));
}
if self.peek_is(b'<') {
if self.starts_with("</") {
self.flush_text(&mut text, node)?;
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);
return Ok(());
} else if self.starts_with("<!--") {
self.flush_text(&mut text, node)?;
let c = self.parse_comment()?;
let _ = self.doc.push(NodeKind::Comment(c), node);
} else if self.starts_with("<![CDATA[") {
self.pos += "<![CDATA[".len();
let end = self.input[self.pos..].find("]]>").ok_or_else(
|| {
Error::new(
ErrorKind::Unterminated("CDATA"),
self.pos,
)
},
)?;
text.push_str(&self.input[self.pos..self.pos + end]);
self.pos += end + 3;
} else if self.starts_with("<?") {
self.flush_text(&mut text, node)?;
let (t, d) = self.parse_pi()?;
let _ = self.doc.push(
NodeKind::ProcessingInstruction { target: t, data: d },
node,
);
} else {
self.flush_text(&mut text, node)?;
self.parse_element(node)?;
}
} else {
self.parse_text_run(&mut text)?;
}
}
}
fn flush_text(&mut self, text: &mut String, node: NodeId) -> Result<()> {
if !text.is_empty() {
let owned = core::mem::take(text);
if self.limits.max_text_length.is_some_and(|m| owned.len() > m) {
return Err(Error::new(ErrorKind::TextTooLong, self.pos));
}
let _ = self.doc.push(NodeKind::Text(owned), node);
}
Ok(())
}
fn parse_text_run(&mut self, out: &mut String) -> 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'&' => {
let s = self.pos;
self.pos += 1;
let end =
self.input[self.pos..].find(';').ok_or_else(|| {
Error::new(ErrorKind::Unterminated("entity"), s)
})?;
let ent = &self.input[self.pos..self.pos + end];
out.push_str(&self.expand_entity(ent, s, false)?);
self.pos += end + 1;
}
_ => {
let start = self.pos;
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_str(run);
}
}
}
Ok(())
}
fn parse_comment(&mut self) -> Result<String> {
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 body = body.to_owned();
self.pos += end + 3;
Ok(body)
}
fn parse_pi(&mut self) -> Result<(String, String)> {
let start = self.pos;
self.pos += 2; let target = self.parse_name()?;
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 data = self.input[self.pos..self.pos + end].trim().to_owned();
self.pos += end + 2;
Ok((target.to_owned(), data))
}
fn parse_attributes(&mut self) -> Result<Vec<(&'a str, String)>> {
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<String> {
let start = self.pos;
if self.pos >= self.bytes.len() {
return Err(Error::new(ErrorKind::UnexpectedEof, start));
}
let quote = self.bytes[self.pos];
if quote != b'"' && quote != b'\'' {
return Err(Error::new(ErrorKind::UnquotedAttributeValue, start));
}
self.pos += 1;
let mut out = String::new();
loop {
if self.pos >= self.bytes.len() {
return Err(Error::new(
ErrorKind::Unterminated("attribute value"),
start,
));
}
match self.bytes[self.pos] {
b if b == quote => {
self.pos += 1;
return Ok(out);
}
b'<' => {
return Err(Error::new(
ErrorKind::IllegalCharacter('<'),
self.pos,
));
}
b'&' => {
let s = self.pos;
self.pos += 1;
let end =
self.input[self.pos..].find(';').ok_or_else(|| {
Error::new(ErrorKind::Unterminated("entity"), s)
})?;
let ent = &self.input[self.pos..self.pos + end];
out.push_str(&self.expand_entity(ent, s, true)?);
self.pos += end + 1;
}
_ => {
let s = self.pos;
while self.pos < self.bytes.len()
&& self.bytes[self.pos] != quote
&& self.bytes[self.pos] != b'&'
&& self.bytes[self.pos] != b'<'
{
self.pos += 1;
}
push_attribute_normalized(
&mut out,
&self.input[s..self.pos],
);
}
}
}
}
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)),
}
}
fn intern_qname(
&mut self,
qname: &str,
is_element: bool,
offset: usize,
) -> Result<crate::tree::NameId> {
{
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
{
return Ok(crate::tree::NameId(id));
}
}
}
}
let name = self.expand(qname, is_element, offset)?;
Ok(self.intern(&name))
}
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))
}
}
}
}
fn peek_end_tag_name(&self) -> String {
self.input[self.pos + 2..]
.split(['>', ' ', '\t', '\n', '\r'])
.next()
.unwrap_or_default()
.to_owned()
}
fn starts_with(&self, s: &str) -> bool {
self.input[self.pos..].starts_with(s)
}
fn peek_is(&self, b: u8) -> bool {
self.bytes.get(self.pos) == Some(&b)
}
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) = decode_predefined(ent) {
if let Some(bad) =
direct.chars().find(|c| !is_xml_char_for(*c, self.version))
{
return Err(Error::new(
ErrorKind::IllegalCharacter(bad),
offset,
));
}
return Ok(direct);
}
let Some(dtd) = self.dtd.as_ref() else {
return Err(Error::new(
ErrorKind::UnknownEntity(ent.to_owned()),
offset,
));
};
match dtd.entity(ent) {
Some(crate::dtd::EntityValue::Internal(text)) => {
let text = text.clone();
let mut budget = self.entity_budget;
let out = self.expand_text(
&text,
offset,
1,
&mut budget,
in_attribute,
);
self.entity_budget = budget;
out
}
Some(crate::dtd::EntityValue::External { system, public }) => {
if in_attribute {
return Err(Error::new(
ErrorKind::ForbiddenEntityReference(ent.to_owned()),
offset,
));
}
let (system, public) = (system.clone(), public.clone());
match self.external.fetch(&system, public.as_deref()) {
Some(content) => {
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,
));
}
let mut budget = self.entity_budget;
let out = self.expand_text(
body,
offset,
1,
&mut budget,
false,
);
self.entity_budget = budget;
out
}
None => Ok(String::new()),
}
}
Some(crate::dtd::EntityValue::Unparsed) => Err(Error::new(
ErrorKind::ForbiddenEntityReference(ent.to_owned()),
offset,
)),
None if dtd.incomplete => Ok(String::new()),
None => Err(Error::new(
ErrorKind::UnknownEntity(ent.to_owned()),
offset,
)),
}
}
fn intern(&mut self, name: &ExpandedName) -> 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 {
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.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.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)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Version {
V10,
V11,
}
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())
})
}
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,
}
}
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(())
}
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 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 = decl[at + name.len()..].trim_start();
let after = after.strip_prefix('=')?.trim_start();
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 = rest.trim_start();
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 = rest[name_end..].trim_start();
let Some(after) = after.strip_prefix('=') else {
return Err(Error::new(ErrorKind::MalformedDeclaration, offset));
};
let after = after.trim_start();
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(())
}
fn declared_version(input: &str) -> Result<Version> {
let Some(rest) = input.strip_prefix("<?xml") else {
return Ok(Version::V10);
};
let Some(end) = rest.find("?>") else {
return Ok(Version::V10);
};
let decl = &rest[..end];
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)),
}
}
#[must_use]
const fn is_xml_char_for(c: char, version: Version) -> bool {
match version {
Version::V10 => is_xml_char(c),
Version::V11 => matches!(c,
'\u{1}'..='\u{D7FF}'
| '\u{E000}'..='\u{FFFD}'
| '\u{10000}'..='\u{10FFFF}'
),
}
}
#[must_use]
const fn is_restricted_char(c: char) -> bool {
matches!(c,
'\u{1}'..='\u{8}' | '\u{B}'..='\u{C}' | '\u{E}'..='\u{1F}'
| '\u{7F}'..='\u{84}' | '\u{86}'..='\u{9F}'
)
}
#[must_use]
const fn is_literal_char_for(c: char, version: Version) -> bool {
match version {
Version::V10 => is_xml_char(c),
Version::V11 => is_xml_char_for(c, version) && !is_restricted_char(c),
}
}
#[must_use]
pub(crate) const fn is_xml_char(c: char) -> bool {
matches!(c,
'\u{9}' | '\u{A}' | '\u{D}'
| '\u{20}'..='\u{D7FF}'
| '\u{E000}'..='\u{FFFD}'
| '\u{10000}'..='\u{10FFFF}'
)
}
pub(crate) fn is_name_start(c: char) -> bool {
matches!(c, 'A'..='Z' | 'a'..='z' | '_' | ':')
|| matches!(c as u32,
0xC0..=0xD6 | 0xD8..=0xF6 | 0xF8..=0x2FF
| 0x370..=0x37D | 0x37F..=0x1FFF | 0x200C..=0x200D
| 0x2070..=0x218F | 0x2C00..=0x2FEF | 0x3001..=0xD7FF
| 0xF900..=0xFDCF | 0xFDF0..=0xFFFD | 0x10000..=0xEFFFF)
}
pub(crate) fn is_name_char(c: char) -> bool {
is_name_start(c)
|| matches!(c, '-' | '.' | '0'..='9')
|| c as u32 == 0xB7
|| matches!(c as u32, 0x300..=0x36F | 0x203F..=0x2040)
}