use std::borrow::Cow;
use rustc_hash::{FxHashMap as HashMap, FxHashSet};
use memchr::{memchr, memchr3};
use crate::error::{ErrorDomain, ErrorLevel, Result, XmlError};
use crate::options::ParseOptions;
use crate::scanner::{Scanner, is_pubid_char, is_xml_char, is_xml_11_char, validate_xml_chars};
#[derive(Debug)]
pub struct BytesAttr<'src> {
pub name: &'src [u8],
pub value: Cow<'src, [u8]>,
}
impl<'src> BytesAttr<'src> {
pub fn name(&self) -> &'src [u8] { self.name }
pub fn value(&self) -> &[u8] { &self.value }
}
pub struct BytesAttrs<'r, 'src> {
scan: Scanner<'src, 'r>,
entities: &'r HashMap<String, EntityDecl>,
expansion_bytes: &'r mut u64,
done: bool,
standalone_yes: bool,
is_xml_11: bool,
}
#[derive(Debug, Clone)]
pub(crate) enum EntityKind {
InternalText(String),
ExternalLoaded(String),
ExternalUnloaded,
}
impl EntityKind {
pub(crate) fn replacement(&self) -> Option<&str> {
match self {
EntityKind::InternalText(s) | EntityKind::ExternalLoaded(s) => Some(s.as_str()),
EntityKind::ExternalUnloaded => None,
}
}
pub(crate) fn is_external_value(&self) -> bool {
matches!(self, EntityKind::ExternalLoaded(_) | EntityKind::ExternalUnloaded)
}
}
#[derive(Debug, Clone)]
pub(crate) struct EntityDecl {
pub(crate) kind: EntityKind,
pub(crate) declared_external: bool,
pub(crate) source_uri: Option<String>,
}
impl EntityDecl {
fn replacement(&self) -> Option<&str> { self.kind.replacement() }
}
#[derive(Debug, Clone)]
pub(crate) struct DeferredExternal {
pub(crate) system_id: String,
pub(crate) public_id: Option<String>,
}
impl<'src> Iterator for BytesAttrs<'_, 'src> {
type Item = Result<BytesAttr<'src>>;
fn next(&mut self) -> Option<Self::Item> {
if self.done { return None; }
self.scan.skip_ws();
if self.scan.is_eof() {
self.done = true;
return None;
}
match self.read_attr() {
Ok(a) => Some(Ok(a)),
Err(e) => { self.done = true; Some(Err(e)) }
}
}
}
impl<'src> BytesAttrs<'_, 'src> {
fn read_attr(&mut self) -> Result<BytesAttr<'src>> {
let (n_start, n_end) = self.scan.scan_name_raw()?;
let name = match self.scan.current_borrowed_bytes() {
Some(src) => &src[n_start..n_end],
None => return Err(self.scan.err(
"attribute name from inside entity-expansion stream not supported"
)),
};
self.scan.skip_ws();
self.scan.expect(b'=')?;
self.scan.skip_ws();
let value = self.scan_att_value_cow()?;
Ok(BytesAttr { name, value })
}
fn scan_att_value_cow(&mut self) -> Result<Cow<'src, [u8]>> {
let q = match self.scan.advance() {
Some(b @ (b'"' | b'\'')) => b,
Some(b) => return Err(self.scan.err(format!("expected quote, got '{}'", b as char))),
None => return Err(self.scan.err("expected quote, got EOF")),
};
let start = self.scan.cur_pos();
let tail = self.scan.cur_tail();
match memchr3(q, b'&', b'<', tail) {
None => return Err(self.scan.err("unterminated attribute value")),
Some(off) => {
self.scan.cur_advance_pos(off);
match self.scan.cur_bytes()[self.scan.cur_pos()] {
b if b == q => {
let s = self.scan.cur_slice(start, self.scan.cur_pos());
let s = maybe_normalize_attr_value(s, self.is_xml_11);
self.scan.advance();
return Ok(s);
}
b'<' => return Err(self.scan.err("'<' not allowed in attribute value")),
b'&' => {} _ => unreachable!(),
}
}
}
let mut buf = Vec::<u8>::new();
append_attr_segment(&self.scan, start, self.scan.cur_pos(), &mut buf, self.is_xml_11);
let budget = self.scan.opts.max_entity_expansion_bytes;
let depth: u32 = 0;
loop {
let tail = self.scan.cur_tail();
match memchr3(q, b'&', b'<', tail) {
None => {
let end = self.scan.cur_len();
let from = self.scan.cur_pos();
append_attr_segment(&self.scan, from, end, &mut buf, self.is_xml_11);
self.scan.cur_set_pos(end);
if self.scan.try_pop_entity_stream() {
continue;
}
return Err(self.scan.err("unterminated attribute value"));
}
Some(off) => {
let from = self.scan.cur_pos();
append_attr_segment(&self.scan, from, from + off, &mut buf, self.is_xml_11);
self.scan.cur_advance_pos(off);
match self.scan.cur_bytes()[self.scan.cur_pos()] {
b if b == q => {
if self.scan.in_entity() {
buf.push(b);
self.scan.advance();
} else {
self.scan.advance();
break;
}
}
b'<' => return Err(self.scan.err("'<' not allowed in attribute value")),
b'&' => expand_reference_bytes(
&mut self.scan, &mut buf, self.entities, &mut *self.expansion_bytes, budget, depth, None,
false,
self.standalone_yes,
self.is_xml_11,
)?,
_ => unreachable!(),
}
}
}
}
Ok(Cow::Owned(buf))
}
}
pub struct BytesStartTag<'r, 'src> {
src: &'src [u8],
name_start: u32,
name_end: u32,
owned_name: Option<Box<[u8]>>,
entity_attrs: Option<Vec<(Vec<u8>, Vec<u8>)>>,
attrs_start: u32,
attrs_end: u32,
entities: &'r HashMap<String, EntityDecl>,
expansion_bytes: &'r mut u64,
opts: &'r ParseOptions,
standalone_yes: bool,
is_xml_11: bool,
}
impl<'r, 'src> BytesStartTag<'r, 'src> {
#[inline]
pub fn name(&self) -> &[u8] {
match &self.owned_name {
Some(b) => b,
None => &self.src[self.name_start as usize..self.name_end as usize],
}
}
pub fn name_cow(&self) -> Cow<'src, [u8]> {
match &self.owned_name {
Some(b) => Cow::Owned(b.to_vec()),
None => Cow::Borrowed(
&self.src[self.name_start as usize..self.name_end as usize]
),
}
}
#[inline]
pub fn name_offset(&self) -> u32 {
self.name_start
}
#[inline]
pub fn attrs_bytes(&self) -> &'src [u8] {
&self.src[self.attrs_start as usize..self.attrs_end as usize]
}
#[inline]
pub fn entity_attrs(&self) -> Option<&[(Vec<u8>, Vec<u8>)]> {
self.entity_attrs.as_deref()
}
pub fn attrs(self) -> BytesAttrs<'r, 'src> {
BytesAttrs {
scan: Scanner::new(self.attrs_bytes(), Cow::Borrowed(self.opts)),
entities: self.entities,
expansion_bytes: self.expansion_bytes,
done: false,
standalone_yes: self.standalone_yes,
is_xml_11: self.is_xml_11,
}
}
}
impl std::fmt::Debug for BytesStartTag<'_, '_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BytesStartTag")
.field("name", &String::from_utf8_lossy(self.name()))
.field("attrs", &String::from_utf8_lossy(self.attrs_bytes()))
.finish()
}
}
pub struct BytesEndTag<'src> {
src: &'src [u8],
name_start: u32,
name_end: u32,
}
impl<'src> BytesEndTag<'src> {
#[inline]
pub fn name(&self) -> &'src [u8] {
&self.src[self.name_start as usize..self.name_end as usize]
}
}
impl std::fmt::Debug for BytesEndTag<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BytesEndTag")
.field("name", &String::from_utf8_lossy(self.name()))
.finish()
}
}
pub struct BytesText<'src> { inner: Cow<'src, [u8]> }
impl<'src> BytesText<'src> {
#[inline] pub fn as_bytes(&self) -> &[u8] { &self.inner }
#[inline] pub fn into_bytes(self) -> Cow<'src, [u8]> { self.inner }
}
impl std::fmt::Debug for BytesText<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "BytesText({:?})", String::from_utf8_lossy(&self.inner))
}
}
pub struct BytesCData<'src> { inner: Cow<'src, [u8]> }
impl<'src> BytesCData<'src> {
#[inline] pub fn as_bytes(&self) -> &[u8] { &self.inner }
#[inline] pub fn into_bytes(self) -> Cow<'src, [u8]> { self.inner }
}
impl std::fmt::Debug for BytesCData<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "BytesCData({:?})", String::from_utf8_lossy(&self.inner))
}
}
pub struct BytesComment<'src> { inner: Cow<'src, [u8]> }
impl<'src> BytesComment<'src> {
#[inline] pub fn as_bytes(&self) -> &[u8] { &self.inner }
#[inline] pub fn into_bytes(self) -> Cow<'src, [u8]> { self.inner }
}
impl std::fmt::Debug for BytesComment<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "BytesComment({:?})", String::from_utf8_lossy(&self.inner))
}
}
pub struct BytesPi<'src> {
target_: Cow<'src, [u8]>,
content_: Cow<'src, [u8]>,
}
impl<'src> BytesPi<'src> {
#[inline] pub fn target(&self) -> &[u8] { &self.target_ }
#[inline] pub fn content(&self) -> &[u8] { &self.content_ }
#[inline] pub fn into_parts(self) -> (Cow<'src, [u8]>, Cow<'src, [u8]>) {
(self.target_, self.content_)
}
}
impl std::fmt::Debug for BytesPi<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BytesPi")
.field("target", &String::from_utf8_lossy(&self.target_))
.field("content", &String::from_utf8_lossy(&self.content_))
.finish()
}
}
#[derive(Debug)]
pub enum BytesEvent<'r, 'src> {
StartElement(BytesStartTag<'r, 'src>),
EndElement(BytesEndTag<'src>),
Text(BytesText<'src>),
CData(BytesCData<'src>),
Comment(BytesComment<'src>),
Pi(BytesPi<'src>),
EntityRef(BytesEntityRef<'src>),
Eof,
}
pub struct BytesEntityRef<'src> {
src: &'src [u8],
name_start: u32,
name_end: u32,
}
impl<'src> BytesEntityRef<'src> {
#[inline]
pub fn name(&self) -> &'src [u8] {
&self.src[self.name_start as usize..self.name_end as usize]
}
}
impl std::fmt::Debug for BytesEntityRef<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BytesEntityRef")
.field("name", &String::from_utf8_lossy(self.name()))
.finish()
}
}
#[derive(Debug)]
pub enum BytesEventInto<'src> {
StartElement { name: Cow<'src, [u8]> },
EndElement { name: Cow<'src, [u8]> },
Text(Cow<'src, [u8]>),
CData(Cow<'src, [u8]>),
Comment(Cow<'src, [u8]>),
Pi { target: Cow<'src, [u8]>, content: Cow<'src, [u8]> },
EntityRef { name: Cow<'src, [u8]> },
Eof,
}
enum NextState {
NeedsProlog,
Steady,
PendingEnd(u32, u32),
PendingEntityRef(u32, u32),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct XmlDeclInfo {
pub version: String,
pub encoding: Option<String>,
pub standalone: Option<bool>,
}
enum ElementStackEntry {
SourceRange(u32, u32),
Owned(String),
}
impl ElementStackEntry {
fn name_bytes<'a>(&'a self, src: &'a [u8]) -> &'a [u8] {
match self {
ElementStackEntry::SourceRange(s, e) => &src[*s as usize..*e as usize],
ElementStackEntry::Owned(s) => s.as_bytes(),
}
}
}
pub struct XmlBytesReader<'src> {
scan: Scanner<'src, 'static>,
entities: HashMap<String, EntityDecl>,
parameter_entities: HashMap<String, EntityDecl>,
deferred_general_entities: HashMap<String, DeferredExternal>,
pe_ref_in_internal_subset_seen: bool,
depth: u32,
expansion_bytes: u64,
element_stack: Vec<ElementStackEntry>,
element_streams: Vec<usize>,
frame_saw_child: Vec<bool>,
state: NextState,
root_seen: bool,
last_start_offset: u32,
recovered_errors: Vec<XmlError>,
xml_decl: Option<XmlDeclInfo>,
standalone_yes: bool,
is_xml_11: bool,
source_has_eol_candidate: bool,
text_decode_buf: Vec<u8>,
dtd: crate::dtd::Dtd,
prolog_misc_count: u32,
}
const MAX_CONTENT_MODEL_DEPTH: u32 = 256;
impl<'src> XmlBytesReader<'src> {
#[allow(clippy::should_implement_trait)] pub fn from_str(input: &'src str) -> Self {
Self::new(input.as_bytes())
}
pub fn from_bytes(src: &'src [u8]) -> Result<Self> {
simdutf8::compat::from_utf8(src).map_err(|e| {
let off = e.valid_up_to();
let (line, col) = crate::scanner::compute_line_col(src, off);
XmlError::new(ErrorDomain::Encoding, ErrorLevel::Fatal, format!("invalid UTF-8: {e}"))
.at("<input>", line, col, off as u64)
})?;
Ok(Self::new(src))
}
pub unsafe fn from_bytes_unchecked(src: &'src [u8]) -> Self {
Self::new(src)
}
pub unsafe fn from_bytes_in_place_unchecked(src: &'src mut [u8]) -> Self {
let source_has_eol_candidate = precompute_source_has_eol(src);
Self {
scan: Scanner::new_in_place(src, Cow::Owned(ParseOptions::default())),
entities: HashMap::default(),
parameter_entities: HashMap::default(),
deferred_general_entities: HashMap::default(),
pe_ref_in_internal_subset_seen: false,
depth: 0,
expansion_bytes: 0,
element_stack: Vec::new(),
element_streams: Vec::new(),
frame_saw_child: Vec::new(),
state: NextState::NeedsProlog,
root_seen: false,
last_start_offset: u32::MAX,
recovered_errors: Vec::new(),
xml_decl: None,
standalone_yes: false,
is_xml_11: false,
source_has_eol_candidate,
text_decode_buf: Vec::new(),
dtd: crate::dtd::Dtd::new(),
prolog_misc_count: 0,
}
}
pub fn with_options(mut self, opts: ParseOptions) -> Self {
self.scan.opts = Cow::Owned(opts);
self
}
#[inline]
pub(crate) unsafe fn rebind_scanner(&mut self, ptr: *const u8, len: usize, cur_pos: usize) {
unsafe { self.scan.rebind(ptr, len, cur_pos) }
}
fn new(src: &'src [u8]) -> Self {
let source_has_eol_candidate = precompute_source_has_eol(src);
Self {
scan: Scanner::new(src, Cow::Owned(ParseOptions::default())),
entities: HashMap::default(),
parameter_entities: HashMap::default(),
deferred_general_entities: HashMap::default(),
pe_ref_in_internal_subset_seen: false,
depth: 0,
expansion_bytes: 0,
element_stack: Vec::new(),
element_streams: Vec::new(),
frame_saw_child: Vec::new(),
state: NextState::NeedsProlog,
root_seen: false,
last_start_offset: u32::MAX,
recovered_errors: Vec::new(),
xml_decl: None,
standalone_yes: false,
is_xml_11: false,
source_has_eol_candidate,
text_decode_buf: Vec::new(),
dtd: crate::dtd::Dtd::new(),
prolog_misc_count: 0,
}
}
pub fn xml_decl(&self) -> Option<&XmlDeclInfo> {
self.xml_decl.as_ref()
}
pub fn dtd(&self) -> &crate::dtd::Dtd {
&self.dtd
}
#[inline]
pub fn src_bytes(&self) -> &'src [u8] {
self.scan.src_bytes()
}
#[inline]
pub fn src_offset(&self) -> usize {
self.scan.src_offset()
}
#[inline]
pub fn last_start_offset(&self) -> Option<usize> {
match self.last_start_offset {
u32::MAX => None,
v => Some(v as usize),
}
}
pub fn take_dtd(&mut self) -> crate::dtd::Dtd {
std::mem::take(&mut self.dtd)
}
pub fn recovered_errors(&self) -> &[XmlError] {
&self.recovered_errors
}
#[inline]
pub(crate) fn maybe_recover(&mut self, err: XmlError) -> Result<()> {
if err.level == ErrorLevel::Fatal || !self.scan.opts.recovery_mode {
return Err(err);
}
self.recovered_errors.push(err);
Ok(())
}
fn synthesize_close(&mut self) -> BytesEvent<'_, 'src> {
let (name_start, name_end) = match self.element_stack.pop() {
Some(ElementStackEntry::SourceRange(s, e)) => (s, e),
Some(ElementStackEntry::Owned(_)) | None => (0, 0),
};
self.element_streams.pop();
self.frame_saw_child.pop();
self.depth = self.depth.saturating_sub(1);
let src = self.scan.src_bytes();
BytesEvent::EndElement(BytesEndTag { src, name_start, name_end })
}
#[allow(clippy::should_implement_trait)] pub fn next(&mut self) -> Result<BytesEvent<'_, 'src>> {
match self.state {
NextState::Steady => {}
NextState::PendingEnd(name_start, name_end) => {
self.state = NextState::Steady;
let src = self.scan.src_bytes();
return Ok(BytesEvent::EndElement(BytesEndTag {
src, name_start, name_end,
}));
}
NextState::PendingEntityRef(name_start, name_end) => {
self.state = NextState::Steady;
let src = self.scan.src_bytes();
return Ok(BytesEvent::EntityRef(BytesEntityRef {
src, name_start, name_end,
}));
}
NextState::NeedsProlog => {
self.parse_prolog()?;
self.state = NextState::Steady;
}
}
if !self.scan.on_original_source() {
return self.next_in_entity();
}
let bytes = self.scan.src_bytes();
let end = bytes.len();
let p_in = self.scan.cur_pos();
let mut p = p_in;
#[inline(always)]
fn is_xml_ws(b: u8) -> bool {
b == b' ' || b == b'\t' || b == b'\n' || b == b'\r'
}
if self.depth == 0 {
while p < end && is_xml_ws(unsafe { *bytes.get_unchecked(p) }) {
p += 1;
}
} else if self.scan.opts.skip_inter_element_whitespace {
let mut q = p;
while q < end && is_xml_ws(unsafe { *bytes.get_unchecked(q) }) {
q += 1;
}
if q > p {
let drop = match bytes.get(q).copied() {
Some(b'<') if bytes.get(q + 1) == Some(&b'/') => {
self.frame_saw_child.last().copied().unwrap_or(false)
}
Some(b'<') => true,
_ => false,
};
if drop {
p = q;
}
}
}
if p >= end {
self.scan.cur_set_pos(p);
if self.depth > 0 && !self.scan.opts.skip_end_tag_check {
let name_lossy = self.element_stack.last()
.map(|e| String::from_utf8_lossy(e.name_bytes(bytes)).into_owned())
.unwrap_or_else(|| "?".to_string());
let err = self.scan.err_with_level(
ErrorLevel::Error,
format!(
"unclosed element '<{name_lossy}>' at end of document \
(XML 1.0 § 3.1 [STag/ETag])"
),
).with_code(crate::error::ErrorCode::TagNotFinished);
self.maybe_recover(err)?;
return Ok(self.synthesize_close());
}
if !self.root_seen && !self.scan.opts.skip_end_tag_check {
let err = self.scan.err_with_level(
ErrorLevel::Error,
"document has no root element (XML 1.0 § 2.1 [document])",
).with_code(crate::error::ErrorCode::DocumentEmpty);
self.maybe_recover(err)?;
}
return Ok(BytesEvent::Eof);
}
let b0 = unsafe { *bytes.get_unchecked(p) };
if b0 != b'<' {
if self.depth == 0 && !self.scan.opts.skip_end_tag_check {
let err = self.scan.err_with_level(
ErrorLevel::Error,
"text content not allowed at the document level \
(XML 1.0 § 2.1 [document])",
);
self.maybe_recover(err)?;
}
if p != p_in {
self.scan.cur_set_pos(p);
}
return self.read_text();
}
let b1 = if p + 1 < end {
unsafe { *bytes.get_unchecked(p + 1) }
} else {
0
};
if p != p_in {
self.scan.cur_set_pos(p);
}
match b1 {
b'/' => self.read_end_element(),
b'?' => self.read_pi(),
b'!' => {
let b2 = if p + 2 < end {
unsafe { *bytes.get_unchecked(p + 2) }
} else {
0
};
match b2 {
b'-' => self.read_comment(),
b'[' => {
if self.depth == 0
&& !self.scan.opts.skip_end_tag_check
{
return Err(self.scan.err(
"CDATA sections are only allowed inside an element \
(XML 1.0 § 3.1 [content])"
));
}
self.read_cdata()
}
b'D' | b'd' if self.depth == 0
&& (self.scan.starts_with(b"<!DOCTYPE")
|| self.scan.starts_with(b"<!doctype")) =>
{
self.parse_doctype()?;
self.next()
}
_ => self.dispatch_start_element(),
}
}
_ => {
let looks_like_name_start = matches!(
b1,
b'A'..=b'Z' | b'a'..=b'z' | b'_' | b':' | 0x80..=0xFF
);
if !looks_like_name_start
&& self.scan.opts.recovery_mode
&& self.depth > 0
{
let err = self.scan.err_with_level(
ErrorLevel::Error,
"bare '<' in text content — kept literal \
(XML 1.0 § 2.4 [CharData])",
);
self.recovered_errors.push(err);
self.scan.cur_set_pos(p + 1);
let src = self.scan.src_bytes();
let lt_slice = &src[p..p + 1];
return Ok(BytesEvent::Text(BytesText {
inner: std::borrow::Cow::Borrowed(lt_slice),
}));
}
self.dispatch_start_element()
}
}
}
fn next_in_entity(&mut self) -> Result<BytesEvent<'_, 'src>> {
if self.scan.opts.skip_inter_element_whitespace {
self.scan.skip_ws();
}
while self.scan.cur_pos() >= self.scan.cur_len()
&& !self.scan.on_original_source()
{
let depth_now = self.element_stack.len() as u32;
if let Some((name, depth_at_push)) = self.scan.top_entity_info() {
if depth_at_push != depth_now {
return Err(self.scan.err(format!(
"entity '&{name};' contains unbalanced element markup — \
element-stack depth was {depth_at_push} when the entity \
was expanded but is {depth_now} at its end \
(XML 1.0 § 4.3.2 WFC 'Logical Structure')"
)));
}
}
if !self.scan.try_pop_entity_stream() {
break;
}
}
if self.scan.on_original_source() {
return self.next();
}
if self.scan.is_eof() {
if self.depth > 0 && !self.scan.opts.skip_end_tag_check {
let bytes = self.scan.src_bytes();
let name_lossy = self.element_stack.last()
.map(|e| String::from_utf8_lossy(e.name_bytes(bytes)).into_owned())
.unwrap_or_else(|| "?".to_string());
let err = self.scan.err_with_level(
ErrorLevel::Error,
format!(
"unclosed element '<{name_lossy}>' at end of document \
(XML 1.0 § 3.1 [STag/ETag])"
),
).with_code(crate::error::ErrorCode::TagNotFinished);
self.maybe_recover(err)?;
return Ok(self.synthesize_close());
}
return Ok(BytesEvent::Eof);
}
if self.scan.peek() != Some(b'<') {
return self.read_text();
}
if self.scan.starts_with(b"</") { self.read_end_element() }
else if self.scan.starts_with(b"<!--") { self.read_comment() }
else if self.scan.starts_with(b"<![CDATA[") { self.read_cdata() }
else if self.scan.starts_with(b"<?") { self.read_pi() }
else { self.dispatch_start_element() }
}
#[inline]
fn dispatch_start_element(&mut self) -> Result<BytesEvent<'_, 'src>> {
if self.depth == 0
&& self.root_seen
&& !self.scan.opts.skip_end_tag_check
{
let err = self.scan.err_with_level(
ErrorLevel::Error,
"only one root element allowed (XML 1.0 § 2.1 [document])",
);
self.maybe_recover(err)?;
}
self.read_start_element()
}
pub fn next_into(&mut self, buf: &mut Vec<BytesAttr<'src>>) -> Result<BytesEventInto<'src>> {
buf.clear();
match self.next()? {
BytesEvent::StartElement(tag) => {
let name = tag.name_cow();
for attr in tag.attrs() {
buf.push(attr?);
}
Ok(BytesEventInto::StartElement { name })
}
BytesEvent::EndElement(tag) => Ok(BytesEventInto::EndElement {
name: Cow::Borrowed(tag.name()),
}),
BytesEvent::Text(t) => Ok(BytesEventInto::Text(t.into_bytes())),
BytesEvent::CData(s) => Ok(BytesEventInto::CData(s.into_bytes())),
BytesEvent::Comment(s) => Ok(BytesEventInto::Comment(s.into_bytes())),
BytesEvent::Pi(pi) => {
let (target, content) = pi.into_parts();
Ok(BytesEventInto::Pi { target, content })
}
BytesEvent::EntityRef(e) => Ok(BytesEventInto::EntityRef {
name: Cow::Borrowed(e.name()),
}),
BytesEvent::Eof => Ok(BytesEventInto::Eof),
}
}
fn parse_prolog(&mut self) -> Result<()> {
if !self.scan.opts.skip_xml_char_validation {
validate_xml_chars(self.scan.cur_bytes())?;
}
if self.scan.starts_with(&[0xEF, 0xBB, 0xBF]) { self.scan.skip_n(3); }
if self.scan.starts_with(b"<?xml")
&& matches!(self.scan.peek_at(5), Some(b' ' | b'\t' | b'\r' | b'\n' | b'?'))
{
if let Err(e) = self.skip_xml_decl() {
if e.level == ErrorLevel::Fatal || !self.scan.opts.recovery_mode {
return Err(e);
}
self.recovered_errors.push(e);
match memchr(b'?', self.scan.cur_tail()) {
Some(off) => {
self.scan.cur_advance_pos(off);
if self.scan.starts_with(b"?>") {
self.scan.skip_n(2);
} else {
self.scan.advance();
}
}
None => {
}
}
}
}
self.skip_misc()
}
fn skip_xml_decl(&mut self) -> Result<()> {
self.scan.expect_str(b"<?xml")?;
self.scan.skip_ws();
if !self.scan.starts_with(b"version") {
return Err(self.scan.err_with_level(
ErrorLevel::Error,
"XML declaration is missing the required `version` attribute \
(XML 1.0 § 2.8 [XMLDecl])"
));
}
let version = self.consume_xmldecl_attr_value(b"version")?;
if !is_valid_version(&version) {
return Err(self.scan.err_with_level(
ErrorLevel::Error,
format!(
"invalid XML version '{}' (XML 1.0 § 2.8 [26] [VersionNum])",
String::from_utf8_lossy(&version)
),
));
}
let mut encoding_bytes: Option<Vec<u8>> = None;
let mut standalone_bool: Option<bool> = None;
let mut saw_ws = self.scan_skip_ws_returning_count() > 0;
if self.scan.starts_with(b"encoding") {
if !saw_ws {
return Err(self.scan.err_with_level(
ErrorLevel::Error,
"expected whitespace before `encoding` in XML declaration \
(XML 1.0 § 2.8 [XMLDecl])"
));
}
let enc = self.consume_xmldecl_attr_value(b"encoding")?;
if !is_valid_encname(&enc) {
return Err(self.scan.err_with_level(
ErrorLevel::Error,
format!(
"invalid encoding name '{}' (XML 1.0 § 4.3.3 [81] [EncName])",
String::from_utf8_lossy(&enc)
),
));
}
encoding_bytes = Some(enc);
saw_ws = self.scan_skip_ws_returning_count() > 0;
}
if self.scan.starts_with(b"standalone") {
if !saw_ws {
return Err(self.scan.err(
"expected whitespace before `standalone` in XML declaration \
(XML 1.0 § 2.8 [XMLDecl])"
));
}
let sd = self.consume_xmldecl_attr_value(b"standalone")?;
standalone_bool = match &sd[..] {
b"yes" => Some(true),
b"no" => Some(false),
_ => return Err(self.scan.err(format!(
"invalid 'standalone' value '{}' — must be \"yes\" or \"no\" \
(XML 1.0 § 2.9 [32] [SDDecl])",
String::from_utf8_lossy(&sd)
))),
};
}
self.scan.skip_ws();
self.scan.expect_str(b"?>")?;
self.is_xml_11 = version.as_slice() == b"1.1";
self.xml_decl = Some(XmlDeclInfo {
version: String::from_utf8(version).expect("validated ASCII"),
encoding: encoding_bytes.map(|b| String::from_utf8(b).expect("validated ASCII")),
standalone: standalone_bool,
});
self.standalone_yes = standalone_bool == Some(true);
Ok(())
}
fn scan_skip_ws_returning_count(&mut self) -> usize {
let before = self.scan.cur_pos();
self.scan.skip_ws();
self.scan.cur_pos() - before
}
fn consume_xmldecl_attr_value(&mut self, name: &[u8]) -> Result<Vec<u8>> {
self.scan.skip_n(name.len());
self.scan.skip_ws();
self.scan.expect(b'=')?;
self.scan.skip_ws();
let q = match self.scan.advance() {
Some(b @ (b'"' | b'\'')) => b,
_ => return Err(self.scan.err("expected quoted XML-decl value")),
};
let val_start = self.scan.cur_pos();
match memchr(q, self.scan.cur_tail()) {
None => Err(self.scan.err("unterminated XML-decl value")),
Some(off) => {
let end = val_start + off;
let bytes = self.scan.cur_slice(val_start, end).into_owned();
self.scan.cur_set_pos(end + 1);
Ok(bytes)
}
}
}
fn skip_misc(&mut self) -> Result<()> {
loop {
self.scan.skip_ws();
if self.scan.starts_with(b"<!DOCTYPE") || self.scan.starts_with(b"<!doctype") {
self.parse_doctype()?;
} else {
break;
}
}
Ok(())
}
fn skip_quoted(&mut self) -> Result<()> {
let q = match self.scan.advance() {
Some(b @ (b'"' | b'\'')) => b,
_ => return Err(self.scan.err("expected quoted value")),
};
match memchr(q, self.scan.cur_tail()) {
None => Err(self.scan.err("unterminated quoted value")),
Some(off) => {
self.scan.cur_advance_pos(off + 1);
Ok(())
}
}
}
fn capture_quoted(&mut self) -> Result<String> {
let q = match self.scan.advance() {
Some(b @ (b'"' | b'\'')) => b,
_ => return Err(self.scan.err("expected quoted value")),
};
match memchr(q, self.scan.cur_tail()) {
None => Err(self.scan.err("unterminated quoted value")),
Some(off) => {
let bytes = self.scan.cur_slice(self.scan.cur_pos(), self.scan.cur_pos() + off);
let s = String::from_utf8_lossy(&bytes).into_owned();
self.scan.cur_advance_pos(off + 1);
Ok(s)
}
}
}
fn read_system_literal(&mut self) -> Result<String> {
let q = match self.scan.advance() {
Some(b @ (b'"' | b'\'')) => b,
_ => return Err(self.scan.err("expected quoted SystemLiteral")),
};
let start = self.scan.cur_pos();
match memchr(q, self.scan.cur_tail()) {
None => Err(self.scan.err("unterminated SystemLiteral")),
Some(off) => {
let end = start + off;
let bytes = &self.scan.cur_bytes()[start..end];
if memchr(b'#', bytes).is_some() {
return Err(self.scan.err(
"URI fragment ('#…') is not allowed in a SystemLiteral \
(XML 1.0 § 4.2.2 [11])"
));
}
let s = unsafe { std::str::from_utf8_unchecked(bytes) }.to_string();
self.scan.cur_advance_pos(off + 1);
Ok(s)
}
}
}
fn read_pubid_literal(&mut self) -> Result<String> {
let q = match self.scan.peek() {
Some(b @ (b'"' | b'\'')) => b,
_ => return Err(self.scan.err("expected quoted PubidLiteral")),
};
self.scan.advance();
let start = self.scan.cur_pos();
let off = memchr(q, self.scan.cur_tail())
.ok_or_else(|| self.scan.err("unterminated PubidLiteral"))?;
let end = start + off;
let bytes = &self.scan.cur_bytes()[start..end];
for &b in bytes {
if !is_pubid_char(b) {
return Err(self.scan.err(format!("invalid PubidChar 0x{b:02X}")));
}
}
let s = unsafe { std::str::from_utf8_unchecked(bytes) }.to_string();
self.scan.cur_advance_pos(off + 1);
Ok(s)
}
fn skip_comment_raw(&mut self) -> Result<()> {
self.scan.expect_str(b"<!--")?;
loop {
match memchr(b'-', self.scan.cur_tail()) {
None => return Err(self.scan.err("unterminated comment")),
Some(off) => {
self.scan.cur_advance_pos(off);
if self.scan.starts_with(b"-->") { self.scan.skip_n(3); return Ok(()); }
if self.scan.starts_with(b"--") { return Err(self.scan.err("'--' inside comment not allowed")); }
self.scan.advance();
}
}
}
}
fn skip_pi_raw(&mut self) -> Result<()> {
self.scan.expect_str(b"<?")?;
let target = self.scan.scan_name_bytes()?;
if target.eq_ignore_ascii_case(b"xml") {
return Err(self.scan.err(
"PI target name 'xml' is reserved (XML 1.0 § 2.6 [17])"
));
}
match self.scan.peek() {
Some(b'?') => {
self.scan.expect_str(b"?>")?;
return Ok(());
}
Some(b' ' | b'\t' | b'\r' | b'\n') => {} Some(b) => return Err(self.scan.err(format!(
"expected whitespace or `?>` after PI target, got '{}' (XML 1.0 § 2.6 [16])",
b as char
))),
None => return Err(self.scan.err("unterminated PI")),
}
loop {
match memchr(b'?', self.scan.cur_tail()) {
None => return Err(self.scan.err("unterminated PI")),
Some(off) => {
self.scan.cur_advance_pos(off);
if self.scan.starts_with(b"?>") { self.scan.skip_n(2); return Ok(()); }
self.scan.advance();
}
}
}
}
fn parse_doctype(&mut self) -> Result<()> {
self.dtd.internal_subset_prolog_index = self.prolog_misc_count;
self.scan.skip_n(9); self.scan.expect_ws()?;
self.scan.skip_ws();
let name_start = self.scan.src_offset();
self.scan.skip_name()?;
let name_end = self.scan.src_offset();
let root_name = unsafe {
std::str::from_utf8_unchecked(&self.scan.src_bytes()[name_start..name_end])
}.to_string();
self.dtd.root_name = root_name;
self.scan.skip_ws();
let mut external_system_id: Option<String> = None;
if self.scan.starts_with(b"SYSTEM") || self.scan.starts_with(b"PUBLIC") {
let is_public = self.scan.starts_with(b"PUBLIC");
self.scan.skip_n(6);
self.scan.expect_ws()?;
self.scan.skip_ws();
if is_public {
let pub_id = self.capture_pubid_literal()?;
self.dtd.public_id = Some(pub_id);
let saw_ws = matches!(self.scan.peek(), Some(b' ' | b'\t' | b'\n' | b'\r'));
self.scan.skip_ws();
if matches!(self.scan.peek(), Some(b'"' | b'\'')) {
if !saw_ws {
return Err(self.scan.err(
"whitespace is required between PubidLiteral and SystemLiteral \
(XML 1.0 § 4.2.2 [75] ExternalID)"
));
}
let sys_id = self.capture_quoted()?;
self.dtd.system_id = Some(sys_id.clone());
if self.scan.opts.load_external_dtd {
external_system_id = Some(sys_id);
}
}
} else {
let sys_id = self.capture_quoted()?;
self.dtd.system_id = Some(sys_id.clone());
if self.scan.opts.load_external_dtd {
external_system_id = Some(sys_id);
}
}
self.scan.skip_ws();
}
if self.scan.peek() == Some(b'[') {
self.scan.advance();
self.parse_internal_subset()?;
}
self.scan.skip_ws();
self.scan.expect(b'>')?;
if let Some(system_id) = external_system_id {
if let Err(e) = self.load_external_subset(&system_id) {
if self.scan.opts.recovery_mode {
self.recovered_errors.push(e);
} else {
return Err(e);
}
}
}
Ok(())
}
fn load_external_subset(&mut self, system_id: &str) -> Result<()> {
let base = self.scan.opts.base_url.clone();
let absolute = resolve_uri(system_id, base.as_deref());
let bytes_result: std::result::Result<Vec<u8>, String> =
if let Some(resolver) = self.scan.opts.external_resolver.clone() {
resolver.resolve(None, &absolute, base.as_deref())
.map_err(|e| e.to_string())
} else if self.scan.opts.load_external_dtd {
if absolute.starts_with("http://") || absolute.starts_with("https://") {
return Ok(());
}
let raw_path: &str = absolute.strip_prefix("file://").unwrap_or(&absolute);
std::fs::read(std::path::Path::new(raw_path)).map_err(|e| e.to_string())
} else {
return Ok(());
};
let bytes = match bytes_result {
Ok(b) => b,
Err(msg) => {
self.recovered_errors.push(
XmlError::new(
ErrorDomain::Dtd,
ErrorLevel::Warning,
format!("external DTD '{system_id}' not loaded: {msg}"),
)
);
return Ok(());
}
};
let text = match crate::encoding::transcode_to_utf8(&bytes)
.map_err(|e| e.message)
.and_then(|c| String::from_utf8(c.into_owned()).map_err(|e| e.to_string()))
{
Ok(s) => s,
Err(msg) => {
self.recovered_errors.push(
XmlError::new(
ErrorDomain::Dtd,
ErrorLevel::Warning,
format!("external DTD '{system_id}' not valid UTF-8: {msg}"),
)
);
return Ok(());
}
};
self.scan.push_entity_stream(
"__external_dtd__".to_string(),
text,
self.depth,
Some(absolute),
)?;
let parse_result = consume_text_decl_if_present(&mut self.scan, self.is_xml_11)
.and_then(|()| self.parse_external_subset_loop());
while !self.scan.on_original_source() {
if !self.scan.try_pop_entity_stream() { break; }
}
parse_result
}
pub(crate) fn parse_standalone_external_subset(&mut self, text: String) -> Result<()> {
self.scan.push_entity_stream(
"__external_dtd__".to_string(),
text,
self.depth,
None,
)?;
let result = consume_text_decl_if_present(&mut self.scan, self.is_xml_11)
.and_then(|()| self.parse_external_subset_loop());
while !self.scan.on_original_source() {
if !self.scan.try_pop_entity_stream() { break; }
}
result
}
fn parse_external_subset_loop(&mut self) -> Result<()> {
loop {
if self.scan.on_original_source() {
return Ok(());
}
self.scan.skip_ws();
if self.scan.peek().is_none() {
if !self.scan.try_pop_entity_stream() {
return Ok(());
}
continue;
}
match self.scan.peek() {
Some(b'<') => {
let start_depth = self.scan.entity_stream_depth();
if self.scan.starts_with(b"<!--") { self.skip_comment_raw()?; }
else if self.scan.starts_with(b"<!ENTITY") { self.parse_entity_decl()?; }
else if self.scan.starts_with(b"<!ATTLIST") { self.parse_attlist_decl()?; }
else if self.scan.starts_with(b"<!ELEMENT") { self.parse_element_decl()?; }
else if self.scan.starts_with(b"<!NOTATION") { self.parse_notation_decl()?; }
else if self.scan.starts_with(b"<?") { self.skip_pi_raw()?; }
else if self.scan.starts_with(b"<![") {
self.parse_conditional_section()?;
}
else {
return Err(self.scan.err(
"unexpected declaration in external DTD subset"
));
}
if self.scan.entity_stream_depth() < start_depth {
return Err(self.scan.err(
"markup declaration is split across a parameter-entity \
boundary (XML 1.0 § 2.8 WFC: PE Between Declarations) — \
the start `<!` and end `>` of a declaration must come \
from the same entity"
));
}
}
Some(b'%') => {
self.expand_pe_ref_at_cursor()?;
}
_ => return Err(self.scan.err("unexpected content in external DTD subset")),
}
}
}
fn expand_pe_ref_at_cursor(&mut self) -> Result<()> {
self.scan.expect(b'%')?;
let name_bytes = self.scan.scan_name_bytes()?;
self.scan.expect(b';')?;
let name = unsafe { std::str::from_utf8_unchecked(&name_bytes) }.to_string();
let kind = match self.parameter_entities.get(&name) {
Some(d) => d.clone(),
None => {
if self.scan.current_base_uri().is_some() {
self.recovered_errors.push(XmlError::new(
ErrorDomain::Parser,
ErrorLevel::Warning,
format!(
"undefined parameter entity '%{name};' inside an external \
entity — WFC: Entity Declared carve-out applies (XML 1.0 § 4.1); \
expansion skipped"
),
));
return Ok(());
}
return Err(self.scan.err(format!(
"undefined parameter entity '%{name};' (XML 1.0 § 4.1 WFC: Entity Declared)"
)));
}
};
self.pe_ref_in_internal_subset_seen = true;
let is_external_value = kind.kind.is_external_value();
let value = match kind.kind {
EntityKind::InternalText(v) | EntityKind::ExternalLoaded(v) => v,
EntityKind::ExternalUnloaded => return Ok(()),
};
let padded = format!(" {value} ");
let depth = self.element_stack.len() as u32;
let frame_base = kind.source_uri.clone();
self.scan.push_entity_stream(name, padded, depth, frame_base)?;
if is_external_value {
consume_text_decl_if_present(&mut self.scan, self.is_xml_11)?;
}
Ok(())
}
fn skip_ws_and_pe_refs(&mut self) -> Result<()> {
loop {
self.scan.skip_ws();
if self.scan.peek().is_none() {
if self.scan.on_original_source() { return Ok(()); }
if !self.scan.try_pop_entity_stream() { return Ok(()); }
continue;
}
if self.scan.peek() != Some(b'%') { return Ok(()); }
if self.scan.on_original_source() { return Ok(()); }
self.expand_pe_ref_at_cursor()?;
}
}
fn expect_ws_with_pe(&mut self) -> Result<()> {
if self.scan.peek() == Some(b'%') && !self.scan.on_original_source() {
self.expand_pe_ref_at_cursor()?;
}
self.scan.expect_ws()?;
self.skip_ws_and_pe_refs()
}
fn expand_entity_value(&self, bytes: &[u8]) -> std::result::Result<Vec<u8>, String> {
let mut out = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
match b {
b'&' => {
let after = i + 1;
if after >= bytes.len() {
return Err("entity value ends with `&`".to_string());
}
if bytes[after] != b'#' {
let semi = bytes[after..].iter().position(|&c| c == b';')
.ok_or_else(|| "named entity reference in entity value missing `;`".to_string())?;
out.extend_from_slice(&bytes[i..after + semi + 1]);
i = after + semi + 1;
} else {
let body_start = after + 1;
let semi = bytes[body_start..].iter().position(|&c| c == b';')
.ok_or_else(|| "character reference missing `;`".to_string())?;
let body = &bytes[body_start..body_start + semi];
let cp: u32 = if body.first() == Some(&b'x') || body.first() == Some(&b'X') {
std::str::from_utf8(&body[1..]).ok()
.and_then(|h| u32::from_str_radix(h, 16).ok())
.ok_or_else(|| format!(
"invalid hex character reference '&#{}'",
String::from_utf8_lossy(body)
))?
} else {
std::str::from_utf8(body).ok()
.and_then(|d| d.parse::<u32>().ok())
.ok_or_else(|| format!(
"invalid decimal character reference '&#{}'",
String::from_utf8_lossy(body)
))?
};
let ch = char::from_u32(cp).ok_or_else(|| format!(
"character reference '&#{};' is not a valid Unicode scalar", cp
))?;
let mut tmp = [0u8; 4];
out.extend_from_slice(ch.encode_utf8(&mut tmp).as_bytes());
i = body_start + semi + 1;
}
}
b'%' => {
let semi = bytes[i + 1..].iter().position(|&c| c == b';')
.ok_or_else(|| "PE reference in entity value missing `;`".to_string())?;
let name_bytes = &bytes[i + 1..i + 1 + semi];
let name = std::str::from_utf8(name_bytes)
.map_err(|e| format!("PE name not valid UTF-8: {e}"))?;
match self.parameter_entities.get(name).map(|d| &d.kind) {
Some(EntityKind::InternalText(v))
| Some(EntityKind::ExternalLoaded(v)) => {
out.extend_from_slice(v.as_bytes());
}
Some(EntityKind::ExternalUnloaded) => {
}
None => {
if self.scan.current_base_uri().is_some() {
} else {
return Err(format!(
"undefined parameter entity '%{name};' \
(XML 1.0 § 4.1 WFC: Entity Declared)"
));
}
}
}
i = i + 1 + semi + 1;
}
_ => {
out.push(b);
i += 1;
}
}
}
Ok(out)
}
fn parse_conditional_section(&mut self) -> Result<()> {
self.scan.skip_n(3); self.skip_ws_and_pe_refs()?;
let kw_start = self.scan.cur_pos();
if self.scan.skip_name().is_err() {
return Err(self.scan.err(
"conditional section needs a keyword after '<![' \
(XML 1.0 § 3.4 [62])"
));
}
let kw_end = self.scan.cur_pos();
let kw = self.scan.cur_slice(kw_start, kw_end).to_vec();
let is_include = match kw.as_slice() {
b"INCLUDE" => true,
b"IGNORE" => false,
other => return Err(self.scan.err(format!(
"conditional section keyword must be INCLUDE or IGNORE, got {:?} \
(XML 1.0 § 3.4 [62])",
String::from_utf8_lossy(other),
))),
};
self.skip_ws_and_pe_refs()?;
if !self.scan.starts_with(b"[") {
return Err(self.scan.err(
"expected '[' after INCLUDE/IGNORE in conditional section \
(XML 1.0 § 3.4 [62])"
));
}
self.scan.skip_n(1);
if is_include {
self.parse_include_section_body()
} else {
self.skip_ignore_section_body()
}
}
fn parse_include_section_body(&mut self) -> Result<()> {
loop {
self.scan.skip_ws();
if self.scan.starts_with(b"]]>") {
self.scan.skip_n(3);
return Ok(());
}
if self.scan.peek().is_none() {
if self.scan.on_original_source() {
return Err(self.scan.err(
"unterminated INCLUDE conditional section (expected ']]>')"
));
}
if !self.scan.try_pop_entity_stream() {
return Err(self.scan.err(
"unterminated INCLUDE conditional section (expected ']]>')"
));
}
continue;
}
match self.scan.peek() {
Some(b'<') => {
if self.scan.starts_with(b"<!--") { self.skip_comment_raw()?; }
else if self.scan.starts_with(b"<!ENTITY") { self.parse_entity_decl()?; }
else if self.scan.starts_with(b"<!ATTLIST") { self.parse_attlist_decl()?; }
else if self.scan.starts_with(b"<!ELEMENT") { self.parse_element_decl()?; }
else if self.scan.starts_with(b"<!NOTATION") { self.parse_notation_decl()?; }
else if self.scan.starts_with(b"<?") { self.skip_pi_raw()?; }
else if self.scan.starts_with(b"<![") { self.parse_conditional_section()?; }
else {
return Err(self.scan.err(
"unexpected declaration in INCLUDE conditional section"
));
}
}
Some(b'%') => self.expand_pe_ref_at_cursor()?,
_ => return Err(self.scan.err(
"unexpected content in INCLUDE conditional section"
)),
}
}
}
fn skip_ignore_section_body(&mut self) -> Result<()> {
let mut depth = 1usize;
loop {
let tail = self.scan.cur_tail();
if tail.is_empty() {
if self.scan.on_original_source() {
return Err(self.scan.err(
"unterminated IGNORE conditional section (expected ']]>')"
));
}
if !self.scan.try_pop_entity_stream() {
return Err(self.scan.err(
"unterminated IGNORE conditional section (expected ']]>')"
));
}
continue;
}
if tail.starts_with(b"<![") {
depth += 1;
self.scan.skip_n(3);
} else if tail.starts_with(b"]]>") {
self.scan.skip_n(3);
depth -= 1;
if depth == 0 { return Ok(()); }
} else {
self.scan.advance();
}
}
}
fn skip_pubid_literal(&mut self) -> Result<()> {
self.capture_pubid_literal().map(|_| ())
}
fn capture_pubid_literal(&mut self) -> Result<String> {
let q = match self.scan.advance() {
Some(b @ (b'"' | b'\'')) => b,
_ => return Err(self.scan.err("expected PubidLiteral")),
};
let tail = self.scan.cur_tail();
let off = memchr(q, tail).ok_or_else(|| self.scan.err("unterminated PubidLiteral"))?;
let body = &tail[..off];
for &b in body {
if !is_pubid_char(b) {
return Err(self.scan.err(format!("invalid PubidChar 0x{b:02X}")));
}
}
let out = unsafe { std::str::from_utf8_unchecked(body) }.to_string();
self.scan.cur_advance_pos(off + 1);
Ok(out)
}
fn capture_internal_decl(&mut self, decl_on_src: bool, decl_start: usize) {
if !decl_on_src || !self.scan.on_original_source() {
return;
}
let end = self.scan.cur_pos();
if end > decl_start {
let text = self.scan.original_slice(decl_start, end);
if !text.is_empty() {
self.dtd.internal_decls.push(text.to_string());
}
}
}
fn parse_internal_subset(&mut self) -> Result<()> {
loop {
self.scan.skip_ws();
while self.scan.peek().is_none() && !self.scan.on_original_source() {
if !self.scan.try_pop_entity_stream() { break; }
self.scan.skip_ws();
}
match self.scan.peek() {
None => return Err(self.scan.err("unterminated DOCTYPE internal subset")),
Some(b']') => { self.scan.advance(); return Ok(()); }
Some(b'<') => {
let decl_on_src = self.scan.on_original_source();
let decl_start = self.scan.cur_pos();
if self.scan.starts_with(b"<!--") { self.skip_comment_raw()?; }
else if self.scan.starts_with(b"<!ENTITY") { self.parse_entity_decl()?; self.capture_internal_decl(decl_on_src, decl_start); }
else if self.scan.starts_with(b"<!ATTLIST") { self.parse_attlist_decl()?; self.capture_internal_decl(decl_on_src, decl_start); }
else if self.scan.starts_with(b"<!ELEMENT") { self.parse_element_decl()?; self.capture_internal_decl(decl_on_src, decl_start); }
else if self.scan.starts_with(b"<!NOTATION"){ self.parse_notation_decl()?; self.capture_internal_decl(decl_on_src, decl_start); }
else if self.scan.starts_with(b"<![") {
if self.scan.on_original_source() {
return Err(self.scan.err(
"conditional sections are only allowed in the external DTD subset \
(XML 1.0 § 3.4 [62])"
));
}
self.parse_conditional_section()?;
}
else if self.scan.starts_with(b"<?") { self.skip_pi_raw()?; }
else {
return Err(self.scan.err(
"unexpected declaration in DOCTYPE internal subset"
));
}
}
Some(b'%') => {
self.scan.advance();
let name_bytes = self.scan.scan_name_bytes()?;
self.scan.expect(b';')?;
let name_str = unsafe {
std::str::from_utf8_unchecked(&name_bytes)
};
self.pe_ref_in_internal_subset_seen = true;
let decl = match self.parameter_entities.get(name_str) {
Some(d) => d.clone(),
None => {
if self.scan.current_base_uri().is_some() {
self.recovered_errors.push(XmlError::new(
ErrorDomain::Parser,
ErrorLevel::Warning,
format!(
"undefined parameter entity '%{name_str};' \
inside an external entity — WFC: Entity \
Declared carve-out applies (XML 1.0 § 4.1); \
expansion skipped"
),
));
continue;
}
return Err(self.scan.err(format!(
"undefined parameter entity '%{name_str};' \
(XML 1.0 § 4.1 WFC: Entity Declared)"
)));
}
};
let is_external = decl.kind.is_external_value();
let frame_base = decl.source_uri.clone();
let value = match decl.kind {
EntityKind::InternalText(v) | EntityKind::ExternalLoaded(v) => v,
EntityKind::ExternalUnloaded => {
continue;
}
};
let depth = self.element_stack.len() as u32;
self.scan.push_entity_stream(name_str.to_string(), value, depth, frame_base)?;
if is_external {
consume_text_decl_if_present(&mut self.scan, self.is_xml_11)?;
}
}
_ => return Err(self.scan.err("unexpected content in DOCTYPE internal subset")),
}
}
}
fn parse_entity_decl(&mut self) -> Result<()> {
let declared_external = !self.scan.on_original_source();
self.scan.skip_n(8); self.scan.expect_ws()?;
self.scan.skip_ws();
let is_param = self.scan.peek() == Some(b'%');
if is_param { self.scan.advance(); self.scan.expect_ws()?; self.scan.skip_ws(); }
let name_bytes = self.scan.scan_name_bytes()?;
if self.scan.opts.namespace_aware && name_bytes.contains(&b':') {
return Err(self.scan.err(format!(
"entity name '{}' must be an NCName (no colon) under \
XML Namespaces 1.0",
String::from_utf8_lossy(&name_bytes)
)));
}
let name = unsafe { std::str::from_utf8_unchecked(&name_bytes) }.to_string();
self.scan.expect_ws()?;
self.scan.skip_ws();
let mut is_external = false;
let mut external_public_id: Option<String> = None;
let mut external_system_id: Option<String> = None;
let model_name = name.clone();
let mut ent_orig: Option<String> = None;
let mut ent_content: Option<String> = None;
let mut ent_ndata: Option<String> = None;
if self.scan.starts_with(b"SYSTEM") {
is_external = true;
self.scan.skip_n(6);
self.scan.expect_ws()?;
self.scan.skip_ws();
if !matches!(self.scan.peek(), Some(b'"' | b'\'')) {
return Err(self.scan.err(
"SYSTEM identifier must be a quoted SystemLiteral (XML 1.0 § 4.2.2 [11])"
));
}
external_system_id = Some(self.read_system_literal()?);
} else if self.scan.starts_with(b"PUBLIC") {
is_external = true;
self.scan.skip_n(6);
self.scan.expect_ws()?;
self.scan.skip_ws();
external_public_id = Some(self.read_pubid_literal()?);
self.scan.expect_ws()?;
self.scan.skip_ws();
if !matches!(self.scan.peek(), Some(b'"' | b'\'')) {
return Err(self.scan.err(
"PUBLIC requires a SystemLiteral after the PubidLiteral (XML 1.0 § 4.2.2 [75])"
));
}
external_system_id = Some(self.read_system_literal()?);
} else {
let q = match self.scan.peek() {
Some(b @ (b'"' | b'\'')) => { self.scan.advance(); b }
_ => return Err(self.scan.err("expected quoted entity value")),
};
let val_start = self.scan.cur_pos();
while !self.scan.is_eof() && self.scan.peek() != Some(q) {
let b = self.scan.peek().unwrap();
if b == b'%' && self.scan.on_original_source() {
return Err(self.scan.err(
"parameter-entity reference '%…;' inside an entity value \
is forbidden in the internal DTD subset \
(XML 1.0 § 2.8 WFC: PEs in Internal Subset)"
));
}
if b == b'&' || b == b'%' {
self.scan.advance();
if self.scan.peek() == Some(b'#') && b == b'&' {
self.scan.advance();
if self.scan.peek() == Some(b'x') || self.scan.peek() == Some(b'X') {
self.scan.advance();
while matches!(self.scan.peek(),
Some(c) if (c as char).is_ascii_hexdigit())
{
self.scan.advance();
}
} else {
while matches!(self.scan.peek(),
Some(c) if (c as char).is_ascii_digit())
{
self.scan.advance();
}
}
if self.scan.peek() != Some(b';') {
return Err(self.scan.err(
"invalid character reference in entity value (missing ';')"
));
}
self.scan.advance();
} else {
self.scan.skip_name()?;
if self.scan.peek() != Some(b';') {
return Err(self.scan.err(format!(
"bare '{}' in entity value — must be a valid reference (XML 1.0 § 2.3 [9])",
b as char
)));
}
self.scan.advance();
}
} else {
self.scan.advance();
}
}
let value_bytes = self.scan.cur_slice(val_start, self.scan.cur_pos());
self.scan.expect(q)?;
let replacement = self.expand_entity_value(&value_bytes)
.map_err(|msg| self.scan.err(msg))?;
let value = unsafe { String::from_utf8_unchecked(replacement) };
ent_orig = Some(String::from_utf8_lossy(&value_bytes).into_owned());
ent_content = Some(value.clone());
let decl = EntityDecl {
kind: EntityKind::InternalText(value),
declared_external,
source_uri: None,
};
if is_param {
self.parameter_entities.entry(name.clone()).or_insert(decl);
} else {
self.entities.entry(name.clone()).or_insert(decl);
}
}
let saw_ws_before_ndata = {
let before = self.scan.cur_pos();
self.scan.skip_ws();
self.scan.cur_pos() != before
};
let mut is_unparsed = false;
if self.scan.starts_with(b"NDATA") {
if !saw_ws_before_ndata {
return Err(self.scan.err(
"whitespace is required before `NDATA` (XML 1.0 § 4.2.2 [76])"
));
}
if is_param {
return Err(self.scan.err(
"NDATA annotation is not allowed on parameter entities (XML 1.0 § 4.2 [74])"
));
}
if !is_external {
return Err(self.scan.err(
"NDATA is only allowed on external (SYSTEM/PUBLIC) general \
entities, not on internal EntityValue declarations \
(XML 1.0 § 4.2.2 [73])"
));
}
self.scan.skip_n(5);
self.scan.expect_ws()?;
self.scan.skip_ws();
let ndata_bytes = self.scan.scan_name_bytes()?;
ent_ndata = Some(String::from_utf8_lossy(&ndata_bytes).into_owned());
self.scan.skip_ws();
if let Some(sys) = &external_system_id {
self.dtd.unparsed_entities
.entry(name.clone())
.or_insert_with(|| sup_xml_tree::UnparsedEntity {
system_id: sys.clone(),
public_id: external_public_id.clone(),
});
}
is_unparsed = true;
}
if is_external && !is_unparsed {
if !is_param && self.scan.opts.external_resolver.is_some() {
if self.scan.opts.resolve_external_entities {
let sys = external_system_id.as_deref().unwrap_or("");
self.deferred_general_entities.insert(
name.clone(),
DeferredExternal {
system_id: sys.to_string(),
public_id: external_public_id.clone(),
},
);
}
self.entities.insert(name, EntityDecl {
kind: EntityKind::ExternalUnloaded,
declared_external,
source_uri: None,
});
} else if let Some(resolver) = self.scan.opts.external_resolver.clone()
.filter(|_| self.scan.opts.load_external_dtd || self.scan.opts.validating)
{
let sys = external_system_id.as_deref().unwrap_or("");
let pid = external_public_id.as_deref();
let base: Option<String> = if is_param {
self.scan.current_base_uri()
.map(str::to_string)
.or_else(|| self.scan.opts.base_url.clone())
} else {
self.scan.opts.base_url.clone()
};
let absolute = resolve_uri(sys, base.as_deref());
match resolver.resolve(pid, &absolute, base.as_deref()) {
Ok(bytes) => {
let transcoded: std::result::Result<Vec<u8>, String> =
match crate::encoding::transcode_to_utf8(&bytes) {
Ok(c) => Ok(c.into_owned()),
Err(e) => Err(e.message.clone()),
};
let value = transcoded.and_then(|v| {
String::from_utf8(v).map_err(|e| e.to_string())
});
match value {
Ok(v) => {
let decl = EntityDecl {
kind: EntityKind::ExternalLoaded(v),
declared_external,
source_uri: Some(absolute.clone()),
};
if is_param {
self.parameter_entities.insert(name, decl);
} else {
self.entities.insert(name, decl);
}
}
Err(msg) => {
let err = self.scan.err_with_level(
ErrorLevel::Error,
format!(
"external entity '&{name};' is not valid UTF-8 \
(system_id={sys:?}): {msg}"
),
);
self.maybe_recover(err)?;
if !is_param {
self.entities.insert(name, EntityDecl {
kind: EntityKind::ExternalUnloaded,
declared_external,
source_uri: None,
});
}
}
}
}
Err(e) => {
let err = self.scan.err_with_level(
ErrorLevel::Error,
format!(
"external resolver failed to load entity '&{name};' \
(system_id={sys:?}, public_id={pid:?}): {e}"
),
);
self.maybe_recover(err)?;
if !is_param {
self.entities.insert(name, EntityDecl {
kind: EntityKind::ExternalUnloaded,
declared_external,
source_uri: None,
});
}
}
}
} else if self.scan.opts.load_external_dtd && !is_param {
let sys = external_system_id.as_deref().unwrap_or("");
let base = self.scan.opts.base_url.clone();
let absolute = resolve_uri(sys, base.as_deref());
match read_external_entity_bytes(&absolute, None)
.and_then(|b| String::from_utf8(b).map_err(|e| e.to_string()))
{
Ok(value) => {
self.entities.insert(name, EntityDecl {
kind: EntityKind::ExternalLoaded(value),
declared_external,
source_uri: Some(absolute),
});
}
Err(msg) => {
self.recovered_errors.push(XmlError::new(
ErrorDomain::Parser,
ErrorLevel::Warning,
format!("external entity '&{name};' SYSTEM {sys:?} not loaded: {msg}"),
));
self.entities.insert(name, EntityDecl {
kind: EntityKind::ExternalUnloaded,
declared_external,
source_uri: None,
});
}
}
} else if !is_param {
self.entities.insert(name, EntityDecl {
kind: EntityKind::ExternalUnloaded,
declared_external,
source_uri: None,
});
} else {
self.parameter_entities.insert(name, EntityDecl {
kind: EntityKind::ExternalUnloaded,
declared_external,
source_uri: None,
});
}
}
if !self.dtd.entities.iter().any(|e| e.name == model_name && e.parameter == is_param) {
let idx = self.dtd.entities.len();
self.dtd.entities.push(crate::dtd::model::EntityDecl {
name: model_name,
parameter: is_param,
orig: ent_orig,
content: ent_content,
system_id: external_system_id.clone(),
public_id: external_public_id.clone(),
ndata: ent_ndata,
});
self.dtd.decl_order.push(crate::dtd::model::DeclRef::Entity(idx));
}
self.scan.expect(b'>')
}
fn parse_attlist_decl(&mut self) -> Result<()> {
use crate::dtd::AttDecl;
self.scan.skip_n(9); self.expect_ws_with_pe()?;
let elem_name = self.scan.scan_name()?.into_owned();
let mut decls: Vec<AttDecl> = Vec::new();
loop {
self.skip_ws_and_pe_refs()?;
if self.scan.peek() == Some(b'>') {
self.scan.advance();
if !decls.is_empty() {
self.dtd.add_attlist(elem_name, decls);
}
return Ok(());
}
let attr_name = self.scan.scan_name()?.into_owned();
self.expect_ws_with_pe()?;
let att_type = self.parse_att_type()?;
self.expect_ws_with_pe()?;
let default = self.parse_att_default()?;
decls.push(AttDecl { name: attr_name, att_type, default });
}
}
fn parse_att_type(&mut self) -> Result<crate::dtd::AttType> {
use crate::dtd::AttType;
if self.scan.peek() == Some(b'(') {
self.scan.advance();
let mut values: Vec<String> = Vec::new();
loop {
self.scan.skip_ws();
values.push(self.scan.scan_nmtoken()?.into_owned());
self.scan.skip_ws();
match self.scan.peek() {
Some(b')') => { self.scan.advance(); return Ok(AttType::Enumeration(values)); }
Some(b'|') => { self.scan.advance(); }
_ => return Err(self.scan.err(
"invalid enumerated attribute type — expected `|` or `)` (XML 1.0 § 3.3.1 [59])"
)),
}
}
}
let kw = self.scan.scan_name_bytes()?;
match &kw[..] {
b"CDATA" => Ok(AttType::CData),
b"ID" => Ok(AttType::Id),
b"IDREF" => Ok(AttType::IdRef),
b"IDREFS" => Ok(AttType::IdRefs),
b"ENTITY" => Ok(AttType::Entity),
b"ENTITIES" => Ok(AttType::Entities),
b"NMTOKEN" => Ok(AttType::Nmtoken),
b"NMTOKENS" => Ok(AttType::Nmtokens),
b"NOTATION" => {
self.scan.expect_ws()?;
self.scan.skip_ws();
self.scan.expect(b'(')?;
let mut values: Vec<String> = Vec::new();
loop {
self.scan.skip_ws();
values.push(self.scan.scan_name()?.into_owned());
self.scan.skip_ws();
match self.scan.peek() {
Some(b')') => { self.scan.advance(); return Ok(AttType::Notation(values)); }
Some(b'|') => { self.scan.advance(); }
_ => return Err(self.scan.err(
"invalid NOTATION enumeration — expected `|` or `)`"
)),
}
}
}
other => Err(self.scan.err(format!(
"invalid attribute type '{}' — not allowed in XML 1.0 (SGML-only or unknown); \
valid types are CDATA, ID, IDREF, IDREFS, ENTITY, ENTITIES, NMTOKEN, NMTOKENS, \
NOTATION, or `(enum)` (XML 1.0 § 3.3.1)",
String::from_utf8_lossy(other)
))),
}
}
fn parse_att_default(&mut self) -> Result<crate::dtd::AttDefault> {
use crate::dtd::AttDefault;
if self.scan.peek() == Some(b'#') {
self.scan.advance();
let kw = self.scan.scan_name_bytes()?;
match &kw[..] {
b"REQUIRED" => Ok(AttDefault::Required),
b"IMPLIED" => Ok(AttDefault::Implied),
b"FIXED" => {
self.scan.expect_ws()?;
self.scan.skip_ws();
let v = self.validate_att_default_value()?;
Ok(AttDefault::Fixed(v))
}
other => Err(self.scan.err(format!(
"invalid attribute default '#{}' — must be #REQUIRED, #IMPLIED, or #FIXED (XML 1.0 § 3.3.2 [60])",
String::from_utf8_lossy(other)
))),
}
} else {
let v = self.validate_att_default_value()?;
Ok(AttDefault::Default(v))
}
}
fn validate_att_default_value(&mut self) -> Result<String> {
let q = match self.scan.advance() {
Some(b @ (b'"' | b'\'')) => b,
_ => return Err(self.scan.err("expected quoted ATTLIST default value")),
};
let val_start = self.scan.cur_pos();
match memchr(q, self.scan.cur_tail()) {
None => Err(self.scan.err("unterminated ATTLIST default value")),
Some(off) => {
let val_end = val_start + off;
let value_bytes = self.scan.cur_slice(val_start, val_end).into_owned();
let mut buf: Vec<u8> = b"_=".to_vec();
buf.push(q);
buf.extend_from_slice(&value_bytes);
buf.push(q);
let inside_external = self.scan.current_base_uri().is_some();
validate_attrs_syntax(&buf, &self.scan.opts, &self.entities, inside_external)
.map_err(|msg| self.scan.err(msg))?;
self.scan.cur_set_pos(val_end + 1);
Ok(String::from_utf8_lossy(&value_bytes).into_owned())
}
}
}
fn parse_element_decl(&mut self) -> Result<()> {
use crate::dtd::{ContentModel, ElementDecl};
self.scan.skip_n(9); self.expect_ws_with_pe()?;
let name = self.scan.scan_name()?.into_owned();
self.expect_ws_with_pe()?;
let content = match self.scan.peek() {
Some(b'(') => self.parse_content_model()?,
Some(_) => {
let kw = self.scan.scan_name_bytes()?;
match &kw[..] {
b"EMPTY" => ContentModel::Empty,
b"ANY" => ContentModel::Any,
other => return Err(self.scan.err(format!(
"invalid content model '{}' — must be EMPTY, ANY, or `(...)` (XML 1.0 § 3.2 [46])",
String::from_utf8_lossy(other)
))),
}
}
None => return Err(self.scan.err("unterminated <!ELEMENT> declaration")),
};
self.skip_ws_and_pe_refs()?;
self.scan.expect(b'>')?;
self.dtd.add_element(ElementDecl { name, content });
Ok(())
}
fn parse_content_model(&mut self) -> Result<crate::dtd::ContentModel> {
use crate::dtd::{ContentModel, Group, GroupKind, Occurrence};
self.scan.expect(b'(')?;
self.skip_ws_and_pe_refs()?;
if self.scan.starts_with(b"#PCDATA") {
self.scan.skip_n(b"#PCDATA".len());
let mut choices: Vec<String> = Vec::new();
loop {
self.skip_ws_and_pe_refs()?;
match self.scan.peek() {
Some(b')') => {
self.scan.advance();
if choices.is_empty() {
if self.scan.peek() == Some(b'*') {
self.scan.advance();
}
} else {
if self.scan.peek() != Some(b'*') {
return Err(self.scan.err(
"Mixed content model with alternatives must end \
with `)*` (XML 1.0 § 3.2.2 [51])"
));
}
self.scan.advance();
}
return Ok(ContentModel::Mixed { choices });
}
Some(b'|') => {
self.scan.advance();
self.skip_ws_and_pe_refs()?;
choices.push(self.scan.scan_name()?.into_owned());
}
_ => return Err(self.scan.err(
"invalid mixed content model — expected `|` or `)` (XML 1.0 § 3.2.2 [51])"
)),
}
}
}
let mut items: Vec<crate::dtd::Particle> = Vec::new();
items.push(self.parse_cp(0)?);
let sep = {
self.skip_ws_and_pe_refs()?;
match self.scan.peek() {
Some(b')') => 0u8,
Some(b @ (b',' | b'|')) => b,
_ => return Err(self.scan.err(
"invalid children content model — expected `,`, `|`, or `)` (XML 1.0 § 3.2.1 [49/50])"
)),
}
};
if sep != 0 {
loop {
self.scan.advance(); self.skip_ws_and_pe_refs()?;
items.push(self.parse_cp(0)?);
self.skip_ws_and_pe_refs()?;
match self.scan.peek() {
Some(b')') => break,
Some(b) if b == sep => continue,
Some(b) => return Err(self.scan.err(format!(
"inconsistent separator in children content model — \
got `{}`, expected `{}` or `)` (XML 1.0 § 3.2.1)",
b as char, sep as char
))),
None => return Err(self.scan.err("unterminated content model")),
}
}
}
self.scan.expect(b')')?;
let outer_occ = read_occurrence(&mut self.scan);
let kind = match sep {
b'|' => GroupKind::Choice,
_ => GroupKind::Sequence,
};
Ok(ContentModel::Children(Group {
kind,
items,
occur: outer_occ.unwrap_or(Occurrence::One),
}))
}
fn parse_cp(&mut self, depth: u32) -> Result<crate::dtd::Particle> {
use crate::dtd::{Item, Occurrence, Particle};
let item = if self.scan.peek() == Some(b'(') {
Item::Group(Box::new(self.parse_content_model_inner(depth + 1)?))
} else {
Item::Name(self.scan.scan_name()?.into_owned())
};
let occur = read_occurrence(&mut self.scan).unwrap_or(Occurrence::One);
Ok(Particle { item, occur })
}
fn parse_content_model_inner(&mut self, depth: u32) -> Result<crate::dtd::Group> {
use crate::dtd::{Group, GroupKind, Occurrence, Particle};
if depth > MAX_CONTENT_MODEL_DEPTH {
return Err(self.scan.err(format!(
"content model nesting depth exceeds limit ({MAX_CONTENT_MODEL_DEPTH})"
)));
}
self.scan.expect(b'(')?;
self.skip_ws_and_pe_refs()?;
if self.scan.starts_with(b"#PCDATA") {
return Err(self.scan.err(
"#PCDATA is only allowed at the top level of a content model, \
not inside a nested group (XML 1.0 § 3.2.2 [51])"
));
}
let mut items: Vec<Particle> = Vec::new();
items.push(self.parse_cp(depth)?);
let sep = {
self.skip_ws_and_pe_refs()?;
match self.scan.peek() {
Some(b')') => 0u8,
Some(b @ (b',' | b'|')) => b,
_ => return Err(self.scan.err(
"invalid children content model — expected `,`, `|`, or `)`"
)),
}
};
if sep != 0 {
loop {
self.scan.advance();
self.skip_ws_and_pe_refs()?;
items.push(self.parse_cp(depth)?);
self.skip_ws_and_pe_refs()?;
match self.scan.peek() {
Some(b')') => break,
Some(b) if b == sep => continue,
Some(b) => return Err(self.scan.err(format!(
"inconsistent separator in content model — got `{}`, expected `{}` or `)`",
b as char, sep as char
))),
None => return Err(self.scan.err("unterminated content model")),
}
}
}
self.scan.expect(b')')?;
let occur = read_occurrence(&mut self.scan).unwrap_or(Occurrence::One);
let kind = if sep == b'|' { GroupKind::Choice } else { GroupKind::Sequence };
Ok(Group { kind, items, occur })
}
fn parse_notation_decl(&mut self) -> Result<()> {
self.scan.skip_n(10); self.expect_ws_with_pe()?;
let notation_name = self.scan.scan_name_bytes()?;
if self.scan.opts.namespace_aware && notation_name.contains(&b':') {
return Err(self.scan.err(format!(
"notation name '{}' must be an NCName (no colon) under \
XML Namespaces 1.0",
String::from_utf8_lossy(¬ation_name)
)));
}
self.expect_ws_with_pe()?;
if self.scan.starts_with(b"PUBLIC") {
self.scan.skip_n(6);
self.expect_ws_with_pe()?;
self.skip_pubid_literal()?;
self.skip_ws_and_pe_refs()?;
if matches!(self.scan.peek(), Some(b'"' | b'\'')) {
self.skip_quoted()?;
self.skip_ws_and_pe_refs()?;
}
} else if self.scan.starts_with(b"SYSTEM") {
self.scan.skip_n(6);
self.expect_ws_with_pe()?;
self.skip_quoted()?;
self.skip_ws_and_pe_refs()?;
} else {
return Err(self.scan.err(
"<!NOTATION> requires PUBLIC or SYSTEM (XML 1.0 § 4.7 [82])"
));
}
self.scan.expect(b'>')
}
fn read_start_element(&mut self) -> Result<BytesEvent<'_, 'src>> {
self.depth += 1;
if self.depth > self.scan.opts.max_element_depth {
self.depth -= 1;
return Err(self.scan.err(format!("element nesting too deep (limit {})", self.scan.opts.max_element_depth)));
}
if self.depth == 1 {
self.root_seen = true;
}
let track_stack = !self.scan.opts.skip_end_tag_check;
self.scan.cur_advance_pos(1);
let (name_start_us, name_end_us) = self.scan.scan_name_raw()?;
let name_start = name_start_us as u32;
let name_end = name_end_us as u32;
let start_stream_depth = self.scan.stream_depth();
if track_stack {
let entry = if !self.scan.opts.stream_owned_names && self.scan.on_original_source() {
ElementStackEntry::SourceRange(name_start, name_end)
} else {
let bytes = &self.scan.cur_bytes()[name_start as usize..name_end as usize];
ElementStackEntry::Owned(
unsafe { std::str::from_utf8_unchecked(bytes) }.to_string()
)
};
if let Some(parent) = self.frame_saw_child.last_mut() {
*parent = true;
}
self.element_stack.push(entry);
self.element_streams.push(start_stream_depth);
self.frame_saw_child.push(false);
}
let attrs_start = self.scan.cur_pos();
let (attrs_end, self_closing) = 'scan: {
let tail = self.scan.cur_tail();
match memchr(b'>', tail) {
None => return Err(self.scan.err("unterminated start tag")),
Some(off) => {
let prefix = &tail[..off];
let mut dq = 0u32;
let mut sq = 0u32;
for off2 in memchr::memchr2_iter(b'"', b'\'', prefix) {
match unsafe { *prefix.get_unchecked(off2) } {
b'"' => dq += 1,
_ => sq += 1,
}
}
if (dq | sq) & 1 == 0 {
self.scan.cur_advance_pos(off);
let self_closing = self.scan.cur_pos() > attrs_start
&& self.scan.cur_bytes()[self.scan.cur_pos() - 1] == b'/';
let attrs_end = if self_closing { self.scan.cur_pos() - 1 } else { self.scan.cur_pos() };
self.scan.cur_advance_pos(1);
break 'scan (attrs_end, self_closing);
}
}
}
loop {
let tail = self.scan.cur_tail();
match memchr3(b'>', b'\'', b'"', tail) {
None => return Err(self.scan.err("unterminated start tag")),
Some(off) => {
self.scan.cur_advance_pos(off);
match self.scan.cur_bytes()[self.scan.cur_pos()] {
b'>' => {
let self_closing = self.scan.cur_pos() > attrs_start
&& self.scan.cur_bytes()[self.scan.cur_pos() - 1] == b'/';
let attrs_end = if self_closing { self.scan.cur_pos() - 1 } else { self.scan.cur_pos() };
self.scan.cur_advance_pos(1);
break 'scan (attrs_end, self_closing);
}
quote @ (b'\'' | b'"') => {
self.scan.cur_advance_pos(1);
let inside = self.scan.cur_tail();
match memchr(quote, inside) {
None => return Err(self.scan.err("unterminated attribute value")),
Some(off2) => self.scan.cur_advance_pos(off2 + 1),
}
}
_ => unreachable!(),
}
}
}
}
};
let cur_for_validate = self.scan.cur_bytes();
let attrs_slice = &cur_for_validate[attrs_start..attrs_end];
if !self.scan.opts.skip_attr_validation
&& !attrs_slice.is_empty()
&& !attrs_slice.iter().all(|&b| matches!(b, b' ' | b'\t' | b'\n' | b'\r'))
{
if let Some(off) = memchr(b'<', attrs_slice) {
return Err(self.scan.err(format!(
"'<' not allowed inside start tag (XML 1.0 § 3.1) at attrs offset {off}"
)));
}
if memchr(b'&', attrs_slice).is_none() {
validate_attrs_structure_fast(attrs_slice, &self.scan.opts)
.map_err(|msg| self.scan.err(msg))?;
} else {
let inside_external = self.scan.current_base_uri().is_some();
validate_attrs_syntax(
attrs_slice,
&self.scan.opts,
&self.entities,
inside_external,
).map_err(|msg| self.scan.err(msg))?;
}
}
if self_closing {
self.depth -= 1;
if track_stack {
self.element_stack.pop();
self.element_streams.pop();
self.frame_saw_child.pop();
}
self.state = NextState::PendingEnd(name_start, name_end);
}
let src = self.scan.src_bytes();
let (out_name_start, out_name_end, out_attrs_start, out_attrs_end, owned_name, entity_attrs) =
if self.scan.on_original_source() {
(name_start, name_end, attrs_start as u32, attrs_end as u32, None, None)
} else {
let name_bytes = &self.scan.cur_bytes()[name_start as usize..name_end as usize];
let owned_name = Some(Box::<[u8]>::from(name_bytes));
let attrs_owned: Vec<u8> = self.scan.cur_bytes()
[attrs_start..attrs_end].to_vec();
let parsed: Option<Vec<(Vec<u8>, Vec<u8>)>> = if attrs_owned.is_empty() {
None
} else {
let mut tmp = BytesAttrs {
scan: Scanner::new(&attrs_owned, Cow::Borrowed(&self.scan.opts)),
entities: &self.entities,
expansion_bytes: &mut self.expansion_bytes,
done: false,
standalone_yes: self.standalone_yes,
is_xml_11: self.is_xml_11,
};
let mut acc: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
while let Some(item) = tmp.next() {
let a = item?;
acc.push((a.name.to_vec(), a.value.into_owned()));
}
if acc.is_empty() { None } else { Some(acc) }
};
(0, 0, 0, 0, owned_name, parsed)
};
self.last_start_offset = if self.scan.on_original_source() {
name_start.saturating_sub(1)
} else {
u32::MAX
};
if out_attrs_end > out_attrs_start {
let attrs_slice = &src[out_attrs_start as usize..out_attrs_end as usize];
let mut tmp_attrs = BytesAttrs {
scan: Scanner::new(attrs_slice, Cow::Borrowed(&self.scan.opts)),
entities: &self.entities,
expansion_bytes: &mut self.expansion_bytes,
done: false,
standalone_yes: self.standalone_yes,
is_xml_11: self.is_xml_11,
};
while let Some(item) = tmp_attrs.next() {
item?;
}
}
Ok(BytesEvent::StartElement(BytesStartTag {
src,
name_start: out_name_start,
name_end: out_name_end,
owned_name,
entity_attrs,
attrs_start: out_attrs_start,
attrs_end: out_attrs_end,
entities: &self.entities,
expansion_bytes: &mut self.expansion_bytes,
opts: &self.scan.opts,
standalone_yes: self.standalone_yes,
is_xml_11: self.is_xml_11,
}))
}
fn read_end_element(&mut self) -> Result<BytesEvent<'_, 'src>> {
let rewind_pos = self.scan.cur_pos();
if !self.scan.opts.skip_end_tag_check {
if let Some(&start_depth) = self.element_streams.last() {
let now = self.scan.stream_depth();
if now != start_depth {
return Err(self.scan.err(
"end tag is in a different entity than its start tag — \
entity replacement must contain matched element pairs \
(XML 1.0 § 4.3.2 WFC 'Logical Structure')",
));
}
}
}
self.scan.cur_advance_pos(2);
let (name_start_us, name_end_us) = self.scan.scan_name_raw()?;
let name_start = name_start_us as u32;
let name_end = name_end_us as u32;
self.scan.skip_ws();
self.scan.expect(b'>')?;
self.depth = self.depth.saturating_sub(1);
if !self.scan.opts.skip_end_tag_check {
let src = self.scan.src_bytes();
let cur = self.scan.cur_bytes();
let got = &cur[name_start as usize..name_end as usize];
let top_matches = self.element_stack.last()
.map(|e| e.name_bytes(src) == got)
.unwrap_or(false);
if top_matches {
self.element_stack.pop();
self.element_streams.pop();
self.frame_saw_child.pop();
} else if let Some(top) = self.element_stack.last() {
let exp = unsafe { std::str::from_utf8_unchecked(top.name_bytes(src)) };
let got_str = unsafe { std::str::from_utf8_unchecked(got) };
let err = self.scan.err_with_level(
ErrorLevel::Error,
format!("mismatched end tag: expected '</{exp}>', got '</{got_str}>'"),
).with_code(crate::error::ErrorCode::TagNameMismatch);
self.maybe_recover(err)?;
let src = self.scan.src_bytes();
let cur = self.scan.cur_bytes();
let got = &cur[name_start as usize..name_end as usize];
let stack_has_match = self.element_stack.iter()
.any(|e| e.name_bytes(src) == got);
if stack_has_match {
self.scan.cur_set_pos(rewind_pos);
return Ok(self.synthesize_close());
}
return Ok(self.synthesize_close());
} else {
let got_str = unsafe { std::str::from_utf8_unchecked(got) };
let err = self.scan.err_with_level(
ErrorLevel::Error,
format!("unexpected end tag '</{got_str}>' with no open element"),
);
self.maybe_recover(err)?;
return self.next();
}
}
let src = self.scan.src_bytes();
let (out_start, out_end) = if self.scan.on_original_source() {
(name_start, name_end)
} else {
(0, 0)
};
Ok(BytesEvent::EndElement(BytesEndTag {
src, name_start: out_start, name_end: out_end,
}))
}
fn read_comment(&mut self) -> Result<BytesEvent<'_, 'src>> {
self.scan.expect_str(b"<!--")?;
let is_prolog_misc =
self.depth == 0 && !self.root_seen && !self.scan.opts.remove_comments;
let start = self.scan.cur_pos();
loop {
match memchr(b'-', self.scan.cur_tail()) {
None => return Err(self.scan.err("unterminated comment")),
Some(off) => {
self.scan.cur_advance_pos(off);
if self.scan.starts_with(b"--") {
let content = self.scan.cur_slice(start, self.scan.cur_pos());
self.scan.skip_n(2);
if self.scan.peek() != Some(b'>') {
return Err(self.scan.err("'--' inside comment content is not allowed"));
}
self.scan.advance();
if is_prolog_misc { self.prolog_misc_count += 1; }
return Ok(BytesEvent::Comment(BytesComment { inner: content }));
}
self.scan.advance(); }
}
}
}
fn read_cdata(&mut self) -> Result<BytesEvent<'_, 'src>> {
self.scan.expect_str(b"<![CDATA[")?;
let start = self.scan.cur_pos();
loop {
match memchr(b']', self.scan.cur_tail()) {
None => return Err(self.scan.err("unterminated CDATA")),
Some(off) => {
self.scan.cur_advance_pos(off);
if self.scan.starts_with(b"]]>") {
let content = self.scan.cur_slice(start, self.scan.cur_pos());
let content = maybe_normalize_eol(
content, self.is_xml_11, self.source_has_eol_candidate,
);
self.scan.skip_n(3);
return Ok(BytesEvent::CData(BytesCData { inner: content }));
}
self.scan.advance(); }
}
}
}
fn read_pi(&mut self) -> Result<BytesEvent<'_, 'src>> {
self.scan.expect_str(b"<?")?;
let is_prolog_misc =
self.depth == 0 && !self.root_seen && !self.scan.opts.remove_pis;
let target = self.scan.scan_name_bytes()?;
if target.eq_ignore_ascii_case(b"xml") {
return Err(self.scan.err("PI target 'xml' is reserved"));
}
if self.scan.opts.namespace_aware && target.contains(&b':') {
return Err(self.scan.err(format!(
"PI target '{}' must be an NCName (no colon) under XML \
Namespaces 1.0",
String::from_utf8_lossy(&target)
)));
}
let content = if matches!(self.scan.peek(), Some(b' ' | b'\t' | b'\r' | b'\n')) {
self.scan.skip_ws();
let start = self.scan.cur_pos();
loop {
if self.scan.is_eof() { return Err(self.scan.err("unterminated PI")); }
if self.scan.starts_with(b"?>") {
let s = self.scan.cur_slice(start, self.scan.cur_pos());
self.scan.skip_n(2);
break s;
}
self.scan.advance();
}
} else {
self.scan.expect_str(b"?>")?;
Cow::Borrowed(&b""[..])
};
if is_prolog_misc { self.prolog_misc_count += 1; }
Ok(BytesEvent::Pi(BytesPi { target_: target, content_: content }))
}
fn try_queue_user_entity_ref(&mut self) -> Result<bool> {
let bytes = self.scan.src_bytes();
let amp_pos = self.scan.cur_pos();
debug_assert_eq!(bytes.get(amp_pos), Some(&b'&'));
let name_start = amp_pos + 1;
if name_start >= bytes.len() { return Ok(false); }
if bytes[name_start] == b'#' { return Ok(false); }
let mut p = name_start;
while p < bytes.len() {
let b = bytes[p];
if b == b';' { break; }
let is_name_byte = matches!(b,
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9'
| b'_' | b'-' | b'.' | b':' | 0x80..=0xFF
);
if !is_name_byte { return Ok(false); }
p += 1;
}
if p == name_start || p >= bytes.len() || bytes[p] != b';' {
return Ok(false);
}
let name_end = p;
let name = &bytes[name_start..name_end];
if matches!(name, b"amp" | b"lt" | b"gt" | b"quot" | b"apos") {
return Ok(false);
}
self.state = NextState::PendingEntityRef(name_start as u32, name_end as u32);
self.scan.cur_set_pos(name_end + 1); Ok(true)
}
fn maybe_load_deferred_entity_at_cursor(&mut self) -> Result<()> {
if self.deferred_general_entities.is_empty() {
return Ok(());
}
let bytes = self.scan.cur_bytes();
let pos = self.scan.cur_pos();
if bytes.get(pos) != Some(&b'&') {
return Ok(());
}
let name_start = pos + 1;
if bytes.get(name_start) == Some(&b'#') {
return Ok(());
}
let mut p = name_start;
while p < bytes.len() && bytes[p] != b';' {
p += 1;
}
if p >= bytes.len() || p == name_start {
return Ok(());
}
let name = match std::str::from_utf8(&bytes[name_start..p]) {
Ok(s) => s.to_string(),
Err(_) => return Ok(()),
};
self.load_deferred_entity(&name)
}
fn load_deferred_entity(&mut self, name: &str) -> Result<()> {
let Some(deferred) = self.deferred_general_entities.remove(name) else {
return Ok(());
};
let Some(resolver) = self.scan.opts.external_resolver.clone() else {
return Ok(());
};
let declared_external = self.entities.get(name)
.map(|d| d.declared_external)
.unwrap_or(false);
let base = self.scan.opts.base_url.clone();
let absolute = resolve_uri(&deferred.system_id, base.as_deref());
match resolver.resolve(deferred.public_id.as_deref(), &absolute, base.as_deref()) {
Ok(raw) => {
let value = match crate::encoding::transcode_to_utf8(&raw) {
Ok(c) => String::from_utf8(c.into_owned()).map_err(|e| e.to_string()),
Err(e) => Err(e.message.clone()),
};
match value {
Ok(v) => {
self.entities.insert(name.to_string(), EntityDecl {
kind: EntityKind::ExternalLoaded(v),
declared_external,
source_uri: Some(absolute),
});
}
Err(msg) => {
let err = self.scan.err_with_level(
ErrorLevel::Error,
format!(
"external entity '&{name};' is not valid UTF-8 \
(system_id={:?}): {msg}",
deferred.system_id
),
);
self.maybe_recover(err)?;
}
}
}
Err(e) => {
let err = self.scan.err_with_level(
ErrorLevel::Error,
format!(
"external resolver failed to load entity '&{name};' \
(system_id={:?}, public_id={:?}): {e}",
deferred.system_id, deferred.public_id
),
);
self.maybe_recover(err)?;
}
}
Ok(())
}
fn read_text(&mut self) -> Result<BytesEvent<'_, 'src>> {
let start = self.scan.cur_pos();
if self.scan.opts.skip_entity_expansion {
match memchr(b'<', self.scan.cur_tail()) {
None => {
self.scan.cur_set_pos(self.scan.cur_len());
}
Some(off) => {
self.scan.cur_advance_pos(off);
}
}
return Ok(BytesEvent::Text(BytesText {
inner: self.scan.cur_slice(start, self.scan.cur_pos()),
}));
}
let is_xml_11 = self.is_xml_11;
let gate = self.source_has_eol_candidate;
loop {
let tail = self.scan.cur_tail();
match memchr3(b'<', b'&', b']', tail) {
None => {
self.scan.cur_set_pos(self.scan.cur_len());
let raw = self.scan.cur_slice(start, self.scan.cur_pos());
return Ok(BytesEvent::Text(BytesText {
inner: maybe_normalize_eol(raw, is_xml_11, gate),
}));
}
Some(off) => {
self.scan.cur_advance_pos(off);
match self.scan.cur_bytes()[self.scan.cur_pos()] {
b'<' => {
let raw = self.scan.cur_slice(start, self.scan.cur_pos());
return Ok(BytesEvent::Text(BytesText {
inner: maybe_normalize_eol(raw, is_xml_11, gate),
}));
}
b'&' => break, b']' => {
if self.scan.starts_with(b"]]>") {
if self.scan.opts.recovery_mode {
break;
}
return Err(self.scan.err("']]>' not allowed in text content"));
}
self.scan.advance(); }
_ => unreachable!(),
}
}
}
}
let mut buf = std::mem::take(&mut self.text_decode_buf);
buf.clear();
let prefix_end = self.scan.cur_pos();
let in_place_orig = self.scan.is_in_place() && self.scan.on_original_source();
if !in_place_orig {
append_text_segment(&self.scan, start, prefix_end, &mut buf, self.is_xml_11, self.source_has_eol_candidate);
}
let budget = self.scan.opts.max_entity_expansion_bytes;
let depth = self.element_stack.len() as u32;
loop {
let tail = self.scan.cur_tail();
match memchr3(b'<', b'&', b']', tail) {
None => {
let end = self.scan.cur_len();
let from = self.scan.cur_pos();
append_text_segment(&self.scan, from, end, &mut buf, self.is_xml_11, self.source_has_eol_candidate);
self.scan.cur_set_pos(end);
if let Some((name, depth_at_push)) = self.scan.top_entity_info() {
if depth_at_push != depth {
return Err(self.scan.err(format!(
"entity '&{name};' contains unbalanced element markup — \
element-stack depth was {depth_at_push} when the entity \
was expanded but is {depth} at its end \
(XML 1.0 § 4.3.2 WFC 'Logical Structure')"
)));
}
}
if self.scan.try_pop_entity_stream() {
continue;
}
break;
}
Some(off) => {
let from = self.scan.cur_pos();
append_text_segment(&self.scan, from, from + off, &mut buf, self.is_xml_11, self.source_has_eol_candidate);
self.scan.cur_advance_pos(off);
match self.scan.cur_bytes()[self.scan.cur_pos()] {
b'<' => break,
b'&' => {
let next = self.scan.peek_at(1);
let is_ref_start = matches!(
next,
Some(b'#')
| Some(b'A'..=b'Z')
| Some(b'a'..=b'z')
| Some(b'_')
| Some(b':')
| Some(0x80..=0xFF) );
if !is_ref_start && self.scan.opts.recovery_mode {
let err = self.scan.err_with_level(
ErrorLevel::Error,
"bare '&' in text content — kept literal \
(XML 1.0 § 4.1 [Reference])",
);
self.recovered_errors.push(err);
buf.push(b'&');
self.scan.advance();
} else if !self.scan.opts.resolve_entities
&& self.scan.on_original_source()
&& self.try_queue_user_entity_ref()?
{
break;
} else {
self.maybe_load_deferred_entity_at_cursor()?;
expand_reference_bytes(
&mut self.scan, &mut buf,
&self.entities,
&mut self.expansion_bytes, budget, depth,
Some(&mut self.recovered_errors),
self.pe_ref_in_internal_subset_seen,
self.standalone_yes,
self.is_xml_11,
)?;
}
}
b']' => {
if self.scan.starts_with(b"]]>") {
let err = self.scan.err_with_level(
ErrorLevel::Error,
"']]>' not allowed in text content — kept literal \
(XML 1.0 § 2.4 [CharData])",
);
self.maybe_recover(err)?;
buf.extend_from_slice(b"]]>");
self.scan.skip_n(3);
} else {
buf.push(b']');
self.scan.advance();
}
}
_ => unreachable!(),
}
}
}
}
if in_place_orig {
let end_pos = self.scan.cur_pos();
let suffix_orig_len = end_pos.saturating_sub(prefix_end);
if buf.len() > suffix_orig_len {
let err = self.scan.err(format!(
"entity expansion ({} bytes) exceeds source span ({} bytes); \
parse_bytes_in_place requires expansion ≤ reference length. \
Use parse_bytes for documents with expanding user-defined entities.",
buf.len(), suffix_orig_len,
));
self.text_decode_buf = buf;
return Err(err);
}
let prefix_bytes = self.scan.src_slice(start, prefix_end);
if self.source_has_eol_candidate
&& first_eol_byte(prefix_bytes, self.is_xml_11).is_some()
{
let mut out = Vec::with_capacity(prefix_bytes.len() + buf.len());
append_eol_normalized(prefix_bytes, &mut out, self.is_xml_11);
out.extend_from_slice(&buf);
self.text_decode_buf = buf;
return Ok(BytesEvent::Text(BytesText { inner: Cow::Owned(out) }));
}
let suffix_written = buf.len();
if suffix_written > 0 {
self.scan.compact_at(prefix_end, end_pos, &buf);
}
self.text_decode_buf = buf;
let total_len = (prefix_end - start) + suffix_written;
return Ok(BytesEvent::Text(BytesText {
inner: Cow::Borrowed(self.scan.src_slice(start, start + total_len)),
}));
}
Ok(BytesEvent::Text(BytesText { inner: Cow::Owned(buf) }))
}
}
fn check_entity_chain<'a>(
name: &'a str,
entities: &'a HashMap<String, EntityDecl>,
chain: &mut Vec<&'a str>,
inside_external: bool,
) -> std::result::Result<(), String> {
if chain.contains(&name) {
return Err(format!(
"entity '&{name};' references itself (XML 1.0 § 4.1 WFC: No Recursion); \
chain: {} -> {name}",
chain.join(" -> ")
));
}
let predef = matches!(name, "amp" | "lt" | "gt" | "quot" | "apos");
if predef { return Ok(()); }
let value = match entities.get(name).and_then(|k| k.replacement()) {
Some(v) => v,
None => {
if inside_external {
return Ok(());
}
return Err(format!(
"undefined entity '&{name};' (XML 1.0 § 4.1 WFC: Entity Declared)"
));
}
};
chain.push(name);
let vb = value.as_bytes();
let mut j = 0;
while j < vb.len() {
let b = vb[j];
if b == b'<' {
return Err(format!(
"entity '&{name};' replacement text contains a literal `<` — \
inclusion in an attribute value would violate XML 1.0 § 3.1 \
WFC: No < in Attribute Values"
));
}
if b != b'&' { j += 1; continue; }
let after = j + 1;
let semi = vb[after..].iter().position(|&c| c == b';');
let Some(off) = semi else {
return Err(format!(
"entity '&{name};' replacement text contains a bare `&` — \
inclusion would violate XML 1.0 § 4.1 [Reference]"
));
};
let body = &vb[after..after + off];
if body.first() != Some(&b'#') {
if let Ok(child) = std::str::from_utf8(body) {
let key: &'a str = entities
.get_key_value(child)
.map(|(k, _)| k.as_str())
.unwrap_or(child);
check_entity_chain(key, entities, chain, inside_external)?;
}
}
j = after + off + 1;
}
chain.pop();
Ok(())
}
pub(crate) fn consume_text_decl_if_present(
scan: &mut Scanner,
is_outer_xml_11: bool,
) -> Result<()> {
if !scan.starts_with(b"<?xml") { return Ok(()); }
if !matches!(scan.peek_at(5), Some(b' ' | b'\t' | b'\r' | b'\n')) {
return Ok(());
}
scan.skip_n(5); scan.skip_ws();
let mut seen_version = false;
let mut seen_encoding = false;
while !scan.starts_with(b"?>") {
if scan.peek().is_none() {
return Err(scan.err("unterminated text declaration (expected '?>')"));
}
let key_start = scan.cur_pos();
scan.skip_name()?;
let key_end = scan.cur_pos();
let key = scan.cur_slice(key_start, key_end).to_vec();
match key.as_slice() {
b"version" => {
if seen_version {
return Err(scan.err("text declaration has duplicate 'version' pseudo-attribute"));
}
if seen_encoding {
return Err(scan.err(
"text declaration must list 'version' before 'encoding' \
(XML 1.0 § 4.3.1 [77])",
));
}
seen_version = true;
scan.skip_ws();
scan.expect(b'=')?;
scan.skip_ws();
let q = match scan.peek() {
Some(b @ (b'"' | b'\'')) => { scan.advance(); b }
_ => return Err(scan.err("expected quoted value in text declaration")),
};
let val_start = scan.cur_pos();
while let Some(b) = scan.peek() {
if b == q { break; }
scan.advance();
}
let val_end = scan.cur_pos();
let ver = scan.cur_slice(val_start, val_end).to_vec();
scan.expect(q)?;
let entity_ver_ok = ver == b"1.0"
|| (is_outer_xml_11 && ver == b"1.1");
if !entity_ver_ok {
return Err(scan.err(format!(
"text declaration version {:?} is not compatible with the \
host document's version (XML §4.3.4)",
String::from_utf8_lossy(&ver),
)));
}
scan.skip_ws();
}
b"encoding" => {
if seen_encoding {
return Err(scan.err("text declaration has duplicate 'encoding' pseudo-attribute"));
}
seen_encoding = true;
scan.skip_ws();
scan.expect(b'=')?;
scan.skip_ws();
let q = match scan.peek() {
Some(b @ (b'"' | b'\'')) => { scan.advance(); b }
_ => return Err(scan.err("expected quoted value in text declaration")),
};
while let Some(b) = scan.peek() {
if b == q { scan.advance(); break; }
scan.advance();
}
scan.skip_ws();
}
other => return Err(scan.err(format!(
"{:?} is not a valid pseudo-attribute in a text declaration \
(XML 1.0 § 4.3.1 forbids anything besides version and encoding — \
note standalone= is document-only, see § 2.9)",
String::from_utf8_lossy(other),
))),
}
}
scan.skip_n(2); if !seen_encoding {
return Err(scan.err(
"text declaration is missing the required encoding= pseudo-attribute \
(XML 1.0 § 4.3.1)",
));
}
Ok(())
}
#[allow(dead_code)]
fn expand_entity_replacement_text(bytes: &[u8]) -> std::result::Result<Vec<u8>, String> {
let mut out = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
if b != b'&' {
out.push(b);
i += 1;
continue;
}
let after = i + 1;
if after >= bytes.len() {
return Err("entity value ends with `&`".to_string());
}
if bytes[after] != b'#' {
let semi = bytes[after..].iter().position(|&c| c == b';')
.ok_or_else(|| "named entity reference in entity value missing `;`".to_string())?;
out.extend_from_slice(&bytes[i..after + semi + 1]);
i = after + semi + 1;
continue;
}
let body_start = after + 1;
let semi = bytes[body_start..].iter().position(|&c| c == b';')
.ok_or_else(|| "character reference missing `;`".to_string())?;
let body = &bytes[body_start..body_start + semi];
let cp: u32 = if body.first() == Some(&b'x') || body.first() == Some(&b'X') {
std::str::from_utf8(&body[1..]).ok()
.and_then(|h| u32::from_str_radix(h, 16).ok())
.ok_or_else(|| format!(
"invalid hex character reference '&#{}'", String::from_utf8_lossy(body)
))?
} else {
std::str::from_utf8(body).ok()
.and_then(|d| d.parse::<u32>().ok())
.ok_or_else(|| format!(
"invalid decimal character reference '&#{}'", String::from_utf8_lossy(body)
))?
};
let ch = char::from_u32(cp).ok_or_else(|| format!(
"character reference '&#{};' is not a valid Unicode scalar", cp
))?;
let mut tmp = [0u8; 4];
out.extend_from_slice(ch.encode_utf8(&mut tmp).as_bytes());
i = body_start + semi + 1;
}
Ok(out)
}
fn is_valid_version(v: &[u8]) -> bool {
v.len() >= 3
&& &v[..2] == b"1."
&& v[2..].iter().all(|c| c.is_ascii_digit())
}
fn is_valid_encname(v: &[u8]) -> bool {
let Some(&first) = v.first() else { return false; };
if !first.is_ascii_alphabetic() { return false; }
v[1..].iter().all(|&c| {
c.is_ascii_alphanumeric() || c == b'.' || c == b'_' || c == b'-'
})
}
fn read_occurrence<'src>(
scan: &mut crate::scanner::Scanner<'src, 'static>,
) -> Option<crate::dtd::Occurrence> {
use crate::dtd::Occurrence;
match scan.peek() {
Some(b'?') => { scan.advance(); Some(Occurrence::ZeroOrOne) }
Some(b'*') => { scan.advance(); Some(Occurrence::ZeroOrMore) }
Some(b'+') => { scan.advance(); Some(Occurrence::OneOrMore) }
_ => None,
}
}
fn validate_attrs_structure_fast(
bytes: &[u8],
opts: &ParseOptions,
) -> std::result::Result<(), String> {
use crate::charsets::{ASCII_XML_NAME, NS};
let mut i = 0;
let n = bytes.len();
let mut seen_inline: [(u32, u32); 8] = [(0, 0); 8];
let mut seen_inline_len: usize = 0;
let mut seen_overflow: Vec<(u32, u32)> = Vec::new();
let mut seen_set: Option<FxHashSet<&[u8]>> = None;
const SEEN_PROMOTE_THRESHOLD: usize = 32;
fn is_ws(b: u8) -> bool { matches!(b, b' ' | b'\t' | b'\n' | b'\r') }
let mut first_attr = true;
while i < n {
let ws_start = i;
while i < n && is_ws(bytes[i]) { i += 1; }
if i >= n { break; }
if !first_attr && i == ws_start {
return Err(
"expected whitespace between attributes (XML 1.0 § 3.1 [40] [STag])".to_string()
);
}
first_attr = false;
let name_start = i;
if !opts.skip_name_validation {
let b = bytes[i];
if b < 0x80 && ASCII_XML_NAME[b as usize] & NS == 0 {
return Err(format!(
"invalid attribute name-start character {:?}",
b as char
));
}
}
let stop = memchr3(b'=', b' ', b'\t', &bytes[i..n])
.map(|o| i + o)
.unwrap_or(n);
let stop = bytes[i..stop].iter().position(|&c| c == b'\n' || c == b'\r')
.map(|o| i + o)
.unwrap_or(stop);
i = stop;
let name_end = i;
if name_end == name_start {
return Err("expected attribute name".to_string());
}
while i < n && is_ws(bytes[i]) { i += 1; }
if i >= n || bytes[i] != b'=' {
return Err("expected '=' after attribute name".to_string());
}
i += 1;
while i < n && is_ws(bytes[i]) { i += 1; }
if i >= n {
return Err("expected quoted attribute value".to_string());
}
let q = bytes[i];
if q != b'"' && q != b'\'' {
return Err(format!(
"attribute value must be quoted (got '{}'); XML 1.0 § 3.1 [41] [AttValue]",
q as char
));
}
i += 1;
let value_start = i;
let val_off = memchr(q, &bytes[value_start..n])
.ok_or_else(|| "unterminated attribute value".to_string())?;
i = value_start + val_off + 1;
let new_name = &bytes[name_start..name_end];
for &(s, e) in &seen_inline[..seen_inline_len] {
if &bytes[s as usize..e as usize] == new_name {
return Err(format!(
"duplicate attribute '{}' in start tag (XML 1.0 § 3.1 WFC: Unique Att Spec)",
String::from_utf8_lossy(new_name)
));
}
}
if let Some(set) = seen_set.as_mut() {
if !set.insert(new_name) {
return Err(format!(
"duplicate attribute '{}' in start tag (XML 1.0 § 3.1 WFC: Unique Att Spec)",
String::from_utf8_lossy(new_name)
));
}
} else {
for &(s, e) in &seen_overflow {
if &bytes[s as usize..e as usize] == new_name {
return Err(format!(
"duplicate attribute '{}' in start tag (XML 1.0 § 3.1 WFC: Unique Att Spec)",
String::from_utf8_lossy(new_name)
));
}
}
if seen_inline_len < seen_inline.len() {
seen_inline[seen_inline_len] = (name_start as u32, name_end as u32);
seen_inline_len += 1;
} else {
seen_overflow.push((name_start as u32, name_end as u32));
if seen_overflow.len() >= SEEN_PROMOTE_THRESHOLD {
let mut set: FxHashSet<&[u8]> = FxHashSet::default();
set.reserve(seen_overflow.len() * 2);
for &(s, e) in &seen_overflow {
set.insert(&bytes[s as usize..e as usize]);
}
seen_set = Some(set);
}
}
}
}
Ok(())
}
fn validate_attrs_syntax(
bytes: &[u8],
opts: &ParseOptions,
entities: &HashMap<String, EntityDecl>,
inside_external: bool,
) -> std::result::Result<(), String> {
use crate::charsets::{ASCII_XML_NAME, NS, NC};
let mut i = 0;
let n = bytes.len();
let mut seen_inline: [(u32, u32); 8] = [(0, 0); 8];
let mut seen_inline_len: usize = 0;
let mut seen_overflow: Vec<(u32, u32)> = Vec::new();
let mut seen_set: Option<FxHashSet<&[u8]>> = None;
const SEEN_PROMOTE_THRESHOLD: usize = 32;
fn is_ws(b: u8) -> bool { matches!(b, b' ' | b'\t' | b'\n' | b'\r') }
let mut first_attr = true;
while i < n {
let ws_start = i;
while i < n && is_ws(bytes[i]) { i += 1; }
if i >= n { break; }
if !first_attr && i == ws_start {
return Err(
"expected whitespace between attributes (XML 1.0 § 3.1 [40] [STag])".to_string()
);
}
first_attr = false;
let name_start = i;
if !opts.skip_name_validation {
let b = bytes[i];
if b < 0x80 && ASCII_XML_NAME[b as usize] & NS == 0 {
return Err(format!(
"invalid attribute name-start character {:?}",
b as char
));
}
}
let stop = memchr3(b'=', b' ', b'\t', &bytes[i..n])
.map(|o| i + o)
.unwrap_or(n);
let stop = {
let s = bytes[i..stop].iter().position(|&c| c == b'\n' || c == b'\r')
.map(|o| i + o)
.unwrap_or(stop);
s
};
i = stop;
let name_end = i;
if name_end == name_start {
return Err("expected attribute name".to_string());
}
while i < n && is_ws(bytes[i]) { i += 1; }
if i >= n || bytes[i] != b'=' {
return Err("expected '=' after attribute name".to_string());
}
i += 1;
while i < n && is_ws(bytes[i]) { i += 1; }
if i >= n {
return Err("expected quoted attribute value".to_string());
}
let q = bytes[i];
if q != b'"' && q != b'\'' {
return Err(format!(
"attribute value must be quoted (got '{}'); XML 1.0 § 3.1 [41] [AttValue]",
q as char
));
}
i += 1;
loop {
let tail = &bytes[i..n];
let off = match memchr3(b'<', b'&', q, tail) {
Some(o) => o,
None => return Err("unterminated attribute value".to_string()),
};
i += off;
let b = bytes[i];
if b == q { i += 1; break; }
if b == b'<' {
return Err(
"'<' not allowed in attribute value (XML 1.0 § 3.1 WFC: No < in Attribute Values)".to_string()
);
}
if b == b'&' {
let after = i + 1;
if after >= n {
return Err("unterminated entity reference in attribute value".to_string());
}
let semi = bytes[after..].iter().position(|&c| c == b';' || c == b'<' || c == q);
let semi_off = match semi {
Some(off) if bytes[after + off] == b';' => off,
_ => return Err(
"bare '&' in attribute value (must be an entity / character reference; XML 1.0 § 4.1)".to_string()
),
};
let body = &bytes[after..after + semi_off];
if body.is_empty() {
return Err("empty reference '&;' in attribute value".to_string());
}
if body[0] == b'#' {
let digits: &[u8] = if body.len() >= 2 && (body[1] == b'x' || body[1] == b'X') {
&body[2..]
} else { &body[1..] };
if digits.is_empty() {
return Err("empty numeric character reference".to_string());
}
let valid = if body.len() >= 2 && (body[1] == b'x' || body[1] == b'X') {
digits.iter().all(|&c| c.is_ascii_hexdigit())
} else {
digits.iter().all(|&c| c.is_ascii_digit())
};
if !valid {
return Err(format!("invalid character reference '&{};'",
String::from_utf8_lossy(body)));
}
} else {
if !opts.skip_name_validation {
let b0 = body[0];
if b0 < 0x80 && ASCII_XML_NAME[b0 as usize] & NS == 0 {
return Err(format!(
"invalid entity-reference name '&{};' in attribute value",
String::from_utf8_lossy(body)
));
}
for &b in &body[1..] {
if b < 0x80 && ASCII_XML_NAME[b as usize] & (NS | NC) == 0 {
return Err(format!(
"invalid entity-reference name '&{};' in attribute value",
String::from_utf8_lossy(body)
));
}
}
}
let name = std::str::from_utf8(body).unwrap_or("?");
let predef = matches!(name, "amp" | "lt" | "gt" | "quot" | "apos");
if !predef {
if entities.get(name).is_some_and(|d| d.kind.is_external_value()) {
return Err(format!(
"external entity '&{name};' is not allowed in an attribute value \
(XML 1.0 § 4.4.4 WFC: No External Entity References)"
));
}
if !entities.is_empty() {
let mut chain: Vec<&str> = Vec::new();
check_entity_chain(name, entities, &mut chain, inside_external)?;
} else if !inside_external {
return Err(format!(
"undefined entity '&{name};' (XML 1.0 § 4.1 WFC: Entity Declared)"
));
}
}
}
i = after + semi_off + 1;
continue;
}
i += 1;
}
let new_name = &bytes[name_start..name_end];
let ns_u32 = name_start as u32;
let ne_u32 = name_end as u32;
for &(s, e) in &seen_inline[..seen_inline_len] {
if &bytes[s as usize..e as usize] == new_name {
return Err(format!(
"duplicate attribute '{}' in start tag (XML 1.0 § 3.1 WFC: Unique Att Spec)",
String::from_utf8_lossy(new_name)
));
}
}
if let Some(set) = seen_set.as_mut() {
if !set.insert(new_name) {
return Err(format!(
"duplicate attribute '{}' in start tag (XML 1.0 § 3.1 WFC: Unique Att Spec)",
String::from_utf8_lossy(new_name)
));
}
} else {
for &(s, e) in &seen_overflow {
if &bytes[s as usize..e as usize] == new_name {
return Err(format!(
"duplicate attribute '{}' in start tag (XML 1.0 § 3.1 WFC: Unique Att Spec)",
String::from_utf8_lossy(new_name)
));
}
}
if seen_inline_len < seen_inline.len() {
seen_inline[seen_inline_len] = (ns_u32, ne_u32);
seen_inline_len += 1;
} else {
seen_overflow.push((ns_u32, ne_u32));
if seen_overflow.len() >= SEEN_PROMOTE_THRESHOLD {
let mut set: FxHashSet<&[u8]> = FxHashSet::default();
set.reserve(seen_overflow.len() * 2);
for &(s, e) in &seen_overflow {
set.insert(&bytes[s as usize..e as usize]);
}
seen_set = Some(set);
}
}
}
}
Ok(())
}
fn append_attr_normalized(src: &[u8], dst: &mut Vec<u8>, is_xml_11: bool) {
let mut i = 0;
while i < src.len() {
let rest = &src[i..];
let off = if is_xml_11 {
let a = memchr3(b'\t', b'\n', b'\r', rest);
let b = memchr::memchr2(0xC2, 0xE2, rest);
match (a, b) {
(Some(x), Some(y)) => Some(x.min(y)),
(a, b) => a.or(b),
}
} else {
memchr3(b'\t', b'\n', b'\r', rest)
};
let stop = match off {
Some(o) => i + o,
None => src.len(),
};
if stop > i {
dst.extend_from_slice(&src[i..stop]);
}
if stop >= src.len() { break; }
let b = src[stop];
let consumed = match b {
b'\t' | b'\n' | b'\r' => {
dst.push(b' ');
1
}
0xC2 if is_xml_11 && src.get(stop + 1) == Some(&0x85) => {
dst.push(b' ');
2
}
0xE2 if is_xml_11
&& src.get(stop + 1) == Some(&0x80)
&& src.get(stop + 2) == Some(&0xA8) =>
{
dst.push(b' ');
3
}
_ => {
dst.push(b);
1
}
};
i = stop + consumed;
}
}
#[inline(always)]
fn first_attr_norm_byte(bytes: &[u8], is_xml_11: bool) -> Option<usize> {
let in_1_0 = memchr3(b'\t', b'\n', b'\r', bytes);
if !is_xml_11 || in_1_0.is_some() {
return in_1_0;
}
memchr::memchr2(0xC2, 0xE2, bytes)
}
#[inline(always)]
fn maybe_normalize_attr_value<'a>(raw: Cow<'a, [u8]>, is_xml_11: bool) -> Cow<'a, [u8]> {
if first_attr_norm_byte(&raw, is_xml_11).is_none() {
return raw;
}
let mut out = Vec::with_capacity(raw.len());
append_attr_normalized(&raw, &mut out, is_xml_11);
Cow::Owned(out)
}
#[inline(always)]
fn append_attr_segment(
scan: &Scanner<'_, '_>,
start: usize,
end: usize,
buf: &mut Vec<u8>,
is_xml_11: bool,
) {
let bytes = &scan.cur_bytes()[start..end];
if first_attr_norm_byte(bytes, is_xml_11).is_none() {
buf.extend_from_slice(bytes);
} else {
append_attr_normalized(bytes, buf, is_xml_11);
}
}
fn precompute_source_has_eol(src: &[u8]) -> bool {
let mut i = 0;
while i < src.len() {
let rest = &src[i..];
let off = match memchr3(b'\r', 0xC2, 0xE2, rest) {
Some(o) => o,
None => return false,
};
let pos = i + off;
match src[pos] {
b'\r' => return true,
0xC2 if src.get(pos + 1) == Some(&0x85) => return true,
0xE2 if src.get(pos + 1) == Some(&0x80)
&& src.get(pos + 2) == Some(&0xA8) =>
{
return true;
}
_ => i = pos + 1,
}
}
false
}
#[inline(always)]
pub(crate) fn first_eol_byte(bytes: &[u8], is_xml_11: bool) -> Option<usize> {
if is_xml_11 {
memchr3(b'\r', 0xC2, 0xE2, bytes)
} else {
memchr(b'\r', bytes)
}
}
pub(crate) fn append_eol_normalized(src: &[u8], dst: &mut Vec<u8>, is_xml_11: bool) {
let mut i = 0;
while i < src.len() {
let rest = &src[i..];
let stop = match first_eol_byte(rest, is_xml_11) {
Some(off) => i + off,
None => src.len(),
};
if stop > i {
dst.extend_from_slice(&src[i..stop]);
}
if stop >= src.len() { break; }
let b = src[stop];
let consumed = if b == b'\r' {
dst.push(b'\n');
if src.get(stop + 1) == Some(&b'\n') {
2
} else if is_xml_11
&& src.get(stop + 1) == Some(&0xC2)
&& src.get(stop + 2) == Some(&0x85)
{
3
} else {
1
}
} else if is_xml_11
&& b == 0xC2
&& src.get(stop + 1) == Some(&0x85)
{
dst.push(b'\n');
2
} else if is_xml_11
&& b == 0xE2
&& src.get(stop + 1) == Some(&0x80)
&& src.get(stop + 2) == Some(&0xA8)
{
dst.push(b'\n');
3
} else {
dst.push(b);
1
};
i = stop + consumed;
}
}
#[inline(always)]
pub(crate) fn append_text_segment(
scan: &Scanner<'_, '_>,
start: usize,
end: usize,
buf: &mut Vec<u8>,
is_xml_11: bool,
gate: bool,
) {
let bytes = &scan.cur_bytes()[start..end];
if !gate {
buf.extend_from_slice(bytes);
return;
}
if first_eol_byte(bytes, is_xml_11).is_none() {
buf.extend_from_slice(bytes);
} else {
append_eol_normalized(bytes, buf, is_xml_11);
}
}
#[inline(always)]
pub(crate) fn maybe_normalize_eol<'a>(
raw: Cow<'a, [u8]>,
is_xml_11: bool,
gate: bool,
) -> Cow<'a, [u8]> {
if !gate {
return raw;
}
if first_eol_byte(&raw, is_xml_11).is_none() {
return raw;
}
let mut out = Vec::with_capacity(raw.len());
append_eol_normalized(&raw, &mut out, is_xml_11);
Cow::Owned(out)
}
pub fn unescape_bytes(bytes: &[u8]) -> Cow<'_, [u8]> {
if memchr(b'&', bytes).is_none() {
return Cow::Borrowed(bytes);
}
let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i] != b'&' {
out.push(bytes[i]);
i += 1;
continue;
}
let rest = &bytes[i + 1..];
let semi = match memchr(b';', rest) {
Some(n) if n <= 16 => n,
_ => { out.push(b'&'); i += 1; continue; }
};
let name = &rest[..semi];
match name {
b"amp" => out.push(b'&'),
b"lt" => out.push(b'<'),
b"gt" => out.push(b'>'),
b"quot" => out.push(b'"'),
b"apos" => out.push(b'\''),
_ if name.starts_with(b"#") => {
let cp: Option<u32> = if name.len() >= 2 && (name[1] == b'x' || name[1] == b'X') {
std::str::from_utf8(&name[2..]).ok()
.and_then(|h| u32::from_str_radix(h, 16).ok())
} else {
std::str::from_utf8(&name[1..]).ok()
.and_then(|d| d.parse::<u32>().ok())
};
match cp.and_then(char::from_u32) {
Some(c) => {
let mut tmp = [0u8; 4];
out.extend_from_slice(c.encode_utf8(&mut tmp).as_bytes());
}
None => {
out.push(b'&');
out.extend_from_slice(name);
out.push(b';');
}
}
}
_ => {
out.push(b'&');
out.extend_from_slice(name);
out.push(b';');
}
}
i += 1 + semi + 1;
}
Cow::Owned(out)
}
pub(crate) fn expand_reference_bytes(
scan: &mut Scanner<'_, '_>,
buf: &mut Vec<u8>,
entities: &HashMap<String, EntityDecl>,
used: &mut u64,
budget: u64,
element_depth: u32,
recovered: Option<&mut Vec<crate::error::XmlError>>,
pe_relax_undefined: bool,
standalone_yes: bool,
is_xml_11: bool,
) -> Result<()> {
scan.expect(b'&')?;
if scan.peek() == Some(b'#') {
scan.advance();
let cp: u32 = if scan.peek() == Some(b'x') {
scan.advance();
let start = scan.cur_pos();
while scan.peek().map(|b| b.is_ascii_hexdigit()).unwrap_or(false) { scan.advance(); }
if scan.cur_pos() == start { return Err(scan.err("empty hex character reference")); }
let hex = scan.cur_str(start, scan.cur_pos());
u32::from_str_radix(&hex, 16)
.map_err(|_| scan.err(format!("invalid hex char ref: {hex}")))?
} else {
let start = scan.cur_pos();
while scan.peek().map(|b| b.is_ascii_digit()).unwrap_or(false) { scan.advance(); }
if scan.cur_pos() == start { return Err(scan.err("empty decimal character reference")); }
let dec = scan.cur_str(start, scan.cur_pos());
dec.parse::<u32>()
.map_err(|_| scan.err(format!("invalid decimal char ref: {dec}")))?
};
scan.expect(b';')?;
let c = char::from_u32(cp).ok_or_else(|| {
scan.err(format!("U+{cp:04X} is not a valid Unicode scalar value"))
})?;
let valid = if is_xml_11 { is_xml_11_char(c) } else { is_xml_char(c) };
if !valid {
let spec = if is_xml_11 { "XML 1.1 § 2.2" } else { "XML 1.0 § 2.2" };
return Err(scan.err(format!("U+{cp:04X} is not a valid XML character ({spec})")));
}
let mut tmp = [0u8; 4];
buf.extend_from_slice(c.encode_utf8(&mut tmp).as_bytes());
} else {
let (ns, ne) = scan.scan_name_raw()?;
scan.expect(b';')?;
let name = scan.cur_str(ns, ne);
match name.as_ref() {
"amp" => buf.push(b'&'),
"lt" => buf.push(b'<'),
"gt" => buf.push(b'>'),
"quot" => buf.push(b'"'),
"apos" => buf.push(b'\''),
other => {
let decl = entities.get(other);
let kind = decl.map(|d| &d.kind);
if standalone_yes
&& decl.is_some_and(|d| d.declared_external)
{
return Err(scan.err(format!(
"reference to entity '&{other};' declared in the external \
subset is not allowed in a standalone='yes' document \
(XML 1.0 § 2.9 / § 4.1 WFC: Entity Declared)"
)));
}
let frame_base = decl.and_then(|d| d.source_uri.clone());
let is_external = matches!(kind,
Some(EntityKind::ExternalLoaded(_)) | Some(EntityKind::ExternalUnloaded));
let value = match kind {
Some(EntityKind::InternalText(v))
| Some(EntityKind::ExternalLoaded(v)) => v.clone(),
Some(EntityKind::ExternalUnloaded) | None => {
if scan.opts.libxml2_compat
&& matches!(kind, Some(EntityKind::ExternalUnloaded))
{
return Ok(());
}
if scan.current_base_uri().is_some() {
if let Some(sink) = recovered {
sink.push(scan.err_with_level(
crate::error::ErrorLevel::Warning,
format!(
"undefined entity '&{other};' inside an external \
entity — WFC: Entity Declared carve-out applies \
(XML 1.0 § 4.1); expansion skipped"
),
));
}
return Ok(());
}
if pe_relax_undefined {
return Ok(());
}
if scan.opts.recovery_mode {
if let Some(sink) = recovered {
sink.push(scan.err_with_level(
crate::error::ErrorLevel::Error,
format!(
"undefined entity '&{other};' — left as literal text \
(XML 1.0 § 4.1 WFC: Entity Declared)"
),
));
}
buf.push(b'&');
buf.extend_from_slice(other.as_bytes());
buf.push(b';');
return Ok(());
}
return Err(scan.err(format!(
"undefined entity '&{other};' (XML 1.0 § 4.1 WFC: Entity Declared)"
)));
}
};
*used = used.saturating_add(value.len() as u64);
if *used > budget {
return Err(scan.err(format!(
"entity expansion limit exceeded ({budget} bytes) — possible \
entity expansion attack (CVE-2003-1564)"
)));
}
scan.push_entity_stream(other.to_string(), value, element_depth, frame_base)?;
if is_external {
consume_text_decl_if_present(scan, is_xml_11)?;
}
}
}
}
Ok(())
}
pub fn resolve_uri(system_id: &str, base: Option<&str>) -> String {
if system_id.contains("://") || system_id.starts_with('/') {
return system_id.to_string();
}
let Some(base) = base else { return system_id.to_string(); };
let (scheme, base_path) = match base.strip_prefix("file://") {
Some(rest) => ("file://", rest),
None => ("", base),
};
let parent = match base_path.rfind('/') {
Some(i) => &base_path[..=i],
None => "",
};
format!("{scheme}{parent}{system_id}")
}
fn read_external_entity_bytes(
system_id: &str,
base_url: Option<&str>,
) -> std::result::Result<Vec<u8>, String> {
if system_id.starts_with("http://") || system_id.starts_with("https://") {
return Err("network URIs not supported".to_string());
}
let raw: &str = system_id.strip_prefix("file://").unwrap_or(system_id);
let resolved: std::path::PathBuf = {
let pb = std::path::Path::new(raw);
if pb.is_absolute() {
pb.to_path_buf()
} else if let Some(base) = base_url {
let base_path = std::path::Path::new(base.strip_prefix("file://").unwrap_or(base));
match base_path.parent() {
Some(dir) => dir.join(pb),
None => pb.to_path_buf(),
}
} else {
pb.to_path_buf()
}
};
std::fs::read(&resolved).map_err(|e| e.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
fn as_utf8(b: &[u8]) -> &str { std::str::from_utf8(b).unwrap() }
fn events(src: &str) -> Vec<String> {
let mut r = XmlBytesReader::from_str(src);
let mut out = Vec::new();
let mut buf = Vec::new();
loop {
match r.next_into(&mut buf).unwrap() {
BytesEventInto::StartElement { name } => {
let a: Vec<_> = buf.iter()
.map(|a| format!("{}={}", as_utf8(&a.name), as_utf8(&a.value)))
.collect();
let name = as_utf8(&name);
if a.is_empty() { out.push(format!("<{name}>")); }
else { out.push(format!("<{name} {}>", a.join(" "))); }
}
BytesEventInto::EndElement { name } => out.push(format!("</{}>", as_utf8(&name))),
BytesEventInto::Text(t) => out.push(format!("T:{}", as_utf8(&t))),
BytesEventInto::CData(s) => out.push(format!("CD:{}", as_utf8(&s))),
BytesEventInto::Comment(s) => out.push(format!("C:{}", as_utf8(&s))),
BytesEventInto::Pi { target, .. } => out.push(format!("PI:{}", as_utf8(&target))),
BytesEventInto::EntityRef { name } => out.push(format!("E:{}", as_utf8(&name))),
BytesEventInto::Eof => break,
}
}
out
}
#[test]
fn minimal() {
assert_eq!(events("<r/>"), vec!["<r>", "</r>"]);
}
#[test]
fn nested() {
assert_eq!(
events("<a><b>hello</b></a>"),
vec!["<a>", "<b>", "T:hello", "</b>", "</a>"],
);
}
#[test]
fn attributes_borrowed() {
let src = r#"<el id="1" class="x"/>"#;
let mut r = XmlBytesReader::from_str(src);
let mut buf = Vec::new();
let ev = r.next_into(&mut buf).unwrap();
assert!(matches!(&ev, BytesEventInto::StartElement { name } if &name[..] == b"el"));
assert_eq!(buf.len(), 2);
assert!(matches!(buf[0].value, Cow::Borrowed(_)), "no entity → should borrow");
}
#[test]
fn attribute_entity_owned() {
let src = r#"<el v="a&b"/>"#;
let mut r = XmlBytesReader::from_str(src);
let mut buf = Vec::new();
r.next_into(&mut buf).unwrap();
assert_eq!(&*buf[0].value(), b"a&b");
assert!(matches!(buf[0].value, Cow::Owned(_)), "entity → must allocate");
}
#[test]
fn text_borrowed() {
let src = "<r>hello world</r>";
let mut r = XmlBytesReader::from_str(src);
let mut buf = Vec::new();
r.next_into(&mut buf).unwrap(); let ev = r.next_into(&mut buf).unwrap();
match ev {
BytesEventInto::Text(Cow::Borrowed(b)) => assert_eq!(b, b"hello world"),
other => panic!("expected borrowed Text, got {other:?}"),
}
}
#[test]
fn lazy_event_methods_smoke_test() {
let mut r = XmlBytesReader::from_str(
"<root><!-- c --><![CDATA[cd]]><?p y?><a x=\"1\">hi</a></root>"
);
let mut got: Vec<String> = Vec::new();
loop {
match r.next().unwrap() {
BytesEvent::StartElement(tag) => {
let attrs: Vec<_> = tag.attrs()
.map(|a| {
let a = a.unwrap();
format!("{}={}", as_utf8(a.name), as_utf8(&a.value))
})
.collect();
let name_part = format!("<{}", as_utf8(
b"_"
));
let _ = name_part;
if attrs.is_empty() { got.push("<a>".into()); }
else { got.push(format!("<a {}>", attrs.join(" "))); }
}
BytesEvent::EndElement(tag) => got.push(format!("</{}>", as_utf8(tag.name()))),
BytesEvent::Text(t) => got.push(format!("T:{}", as_utf8(t.as_bytes()))),
BytesEvent::CData(s) => got.push(format!("CD:{}", as_utf8(s.as_bytes()))),
BytesEvent::Comment(s) => got.push(format!("C:{}", as_utf8(s.as_bytes()))),
BytesEvent::Pi(p) => got.push(format!("PI:{}", as_utf8(p.target()))),
BytesEvent::EntityRef(e) => got.push(format!("E:{}", as_utf8(e.name()))),
BytesEvent::Eof => break,
}
}
assert!(got.iter().any(|s| s == "C: c "));
assert!(got.iter().any(|s| s == "CD:cd"));
assert!(got.iter().any(|s| s == "PI:p"));
assert!(got.iter().any(|s| s == "<a x=1>"));
assert!(got.iter().any(|s| s == "T:hi"));
assert!(got.iter().any(|s| s == "</a>"));
assert!(got.iter().any(|s| s == "</root>"));
}
#[test]
fn cdata_borrowed() {
let src = "<r><![CDATA[raw <data>]]></r>";
assert_eq!(events(src), vec!["<r>", "CD:raw <data>", "</r>"]);
}
#[test]
fn empty_element_emits_both_events() {
assert_eq!(events("<root><br/></root>"), vec!["<root>", "<br>", "</br>", "</root>"]);
}
#[test]
fn buffer_reuse() {
let src = "<a x='1'/><b y='2'/>";
let src = format!("<root>{src}</root>");
let mut r = XmlBytesReader::from_str(&src);
let mut buf: Vec<BytesAttr> = Vec::new();
let cap_before;
loop {
match r.next_into(&mut buf).unwrap() {
BytesEventInto::StartElement { name } if &name[..] == b"a" => {
cap_before = buf.capacity();
break;
}
BytesEventInto::Eof => panic!("unexpected EOF"),
_ => {}
}
}
loop {
match r.next_into(&mut buf).unwrap() {
BytesEventInto::StartElement { name } if &name[..] == b"b" => {
assert_eq!(buf.capacity(), cap_before, "capacity should not grow for same-size attrs");
break;
}
BytesEventInto::Eof => panic!("unexpected EOF"),
_ => {}
}
}
}
#[test]
fn lazy_attrs_iter() {
let src = r#"<el id="1" class="x"/>"#;
let mut r = XmlBytesReader::from_str(src);
match r.next().unwrap() {
BytesEvent::StartElement(tag) => {
assert_eq!(tag.name(), b"el");
let pairs: Vec<(Vec<u8>, Vec<u8>)> = tag.attrs()
.map(|a| a.map(|a| (a.name.to_vec(), a.value.into_owned())).unwrap())
.collect();
assert_eq!(pairs, vec![
(b"id".to_vec(), b"1".to_vec()),
(b"class".to_vec(), b"x".to_vec()),
]);
}
_ => panic!("expected StartElement"),
}
}
#[test]
fn lazy_attrs_skipped_costs_nothing() {
let src = r#"<el id="1" class="x"/>"#;
let mut r = XmlBytesReader::from_str(src);
match r.next().unwrap() {
BytesEvent::StartElement(tag) => assert_eq!(tag.name(), b"el"),
_ => panic!(),
}
match r.next().unwrap() {
BytesEvent::EndElement(tag) => assert_eq!(tag.name(), b"el"),
_ => panic!(),
}
match r.next().unwrap() {
BytesEvent::Eof => {}
_ => panic!(),
}
}
fn count_text_events(src: &str, opts: ParseOptions) -> (u32, u32) {
let mut r = unsafe { XmlBytesReader::from_bytes_unchecked(src.as_bytes()) }
.with_options(opts);
let (mut total, mut ws) = (0u32, 0u32);
loop {
match r.next().unwrap() {
BytesEvent::Eof => return (total, ws),
BytesEvent::Text(t) => {
total += 1;
if !t.as_bytes().is_empty()
&& t.as_bytes().iter().all(|b| b.is_ascii_whitespace())
{
ws += 1;
}
}
_ => {}
}
}
}
#[test]
fn whitespace_preserved_with_default_options() {
let src = "<root>\n <a/>\n <b/>\n</root>";
let (total, ws) = count_text_events(src, ParseOptions::default());
assert_eq!(ws, 3, "default mode should emit 3 whitespace text events");
assert_eq!(total, 3);
}
#[test]
fn whitespace_preserved_with_skip_end_tag_check() {
let src = "<root>\n <a/>\n <b/>\n</root>";
let opts = ParseOptions { skip_end_tag_check: true, ..ParseOptions::default() };
let (total, ws) = count_text_events(src, opts);
assert_eq!(ws, 3,
"skip_end_tag_check should NOT swallow inter-element whitespace");
assert_eq!(total, 3);
}
#[test]
fn skip_end_tag_check_still_disables_end_tag_match() {
let opts = ParseOptions { skip_end_tag_check: true, ..ParseOptions::default() };
let mut r = XmlBytesReader::from_str("<a></b>").with_options(opts);
let mut events = 0u32;
loop {
match r.next().unwrap() {
BytesEvent::Eof => break,
_ => events += 1,
}
}
assert!(events >= 2, "got both Start and End despite mismatch");
}
#[test]
fn top_level_whitespace_still_skipped_in_both_modes() {
let src = " \n <root/> \n ";
for opts in [
ParseOptions::default(),
ParseOptions { skip_end_tag_check: true, ..ParseOptions::default() },
] {
let (total, ws) = count_text_events(src, opts.clone());
assert_eq!(ws, 0, "no whitespace text events at depth 0 (opts: {opts:?})");
assert_eq!(total, 0);
}
}
fn parse_all(src: &str, opts: ParseOptions) -> Result<u32> {
let mut r = unsafe { XmlBytesReader::from_bytes_unchecked(src.as_bytes()) }
.with_options(opts);
let mut n = 0u32;
loop {
match r.next()? {
BytesEvent::Eof => return Ok(n),
_ => n += 1,
}
}
}
#[test]
fn rejects_unclosed_element_at_eof() {
let err = parse_all("<r><x>", ParseOptions::default()).unwrap_err();
assert!(err.to_string().contains("unclosed"),
"expected 'unclosed' in error, got: {err}");
}
#[test]
fn unclosed_at_eof_relaxed_under_skip_end_tag_check() {
let opts = ParseOptions { skip_end_tag_check: true, ..ParseOptions::default() };
assert!(parse_all("<r><x>", opts).is_ok());
}
#[test]
fn recover_mode_synthesises_closes_for_unclosed_at_eof() {
let opts = ParseOptions { recovery_mode: true, ..ParseOptions::default() };
let mut r = unsafe { XmlBytesReader::from_bytes_unchecked(b"<r><a><b>") }
.with_options(opts);
let mut closed: Vec<Vec<u8>> = Vec::new();
loop {
match r.next().unwrap() {
BytesEvent::Eof => break,
BytesEvent::EndElement(tag) => closed.push(tag.name().to_vec()),
_ => {}
}
}
assert_eq!(
closed.iter().map(|n| String::from_utf8_lossy(n).into_owned()).collect::<Vec<_>>(),
vec!["b".to_string(), "a".to_string(), "r".to_string()],
);
let errs = r.recovered_errors();
assert_eq!(errs.len(), 3, "got {} errors", errs.len());
assert!(errs.iter().all(|e| e.message.contains("unclosed")),
"expected 'unclosed' in every recovery message");
}
#[test]
fn recover_mode_logs_empty_doc_error_but_continues() {
let opts = ParseOptions { recovery_mode: true, ..ParseOptions::default() };
let mut r = unsafe { XmlBytesReader::from_bytes_unchecked(b"") }
.with_options(opts);
assert!(matches!(r.next().unwrap(), BytesEvent::Eof));
let errs = r.recovered_errors();
assert_eq!(errs.len(), 1);
assert!(errs[0].message.contains("no root element"));
}
#[test]
fn strict_mode_errors_unchanged_in_phase1() {
assert!(parse_all("<r><x>", ParseOptions::default()).is_err());
}
#[test]
fn recover_mode_walks_stack_for_mismatched_end_tag() {
let opts = ParseOptions { recovery_mode: true, ..ParseOptions::default() };
let mut r = unsafe { XmlBytesReader::from_bytes_unchecked(b"<a><b><c></a>") }
.with_options(opts);
let mut closed: Vec<Vec<u8>> = Vec::new();
loop {
match r.next().unwrap() {
BytesEvent::Eof => break,
BytesEvent::EndElement(tag) => closed.push(tag.name().to_vec()),
_ => {}
}
}
assert_eq!(
closed.iter().map(|n| String::from_utf8_lossy(n).into_owned()).collect::<Vec<_>>(),
vec!["c".to_string(), "b".to_string(), "a".to_string()],
"expected innermost-first synth closes then real `</a>`",
);
let errs = r.recovered_errors();
assert!(errs.len() >= 1, "got {} errors", errs.len());
assert!(errs.iter().all(|e| e.message.contains("mismatched")),
"expected 'mismatched' in recovery messages");
}
#[test]
fn recover_mode_drops_orphan_end_tag() {
let opts = ParseOptions { recovery_mode: true, ..ParseOptions::default() };
let r = unsafe { XmlBytesReader::from_bytes_unchecked(b"<r></orphan></r>") }
.with_options(opts.clone());
let mut r2 = unsafe { XmlBytesReader::from_bytes_unchecked(b"</orphan><r/>") }
.with_options(opts);
let mut events = 0u32;
loop {
match r2.next().unwrap() {
BytesEvent::Eof => break,
_ => events += 1,
}
}
assert!(events >= 2, "got {} events from `</orphan><r/>`", events);
let errs = r2.recovered_errors();
assert!(errs.iter().any(|e| e.message.contains("orphan")
|| e.message.contains("no open")),
"expected orphan/no-open error, got: {:?}",
errs.iter().map(|e| &e.message).collect::<Vec<_>>());
let _ = r;
}
#[test]
fn recover_mode_accepts_second_root() {
let opts = ParseOptions { recovery_mode: true, ..ParseOptions::default() };
let mut r = unsafe { XmlBytesReader::from_bytes_unchecked(b"<a/><b/>") }
.with_options(opts);
let mut starts: Vec<Vec<u8>> = Vec::new();
loop {
match r.next().unwrap() {
BytesEvent::Eof => break,
BytesEvent::StartElement(t) => starts.push(t.name().to_vec()),
_ => {}
}
}
assert_eq!(
starts.iter().map(|n| String::from_utf8_lossy(n).into_owned()).collect::<Vec<_>>(),
vec!["a".to_string(), "b".to_string()],
"both root elements should be emitted in recover mode",
);
let errs = r.recovered_errors();
assert!(errs.iter().any(|e| e.message.contains("one root")),
"expected 'one root' error, got: {:?}",
errs.iter().map(|e| &e.message).collect::<Vec<_>>());
}
#[test]
fn recover_mode_keeps_bare_lt_literal_in_text() {
let opts = ParseOptions { recovery_mode: true, ..ParseOptions::default() };
let mut r = unsafe { XmlBytesReader::from_bytes_unchecked(b"<r>1 < 2</r>") }
.with_options(opts);
let mut text = String::new();
loop {
match r.next().unwrap() {
BytesEvent::Eof => break,
BytesEvent::Text(t) => text.push_str(&String::from_utf8_lossy(t.as_bytes())),
_ => {}
}
}
assert_eq!(text, "1 < 2",
"all bytes preserved (libxml2 would silently drop the '<')");
assert!(r.recovered_errors().iter().any(|e| e.message.contains("bare '<'")),
"expected bare-< error logged");
}
#[test]
fn recover_mode_accepts_text_before_root() {
let opts = ParseOptions { recovery_mode: true, ..ParseOptions::default() };
let mut r = unsafe { XmlBytesReader::from_bytes_unchecked(b"hello<r/>") }
.with_options(opts);
let mut events: Vec<String> = Vec::new();
loop {
match r.next().unwrap() {
BytesEvent::Eof => break,
BytesEvent::Text(t) =>
events.push(format!("Text({:?})",
String::from_utf8_lossy(t.as_bytes()))),
BytesEvent::StartElement(t) =>
events.push(format!("Start({})",
String::from_utf8_lossy(t.name()))),
BytesEvent::EndElement(t) =>
events.push(format!("End({})",
String::from_utf8_lossy(t.name()))),
_ => {}
}
}
assert!(events.iter().any(|e| e.contains("Text(\"hello\")")),
"expected leading text preserved, events: {:?}", events);
assert!(events.iter().any(|e| e.contains("Start(r)")),
"expected root start emitted, events: {:?}", events);
assert!(r.recovered_errors().iter().any(|e| e.message.contains("document level")),
"expected doc-level error logged");
}
#[test]
fn recover_mode_accepts_text_after_root() {
let opts = ParseOptions { recovery_mode: true, ..ParseOptions::default() };
let mut r = unsafe { XmlBytesReader::from_bytes_unchecked(b"<r/>trailing text") }
.with_options(opts);
let mut text = String::new();
loop {
match r.next().unwrap() {
BytesEvent::Eof => break,
BytesEvent::Text(t) => text.push_str(&String::from_utf8_lossy(t.as_bytes())),
_ => {}
}
}
assert_eq!(text, "trailing text",
"trailing text preserved in recover mode");
}
#[test]
fn recover_mode_keeps_cdata_close_literal_in_text() {
let opts = ParseOptions { recovery_mode: true, ..ParseOptions::default() };
let mut r = unsafe { XmlBytesReader::from_bytes_unchecked(b"<r>oops]]>more</r>") }
.with_options(opts);
let mut text: Option<Vec<u8>> = None;
loop {
match r.next().unwrap() {
BytesEvent::Eof => break,
BytesEvent::Text(t) => text = Some(t.as_bytes().to_vec()),
_ => {}
}
}
let text = text.expect("expected a text event");
assert_eq!(
String::from_utf8_lossy(&text), "oops]]>more",
"all bytes preserved literal in recover mode",
);
assert!(r.recovered_errors().iter().any(|e| e.message.contains("]]>")),
"expected ']]>' error logged");
}
#[test]
fn recover_mode_keeps_bare_amp_literal_in_text() {
let opts = ParseOptions { recovery_mode: true, ..ParseOptions::default() };
let mut r = unsafe { XmlBytesReader::from_bytes_unchecked(b"<r>tom & jerry</r>") }
.with_options(opts);
let mut text: Option<Vec<u8>> = None;
loop {
match r.next().unwrap() {
BytesEvent::Eof => break,
BytesEvent::Text(t) => text = Some(t.as_bytes().to_vec()),
_ => {}
}
}
let text = text.expect("expected a text event");
assert_eq!(
String::from_utf8_lossy(&text), "tom & jerry",
"bare '&' preserved in recover mode (libxml2 silently drops it)",
);
assert!(r.recovered_errors().iter().any(|e| e.message.contains("bare '&'")),
"expected bare-& error logged");
}
#[test]
fn recover_mode_skips_malformed_xml_decl() {
let opts = ParseOptions { recovery_mode: true, ..ParseOptions::default() };
let mut r = unsafe { XmlBytesReader::from_bytes_unchecked(b"<?xml?><r>ok</r>") }
.with_options(opts);
let mut starts: Vec<Vec<u8>> = Vec::new();
loop {
match r.next() {
Ok(BytesEvent::Eof) => break,
Ok(BytesEvent::StartElement(t)) => starts.push(t.name().to_vec()),
Ok(_) => {}
Err(e) => panic!("next() errored unexpectedly in recover mode: {e}"),
}
}
assert_eq!(
starts.iter().map(|n| String::from_utf8_lossy(n).into_owned()).collect::<Vec<_>>(),
vec!["r".to_string()],
"root element should still parse after malformed decl",
);
assert!(r.recovered_errors().iter().any(|e| e.message.to_lowercase().contains("xml decl")
|| e.message.contains("XMLDecl")
|| e.message.contains("version")),
"expected XML-decl error logged, got: {:?}",
r.recovered_errors().iter().map(|e| &e.message).collect::<Vec<_>>());
}
#[test]
fn name_start_combining_mark_accepted_5e_rejected_4e() {
let src = "<\u{309A}/>".as_bytes().to_vec();
let mut r5 = unsafe { XmlBytesReader::from_bytes_unchecked(&src) };
let mut hit_start_5e = false;
loop {
match r5.next().expect("5th-edition parse should accept U+309A") {
BytesEvent::Eof => break,
BytesEvent::StartElement(_) => hit_start_5e = true,
_ => {}
}
}
assert!(hit_start_5e, "5th-edition: U+309A should open a Start event");
let opts = ParseOptions {
xml10_fourth_edition: true,
..ParseOptions::default()
};
let mut r4 = unsafe { XmlBytesReader::from_bytes_unchecked(&src) }
.with_options(opts);
let err = loop {
match r4.next() {
Ok(BytesEvent::Eof) => panic!("4th-edition: should have rejected U+309A name start"),
Ok(_) => continue,
Err(e) => break e,
}
};
assert!(err.message.contains("name-start") || err.message.contains("name start")
|| err.message.to_lowercase().contains("invalid name"),
"4th-edition rejection should mention name-start invalidity, got: {}",
err.message);
}
#[test]
fn name_start_middle_dot_rejected_in_both_editions() {
let src = "<\u{00B7}/>".as_bytes().to_vec();
for fourth in [false, true] {
let opts = ParseOptions { xml10_fourth_edition: fourth, ..ParseOptions::default() };
let mut r = unsafe { XmlBytesReader::from_bytes_unchecked(&src) }
.with_options(opts);
let err = loop {
match r.next() {
Ok(BytesEvent::Eof) => panic!("edition={fourth}: U+00B7 should be rejected"),
Ok(_) => continue,
Err(e) => break e,
}
};
assert!(err.message.to_lowercase().contains("name"),
"edition={fourth}: rejection should mention name, got: {}", err.message);
}
}
#[test]
fn recover_mode_leaves_undefined_entity_literal() {
let opts = ParseOptions { recovery_mode: true, ..ParseOptions::default() };
let mut r = unsafe { XmlBytesReader::from_bytes_unchecked(b"<r>before &xyz; after</r>") }
.with_options(opts);
let mut text_seen: Option<Vec<u8>> = None;
loop {
match r.next().unwrap() {
BytesEvent::Eof => break,
BytesEvent::Text(t) => text_seen = Some(t.as_bytes().to_vec()),
_ => {}
}
}
let text = text_seen.expect("expected a text event");
assert!(text.windows(5).any(|w| w == b"&xyz;"),
"expected `&xyz;` literal in text, got: {:?}",
String::from_utf8_lossy(&text));
let errs = r.recovered_errors();
assert!(errs.iter().any(|e| e.message.contains("undefined entity")
&& e.message.contains("xyz")),
"expected undefined-entity error, got: {:?}",
errs.iter().map(|e| &e.message).collect::<Vec<_>>());
}
#[test]
fn external_entity_with_resolver_loads_replacement_text() {
use crate::entity_resolver::InMemoryResolver;
use std::sync::Arc;
let resolver = Arc::new(
InMemoryResolver::new()
.with_system("file:///fake/foo.ent", b"hello world".to_vec())
);
let opts = ParseOptions {
external_resolver: Some(resolver),
..ParseOptions::default()
};
let src = br#"<!DOCTYPE doc [
<!ENTITY foo SYSTEM "file:///fake/foo.ent">
]>
<doc>&foo;</doc>"#;
let mut r = unsafe { XmlBytesReader::from_bytes_unchecked(src) }
.with_options(opts);
let mut got: Option<Vec<u8>> = None;
loop {
match r.next().unwrap() {
BytesEvent::Eof => break,
BytesEvent::Text(t) if t.as_bytes() != b"\n "
&& !t.as_bytes().iter().all(u8::is_ascii_whitespace)
=> got = Some(t.as_bytes().to_vec()),
_ => {}
}
}
let text = got.expect("expected text event with resolved content");
assert_eq!(String::from_utf8_lossy(&text), "hello world",
"resolver bytes should be the replacement text");
}
#[test]
fn external_entity_with_markup_replacement_text_parses_as_subtree() {
use crate::entity_resolver::InMemoryResolver;
use std::sync::Arc;
let resolver = Arc::new(
InMemoryResolver::new()
.with_system("file:///fake/foo.ent", b"<evil>XML</evil>".to_vec())
);
let opts = ParseOptions {
external_resolver: Some(resolver),
..ParseOptions::default()
};
let src = br#"<!DOCTYPE doc [
<!ENTITY foo SYSTEM "file:///fake/foo.ent">
]>
<doc>&foo;</doc>"#;
let mut r = unsafe { XmlBytesReader::from_bytes_unchecked(src) }
.with_options(opts);
let mut starts = 0;
let mut ends = 0;
let mut texts: Vec<Vec<u8>> = Vec::new();
loop {
match r.next().expect("parse should succeed with markup-bearing entity") {
BytesEvent::Eof => break,
BytesEvent::StartElement(_) => starts += 1,
BytesEvent::EndElement(_) => ends += 1,
BytesEvent::Text(t) => texts.push(t.as_bytes().to_vec()),
_ => {}
}
}
assert_eq!(starts, 2, "expected <doc> and entity-pushed <evil> as Start events");
assert_eq!(ends, 2, "expected balancing End events for both");
assert!(texts.iter().any(|t| t == b"XML"),
"expected `XML` Text event from inside the entity, got: {texts:?}");
}
#[test]
fn external_entity_without_resolver_errors_on_reference() {
let src = br#"<!DOCTYPE doc [
<!ENTITY foo SYSTEM "file:///fake/foo.ent">
]>
<doc>&foo;</doc>"#;
let err = parse_all(std::str::from_utf8(src).unwrap(),
ParseOptions::default()).unwrap_err();
assert!(err.to_string().contains("undefined entity")
|| err.to_string().contains("foo"),
"expected undefined-entity error, got: {err}");
}
#[test]
fn external_entity_resolver_refused_propagates_error() {
use crate::entity_resolver::InMemoryResolver;
use std::sync::Arc;
let opts = ParseOptions {
external_resolver: Some(Arc::new(InMemoryResolver::new())),
..ParseOptions::default()
};
let src = br#"<!DOCTYPE doc [
<!ENTITY foo SYSTEM "file:///fake/foo.ent">
]>
<doc>&foo;</doc>"#;
let mut r = unsafe { XmlBytesReader::from_bytes_unchecked(src) }
.with_options(opts);
let mut hit_err = None;
loop {
match r.next() {
Ok(BytesEvent::Eof) => break,
Ok(_) => continue,
Err(e) => { hit_err = Some(e); break; }
}
}
let e = hit_err.expect("resolver-refused entity reference should error in strict mode");
assert!(e.to_string().contains("resolver"),
"expected resolver-related error, got: {e}");
}
#[test]
fn unreferenced_external_entity_is_not_loaded() {
use crate::entity_resolver::InMemoryResolver;
use std::sync::Arc;
let opts = ParseOptions {
external_resolver: Some(Arc::new(InMemoryResolver::new())),
..ParseOptions::default()
};
let src = br#"<!DOCTYPE doc [
<!ENTITY foo SYSTEM "file:///fake/foo.ent">
]>
<doc/>"#;
let mut r = unsafe { XmlBytesReader::from_bytes_unchecked(src) }
.with_options(opts);
loop {
match r.next() {
Ok(BytesEvent::Eof) => break,
Ok(_) => continue,
Err(e) => panic!("unreferenced external entity must not load: {e}"),
}
}
assert!(r.recovered_errors().iter().all(|e| !e.message.contains("resolver")),
"no resolver should run for an unreferenced external entity");
}
#[test]
fn external_entity_resolver_invalid_utf8_propagates_error() {
use crate::entity_resolver::InMemoryResolver;
use std::sync::Arc;
let resolver = Arc::new(
InMemoryResolver::new()
.with_system("file:///fake/foo.ent", vec![0x80, 0x80, 0x80])
);
let opts = ParseOptions {
external_resolver: Some(resolver),
..ParseOptions::default()
};
let src = br#"<!DOCTYPE doc [
<!ENTITY foo SYSTEM "file:///fake/foo.ent">
]>
<doc>&foo;</doc>"#;
let mut r = unsafe { XmlBytesReader::from_bytes_unchecked(src) }
.with_options(opts);
let mut hit_err: Option<XmlError> = None;
loop {
match r.next() {
Ok(BytesEvent::Eof) => break,
Ok(_) => continue,
Err(e) => { hit_err = Some(e); break; }
}
}
let e = hit_err.expect(
"resolver returning non-UTF-8 bytes must surface a parse error, \
not silently UB-coerce into a String"
);
let msg = e.to_string();
assert!(
msg.to_lowercase().contains("utf-8") || msg.to_lowercase().contains("utf8"),
"expected UTF-8-related error, got: {msg}"
);
}
#[test]
fn external_entity_resolver_refused_recovered() {
use crate::entity_resolver::InMemoryResolver;
use std::sync::Arc;
let opts = ParseOptions {
external_resolver: Some(Arc::new(InMemoryResolver::new())),
recovery_mode: true,
..ParseOptions::default()
};
let src = br#"<!DOCTYPE doc [
<!ENTITY foo SYSTEM "file:///fake/foo.ent">
]>
<doc>&foo;</doc>"#;
let mut r = unsafe { XmlBytesReader::from_bytes_unchecked(src) }
.with_options(opts);
loop {
match r.next().unwrap() {
BytesEvent::Eof => break,
_ => {}
}
}
assert!(r.recovered_errors().iter().any(|e| e.message.contains("resolver")),
"expected resolver error in recovered list");
}
#[test]
fn rejects_two_root_elements() {
let err = parse_all("<a/><b/>", ParseOptions::default()).unwrap_err();
assert!(err.to_string().contains("one root"),
"expected 'one root' in error, got: {err}");
}
#[test]
fn rejects_text_at_document_level_before_root() {
let err = parse_all("hello<r/>", ParseOptions::default()).unwrap_err();
assert!(err.to_string().contains("document level"),
"expected 'document level' in error, got: {err}");
}
#[test]
fn rejects_text_after_root() {
let err = parse_all("<r/>trailing text", ParseOptions::default()).unwrap_err();
assert!(err.to_string().contains("document level"),
"expected 'document level' in error, got: {err}");
}
#[test]
fn allows_comments_and_pis_at_document_level() {
assert!(parse_all("<!-- before --><r/><!-- after -->", ParseOptions::default()).is_ok());
assert!(parse_all("<?pi ?><r/><?pi ?>", ParseOptions::default()).is_ok());
assert!(parse_all(" <r/>\n", ParseOptions::default()).is_ok());
}
#[test]
fn rejects_empty_xml_declaration() {
let err = parse_all("<?xml?><r/>", ParseOptions::default()).unwrap_err();
assert!(err.to_string().contains("version") || err.to_string().contains("XMLDecl"),
"expected 'version' / 'XMLDecl' in error, got: {err}");
}
#[test]
fn rejects_xml_decl_without_version() {
let err = parse_all(r#"<?xml encoding="UTF-8"?><r/>"#, ParseOptions::default()).unwrap_err();
assert!(err.to_string().contains("version"),
"expected 'version' in error, got: {err}");
}
#[test]
fn accepts_full_xml_decl() {
assert!(parse_all(r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><r/>"#,
ParseOptions::default()).is_ok());
}
#[test]
fn skip_inter_element_whitespace_drops_indent_runs() {
let src = "<root>\n <a/>\n <b/>\n</root>";
let opts = ParseOptions {
skip_inter_element_whitespace: true,
..ParseOptions::default()
};
let (total, ws) = count_text_events(src, opts);
assert_eq!(ws, 0, "opt-in should suppress whitespace-only text");
assert_eq!(total, 0, "no other text in this document");
}
#[test]
fn skip_inter_element_whitespace_keeps_non_blank_text_verbatim() {
let src = "<p>foo <b>bar</b> baz</p>";
let opts = ParseOptions {
skip_inter_element_whitespace: true,
..ParseOptions::default()
};
let mut r = unsafe { XmlBytesReader::from_bytes_unchecked(src.as_bytes()) }
.with_options(opts);
let mut texts = Vec::new();
loop {
match r.next().unwrap() {
BytesEvent::Eof => break,
BytesEvent::Text(t) => texts.push(t.as_bytes().to_vec()),
_ => {}
}
}
assert_eq!(
texts.iter().map(|t| String::from_utf8_lossy(t).into_owned()).collect::<Vec<_>>(),
vec!["foo ".to_string(), "bar".to_string(), " baz".to_string()],
);
}
#[test]
fn lazy_discard_event_advances_correctly() {
let mut r = XmlBytesReader::from_str("<root><a/><b/><c/></root>");
match r.next().unwrap() {
BytesEvent::StartElement(_) => {}
other => panic!("expected root StartElement, got {other:?}"),
}
for expected in [b"a", b"b", b"c"] {
match r.next().unwrap() {
BytesEvent::StartElement(_) => {} other => panic!("expected StartElement, got {other:?}"),
}
match r.next().unwrap() {
BytesEvent::EndElement(tag) => assert_eq!(tag.name(), &expected[..]),
other => panic!("expected EndElement, got {other:?}"),
}
}
match r.next().unwrap() {
BytesEvent::EndElement(tag) => assert_eq!(tag.name(), &b"root"[..]),
other => panic!("expected root EndElement, got {other:?}"),
}
assert!(matches!(r.next().unwrap(), BytesEvent::Eof));
}
#[test]
fn debug_impls_for_event_payloads() {
let mut r = XmlBytesReader::from_str(
r#"<root attr="v"><![CDATA[cd]]>text<!-- c --><?pi data?></root>"#,
);
match r.next().unwrap() {
BytesEvent::StartElement(t) => {
let s = format!("{t:?}");
assert!(s.contains("BytesStartTag"), "got {s}");
assert!(s.contains("root"), "got {s}");
}
_ => panic!(),
}
match r.next().unwrap() {
BytesEvent::CData(t) => {
let s = format!("{t:?}");
assert!(s.contains("BytesCData"), "got {s}");
assert!(s.contains("cd"), "got {s}");
}
_ => panic!(),
}
match r.next().unwrap() {
BytesEvent::Text(t) => {
let s = format!("{t:?}");
assert!(s.contains("BytesText"), "got {s}");
assert!(s.contains("text"), "got {s}");
}
_ => panic!(),
}
match r.next().unwrap() {
BytesEvent::Comment(t) => {
let s = format!("{t:?}");
assert!(s.contains("BytesComment"), "got {s}");
assert!(s.contains(" c "), "got {s}");
}
_ => panic!(),
}
match r.next().unwrap() {
BytesEvent::Pi(t) => {
let s = format!("{t:?}");
assert!(s.contains("BytesPi"), "got {s}");
assert!(s.contains("pi"), "got {s}");
assert!(s.contains("data"), "got {s}");
}
_ => panic!(),
}
match r.next().unwrap() {
BytesEvent::EndElement(t) => {
let s = format!("{t:?}");
assert!(s.contains("BytesEndTag"), "got {s}");
assert!(s.contains("root"), "got {s}");
}
_ => panic!(),
}
}
#[test]
fn debug_impl_for_entity_ref() {
let src = r#"<?xml version="1.0"?>
<!DOCTYPE r [<!ENTITY foo "bar">]>
<r>&foo;</r>"#;
let opts = crate::options::ParseOptions {
resolve_entities: false,
..crate::options::ParseOptions::default()
};
let mut r = XmlBytesReader::from_str(src).with_options(opts);
loop {
match r.next().unwrap() {
BytesEvent::EntityRef(e) => {
assert_eq!(e.name(), b"foo");
let s = format!("{e:?}");
assert!(s.contains("BytesEntityRef"), "got {s}");
assert!(s.contains("foo"), "got {s}");
break;
}
BytesEvent::Eof => panic!("EntityRef not seen"),
_ => continue,
}
}
}
#[test]
fn start_tag_name_inside_entity_replacement_stream_is_captured() {
let src = r#"<?xml version="1.0"?>
<!DOCTYPE r [<!ENTITY inner "<span/>">]>
<r>&inner;</r>"#;
let mut r = XmlBytesReader::from_str(src);
let mut saw_span = false;
loop {
match r.next().unwrap() {
BytesEvent::StartElement(tag) => {
if tag.name() == b"span" {
saw_span = true;
assert!(matches!(tag.name_cow(), Cow::Owned(_)),
"entity-stream start tag must carry an owned name");
}
}
BytesEvent::Eof => break,
_ => continue,
}
}
assert!(saw_span, "<span/> from inside &inner; was never emitted");
}
#[test]
fn start_tag_name_on_original_source_stays_borrowed() {
let mut r = XmlBytesReader::from_str("<el/>");
match r.next().unwrap() {
BytesEvent::StartElement(tag) => {
assert_eq!(tag.name(), b"el");
assert!(matches!(tag.name_cow(), Cow::Borrowed(_)),
"source-borrowed start tag must avoid the heap copy");
}
_ => panic!(),
}
}
#[test]
fn bytes_attr_name_accessor() {
let src = r#"<el id="1" class="x"/>"#;
let mut r = XmlBytesReader::from_str(src);
match r.next().unwrap() {
BytesEvent::StartElement(tag) => {
let attrs: Vec<_> = tag.attrs().map(|a| a.unwrap()).collect();
assert_eq!(attrs[0].name(), b"id");
assert_eq!(attrs[1].name(), b"class");
}
_ => panic!(),
}
}
#[test]
fn unescape_bytes_no_amp_returns_borrowed() {
let out = unescape_bytes(b"no entities here");
assert!(matches!(out, Cow::Borrowed(_)));
assert_eq!(&*out, b"no entities here");
}
#[test]
fn unescape_bytes_predefined() {
assert_eq!(&*unescape_bytes(b"&"), b"&");
assert_eq!(&*unescape_bytes(b"<"), b"<");
assert_eq!(&*unescape_bytes(b">"), b">");
assert_eq!(&*unescape_bytes(b"""), b"\"");
assert_eq!(&*unescape_bytes(b"'"), b"'");
}
#[test]
fn unescape_bytes_numeric_decimal_and_hex() {
assert_eq!(&*unescape_bytes(b"A"), b"A");
assert_eq!(&*unescape_bytes(b"A"), b"A");
assert_eq!(&*unescape_bytes(b"A"), b"A");
let euro = unescape_bytes(b"€");
assert_eq!(&*euro, "€".as_bytes());
}
#[test]
fn probe_p68_standalone_yes_dom_path_rejects() {
use crate::parser::parse_bytes;
use crate::options::ParseOptions;
use std::sync::Arc;
use std::collections::HashMap;
use crate::entity_resolver::{EntityResolver, ResolveError};
#[derive(Debug)] struct Stub { served: HashMap<String, Vec<u8>> }
impl EntityResolver for Stub {
fn resolve(&self, _: Option<&str>, sid: &str, _: Option<&str>)
-> std::result::Result<Vec<u8>, ResolveError>
{
self.served.get(sid).cloned().ok_or_else(|| ResolveError::Io(format!("no: {sid}")))
}
}
let mut served = HashMap::new();
served.insert(
"file:///x/ibm.dtd".to_string(),
br#"<!ENTITY aaa "aString">"#.to_vec(),
);
let opts = ParseOptions {
load_external_dtd: true,
base_url: Some("file:///x/ibm.xml".to_string()),
external_resolver: Some(Arc::new(Stub { served }) as Arc<dyn EntityResolver>),
..ParseOptions::default()
};
let src = br#"<?xml version="1.0" standalone="yes"?>
<!DOCTYPE root SYSTEM "ibm.dtd" [<!ELEMENT root (#PCDATA)><!ATTLIST root att CDATA #IMPLIED>]>
<root att="&aaa;">x</root>"#;
let res = parse_bytes(src, &opts);
assert!(res.is_err(),
"standalone=yes + external entity decl must be rejected; got {:?}", res);
}
#[test]
fn probe_p66_sax_path_must_also_reject() {
let src = br#"<!DOCTYPE r [<!ELEMENT r EMPTY><!ATTLIST r att CDATA #IMPLIED>]><r att="�"/>"#;
let opts = crate::options::ParseOptions {
load_external_dtd: false,
..crate::options::ParseOptions::default()
};
let mut r = XmlBytesReader::from_bytes(src)
.expect("UTF-8 ok")
.with_options(opts);
let mut hit_error = false;
loop {
match r.next() {
Ok(BytesEvent::Eof) => break,
Ok(_) => continue,
Err(_) => { hit_error = true; break; }
}
}
assert!(hit_error, "SAX path must surface invalid Char in attr value");
}
#[test]
fn probe_p66_attr_charref_to_u0000() {
use crate::parser::parse_bytes;
use crate::options::ParseOptions;
let src = br#"<!DOCTYPE r [<!ELEMENT r EMPTY><!ATTLIST r att CDATA #IMPLIED>]><r att="�"/>"#;
let res = parse_bytes(src, &ParseOptions::default());
assert!(res.is_err(), "U+0000 char ref in attr value must be rejected; got Ok");
}
#[test]
fn probe_p66_attr_charref_to_ufffe() {
use crate::parser::parse_bytes;
use crate::options::ParseOptions;
let src = br#"<!DOCTYPE r [<!ELEMENT r EMPTY><!ATTLIST r att CDATA #IMPLIED>]><r att=""/>"#;
let res = parse_bytes(src, &ParseOptions::default());
assert!(res.is_err(), "U+FFFE char ref in attr value must be rejected; got Ok");
}
#[test]
fn unescape_bytes_invalid_charref_left_literal() {
assert_eq!(&*unescape_bytes(b"&#abc;"), b"&#abc;");
assert_eq!(&*unescape_bytes(b"�"), b"�");
}
#[test]
fn unescape_bytes_unknown_entity_left_literal() {
assert_eq!(&*unescape_bytes(b"&bogus;"), b"&bogus;");
}
#[test]
fn unescape_bytes_ampersand_without_semicolon_left_literal() {
assert_eq!(&*unescape_bytes(b"& not an entity"), b"& not an entity");
let s = b"&very_long_pseudo_entity_name_that_is_too_far_away;";
assert_eq!(&*unescape_bytes(s), s);
}
#[test]
fn unescape_bytes_mixed_content() {
let out = unescape_bytes(b"a & b < c > d");
assert_eq!(&*out, b"a & b < c > d");
}
#[test]
fn resolve_uri_absolute_file_url_passes_through() {
assert_eq!(
resolve_uri("file:///abs/path/foo.xml", Some("file:///other/base.xml")),
"file:///abs/path/foo.xml"
);
}
#[test]
fn resolve_uri_absolute_path_passes_through() {
assert_eq!(
resolve_uri("/abs/foo.xml", Some("file:///base/dir.xml")),
"/abs/foo.xml"
);
}
#[test]
fn resolve_uri_http_url_passes_through() {
assert_eq!(
resolve_uri("http://example.com/x.dtd", Some("file:///doc.xml")),
"http://example.com/x.dtd"
);
}
#[test]
fn resolve_uri_relative_joined_against_parent_of_base() {
assert_eq!(
resolve_uri("rel/path/foo.ent", Some("file:///docs/E18.xml")),
"file:///docs/rel/path/foo.ent"
);
}
#[test]
fn resolve_uri_dotdot_components_preserved_for_resolver() {
let r = resolve_uri("../sib/foo.ent", Some("file:///docs/sub/E18.xml"));
assert_eq!(r, "file:///docs/sub/../sib/foo.ent");
}
#[test]
fn resolve_uri_no_base_passes_through() {
assert_eq!(resolve_uri("rel/foo.ent", None), "rel/foo.ent");
}
#[test]
fn resolve_uri_non_file_base_preserved_without_scheme() {
assert_eq!(
resolve_uri("foo.ent", Some("/abs/docs/E18.xml")),
"/abs/docs/foo.ent"
);
}
#[test]
fn parser_e18_pe_decl_uses_containing_entity_base_uri() {
use crate::entity_resolver::{EntityResolver, ResolveError};
use std::sync::{Arc, Mutex};
#[derive(Debug, Default)]
struct Recorder {
asked: Mutex<Vec<String>>,
served: std::collections::HashMap<String, Vec<u8>>,
}
impl EntityResolver for Recorder {
fn resolve(
&self,
_public_id: Option<&str>,
system_id: &str,
_base_uri: Option<&str>,
) -> std::result::Result<Vec<u8>, ResolveError> {
self.asked.lock().unwrap().push(system_id.to_string());
self.served.get(system_id).cloned().ok_or_else(|| {
ResolveError::Io(format!("no fixture for {system_id}"))
})
}
}
let mut served = std::collections::HashMap::new();
served.insert(
"file:///docs/sub/outer.ent".to_string(),
b"<!ENTITY % inner SYSTEM 'sib.ent'>".to_vec(),
);
served.insert(
"file:///docs/sub/sib.ent".to_string(),
b"<!-- inner -->".to_vec(),
);
let recorder = Arc::new(Recorder { asked: Mutex::new(Vec::new()), served });
let opts = ParseOptions {
base_url: Some("file:///docs/E18.xml".to_string()),
external_resolver: Some(recorder.clone() as Arc<dyn EntityResolver>),
load_external_dtd: true,
..ParseOptions::default()
};
let src = r#"<?xml version="1.0"?>
<!DOCTYPE r [
<!ENTITY % outer SYSTEM "sub/outer.ent">
%outer;
%inner;
]>
<r/>"#;
let mut r = XmlBytesReader::from_str(src).with_options(opts);
loop {
match r.next().unwrap() {
BytesEvent::Eof => break,
_ => {}
}
}
let asked = recorder.asked.lock().unwrap().clone();
assert_eq!(asked, vec![
"file:///docs/sub/outer.ent".to_string(),
"file:///docs/sub/sib.ent".to_string(),
]);
}
#[test]
fn parser_e18_general_entity_decl_uses_document_base_uri() {
use crate::entity_resolver::{EntityResolver, ResolveError};
use std::sync::{Arc, Mutex};
#[derive(Debug, Default)]
struct Recorder {
asked: Mutex<Vec<String>>,
served: std::collections::HashMap<String, Vec<u8>>,
}
impl EntityResolver for Recorder {
fn resolve(
&self,
_public_id: Option<&str>,
system_id: &str,
_base_uri: Option<&str>,
) -> std::result::Result<Vec<u8>, ResolveError> {
self.asked.lock().unwrap().push(system_id.to_string());
self.served.get(system_id).cloned().ok_or_else(|| {
ResolveError::Io(format!("no fixture for {system_id}"))
})
}
}
let mut served = std::collections::HashMap::new();
served.insert(
"file:///docs/sub/outer.ent".to_string(),
b"<!ENTITY ent SYSTEM 'E18-ent'>".to_vec(),
);
served.insert(
"file:///docs/E18-ent".to_string(),
b"main-dir-content".to_vec(),
);
let recorder = Arc::new(Recorder { asked: Mutex::new(Vec::new()), served });
let opts = ParseOptions {
base_url: Some("file:///docs/E18.xml".to_string()),
external_resolver: Some(recorder.clone() as Arc<dyn EntityResolver>),
load_external_dtd: true,
..ParseOptions::default()
};
let src = r#"<?xml version="1.0"?>
<!DOCTYPE r [
<!ENTITY % outer SYSTEM "sub/outer.ent">
%outer;
]>
<r>&ent;</r>"#;
let mut r = XmlBytesReader::from_str(src).with_options(opts);
loop {
match r.next().unwrap() {
BytesEvent::Eof => break,
_ => {}
}
}
let asked = recorder.asked.lock().unwrap().clone();
assert_eq!(asked, vec![
"file:///docs/sub/outer.ent".to_string(),
"file:///docs/E18-ent".to_string(),
]);
}
#[test]
fn carve_out_undeclared_pe_inside_external_pe_accepted() {
use crate::entity_resolver::{EntityResolver, ResolveError};
use std::sync::{Arc, Mutex};
use std::collections::HashMap;
#[derive(Debug)]
struct Stub { served: HashMap<String, Vec<u8>> }
impl EntityResolver for Stub {
fn resolve(&self, _: Option<&str>, sid: &str, _: Option<&str>)
-> std::result::Result<Vec<u8>, ResolveError>
{
self.served.get(sid).cloned().ok_or_else(|| {
ResolveError::Io(format!("no fixture: {sid}"))
})
}
}
let mut served = HashMap::new();
served.insert(
"file:///docs/outer.ent".to_string(),
b"<!ELEMENT root EMPTY> %pe_missing;".to_vec(),
);
let opts = ParseOptions {
base_url: Some("file:///docs/doc.xml".to_string()),
external_resolver: Some(Arc::new(Stub { served }) as Arc<dyn EntityResolver>),
..ParseOptions::default()
};
let src = r#"<?xml version="1.0"?>
<!DOCTYPE root [
<!ENTITY % outer SYSTEM "outer.ent">
%outer;
]>
<root/>"#;
let mut r = XmlBytesReader::from_str(src).with_options(opts);
let _ = Mutex::new(()); loop {
match r.next().expect("WFC carve-out should let parse succeed") {
BytesEvent::Eof => break,
_ => {}
}
}
}
#[test]
fn carve_out_undeclared_ge_inside_external_pe_accepted() {
use crate::entity_resolver::{EntityResolver, ResolveError};
use std::sync::Arc;
use std::collections::HashMap;
#[derive(Debug)]
struct Stub { served: HashMap<String, Vec<u8>> }
impl EntityResolver for Stub {
fn resolve(&self, _: Option<&str>, sid: &str, _: Option<&str>)
-> std::result::Result<Vec<u8>, ResolveError>
{
self.served.get(sid).cloned().ok_or_else(|| {
ResolveError::Io(format!("no fixture: {sid}"))
})
}
}
let mut served = HashMap::new();
served.insert(
"file:///docs/outer.ent".to_string(),
br#"<!ATTLIST root attr CDATA "&ge_missing;">"#.to_vec(),
);
let opts = ParseOptions {
base_url: Some("file:///docs/doc.xml".to_string()),
external_resolver: Some(Arc::new(Stub { served }) as Arc<dyn EntityResolver>),
..ParseOptions::default()
};
let src = r#"<?xml version="1.0"?>
<!DOCTYPE root [
<!ELEMENT root EMPTY>
<!ENTITY % outer SYSTEM "outer.ent">
%outer;
]>
<root/>"#;
let mut r = XmlBytesReader::from_str(src).with_options(opts);
loop {
match r.next().expect("WFC carve-out should let parse succeed") {
BytesEvent::Eof => break,
_ => {}
}
}
}
#[test]
fn undeclared_ge_in_original_source_still_rejected() {
let src = r#"<?xml version="1.0"?>
<!DOCTYPE root [
<!ELEMENT root (#PCDATA)>
]>
<root>&undeclared;</root>"#;
let opts = ParseOptions::default();
let mut r = XmlBytesReader::from_str(src).with_options(opts);
let mut got_err = false;
loop {
match r.next() {
Ok(BytesEvent::Eof) => break,
Ok(_) => {}
Err(e) => {
assert!(
e.message.contains("undefined entity") &&
e.message.contains("WFC: Entity Declared"),
"expected WFC: Entity Declared error, got: {}", e.message
);
got_err = true;
break;
}
}
}
assert!(got_err, "expected undeclared-entity error outside external content");
}
#[test]
fn undeclared_pe_in_internal_subset_still_rejected() {
let src = r#"<?xml version="1.0"?>
<!DOCTYPE root [
%missing_pe;
<!ELEMENT root EMPTY>
]>
<root/>"#;
let opts = ParseOptions::default();
let mut r = XmlBytesReader::from_str(src).with_options(opts);
let mut got_err = false;
loop {
match r.next() {
Ok(BytesEvent::Eof) => break,
Ok(_) => {}
Err(e) => {
assert!(
e.message.contains("undefined parameter entity"),
"expected undefined-PE error, got: {}", e.message
);
got_err = true;
break;
}
}
}
assert!(got_err, "expected undeclared-PE error outside external content");
}
}