use crate::xml::dom::storage::Values;
use crate::xml::dom::storage::names::{NameStorage, Names};
use crate::xml::dom::storage::{Native, Storage};
use crate::parser_xmloxide::error::{ErrorSeverity, ParseDiagnostic, SourceLocation};
use crate::parser_xmloxide::parser::{EntityResolver, ExternalEntityRequest};
use crate::parser_xmloxide::validation::dtd::{EntityDecl, EntityKind};
#[derive(Clone, Copy, PartialEq, Eq)]
enum ReferenceContext {
Content,
Attribute,
}
#[derive(Clone, Copy)]
enum NameTokenKind {
Name,
NmToken,
}
enum Replacement<'a, S: Storage> {
Borrowed(&'a str),
Decoded(S::Text),
External(String),
}
impl<S: Storage> AsRef<str> for Replacement<'_, S> {
fn as_ref(&self) -> &str {
match self {
Self::Borrowed(value) => value,
Self::Decoded(value) => value.as_ref(),
Self::External(value) => value,
}
}
}
fn append_attribute_literal<S: Storage>(
storage: S,
output: &mut S::Text,
text: &str,
fold_line_ends: bool,
) -> Result<(), crate::xml::dom::ParseError> {
storage.charge(text.len())?;
let bytes = text.as_bytes();
let mut start = 0;
let mut index = 0;
while index < bytes.len() {
if matches!(bytes[index], b'\t' | b'\n' | b'\r') {
storage.append(output, &text[start..index])?;
storage.append(output, " ")?;
if fold_line_ends && bytes[index] == b'\r' && bytes.get(index + 1) == Some(&b'\n') {
index += 1;
}
start = index + 1;
}
index += 1;
}
storage.append(output, &text[start..])
}
pub(crate) fn append_normalized_line_ends<S: Storage>(
storage: S,
output: &mut S::Text,
text: &str,
) -> Result<(), crate::xml::dom::ParseError> {
storage.charge(text.len())?;
let bytes = text.as_bytes();
let mut start = 0;
let mut index = 0;
while index < bytes.len() {
if bytes[index] == b'\r' {
storage.append(output, &text[start..index])?;
storage.append(output, "\n")?;
if bytes.get(index + 1) == Some(&b'\n') {
index += 1;
}
start = index + 1;
}
index += 1;
}
storage.append(output, &text[start..])
}
pub(crate) const DEFAULT_MAX_DEPTH: u32 = 256;
pub(crate) const DEFAULT_MAX_ATTRIBUTES: u32 = 256;
pub(crate) const DEFAULT_MAX_ATTRIBUTE_LENGTH: usize = 10 * 1024 * 1024;
pub(crate) const DEFAULT_MAX_TEXT_LENGTH: usize = 10 * 1024 * 1024;
pub(crate) const DEFAULT_MAX_NAME_LENGTH: usize = 50_000;
pub(crate) const DEFAULT_MAX_ENTITY_EXPANSIONS: u32 = 10_000;
pub(crate) fn is_xml_char(c: char) -> bool {
matches!(c as u32,
0x09 | 0x0A | 0x0D | 0x20..=0xD7FF | 0xE000..=0xFFFD | 0x0001_0000..=0x0010_FFFF
)
}
pub(crate) fn is_name_start_char(c: char) -> bool {
matches!(c,
':' | 'A'..='Z' | '_' | 'a'..='z' |
'\u{C0}'..='\u{D6}' | '\u{D8}'..='\u{F6}' | '\u{F8}'..='\u{2FF}' |
'\u{370}'..='\u{37D}' | '\u{37F}'..='\u{1FFF}' |
'\u{200C}'..='\u{200D}' | '\u{2070}'..='\u{218F}' |
'\u{2C00}'..='\u{2FEF}' | '\u{3001}'..='\u{D7FF}' |
'\u{F900}'..='\u{FDCF}' | '\u{FDF0}'..='\u{FFFD}' |
'\u{10000}'..='\u{EFFFF}'
)
}
pub(crate) fn is_name_char(c: char) -> bool {
is_name_start_char(c)
|| matches!(c,
'-' | '.' | '0'..='9' | '\u{B7}' |
'\u{300}'..='\u{36F}' | '\u{203F}'..='\u{2040}'
)
}
fn is_ascii_name_start(b: u8) -> bool {
b.is_ascii_alphabetic() || b == b'_' || b == b':'
}
fn is_ascii_name_char(b: u8) -> bool {
b.is_ascii_alphanumeric() || b == b'_' || b == b':' || b == b'-' || b == b'.'
}
fn match_builtin_entity(bytes: &[u8]) -> Option<(&'static str, usize)> {
match bytes.first() {
Some(b'a') => {
if bytes.starts_with(b"amp;") {
return Some(("&", 4));
}
if bytes.starts_with(b"apos;") {
return Some(("'", 5));
}
None
}
Some(b'l') => {
if bytes.starts_with(b"lt;") {
return Some(("<", 3));
}
None
}
Some(b'g') => {
if bytes.starts_with(b"gt;") {
return Some((">", 3));
}
None
}
Some(b'q') => {
if bytes.starts_with(b"quot;") {
return Some(("\"", 5));
}
None
}
_ => None,
}
}
pub(crate) fn find_invalid_xml_char(s: &str) -> Option<char> {
s.chars().find(|&ch| !is_xml_char(ch))
}
pub(crate) fn may_contain_invalid_xml_chars(bytes: &[u8]) -> bool {
let len = bytes.len();
let mut i = 0;
while i < len {
let b = bytes[i];
if (b < 0x20 && b != b'\t' && b != b'\n' && b != b'\r') || b == 0x7F {
return true;
}
if b == 0xEF
&& i + 2 < len
&& bytes[i + 1] == 0xBF
&& (bytes[i + 2] == 0xBE || bytes[i + 2] == 0xBF)
{
return true;
}
i += 1;
}
false
}
pub(crate) fn split_name(name: &str) -> (Option<&str>, &str) {
match name.find(':') {
Some(pos) => (Some(&name[..pos]), &name[pos + 1..]),
None => (None, name),
}
}
#[allow(dead_code)]
pub(crate) fn validate_qname(name: &str) -> Option<&'static str> {
let colon_count = name.chars().filter(|&c| c == ':').count();
if colon_count > 1 {
return Some("QName contains multiple colons");
}
if colon_count == 1 && (name.starts_with(':') || name.ends_with(':')) {
return Some("QName has empty prefix or local part");
}
None
}
pub(crate) const XMLNS_NAMESPACE: &str = "http://www.w3.org/2000/xmlns/";
pub(crate) fn is_pubid_char(c: char) -> bool {
matches!(c,
' ' | '\r' | '\n' |
'a'..='z' | 'A'..='Z' | '0'..='9' |
'-' | '\'' | '(' | ')' | '+' | ',' | '.' | '/' | ':' |
'=' | '?' | ';' | '!' | '*' | '#' | '@' | '$' | '_' | '%'
)
}
#[derive(Debug, Clone, Copy)]
#[allow(dead_code)]
pub(crate) struct SavedPosition {
pos: usize,
line: u32,
column: u32,
}
type ParseError<S = Native> = crate::parser_xmloxide::error::ParseError<
<S as Storage>::FrozenText,
<S as Storage>::Values<ParseDiagnostic<<S as Storage>::FrozenText>>,
>;
pub(crate) struct ParserInput<'a, S: NameStorage = Native> {
storage: S,
input: &'a [u8],
pos: usize,
line: u32,
column: u32,
depth: u32,
max_depth: u32,
max_name_length: usize,
pub(crate) entity_expansions: u32,
max_entity_expansions: u32,
recover: bool,
pub(crate) diagnostics: S::Values<ParseDiagnostic<S::FrozenText>>,
pub(crate) entity_map: S::Names<S::Text, EntityDecl<S>>,
pub(crate) has_pe_references: bool,
pub(crate) has_external_dtd: bool,
entity_resolver: Option<EntityResolver>,
}
impl<'a> ParserInput<'a> {
pub fn new(input: &'a str) -> Self {
Self::with_storage(input, Native).expect("empty native input state has no allocation")
}
pub fn set_entity_resolver(&mut self, resolver: Option<EntityResolver>) {
self.entity_resolver = resolver;
}
}
impl<'a, S: NameStorage> ParserInput<'a, S> {
pub(crate) fn external_entity_resolver(&self) -> Option<&EntityResolver> {
self.entity_resolver.as_ref()
}
pub(crate) fn inherit_entity_resolver(&mut self, parent: &Self) {
self.entity_resolver = parent.entity_resolver.clone();
}
pub fn with_storage(input: &'a str, storage: S) -> Result<Self, ParseError<S>> {
Ok(Self {
storage,
input: input.as_bytes(),
pos: 0,
line: 1,
column: 1,
depth: 0,
max_depth: DEFAULT_MAX_DEPTH,
max_name_length: DEFAULT_MAX_NAME_LENGTH,
entity_expansions: 0,
max_entity_expansions: DEFAULT_MAX_ENTITY_EXPANSIONS,
recover: false,
diagnostics: storage.values(0)?,
entity_map: storage.names(),
has_pe_references: false,
has_external_dtd: false,
entity_resolver: None,
})
}
pub fn set_max_depth(&mut self, max: u32) {
self.max_depth = max;
}
pub fn set_max_name_length(&mut self, max: usize) {
self.max_name_length = max;
}
pub fn set_max_entity_expansions(&mut self, max: u32) {
self.max_entity_expansions = max;
}
pub fn set_recover(&mut self, recover: bool) {
self.recover = recover;
}
pub fn recover(&self) -> bool {
self.recover
}
pub fn validate_pubid(&mut self, value: &str) -> Result<(), ParseError<S>> {
self.storage.charge(value.len())?;
for character in value.chars() {
if !is_pubid_char(character) {
let message = format_args!(
"invalid character '{}' (U+{:04X}) in public ID",
character.escape_default(),
character as u32
);
if self.recover {
self.push_diagnostic(ErrorSeverity::Warning, message)?;
return Ok(());
}
return Err(self.fatal(message));
}
}
Ok(())
}
pub fn increment_depth(&mut self) -> Result<(), ParseError<S>> {
self.depth += 1;
if self.depth > self.max_depth {
return Err(self.fatal(format_args!(
"maximum nesting depth exceeded ({})",
self.max_depth
)));
}
Ok(())
}
pub fn decrement_depth(&mut self) {
self.depth = self.depth.saturating_sub(1);
}
pub fn depth(&self) -> u32 {
self.depth
}
pub fn set_depth(&mut self, depth: u32) {
self.depth = depth;
}
pub fn location(&self) -> SourceLocation {
SourceLocation {
line: self.line,
column: self.column,
byte_offset: self.pos,
}
}
#[inline]
pub fn at_end(&self) -> bool {
self.pos >= self.input.len()
}
#[allow(dead_code)]
#[inline]
pub fn pos(&self) -> usize {
self.pos
}
#[allow(dead_code)]
#[inline]
pub fn slice(&self, start: usize, end: usize) -> &'a [u8] {
&self.input[start..end]
}
#[allow(dead_code)]
#[inline]
pub fn remaining(&self) -> &[u8] {
&self.input[self.pos..]
}
#[allow(dead_code)]
pub fn save_position(&self) -> SavedPosition {
SavedPosition {
pos: self.pos,
line: self.line,
column: self.column,
}
}
#[allow(dead_code)]
pub fn restore_position(&mut self, saved: SavedPosition) {
self.pos = saved.pos;
self.line = saved.line;
self.column = saved.column;
}
#[inline]
pub fn peek(&self) -> Option<u8> {
self.input.get(self.pos).copied()
}
pub fn peek_at(&self, offset: usize) -> Option<u8> {
self.input.get(self.pos + offset).copied()
}
#[inline]
pub fn peek_char(&self) -> Option<char> {
if self.pos >= self.input.len() {
return None;
}
let first = self.input[self.pos];
if first < 0x80 {
return Some(first as char);
}
let len = match first {
0xC0..=0xDF => 2,
0xE0..=0xEF => 3,
0xF0..=0xF7 => 4,
_ => return None, };
let remaining = &self.input[self.pos..];
if remaining.len() < len {
return None;
}
std::str::from_utf8(&remaining[..len])
.ok()
.and_then(|s| s.chars().next())
}
#[inline]
pub fn advance(&mut self, count: usize) -> Result<(), ParseError<S>> {
if count == 1 {
self.storage.charge(1)?;
if self.pos < self.input.len() {
if self.input[self.pos] == b'\n' {
self.line += 1;
self.column = 1;
} else {
self.column += 1;
}
self.pos += 1;
}
return Ok(());
}
self.advance_counting_lines(count.min(self.input.len() - self.pos))
}
#[inline]
pub fn advance_char(&mut self, ch: char) -> Result<(), ParseError<S>> {
let len = ch.len_utf8();
self.storage.charge(len)?;
if ch == '\n' {
self.line += 1;
self.column = 1;
} else {
self.column += 1;
}
self.pos += len;
Ok(())
}
#[inline]
pub fn next_byte(&mut self) -> Result<u8, ParseError<S>> {
if self.at_end() {
return Err(self.fatal("unexpected end of input"));
}
let b = self.input[self.pos];
self.advance(1)?;
Ok(b)
}
#[inline]
pub fn next_char(&mut self) -> Result<char, ParseError<S>> {
self.storage.charge(4)?;
if self.pos >= self.input.len() {
return Err(self.fatal("unexpected end of input"));
}
let first = self.input[self.pos];
if first < 0x80 {
self.pos += 1;
if first == b'\n' {
self.line += 1;
self.column = 1;
} else if first == b'\r' {
self.line += 1;
self.column = 1;
if self.pos < self.input.len() && self.input[self.pos] == b'\n' {
self.pos += 1;
}
return Ok('\n');
} else {
self.column += 1;
if first < 0x20 && first != b'\t' {
let ch = first as char;
if self.recover {
self.push_diagnostic(
ErrorSeverity::Error,
format_args!("invalid XML character: U+{:04X}", ch as u32),
)?;
} else {
return Err(
self.fatal(format_args!("invalid XML character: U+{:04X}", ch as u32))
);
}
}
}
return Ok(first as char);
}
let ch = self
.peek_char()
.ok_or_else(|| self.fatal("unexpected end of input"))?;
self.advance_char(ch)?;
if !is_xml_char(ch) {
if self.recover {
self.push_diagnostic(
ErrorSeverity::Error,
format_args!("invalid XML character: U+{:04X}", ch as u32),
)?;
} else {
return Err(self.fatal(format_args!("invalid XML character: U+{:04X}", ch as u32)));
}
}
Ok(ch)
}
fn scan_bytes(
&self,
candidates: usize,
width: usize,
mut stop: impl FnMut(&[u8], usize) -> bool,
) -> Result<usize, ParseError<S>> {
let bytes = &self.input[self.pos..];
let mut index = 0;
while index < candidates {
let end = index + (candidates - index).min(64);
self.storage.charge((end - index) * width)?;
while index < end {
if stop(bytes, index) {
return Ok(index);
}
index += 1;
}
}
Ok(candidates)
}
#[inline]
pub fn scan_char_data(&self) -> Result<usize, ParseError<S>> {
self.scan_bytes(self.input.len() - self.pos, 3, |bytes, i| {
let b = bytes[i];
match b {
b'<' | b'&' => true,
b']' if i + 2 < bytes.len() && bytes[i + 1] == b']' && bytes[i + 2] == b'>' => true,
_ => {
b < 0x20 && b != b'\t' && b != b'\n' && b != b'\r'
}
}
})
}
#[inline]
pub fn scan_attr_value(&self, quote: u8) -> Result<usize, ParseError<S>> {
self.scan_bytes(self.input.len() - self.pos, 1, |bytes, i| {
let b = bytes[i];
if b == quote || b == b'&' || b == b'<' {
return true;
}
b < 0x20 && b != b'\t' && b != b'\n' && b != b'\r'
})
}
#[allow(dead_code)]
pub fn scan_until_any(&self, markers: &[u8]) -> Result<usize, ParseError<S>> {
self.storage.charge(markers.len())?;
let mut marker_set = [false; 256];
for &m in markers {
marker_set[m as usize] = true;
}
self.scan_bytes(self.input.len() - self.pos, 1, |bytes, i| {
marker_set[bytes[i] as usize]
})
}
pub fn scan_for_2byte_terminator(
&self,
t0: u8,
t1: u8,
) -> Result<Option<usize>, ParseError<S>> {
let bytes = &self.input[self.pos..];
if bytes.len() < 2 {
return Ok(None);
}
let end = bytes.len() - 1;
let found = self.scan_bytes(end, 2, |bytes, i| bytes[i] == t0 && bytes[i + 1] == t1)?;
if found < end {
Ok(Some(found))
} else {
Ok(None)
}
}
pub fn scan_for_3byte_terminator(
&self,
t0: u8,
t1: u8,
t2: u8,
) -> Result<Option<usize>, ParseError<S>> {
let bytes = &self.input[self.pos..];
if bytes.len() < 3 {
return Ok(None);
}
let end = bytes.len() - 2;
let found = self.scan_bytes(end, 3, |bytes, i| {
bytes[i] == t0 && bytes[i + 1] == t1 && bytes[i + 2] == t2
})?;
if found < end {
Ok(Some(found))
} else {
Ok(None)
}
}
#[inline]
pub fn advance_counting_lines(&mut self, count: usize) -> Result<(), ParseError<S>> {
debug_assert!(count <= self.input.len() - self.pos);
debug_assert!(count <= usize::MAX / 2);
self.storage.charge(count * 2)?;
let end = self.pos + count;
let slice = &self.input[self.pos..end];
if slice.contains(&b'\n') {
for &b in slice {
if b == b'\n' {
self.line += 1;
self.column = 1;
} else {
self.column += 1;
}
}
} else {
#[allow(clippy::cast_possible_truncation)]
{
self.column += count as u32;
}
}
self.pos = end;
Ok(())
}
#[inline]
pub fn expect_byte(&mut self, expected: u8) -> Result<(), ParseError<S>> {
let b = self.next_byte()?;
if b != expected {
return Err(self.fatal(format_args!(
"expected '{}', found '{}'",
expected as char, b as char
)));
}
Ok(())
}
#[inline]
pub fn expect_str(&mut self, expected: &[u8]) -> Result<(), ParseError<S>> {
if self.pos + expected.len() > self.input.len() {
return Err(self.fatal("unexpected end of input"));
}
self.storage.charge(expected.len())?;
if &self.input[self.pos..self.pos + expected.len()] == expected {
self.advance_counting_lines(expected.len())?;
} else {
for &b in expected {
self.expect_byte(b)?;
}
}
Ok(())
}
#[inline]
pub fn looking_at(&self, s: &[u8]) -> bool {
self.input[self.pos..].starts_with(s)
}
#[allow(dead_code)]
pub fn looking_at_ci(&self, expected: &[u8]) -> bool {
if self.pos + expected.len() > self.input.len() {
return false;
}
self.input[self.pos..self.pos + expected.len()].eq_ignore_ascii_case(expected)
}
#[inline]
pub fn skip_whitespace(&mut self) -> Result<bool, ParseError<S>> {
let start = self.pos;
let count = self.scan_bytes(self.input.len() - self.pos, 1, |bytes, i| {
!matches!(bytes[i], b' ' | b'\t' | b'\n' | b'\r')
})?;
self.advance_counting_lines(count)?;
Ok(self.pos > start)
}
pub fn consume_whitespace(&mut self) -> Result<&'a str, ParseError<S>> {
let start = self.pos;
self.skip_whitespace()?;
self.storage.charge(self.pos - start)?;
Ok(std::str::from_utf8(&self.input[start..self.pos])
.expect("whitespace scanner consumes ASCII"))
}
pub fn skip_whitespace_required(&mut self) -> Result<(), ParseError<S>> {
if !self.skip_whitespace()? {
return Err(self.fatal("whitespace required"));
}
Ok(())
}
pub fn take_while(&mut self, pred: impl Fn(u8) -> bool) -> Result<&'a str, ParseError<S>> {
let start = self.pos;
let count = self.scan_bytes(self.input.len() - self.pos, 1, |bytes, i| {
!bytes[i].is_ascii() || !pred(bytes[i])
})?;
self.advance_counting_lines(count)?;
self.storage.charge(count)?;
Ok(std::str::from_utf8(&self.input[start..self.pos])
.expect("numeric scanners consume ASCII on UTF-8 boundaries"))
}
#[inline]
pub fn parse_name(&mut self) -> Result<&'a str, ParseError<S>> {
self.parse_name_token(NameTokenKind::Name)
}
pub(crate) fn parse_nmtoken(&mut self) -> Result<&'a str, ParseError<S>> {
self.parse_name_token(NameTokenKind::NmToken)
}
fn parse_name_token(&mut self, kind: NameTokenKind) -> Result<&'a str, ParseError<S>> {
self.storage.charge(4)?;
let start = self.pos;
if self.pos >= self.input.len() {
return Err(self.fatal("expected name, found end of input"));
}
let first = self.input[self.pos];
let valid_start = match kind {
NameTokenKind::Name => is_ascii_name_start(first),
NameTokenKind::NmToken => is_ascii_name_char(first),
};
if valid_start {
self.pos += 1;
self.column += 1;
let count = self.scan_bytes(self.input.len() - self.pos, 1, |bytes, i| {
!is_ascii_name_char(bytes[i])
})?;
self.advance_counting_lines(count)?;
if self.pos >= self.input.len() || self.input[self.pos] < 0x80 {
let len = self.pos - start;
if len > self.max_name_length {
return Err(self.fatal(format_args!(
"name length ({len}) exceeds maximum ({})",
self.max_name_length
)));
}
self.storage.charge(len)?;
let name = std::str::from_utf8(&self.input[start..self.pos])
.map_err(|_| self.fatal("invalid UTF-8 in name"))?;
return Ok(name);
}
} else {
let ch = self
.peek_char()
.ok_or_else(|| self.fatal("expected name"))?;
let valid_start = match kind {
NameTokenKind::Name => is_name_start_char(ch),
NameTokenKind::NmToken => is_name_char(ch),
};
if !valid_start {
return Err(self.fatal(format_args!("invalid name start character: '{ch}'")));
}
self.advance_char(ch)?;
}
while self.pos < self.input.len() {
self.storage.charge(4)?;
let Some(ch) = self.peek_char() else { break };
if is_name_char(ch) {
self.advance_char(ch)?;
} else {
break;
}
}
let len = self.pos - start;
if len > self.max_name_length {
return Err(self.fatal(format_args!(
"name length ({len}) exceeds maximum ({})",
self.max_name_length
)));
}
self.storage.charge(len)?;
let name = std::str::from_utf8(&self.input[start..self.pos])
.map_err(|_| self.fatal("invalid UTF-8 in name"))?;
Ok(name)
}
#[allow(dead_code)]
pub fn parse_name_eq(&mut self, expected: &str) -> Result<Option<&'a str>, ParseError<S>> {
let name = self.parse_name()?;
self.storage.charge(name.len())?;
if name == expected {
return Ok(None);
}
Ok(Some(name))
}
pub fn parse_name_eq_parts(
&mut self,
prefix: Option<&str>,
local: &str,
) -> Result<Option<&'a str>, ParseError<S>> {
let name = self.parse_name()?;
self.storage.charge(name.len())?;
let matches = match prefix {
Some(prefix) => {
if let Some(suffix) = name.strip_prefix(prefix)
&& let Some(local_name) = suffix.strip_prefix(':')
{
local_name == local
} else {
false
}
}
None => name == local,
};
if matches {
return Ok(None);
}
Ok(Some(name))
}
pub fn count_entity_expansion(&mut self) -> Result<(), ParseError<S>> {
self.entity_expansions += 1;
if self.entity_expansions > self.max_entity_expansions {
return Err(self.fatal(format_args!(
"entity expansion limit exceeded ({})",
self.max_entity_expansions
)));
}
Ok(())
}
}
impl<'a, S: NameStorage> ParserInput<'a, S> {
#[cfg(test)]
pub fn parse_reference(&mut self) -> Result<S::Text, ParseError<S>> {
let mut buf = self.storage.text("")?;
self.parse_reference_into(&mut buf)?;
Ok(buf)
}
pub fn parse_reference_into<'b>(
&mut self,
buf: &'b mut S::Text,
) -> Result<&'b str, ParseError<S>> {
self.parse_reference_in(buf, ReferenceContext::Content)
}
fn parse_reference_in<'b>(
&mut self,
buf: &'b mut S::Text,
context: ReferenceContext,
) -> Result<&'b str, ParseError<S>> {
let reference_start = self.pos;
self.expect_byte(b'&')?;
let remaining = &self.input[self.pos..];
if let Some(result) = match_builtin_entity(remaining) {
self.count_entity_expansion()?;
let advance_len = result.1;
self.advance_counting_lines(advance_len)?;
let start = buf.len();
self.storage.append(buf, result.0)?;
return Ok(&buf[start..]);
}
if self.peek() == Some(b'#') {
self.count_entity_expansion()?;
self.advance(1)?;
let value = if self.peek() == Some(b'x') {
self.advance(1)?;
let hex = self.take_while(|b| b.is_ascii_hexdigit())?;
if hex.is_empty() {
return Err(self.fatal("empty hex character reference"));
}
u32::from_str_radix(hex, 16)
.map_err(|_| self.fatal("invalid hex character reference"))?
} else {
let dec = self.take_while(|b| b.is_ascii_digit())?;
if dec.is_empty() {
return Err(self.fatal("empty decimal character reference"));
}
dec.parse::<u32>()
.map_err(|_| self.fatal("invalid decimal character reference"))?
};
self.expect_byte(b';')?;
let ch = char::from_u32(value).ok_or_else(|| {
self.fatal(format_args!("invalid character reference: U+{value:04X}"))
})?;
if !is_xml_char(ch) {
return Err(self.fatal(format_args!(
"character reference &#x{value:X}; does not refer to a valid XML character"
)));
}
let start = buf.len();
let mut encoded = [0; 4];
self.storage.append(buf, ch.encode_utf8(&mut encoded))?;
Ok(&buf[start..])
} else {
self.parse_name()?;
self.expect_byte(b';')?;
let start = buf.len();
self.storage.charge(self.pos - reference_start)?;
let reference = std::str::from_utf8(self.slice(reference_start, self.pos))
.expect("a parsed reference preserves UTF-8 boundaries");
self.expand_entity_into(reference, buf, context)?;
Ok(&buf[start..])
}
}
}
impl<S: NameStorage> ParserInput<'_, S> {
#[allow(clippy::too_many_lines)]
fn expand_entity_text(&mut self, text: &str) -> Result<S::Text, ParseError<S>> {
let mut result = self.storage.text("")?;
self.expand_entity_into(text, &mut result, ReferenceContext::Content)?;
Ok(result)
}
fn expand_entity_into(
&mut self,
text: &str,
output: &mut S::Text,
context: ReferenceContext,
) -> Result<(), ParseError<S>> {
self.storage.charge(text.len())?;
if context == ReferenceContext::Content && !text.contains('&') {
self.storage.append(output, text)?;
return Ok(());
}
struct Frame<'a, S: Storage> {
text: Replacement<'a, S>,
position: usize,
fold_line_ends: bool,
}
let mut frames: S::Values<Frame<'_, S>> = self.storage.values(0)?;
Values::push(
&mut frames,
Frame {
text: Replacement::Borrowed(text),
position: 0,
fold_line_ends: true,
},
)?;
let result = output;
while let Some(mut frame) = frames.pop() {
let text = frame.text.as_ref();
let bytes = text.as_bytes();
let mut i = frame.position;
if i == 0 {
self.storage.charge(bytes.len())?;
self.storage.charge(bytes.len())?;
self.storage.charge(bytes.len())?;
}
while i < bytes.len() {
if context == ReferenceContext::Content && bytes[i..].starts_with(b"<![CDATA[") {
let start = i;
i += 9;
while i < bytes.len() && !bytes[i..].starts_with(b"]]>") {
i += 1;
}
if i < bytes.len() {
i += 3;
}
self.storage.append(result, &text[start..i])?;
continue;
}
if bytes[i] == b'&' {
i += 1;
if i < bytes.len() && bytes[i] == b'#' {
i += 1;
let char_val = if i < bytes.len() && bytes[i] == b'x' {
i += 1;
let start = i;
while i < bytes.len() && bytes[i].is_ascii_hexdigit() {
i += 1;
}
let hex = std::str::from_utf8(&bytes[start..i])
.map_err(|_| self.fatal("invalid UTF-8 in entity value"))?;
u32::from_str_radix(hex, 16)
.map_err(|_| self.fatal("invalid hex character reference"))?
} else {
let start = i;
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
}
let dec = std::str::from_utf8(&bytes[start..i])
.map_err(|_| self.fatal("invalid UTF-8 in entity value"))?;
dec.parse::<u32>()
.map_err(|_| self.fatal("invalid decimal character reference"))?
};
if i >= bytes.len() || bytes[i] != b';' {
return Err(
self.fatal("incomplete character reference in entity value")
);
}
i += 1;
let ch = char::from_u32(char_val).ok_or_else(|| {
self.fatal(format_args!(
"invalid character reference: U+{char_val:04X}"
))
})?;
if !is_xml_char(ch) {
return Err(self.fatal("invalid XML character in entity reference"));
}
let mut encoded = [0; 4];
self.storage.append(result, ch.encode_utf8(&mut encoded))?;
} else {
let start = i;
while i < bytes.len() && bytes[i] != b';' {
i += 1;
}
if i >= bytes.len() {
return Err(self.fatal("incomplete entity reference in entity value"));
}
let name = std::str::from_utf8(&bytes[start..i])
.map_err(|_| self.fatal("invalid UTF-8 in entity name"))?;
i += 1;
self.entity_expansions += 1;
if self.entity_expansions > self.max_entity_expansions {
return Err(self.fatal(format_args!(
"entity expansion limit exceeded ({})",
self.max_entity_expansions
)));
}
match name {
"amp" => {
self.storage.append(result, "&")?;
continue;
}
"lt" => {
self.storage.append(result, "<")?;
continue;
}
"gt" => {
self.storage.append(result, ">")?;
continue;
}
"apos" => {
self.storage.append(result, "'")?;
continue;
}
"quot" => {
self.storage.append(result, "\"")?;
continue;
}
_ => {}
}
let declaration = self.entity_map.get(name, self.storage)?;
let replacement = if let Some(EntityDecl {
kind:
EntityKind::External {
system_id,
public_id,
},
..
}) = declaration
{
if context == ReferenceContext::Attribute {
return Err(
self.fatal("external entity reference in attribute value")
);
}
if let Some(resolver) = &self.entity_resolver {
let request = ExternalEntityRequest {
name,
system_id,
public_id: public_id.as_deref(),
};
if let Some(resolved) = resolver(request) {
Replacement::External(resolved)
} else {
return Err(self.fatal(format_args!(
"reference to external entity '{name}' is not supported"
)));
}
} else {
return Err(self.fatal(format_args!(
"reference to external entity '{name}' is not supported"
)));
}
} else if let Some(EntityDecl {
kind: EntityKind::Internal(value),
..
}) = declaration
{
self.storage.charge(value.len())?;
if context == ReferenceContext::Attribute && value.contains("&#") {
Replacement::Decoded(
crate::parser_xmloxide::validation::dtd::expand_char_refs_only(
value,
self.storage,
)?,
)
} else {
Replacement::Borrowed(value.as_ref())
}
} else if self.recover || self.has_pe_references || self.has_external_dtd {
let diagnostic = ParseDiagnostic {
severity: ErrorSeverity::Warning,
message: S::freeze(self.storage.formatted(format_args!(
"unknown entity reference: &{name};"
))?),
location: self.location(),
};
Values::push(&mut self.diagnostics, diagnostic)?;
continue;
} else {
return Err(
self.fatal(format_args!("unknown entity reference: &{name};"))
);
};
frame.position = i;
Values::push(&mut frames, frame)?;
let fold_line_ends = !matches!(replacement, Replacement::Decoded(_));
Values::push(
&mut frames,
Frame {
text: replacement,
position: 0,
fold_line_ends,
},
)?;
break;
}
} else {
let start = i;
i += 1;
while i < bytes.len() && bytes[i] != b'&' && bytes[i] != b'<' {
i += 1;
}
if context == ReferenceContext::Attribute {
if bytes[start] == b'<' {
return Err(self.fatal("'<' not allowed in attribute values (from entity replacement text)"));
}
append_attribute_literal(
self.storage,
result,
&text[start..i],
frame.fold_line_ends,
)?;
} else {
self.storage.append(result, &text[start..i])?;
}
}
}
}
Ok(())
}
}
impl<S: NameStorage> ParserInput<'_, S> {
pub fn parse_attribute_value(&mut self) -> Result<S::Text, ParseError<S>> {
let quote = self.next_byte()?;
if quote != b'"' && quote != b'\'' {
return Err(self.fatal("attribute value must be quoted"));
}
let mut value = self.storage.text("")?;
loop {
let safe_len = self.scan_attr_value(quote)?;
if safe_len > 0 {
let start = self.pos;
self.storage.charge(safe_len)?;
self.storage.charge(safe_len)?;
self.storage.charge(safe_len)?;
self.storage.charge(safe_len)?;
let chunk = std::str::from_utf8(&self.input[start..start + safe_len])
.map_err(|_| self.fatal("invalid UTF-8 in attribute value"))?;
if let Some(bad) = may_contain_invalid_xml_chars(chunk.as_bytes())
.then(|| find_invalid_xml_char(chunk))
.flatten()
{
if self.recover {
self.push_diagnostic(
ErrorSeverity::Error,
format_args!("invalid XML character: U+{:04X}", bad as u32),
)?;
} else {
return Err(
self.fatal(format_args!("invalid XML character: U+{:04X}", bad as u32))
);
}
}
append_attribute_literal(self.storage, &mut value, chunk, true)?;
self.advance_counting_lines(safe_len)?;
}
if self.at_end() {
return Err(self.fatal("unexpected end of input in attribute value"));
}
let b = self.input[self.pos];
if b == quote {
self.advance(1)?;
break;
}
if b == b'&' {
self.parse_reference_in(&mut value, ReferenceContext::Attribute)?;
} else if b == b'<' {
return Err(self.fatal("'<' not allowed in attribute values"));
} else {
let ch = self.next_char()?;
if ch == '\r' || ch == '\n' || ch == '\t' {
self.storage.append(&mut value, " ")?;
} else {
let mut encoded = [0; 4];
self.storage
.append(&mut value, ch.encode_utf8(&mut encoded))?;
}
}
}
Ok(value)
}
}
impl<'a, S: NameStorage> ParserInput<'a, S> {
pub fn parse_quoted_value(&mut self) -> Result<&'a str, ParseError<S>> {
let quote = self.next_byte()?;
if quote != b'"' && quote != b'\'' {
return Err(self.fatal("expected quoted value"));
}
let start = self.pos;
let count =
self.scan_bytes(self.input.len() - self.pos, 1, |bytes, i| bytes[i] == quote)?;
self.advance_counting_lines(count)?;
self.storage.charge(count)?;
let value = std::str::from_utf8(&self.input[start..self.pos])
.map_err(|_| self.fatal("invalid UTF-8 in quoted value"))?;
self.expect_byte(quote)?;
Ok(value)
}
pub fn fatal(&self, message: impl std::fmt::Display) -> ParseError<S> {
let snapshot = || -> Result<_, crate::xml::dom::ParseError> {
let message = S::freeze(self.storage.formatted(format_args!("{message}"))?);
self.storage.charge(self.diagnostics.len())?;
let mut diagnostics = self.storage.values(self.diagnostics.len())?;
for diagnostic in &*self.diagnostics {
diagnostics.push(ParseDiagnostic {
severity: diagnostic.severity,
message: S::freeze(self.storage.text(diagnostic.message.as_ref())?),
location: diagnostic.location,
})?;
}
Ok((message, diagnostics))
};
match snapshot() {
Ok((message, diagnostics)) => ParseError::<S>::Syntax {
message,
location: self.location(),
diagnostics,
},
Err(error) => ParseError::<S>::Resource(error),
}
}
pub fn push_diagnostic(
&mut self,
severity: ErrorSeverity,
message: impl std::fmt::Display,
) -> Result<(), ParseError<S>> {
let message = S::freeze(self.storage.formatted(format_args!("{message}"))?);
let diagnostic = ParseDiagnostic {
severity,
message,
location: self.location(),
};
self.diagnostics.push(diagnostic)?;
Ok(())
}
}
pub(crate) struct NamespaceResolver<S: NameStorage = Native> {
stack: S::Values<S::Values<NamespaceUndo<S>>>,
default_ns: Option<S::Text>,
prefixed_ns: S::Names<S::Text, S::Text>,
storage: S,
}
enum NamespaceUndo<S: Storage> {
Default(Option<S::Text>),
Prefix(S::Text, Option<S::Text>),
}
pub(crate) const XML_NAMESPACE: &str = "http://www.w3.org/XML/1998/namespace";
impl NamespaceResolver<Native> {
pub fn new() -> Result<Self, crate::xml::dom::ParseError> {
Self::with_storage(Native)
}
}
impl<S: NameStorage> NamespaceResolver<S> {
pub fn with_storage(storage: S) -> Result<Self, crate::xml::dom::ParseError> {
let mut resolver = Self {
stack: storage.values(0)?,
default_ns: None,
prefixed_ns: storage.names(),
storage,
};
resolver.push_scope()?;
resolver.bind(Some(storage.text("xml")?), storage.text(XML_NAMESPACE)?)?;
Ok(resolver)
}
pub fn push_scope(&mut self) -> Result<(), crate::xml::dom::ParseError> {
self.stack.push(self.storage.values(0)?)
}
pub fn pop_scope(&mut self) -> Result<(), crate::xml::dom::ParseError> {
if let Some(mut bindings) = self.stack.pop() {
while let Some(binding) = bindings.pop() {
self.storage.charge(1)?;
match binding {
NamespaceUndo::Default(previous) => {
self.default_ns = previous;
}
NamespaceUndo::Prefix(prefix, previous) => {
if let Some(uri) = previous {
self.prefixed_ns.insert(prefix, uri)?;
} else {
self.prefixed_ns.remove(prefix.as_ref(), self.storage)?;
}
}
}
}
}
Ok(())
}
pub fn bind(
&mut self,
prefix: Option<S::Text>,
uri: S::Text,
) -> Result<(), crate::xml::dom::ParseError> {
self.storage.charge(1)?;
let undo = match prefix {
None => NamespaceUndo::Default(self.default_ns.replace(uri)),
Some(prefix) => {
let key = self.storage.text(prefix.as_ref())?;
let previous = self.prefixed_ns.insert(prefix, uri)?;
NamespaceUndo::Prefix(key, previous)
}
};
if let Some(frame) = self.stack.last_mut() {
frame.push(undo)?;
}
Ok(())
}
pub fn resolve(
&self,
prefix: Option<&str>,
) -> Result<Option<&str>, crate::xml::dom::ParseError> {
self.storage.charge(1)?;
Ok(match prefix {
None => self.default_ns.as_deref().filter(|s| !s.is_empty()),
Some(pfx) => self
.prefixed_ns
.get(pfx, self.storage)?
.map(AsRef::as_ref)
.filter(|s| !s.is_empty()),
})
}
}
pub(crate) fn parse_comment_content<'a, S: NameStorage>(
input: &mut ParserInput<'a, S>,
) -> Result<&'a str, ParseError<S>> {
input.expect_str(b"<!--")?;
let content_start = input.pos();
loop {
match input.scan_for_2byte_terminator(b'-', b'-')? {
Some(safe_len) => {
if safe_len > 0 {
let start = input.pos();
input.storage.charge(safe_len)?;
input.storage.charge(safe_len)?;
input.storage.charge(safe_len)?;
let has_bad =
may_contain_invalid_xml_chars(input.slice(start, start + safe_len));
let chunk = std::str::from_utf8(input.slice(start, start + safe_len))
.map_err(|_| input.fatal("invalid UTF-8 in comment"))?;
if has_bad && let Some(bad) = find_invalid_xml_char(chunk) {
if input.recover() {
input.push_diagnostic(
ErrorSeverity::Error,
format_args!("invalid XML character: U+{:04X}", bad as u32),
)?;
} else {
return Err(input.fatal(format_args!(
"invalid XML character: U+{:04X}",
bad as u32
)));
}
}
input.advance_counting_lines(safe_len)?;
}
if input.looking_at(b"-->") {
let content = std::str::from_utf8(input.slice(content_start, input.pos()))
.map_err(|_| input.fatal("invalid UTF-8 in comment"))?;
input.advance_counting_lines(3)?;
return Ok(content);
}
if input.recover() {
input.push_diagnostic(
ErrorSeverity::Error,
"'--' not allowed inside comments",
)?;
input.advance_counting_lines(2)?;
} else {
return Err(input.fatal("'--' not allowed inside comments"));
}
}
None => {
return Err(input.fatal("unexpected end of input in comment"));
}
}
}
}
pub(crate) fn parse_cdata_content<'a, S: NameStorage>(
input: &mut ParserInput<'a, S>,
) -> Result<&'a str, ParseError<S>> {
input.expect_str(b"<![CDATA[")?;
match input.scan_for_3byte_terminator(b']', b']', b'>')? {
Some(safe_len) => {
let start = input.pos();
input.storage.charge(safe_len)?;
input.storage.charge(safe_len)?;
input.storage.charge(safe_len)?;
let has_bad = may_contain_invalid_xml_chars(input.slice(start, start + safe_len));
let content = std::str::from_utf8(input.slice(start, start + safe_len))
.map_err(|_| input.fatal("invalid UTF-8 in CDATA section"))?;
if has_bad && let Some(bad) = find_invalid_xml_char(content) {
if input.recover() {
input.push_diagnostic(
ErrorSeverity::Error,
format_args!("invalid XML character: U+{:04X}", bad as u32),
)?;
} else {
return Err(
input.fatal(format_args!("invalid XML character: U+{:04X}", bad as u32))
);
}
}
input.advance_counting_lines(safe_len + 3)?; Ok(content)
}
None => Err(input.fatal("unexpected end of input in CDATA section")),
}
}
pub(crate) fn parse_pi_content<'a, S: NameStorage>(
input: &mut ParserInput<'a, S>,
) -> Result<(&'a str, Option<&'a str>), ParseError<S>> {
input.expect_str(b"<?")?;
let target = input.parse_name()?;
if target.eq_ignore_ascii_case("xml") {
return Err(input.fatal("PI target 'xml' is reserved"));
}
input.storage.charge(target.len())?;
if target.contains(':') {
return Err(input.fatal("PI target must not contain a colon"));
}
let data = if input.skip_whitespace()? {
match input.scan_for_2byte_terminator(b'?', b'>')? {
Some(data_len) => {
let start = input.pos();
input.storage.charge(data_len)?;
input.storage.charge(data_len)?;
input.storage.charge(data_len)?;
let has_bad = may_contain_invalid_xml_chars(input.slice(start, start + data_len));
let data = std::str::from_utf8(input.slice(start, start + data_len))
.map_err(|_| input.fatal("invalid UTF-8 in processing instruction"))?;
if has_bad && let Some(bad) = find_invalid_xml_char(data) {
if input.recover() {
input.push_diagnostic(
ErrorSeverity::Error,
format_args!("invalid XML character: U+{:04X}", bad as u32),
)?;
} else {
return Err(input
.fatal(format_args!("invalid XML character: U+{:04X}", bad as u32)));
}
}
input.advance_counting_lines(data_len + 2)?; if data.is_empty() { None } else { Some(data) }
}
None => {
return Err(input.fatal("unexpected end of input in processing instruction"));
}
}
} else {
input.expect_str(b"?>")?;
None
};
Ok((target, data))
}
#[derive(Debug, Clone)]
pub(crate) struct XmlDeclaration<'a> {
pub version: &'a str,
pub encoding: Option<&'a str>,
pub standalone: Option<bool>,
}
pub(crate) fn parse_xml_decl<'a, S: NameStorage>(
input: &mut ParserInput<'a, S>,
) -> Result<XmlDeclaration<'a>, ParseError<S>> {
input.expect_str(b"<?xml")?;
input.skip_whitespace_required()?;
input.expect_str(b"version")?;
input.skip_whitespace()?;
input.expect_byte(b'=')?;
input.skip_whitespace()?;
let version = input.parse_quoted_value()?;
input.storage.charge(version.len())?;
if !is_valid_version_num(version) {
return Err(input.fatal(format_args!("invalid version number: '{version}'")));
}
let had_ws = input.skip_whitespace()?;
let encoding = if input.looking_at(b"encoding") {
if !had_ws {
return Err(input.fatal("whitespace required before encoding"));
}
input.expect_str(b"encoding")?;
input.skip_whitespace()?;
input.expect_byte(b'=')?;
input.skip_whitespace()?;
let enc = input.parse_quoted_value()?;
input.storage.charge(enc.len())?;
if !is_valid_encoding_name(enc) {
return Err(input.fatal(format_args!("invalid encoding name: '{enc}'")));
}
Some(enc)
} else {
None
};
let had_ws2 = input.skip_whitespace()? || (encoding.is_none() && had_ws);
let standalone = if input.looking_at(b"standalone") {
if !had_ws2 {
return Err(input.fatal("whitespace required before standalone"));
}
input.expect_str(b"standalone")?;
input.skip_whitespace()?;
input.expect_byte(b'=')?;
input.skip_whitespace()?;
let val = input.parse_quoted_value()?;
match val {
"yes" => Some(true),
"no" => Some(false),
_ => return Err(input.fatal("standalone must be 'yes' or 'no'")),
}
} else {
None
};
input.skip_whitespace()?;
input.expect_str(b"?>")?;
Ok(XmlDeclaration {
version,
encoding,
standalone,
})
}
fn is_valid_version_num(s: &str) -> bool {
if let Some(rest) = s.strip_prefix("1.") {
!rest.is_empty() && rest.bytes().all(|b| b.is_ascii_digit())
} else {
false
}
}
fn is_valid_encoding_name(s: &str) -> bool {
let bytes = s.as_bytes();
if bytes.is_empty() {
return false;
}
if !bytes[0].is_ascii_alphabetic() {
return false;
}
bytes[1..]
.iter()
.all(|&b| b.is_ascii_alphanumeric() || b == b'.' || b == b'_' || b == b'-')
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_peek_and_advance() {
let mut input = ParserInput::new("abc");
assert_eq!(input.peek(), Some(b'a'));
assert_eq!(input.peek_at(1), Some(b'b'));
input.advance(1).unwrap();
assert_eq!(input.peek(), Some(b'b'));
input.advance(2).unwrap();
assert!(input.at_end());
}
#[test]
fn test_line_column_tracking() {
let mut input = ParserInput::new("ab\ncd");
assert_eq!(input.location().line, 1);
assert_eq!(input.location().column, 1);
input.advance(2).unwrap(); assert_eq!(input.location().column, 3);
input.advance(1).unwrap(); assert_eq!(input.location().line, 2);
assert_eq!(input.location().column, 1);
}
#[test]
fn test_next_char_cr_normalization() {
let mut input = ParserInput::new("a\r\nb");
assert_eq!(input.next_char().unwrap(), 'a');
assert_eq!(input.next_char().unwrap(), '\n'); assert_eq!(input.next_char().unwrap(), 'b');
}
#[test]
fn test_parse_name() {
let mut input = ParserInput::new("foo:bar ");
let name = input.parse_name().unwrap();
assert_eq!(name, "foo:bar");
}
#[test]
fn name_and_numeric_scanners_borrow_the_original_input_window() {
for source in ["prefix:name ", "préfixe:имя "] {
let mut input = ParserInput::new(source);
let name = input.parse_name().unwrap();
assert_eq!(name.as_ptr(), source.as_ptr());
}
let source = "123456;";
let mut input = ParserInput::new(source);
let digits = input.take_while(|byte| byte.is_ascii_digit()).unwrap();
assert_eq!(digits.as_ptr(), source.as_ptr());
assert_eq!(digits, "123456");
}
#[test]
fn test_parse_name_length_limit() {
let long_name = "a".repeat(100);
let mut input = ParserInput::new(&long_name);
input.set_max_name_length(50);
let result = input.parse_name();
assert!(result.is_err());
assert!(result.unwrap_err().syntax_message().contains("name length"));
}
#[test]
fn test_parse_reference_builtin() {
let mut input = ParserInput::new("&");
assert_eq!(input.parse_reference().unwrap(), "&");
let mut input = ParserInput::new("<");
assert_eq!(input.parse_reference().unwrap(), "<");
let mut input = ParserInput::new(">");
assert_eq!(input.parse_reference().unwrap(), ">");
let mut input = ParserInput::new("'");
assert_eq!(input.parse_reference().unwrap(), "'");
let mut input = ParserInput::new(""");
assert_eq!(input.parse_reference().unwrap(), "\"");
}
#[test]
fn test_parse_reference_char_decimal() {
let mut input = ParserInput::new("A");
assert_eq!(input.parse_reference().unwrap(), "A");
}
#[test]
fn test_parse_reference_char_hex() {
let mut input = ParserInput::new("A");
assert_eq!(input.parse_reference().unwrap(), "A");
}
#[test]
fn test_parse_reference_unknown_error() {
let mut input = ParserInput::new("&bogus;");
assert!(input.parse_reference().is_err());
}
#[test]
fn test_parse_reference_unknown_recovery() {
let mut input = ParserInput::new("&bogus;");
input.set_recover(true);
let result = input.parse_reference().unwrap();
assert_eq!(result, "");
assert_eq!(input.diagnostics.len(), 1);
}
#[test]
fn test_entity_expansion_limit() {
let mut input = ParserInput::new("&&&");
input.set_max_entity_expansions(2);
assert!(input.parse_reference().is_ok());
assert!(input.parse_reference().is_ok());
assert!(input.parse_reference().is_err());
}
#[test]
fn test_depth_limit() {
let mut input = ParserInput::new("");
input.set_max_depth(2);
assert!(input.increment_depth().is_ok()); assert!(input.increment_depth().is_ok()); assert!(input.increment_depth().is_err()); }
#[test]
fn test_parse_attribute_value() {
let mut input = ParserInput::new("\"hello & world\"");
let value = input.parse_attribute_value().unwrap();
assert_eq!(value, "hello & world");
}
#[test]
fn test_parse_attribute_value_whitespace_normalization() {
let mut input = ParserInput::new("\"a\tb\nc\"");
let value = input.parse_attribute_value().unwrap();
assert_eq!(value, "a b c");
}
#[test]
fn attribute_crlf_is_one_normalized_space() {
let mut input = ParserInput::new("'a\r\nb\rc\nd e'");
assert_eq!(input.parse_attribute_value().unwrap(), "a b c d\r\ne");
}
#[test]
fn test_parse_quoted_value() {
let mut input = ParserInput::new("'hello'");
let value = input.parse_quoted_value().unwrap();
assert_eq!(value, "hello");
}
#[test]
fn test_skip_whitespace() {
let mut input = ParserInput::new(" \t\n abc");
assert!(input.skip_whitespace().unwrap());
assert_eq!(input.peek(), Some(b'a'));
}
#[test]
fn test_looking_at() {
let input = ParserInput::new("<!--comment-->");
assert!(input.looking_at(b"<!--"));
assert!(!input.looking_at(b"<![CDATA["));
}
#[test]
fn test_take_while() {
let mut input = ParserInput::new("12345abc");
let digits = input.take_while(|b| b.is_ascii_digit()).unwrap();
assert_eq!(digits, "12345");
assert_eq!(input.peek(), Some(b'a'));
}
#[test]
fn lexical_text_windows_borrow_the_original_source() {
let source = "<!--π text--><![CDATA[😀]]><?target data?>'value'";
let mut input = ParserInput::new(source);
let comment = parse_comment_content(&mut input).unwrap();
assert_eq!(comment, "π text");
assert_eq!(comment.as_ptr(), source[4..].as_ptr());
let cdata_start = input.pos() + "<![CDATA[".len();
let cdata = parse_cdata_content(&mut input).unwrap();
assert_eq!(cdata, "😀");
assert_eq!(cdata.as_ptr(), source[cdata_start..].as_ptr());
let pi_start = input.pos() + 2;
let (target, data) = parse_pi_content(&mut input).unwrap();
assert_eq!(target, "target");
assert_eq!(target.as_ptr(), source[pi_start..].as_ptr());
assert_eq!(data, Some("data"));
assert_eq!(
data.as_ref().unwrap().as_ptr(),
source[pi_start + 7..].as_ptr()
);
let quoted_start = input.pos() + 1;
let quoted = input.parse_quoted_value().unwrap();
assert_eq!(quoted, "value");
assert_eq!(quoted.as_ptr(), source[quoted_start..].as_ptr());
assert!(input.at_end());
}
#[cfg(feature = "xmldsig")]
#[test]
fn current_input_scans_admit_local_work_before_touching_source() {
use crate::xml::dom::storage::Accounted;
use structured_asn1::{
Encoding, Limits, Parser,
memory::{Memory, MemoryAccount, MemoryError},
};
let source = format!("<!--{}-->", "\u{e9} content ".repeat(1024));
let memory = Memory::new(std::sync::Arc::new(MemoryAccount::new(0, 1 << 24)));
let mut input = ParserInput::with_storage(&source, Accounted(&memory)).unwrap();
let content = parse_comment_content(&mut input).unwrap();
assert_eq!(content.as_ptr(), source[4..].as_ptr());
assert_eq!(content, &source[4..source.len() - 3]);
assert_eq!(memory.used(), 0);
let needed = memory.work_used();
assert!(needed > source.len());
for limit in [0, needed as u64 - 1, needed as u64] {
let memory = Memory::new(std::sync::Arc::new(MemoryAccount::new(0, 1 << 24)));
let parser = Parser::new(
Encoding::Der,
Limits {
max_work: limit,
..Default::default()
},
memory.clone(),
);
let reader = parser.read(&[], 0).unwrap();
let work = reader.execution();
let mut input = ParserInput::with_storage(&source, Accounted(&work)).unwrap();
let result = parse_comment_content(&mut input);
if limit == needed as u64 {
assert_eq!(result.unwrap(), content);
} else {
assert!(matches!(
result,
Err(ParseError::<Accounted<'_, _>>::Resource(
crate::xml::dom::ParseError::SharedExecution(MemoryError::WorkExceeded)
))
));
if limit == 0 {
assert_eq!(input.pos(), 0);
}
}
assert_eq!(memory.used(), 0);
}
}
#[cfg(feature = "xmldsig")]
#[test]
fn current_input_diagnostics_keep_capacity_and_preserve_refusal() {
use crate::xml::dom::storage::Accounted;
use structured_asn1::memory::{Memory, MemoryAccount, MemoryError};
let memory = Memory::new(std::sync::Arc::new(MemoryAccount::new(1 << 20, 1 << 24)));
let mut input = ParserInput::with_storage("", Accounted(&memory)).unwrap();
input
.push_diagnostic(ErrorSeverity::Warning, "preceding warning")
.unwrap();
let error = input.fatal(format_args!("invalid name: {}", "\u{540d}"));
let ParseError::<Accounted<'_, _>>::Syntax {
message,
location,
diagnostics,
} = &error
else {
panic!("paid syntax snapshot");
};
assert_eq!(message.as_ref(), "invalid name: \u{540d}");
assert_eq!(*location, input.location());
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].message.as_ref(), "preceding warning");
drop(input);
assert!(memory.used() > 0);
drop(error);
assert_eq!(memory.used(), 0);
let denied = Memory::new(std::sync::Arc::new(MemoryAccount::new(0, 1 << 20)));
let input = ParserInput::with_storage("", Accounted(&denied)).unwrap();
assert!(matches!(
input.fatal("diagnostic"),
ParseError::<Accounted<'_, _>>::Resource(crate::xml::dom::ParseError::SharedExecution(
MemoryError::LimitExceeded
))
));
assert_eq!(denied.used(), 0);
}
#[cfg(feature = "xmldsig")]
#[test]
fn current_attribute_entities_keep_storage_and_refuse_before_parsing() {
use crate::xml::dom::storage::Accounted;
use structured_asn1::{
Encoding, Limits, Parser,
memory::{Memory, MemoryAccount, MemoryError, Work},
};
const SOURCE: &str = "'&a; A & &b;'";
let memory = Memory::new(std::sync::Arc::new(MemoryAccount::new(1 << 20, 1 << 24)));
let parser = Parser::new(
Encoding::Der,
Limits {
max_work: 1 << 20,
..Default::default()
},
memory.clone(),
);
let reader = parser.read(&[], 0).unwrap();
let work = reader.execution();
let storage = Accounted(&work);
let mut input = ParserInput::with_storage(SOURCE, storage).unwrap();
Names::insert(
&mut input.entity_map,
storage.text("a").unwrap(),
EntityDecl {
name: storage.text("").unwrap(),
kind: EntityKind::Internal(storage.text("hello").unwrap()),
},
)
.unwrap();
Names::insert(
&mut input.entity_map,
storage.text("b").unwrap(),
EntityDecl {
name: storage.text("").unwrap(),
kind: EntityKind::Internal(storage.text("&a;").unwrap()),
},
)
.unwrap();
let retained = memory.used();
assert!(retained > 0);
let result = input.parse_attribute_value().unwrap();
assert_eq!(result.as_ref(), "hello A & hello");
assert!(memory.used() > retained);
drop(result);
assert_eq!(memory.used(), retained);
input.restore_position(SavedPosition {
pos: 0,
line: 1,
column: 1,
});
work.charge_work((1 << 20) - memory.work_used() as u64)
.unwrap();
assert!(matches!(
input.parse_attribute_value(),
Err(ParseError::<Accounted<'_, _>>::Resource(
crate::xml::dom::ParseError::SharedExecution(MemoryError::WorkExceeded)
))
));
assert_eq!(input.pos(), 0);
drop(input);
assert_eq!(memory.used(), 0);
let denied = Memory::new(std::sync::Arc::new(MemoryAccount::new(0, 1 << 20)));
let mut input = ParserInput::with_storage("'payload'", Accounted(&denied)).unwrap();
assert!(matches!(
input.parse_attribute_value(),
Err(ParseError::<Accounted<'_, _>>::Resource(
crate::xml::dom::ParseError::SharedExecution(MemoryError::LimitExceeded)
))
));
assert_eq!(denied.used(), 0);
}
#[test]
fn test_split_name() {
assert_eq!(split_name("foo:bar"), (Some("foo"), "bar"));
assert_eq!(split_name("bar"), (None, "bar"));
assert_eq!(split_name(":bar"), (Some(""), "bar"));
}
#[test]
fn test_namespace_resolver() {
let mut ns = NamespaceResolver::new().unwrap();
assert_eq!(ns.resolve(Some("xml")).unwrap(), Some(XML_NAMESPACE));
assert_eq!(ns.resolve(None).unwrap(), None);
ns.push_scope().unwrap();
ns.bind(None, "http://default".to_string()).unwrap();
ns.bind(Some("foo".to_string()), "http://foo".to_string())
.unwrap();
assert_eq!(ns.resolve(None).unwrap(), Some("http://default"));
assert_eq!(ns.resolve(Some("foo")).unwrap(), Some("http://foo"));
ns.pop_scope().unwrap();
assert_eq!(ns.resolve(None).unwrap(), None);
assert_eq!(ns.resolve(Some("foo")).unwrap(), None);
}
#[cfg(feature = "xmldsig")]
#[test]
fn namespace_undo_moves_uris_and_keeps_the_current_work_domain() {
use crate::xml::dom::storage::Accounted;
use structured_asn1::{
Encoding, Limits, Parser,
memory::{Memory, MemoryAccount, MemoryError, Work},
};
let memory = Memory::new(std::sync::Arc::new(MemoryAccount::new(1 << 20, 1 << 24)));
let parser = Parser::new(
Encoding::Der,
Limits {
max_work: 1 << 20,
..Default::default()
},
memory.clone(),
);
let reader = parser.read(&[], 0).unwrap();
let work = reader.execution();
let storage = Accounted(&work);
let mut resolver = NamespaceResolver::with_storage(storage).unwrap();
resolver.push_scope().unwrap();
let original_uri = storage.text("urn:retained:outer").unwrap();
let original_pointer = original_uri.as_ptr();
resolver
.bind(Some(storage.text("p").unwrap()), original_uri)
.unwrap();
resolver.push_scope().unwrap();
resolver
.bind(
Some(storage.text("p").unwrap()),
storage.text("urn:inner").unwrap(),
)
.unwrap();
assert_eq!(resolver.resolve(Some("p")).unwrap(), Some("urn:inner"));
resolver.pop_scope().unwrap();
let restored = resolver.resolve(Some("p")).unwrap().unwrap();
assert_eq!(restored, "urn:retained:outer");
assert_eq!(restored.as_ptr(), original_pointer);
assert!(memory.used() > 0);
work.charge_work((1 << 20) - memory.work_used() as u64)
.unwrap();
assert!(matches!(
resolver.resolve(Some("p")),
Err(crate::xml::dom::ParseError::SharedExecution(
MemoryError::WorkExceeded
))
));
drop(resolver);
assert_eq!(memory.used(), 0);
}
#[test]
fn test_namespace_undeclare_default() {
let mut ns = NamespaceResolver::new().unwrap();
ns.push_scope().unwrap();
ns.bind(None, "http://default".to_string()).unwrap();
assert_eq!(ns.resolve(None).unwrap(), Some("http://default"));
ns.push_scope().unwrap();
ns.bind(None, String::new()).unwrap(); assert_eq!(ns.resolve(None).unwrap(), None);
ns.pop_scope().unwrap();
assert_eq!(ns.resolve(None).unwrap(), Some("http://default"));
}
#[test]
fn test_parse_comment_content() {
let mut input = ParserInput::new("<!-- hello -->");
let content = parse_comment_content(&mut input).unwrap();
assert_eq!(content, " hello ");
}
#[test]
fn test_parse_cdata_content() {
let mut input = ParserInput::new("<![CDATA[some <data>]]>");
let content = parse_cdata_content(&mut input).unwrap();
assert_eq!(content, "some <data>");
}
#[test]
fn test_parse_pi_content() {
let mut input = ParserInput::new("<?target data?>");
let (target, data) = parse_pi_content(&mut input).unwrap();
assert_eq!(target, "target");
assert_eq!(data, Some("data"));
}
#[test]
fn test_parse_pi_no_data() {
let mut input = ParserInput::new("<?target?>");
let (target, data) = parse_pi_content(&mut input).unwrap();
assert_eq!(target, "target");
assert_eq!(data, None);
}
#[test]
fn test_parse_xml_decl() {
let mut input = ParserInput::new("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
let decl = parse_xml_decl(&mut input).unwrap();
assert_eq!(decl.version, "1.0");
assert_eq!(decl.encoding, Some("UTF-8"));
assert_eq!(decl.standalone, None);
}
#[test]
fn test_parse_xml_decl_standalone() {
let mut input =
ParserInput::new("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>");
let decl = parse_xml_decl(&mut input).unwrap();
assert_eq!(decl.standalone, Some(true));
}
#[test]
fn test_is_name_chars() {
assert!(is_name_start_char('a'));
assert!(is_name_start_char('Z'));
assert!(is_name_start_char('_'));
assert!(is_name_start_char(':'));
assert!(!is_name_start_char('0'));
assert!(!is_name_start_char('-'));
assert!(is_name_char('a'));
assert!(is_name_char('0'));
assert!(is_name_char('-'));
assert!(is_name_char('.'));
assert!(!is_name_char(' '));
}
#[test]
fn test_increment_depth_exact_boundary() {
let mut input = ParserInput::new("");
input.set_max_depth(3);
assert!(input.increment_depth().is_ok()); assert!(input.increment_depth().is_ok()); assert!(input.increment_depth().is_ok()); assert!(input.increment_depth().is_err()); }
#[test]
fn test_increment_depth_max_depth_one() {
let mut input = ParserInput::new("");
input.set_max_depth(1);
assert!(input.increment_depth().is_ok()); let err = input.increment_depth().unwrap_err();
assert!(
err.syntax_message()
.contains("maximum nesting depth exceeded")
);
}
#[test]
fn test_increment_depth_max_depth_zero() {
let mut input = ParserInput::new("");
input.set_max_depth(0);
let err = input.increment_depth().unwrap_err();
assert!(
err.syntax_message()
.contains("maximum nesting depth exceeded")
);
}
#[test]
fn test_decrement_depth_saturates_at_zero() {
let mut input = ParserInput::new("");
input.decrement_depth();
assert_eq!(input.depth(), 0);
input.increment_depth().unwrap();
assert_eq!(input.depth(), 1);
input.decrement_depth();
assert_eq!(input.depth(), 0);
input.decrement_depth();
assert_eq!(input.depth(), 0);
}
#[test]
fn test_depth_resets_after_decrement_allows_reentry() {
let mut input = ParserInput::new("");
input.set_max_depth(2);
assert!(input.increment_depth().is_ok()); assert!(input.increment_depth().is_ok()); input.decrement_depth(); assert!(input.increment_depth().is_ok()); assert!(input.increment_depth().is_err()); }
#[test]
fn test_entity_expansion_limit_exact_boundary() {
let mut input = ParserInput::new("&&&&");
input.set_max_entity_expansions(3);
assert!(input.parse_reference().is_ok()); assert!(input.parse_reference().is_ok()); assert!(input.parse_reference().is_ok()); let err = input.parse_reference().unwrap_err();
assert!(
err.syntax_message()
.contains("entity expansion limit exceeded")
);
}
#[test]
fn test_entity_expansion_limit_zero() {
let mut input = ParserInput::new("&");
input.set_max_entity_expansions(0);
let err = input.parse_reference().unwrap_err();
assert!(
err.syntax_message()
.contains("entity expansion limit exceeded")
);
}
#[test]
fn test_entity_expansion_limit_one() {
let mut input = ParserInput::new("&<");
input.set_max_entity_expansions(1);
assert!(input.parse_reference().is_ok());
let err = input.parse_reference().unwrap_err();
assert!(
err.syntax_message()
.contains("entity expansion limit exceeded")
);
}
#[test]
fn test_entity_expansion_counter_includes_char_refs() {
let mut input = ParserInput::new("ABC");
input.set_max_entity_expansions(2);
assert!(input.parse_reference().is_ok()); assert!(input.parse_reference().is_ok()); let err = input.parse_reference().unwrap_err();
assert!(
err.syntax_message()
.contains("entity expansion limit exceeded")
);
}
#[test]
fn test_entity_expansion_limit_via_parse_str() {
use crate::parser_xmloxide::parser::{ParseOptions, parse_str_with_options};
let refs: String = (0..50).map(|_| "A").collect();
let xml = format!("<r>{refs}</r>");
let opts = ParseOptions::default().max_entity_expansions(10);
let result = parse_str_with_options(&xml, &opts);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.syntax_message()
.contains("entity expansion limit")
);
}
#[test]
fn test_entity_expansion_dtd_internal_entity() {
use crate::parser_xmloxide::parser::{ParseOptions, parse_str_with_options};
let xml = r#"<!DOCTYPE r [
<!ENTITY greet "Hello">
]>
<r>&greet;</r>"#;
let doc = parse_str_with_options(xml, &ParseOptions::default()).unwrap();
let root = doc.root_element().unwrap();
assert_eq!(doc.text_content(root), "Hello");
}
#[test]
fn test_entity_expansion_nested_dtd_entities() {
use crate::parser_xmloxide::parser::{ParseOptions, parse_str_with_options};
let xml = r#"<!DOCTYPE r [
<!ENTITY a "world">
<!ENTITY b "hello &a;">
]>
<r>&b;</r>"#;
let doc = parse_str_with_options(xml, &ParseOptions::default()).unwrap();
let root = doc.root_element().unwrap();
assert_eq!(doc.text_content(root), "hello world");
}
#[test]
fn test_entity_expansion_limit_nested_dtd_entities_in_attributes() {
use crate::parser_xmloxide::parser::{ParseOptions, parse_str_with_options};
let xml = r#"<!DOCTYPE r [
<!ENTITY a "x">
<!ENTITY b "&a;&a;&a;">
<!ENTITY c "&b;&b;&b;">
]>
<r v="&c;"/>"#;
let opts = ParseOptions::default().max_entity_expansions(5);
let result = parse_str_with_options(xml, &opts);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.syntax_message()
.contains("entity expansion limit")
);
}
#[test]
fn test_billion_laughs_entity_bomb_in_attribute() {
use crate::parser_xmloxide::parser::{ParseOptions, parse_str_with_options};
let xml = r#"<!DOCTYPE r [
<!ENTITY lol "lol">
<!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
<!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
<!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
]>
<r v="&lol4;"/>"#;
let opts = ParseOptions::default().max_entity_expansions(100);
let result = parse_str_with_options(xml, &opts);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.syntax_message().contains("entity expansion limit"),
"billion laughs should be caught by expansion limit, got: {}",
err.syntax_message()
);
}
#[test]
fn test_billion_laughs_in_text_content_with_markup() {
use crate::parser_xmloxide::parser::{ParseOptions, parse_str_with_options};
let xml = r#"<!DOCTYPE r [
<!ENTITY lol "lol">
<!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
<!ENTITY lol3 "<i>&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;</i>">
]>
<r>&lol3;</r>"#;
let opts = ParseOptions::default().max_entity_expansions(50);
let result = parse_str_with_options(xml, &opts);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.syntax_message().contains("entity expansion limit"),
"billion laughs with markup should be caught, got: {}",
err.syntax_message()
);
}
#[test]
fn test_xxe_external_entity_rejected_by_default() {
use crate::parser_xmloxide::parser::{ParseOptions, parse_str_with_options};
let xml = r#"<!DOCTYPE r [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<r>&xxe;</r>"#;
let result = parse_str_with_options(xml, &ParseOptions::default());
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.syntax_message().contains("external entity"),
"XXE should be rejected by default, got: {}",
err.syntax_message()
);
}
#[test]
fn test_xxe_external_entity_with_public_id_rejected() {
use crate::parser_xmloxide::parser::{ParseOptions, parse_str_with_options};
let xml = r#"<!DOCTYPE r [
<!ENTITY xxe PUBLIC "-//Evil//EN" "http://evil.com/payload">
]>
<r>&xxe;</r>"#;
let result = parse_str_with_options(xml, &ParseOptions::default());
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.syntax_message().contains("external entity"),
"XXE with PUBLIC id should be rejected, got: {}",
err.syntax_message()
);
}
#[test]
fn test_xxe_external_entity_in_attribute_rejected() {
use crate::parser_xmloxide::parser::{ParseOptions, parse_str_with_options};
let xml = r#"<!DOCTYPE r [
<!ENTITY xxe SYSTEM "file:///etc/shadow">
]>
<r a="&xxe;"/>"#;
let result = parse_str_with_options(xml, &ParseOptions::default());
assert!(result.is_err(), "XXE in attribute should be rejected");
}
#[test]
fn test_xxe_multiple_external_entities_all_rejected() {
use crate::parser_xmloxide::parser::{ParseOptions, parse_str_with_options};
let xml = r#"<!DOCTYPE r [
<!ENTITY safe "ok">
<!ENTITY evil SYSTEM "file:///etc/passwd">
]>
<r>&safe;&evil;</r>"#;
let result = parse_str_with_options(xml, &ParseOptions::default());
assert!(result.is_err());
}
#[test]
fn test_char_ref_null_character_rejected() {
let mut input = ParserInput::new("�");
let err = input.parse_reference().unwrap_err();
assert!(
err.syntax_message().contains("valid XML character"),
"null char ref should be rejected as invalid XML char, got: {}",
err.syntax_message()
);
}
#[test]
fn test_char_ref_null_hex_rejected() {
let mut input = ParserInput::new("�");
let err = input.parse_reference().unwrap_err();
assert!(
err.syntax_message().contains("valid XML character"),
"� should be rejected, got: {}",
err.syntax_message()
);
}
#[test]
fn test_char_ref_control_chars_rejected() {
for codepoint in [1u32, 2, 7, 8, 0x0B, 0x0C, 0x0E, 0x1F] {
let ref_str = format!("&#x{codepoint:X};");
let mut input = ParserInput::new(&ref_str);
let result = input.parse_reference();
assert!(
result.is_err(),
"&#x{codepoint:X}; should be rejected as invalid XML character"
);
}
}
#[test]
fn test_char_ref_allowed_control_chars() {
let mut input = ParserInput::new("	");
assert_eq!(input.parse_reference().unwrap(), "\t");
let mut input = ParserInput::new("
");
assert_eq!(input.parse_reference().unwrap(), "\n");
let mut input = ParserInput::new("
");
assert_eq!(input.parse_reference().unwrap(), "\r");
}
#[test]
fn test_char_ref_surrogate_codepoints_rejected() {
let mut input = ParserInput::new("�");
let err = input.parse_reference().unwrap_err();
assert!(
err.syntax_message().contains("invalid character reference"),
"surrogate � should be rejected, got: {}",
err.syntax_message()
);
let mut input = ParserInput::new("�");
let err = input.parse_reference().unwrap_err();
assert!(
err.syntax_message().contains("invalid character reference"),
"surrogate � should be rejected, got: {}",
err.syntax_message()
);
}
#[test]
fn test_char_ref_fffe_and_ffff_rejected() {
let mut input = ParserInput::new("");
let err = input.parse_reference().unwrap_err();
assert!(
err.syntax_message().contains("valid XML character"),
" should be rejected, got: {}",
err.syntax_message()
);
let mut input = ParserInput::new("");
let err = input.parse_reference().unwrap_err();
assert!(
err.syntax_message().contains("valid XML character"),
" should be rejected, got: {}",
err.syntax_message()
);
}
#[test]
fn test_char_ref_max_valid_codepoint() {
let mut input = ParserInput::new("");
let result = input.parse_reference().unwrap();
assert_eq!(result, "\u{10FFFF}");
}
#[test]
fn test_char_ref_beyond_unicode_range() {
let mut input = ParserInput::new("�");
let err = input.parse_reference().unwrap_err();
assert!(
err.syntax_message().contains("invalid character reference"),
"codepoint beyond Unicode range should be rejected, got: {}",
err.syntax_message()
);
}
#[test]
fn test_char_ref_very_large_decimal_rejected() {
let mut input = ParserInput::new("�");
let err = input.parse_reference().unwrap_err();
assert!(
err.syntax_message()
.contains("invalid decimal character reference"),
"overflowing decimal char ref should be rejected, got: {}",
err.syntax_message()
);
}
#[test]
fn test_char_ref_very_large_hex_rejected() {
let mut input = ParserInput::new("�");
let err = input.parse_reference().unwrap_err();
assert!(
err.syntax_message()
.contains("invalid hex character reference"),
"overflowing hex char ref should be rejected, got: {}",
err.syntax_message()
);
}
#[test]
fn test_char_ref_empty_decimal_rejected() {
let mut input = ParserInput::new("&#;");
let err = input.parse_reference().unwrap_err();
assert!(
err.syntax_message()
.contains("empty decimal character reference"),
"empty decimal ref should be rejected, got: {}",
err.syntax_message()
);
}
#[test]
fn test_char_ref_empty_hex_rejected() {
let mut input = ParserInput::new("&#x;");
let err = input.parse_reference().unwrap_err();
assert!(
err.syntax_message()
.contains("empty hex character reference"),
"empty hex ref should be rejected, got: {}",
err.syntax_message()
);
}
#[test]
fn test_char_ref_valid_bmp_characters() {
let mut input = ParserInput::new(" ");
assert_eq!(input.parse_reference().unwrap(), " ");
let mut input = ParserInput::new("A");
assert_eq!(input.parse_reference().unwrap(), "A");
let mut input = ParserInput::new("中"); assert_eq!(input.parse_reference().unwrap(), "\u{4E2D}");
}
#[test]
fn test_char_ref_supplementary_plane() {
let mut input = ParserInput::new("𝄞");
assert_eq!(input.parse_reference().unwrap(), "\u{1D11E}");
}
#[test]
fn test_parse_name_at_exact_length_limit() {
let name = "a".repeat(50);
let input_str = format!("{name} ");
let mut input = ParserInput::new(&input_str);
input.set_max_name_length(50);
let result = input.parse_name().unwrap();
assert_eq!(result.len(), 50);
}
#[test]
fn test_parse_name_one_over_length_limit() {
let name = "a".repeat(51);
let input_str = format!("{name} ");
let mut input = ParserInput::new(&input_str);
input.set_max_name_length(50);
let result = input.parse_name();
assert!(result.is_err());
assert!(result.unwrap_err().syntax_message().contains("name length"));
}
#[test]
fn test_parse_name_length_limit_one() {
let mut input = ParserInput::new("a ");
input.set_max_name_length(1);
assert_eq!(input.parse_name().unwrap(), "a");
let mut input = ParserInput::new("ab ");
input.set_max_name_length(1);
assert!(input.parse_name().is_err());
}
#[test]
fn test_parse_name_unicode_length_counted_in_bytes() {
let name = "\u{C0}\u{C0}\u{C0}"; let input_str = format!("{name} ");
let mut input = ParserInput::new(&input_str);
input.set_max_name_length(5);
let result = input.parse_name();
assert!(
result.is_err(),
"6-byte unicode name should exceed 5-byte limit"
);
let mut input = ParserInput::new(&input_str);
input.set_max_name_length(6);
assert!(
input.parse_name().is_ok(),
"6-byte unicode name should fit 6-byte limit"
);
}
#[test]
fn test_parse_name_eq_length_limit() {
let name = "a".repeat(51);
let input_str = format!("{name} ");
let mut input = ParserInput::new(&input_str);
input.set_max_name_length(50);
let result = input.parse_name_eq("something");
assert!(result.is_err());
assert!(result.unwrap_err().syntax_message().contains("name length"));
}
#[test]
fn test_parse_name_eq_parts_length_limit() {
let name = "a".repeat(51);
let input_str = format!("{name} ");
let mut input = ParserInput::new(&input_str);
input.set_max_name_length(50);
let result = input.parse_name_eq_parts(None, "something");
assert!(result.is_err());
assert!(result.unwrap_err().syntax_message().contains("name length"));
}
#[test]
fn matching_names_share_grammar_limits_and_source_windows() {
for source in [
"ns:local>",
"n\u{e9}:\u{540d}>",
"\u{540d}>",
"a:b:c>",
"9bad>",
"",
] {
for limit in [0, 1, 4, 64] {
let mut ordinary = ParserInput::new(source);
ordinary.set_max_name_length(limit);
let expected = ordinary.parse_name();
let location = ordinary.location();
let mut exact = ParserInput::new(source);
exact.set_max_name_length(limit);
let actual = exact.parse_name_eq("different");
let mut parts = ParserInput::new(source);
parts.set_max_name_length(limit);
let split = parts.parse_name_eq_parts(Some("different"), "local");
assert_eq!(exact.location(), location);
assert_eq!(parts.location(), location);
match expected {
Ok(name) => {
let actual = actual.unwrap().unwrap();
let split = split.unwrap().unwrap();
assert_eq!(actual, name);
assert_eq!(split, name);
assert_eq!(actual.as_ptr(), name.as_ptr());
assert_eq!(split.as_ptr(), name.as_ptr());
}
Err(error) => {
assert_eq!(actual.unwrap_err().syntax_message(), error.syntax_message());
assert_eq!(split.unwrap_err().syntax_message(), error.syntax_message());
}
}
}
}
for (source, prefix, local) in [
("ns:local>", Some("ns"), "local"),
("n\u{e9}:\u{540d}>", Some("n\u{e9}"), "\u{540d}"),
("\u{540d}>", None, "\u{540d}"),
] {
let mut input = ParserInput::new(source);
assert_eq!(input.parse_name_eq_parts(prefix, local).unwrap(), None);
assert_eq!(input.peek(), Some(b'>'));
}
}
#[test]
fn test_comment_double_dash_rejected() {
let mut input = ParserInput::new("<!-- bad -- comment -->");
let result = parse_comment_content(&mut input);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.syntax_message()
.contains("'--' not allowed"),
"double dash inside comment should be rejected"
);
}
#[test]
fn test_comment_double_dash_recovery() {
let mut input = ParserInput::new("<!-- bad -- comment -->");
input.set_recover(true);
let content = parse_comment_content(&mut input).unwrap();
assert!(content.contains("--"));
assert!(!input.diagnostics.is_empty());
}
#[test]
fn test_comment_unterminated() {
let mut input = ParserInput::new("<!-- no end");
let result = parse_comment_content(&mut input);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.syntax_message()
.contains("unexpected end of input in comment")
);
}
#[test]
fn test_comment_empty() {
let mut input = ParserInput::new("<!---->");
let content = parse_comment_content(&mut input).unwrap();
assert_eq!(content, "");
}
#[test]
fn test_comment_single_dash_allowed() {
let mut input = ParserInput::new("<!-- a - b -->");
let content = parse_comment_content(&mut input).unwrap();
assert_eq!(content, " a - b ");
}
#[test]
fn test_comment_ending_with_triple_dash_rejected() {
let mut input = ParserInput::new("<!----->");
let result = parse_comment_content(&mut input);
assert!(
result.is_err() || {
false
}
);
}
#[test]
fn test_cdata_unterminated() {
let mut input = ParserInput::new("<![CDATA[no end");
let result = parse_cdata_content(&mut input);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.syntax_message()
.contains("unexpected end of input in CDATA")
);
}
#[test]
fn test_cdata_empty() {
let mut input = ParserInput::new("<![CDATA[]]>");
let content = parse_cdata_content(&mut input).unwrap();
assert_eq!(content, "");
}
#[test]
fn test_cdata_with_angle_brackets() {
let mut input = ParserInput::new("<![CDATA[<div>hello</div>]]>");
let content = parse_cdata_content(&mut input).unwrap();
assert_eq!(content, "<div>hello</div>");
}
#[test]
fn test_cdata_with_double_bracket_not_terminator() {
let mut input = ParserInput::new("<![CDATA[a]]b]]>");
let content = parse_cdata_content(&mut input).unwrap();
assert_eq!(content, "a]]b");
}
#[test]
fn test_cdata_with_ampersand() {
let mut input = ParserInput::new("<![CDATA[& <]]>");
let content = parse_cdata_content(&mut input).unwrap();
assert_eq!(content, "& <");
}
#[test]
fn test_pi_target_xml_reserved() {
let mut input = ParserInput::new("<?xml data?>");
let result = parse_pi_content(&mut input);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.syntax_message()
.contains("PI target 'xml' is reserved")
);
}
#[test]
fn test_pi_target_xml_case_insensitive() {
for target in ["XML", "Xml", "xMl", "xmL"] {
let pi = format!("<?{target} data?>");
let mut input = ParserInput::new(&pi);
let result = parse_pi_content(&mut input);
assert!(result.is_err(), "PI target '{target}' should be reserved");
}
}
#[test]
fn test_pi_target_with_colon_rejected() {
let mut input = ParserInput::new("<?ns:target data?>");
let result = parse_pi_content(&mut input);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.syntax_message()
.contains("must not contain a colon")
);
}
#[test]
fn test_pi_unterminated() {
let mut input = ParserInput::new("<?target no end");
let result = parse_pi_content(&mut input);
assert!(result.is_err());
}
#[test]
fn test_pi_empty_data_after_whitespace() {
let mut input = ParserInput::new("<?target ?>");
let (target, data) = parse_pi_content(&mut input).unwrap();
assert_eq!(target, "target");
assert_eq!(data, None); }
#[test]
fn test_attribute_value_less_than_rejected() {
let mut input = ParserInput::new("\"abc<def\"");
let result = input.parse_attribute_value();
assert!(result.is_err());
assert!(
result
.unwrap_err()
.syntax_message()
.contains("'<' not allowed in attribute values")
);
}
#[test]
fn test_attribute_value_unterminated() {
let mut input = ParserInput::new("\"no closing quote");
let result = input.parse_attribute_value();
assert!(result.is_err());
assert!(
result
.unwrap_err()
.syntax_message()
.contains("unexpected end of input")
);
}
#[test]
fn test_attribute_value_not_quoted() {
let mut input = ParserInput::new("unquoted");
let result = input.parse_attribute_value();
assert!(result.is_err());
assert!(
result
.unwrap_err()
.syntax_message()
.contains("attribute value must be quoted")
);
}
#[test]
fn test_attribute_value_single_quotes() {
let mut input = ParserInput::new("'hello'");
let value = input.parse_attribute_value().unwrap();
assert_eq!(value, "hello");
}
#[test]
fn attribute_entity_builtin_less_than_is_not_literal_replacement_markup() {
use crate::parser_xmloxide::parser::{ParseOptions, parse_str_with_options};
let xml = r#"<!DOCTYPE r [
<!ENTITY bad "a<b">
]>
<r a="&bad;"/>"#;
let document = parse_str_with_options(xml, &ParseOptions::default()).unwrap();
assert_eq!(
document.attribute(document.root_element().unwrap(), "a"),
Some("a<b")
);
}
#[test]
fn test_is_xml_char_boundary_values() {
assert!(is_xml_char('\t')); assert!(is_xml_char('\n')); assert!(is_xml_char('\r')); assert!(is_xml_char(' ')); assert!(is_xml_char('\u{D7FF}'));
assert!(is_xml_char('\u{E000}'));
assert!(is_xml_char('\u{FFFD}'));
assert!(is_xml_char('\u{10000}'));
assert!(is_xml_char('\u{10FFFF}'));
assert!(!is_xml_char('\0')); assert!(!is_xml_char('\u{0001}')); assert!(!is_xml_char('\u{0008}')); assert!(!is_xml_char('\u{000B}')); assert!(!is_xml_char('\u{000C}')); assert!(!is_xml_char('\u{000E}')); assert!(!is_xml_char('\u{001F}')); assert!(!is_xml_char('\u{FFFE}')); assert!(!is_xml_char('\u{FFFF}')); }
#[test]
fn test_next_char_rejects_control_characters() {
let input_bytes = "\x01";
let mut input = ParserInput::new(input_bytes);
let result = input.next_char();
assert!(result.is_err());
assert!(
result
.unwrap_err()
.syntax_message()
.contains("invalid XML character")
);
}
#[test]
fn test_next_char_control_char_recovery() {
let input_bytes = "\x01X";
let mut input = ParserInput::new(input_bytes);
input.set_recover(true);
let ch = input.next_char().unwrap();
assert_eq!(ch, '\x01');
assert!(!input.diagnostics.is_empty());
assert_eq!(input.next_char().unwrap(), 'X');
}
#[test]
fn test_scan_char_data_cdata_end_marker() {
let input = ParserInput::new("text]]>more");
let len = input.scan_char_data().unwrap();
assert_eq!(len, 4); }
#[test]
fn test_scan_char_data_empty() {
let input = ParserInput::new("<");
assert_eq!(input.scan_char_data().unwrap(), 0);
}
#[test]
fn test_scan_char_data_stops_at_ampersand() {
let input = ParserInput::new("text&ref;");
assert_eq!(input.scan_char_data().unwrap(), 4);
}
#[test]
fn test_scan_char_data_stops_at_less_than() {
let input = ParserInput::new("text<elem");
assert_eq!(input.scan_char_data().unwrap(), 4);
}
#[test]
fn test_scan_for_2byte_terminator_at_end() {
let input = ParserInput::new("x");
assert_eq!(input.scan_for_2byte_terminator(b'-', b'-').unwrap(), None);
}
#[test]
fn test_scan_for_2byte_terminator_exact_2_bytes() {
let input = ParserInput::new("--");
assert_eq!(
input.scan_for_2byte_terminator(b'-', b'-').unwrap(),
Some(0)
);
}
#[test]
fn test_scan_for_3byte_terminator_at_end() {
let input = ParserInput::new("]]");
assert_eq!(
input.scan_for_3byte_terminator(b']', b']', b'>').unwrap(),
None
);
}
#[test]
fn test_scan_for_3byte_terminator_exact_3_bytes() {
let input = ParserInput::new("]]>");
assert_eq!(
input.scan_for_3byte_terminator(b']', b']', b'>').unwrap(),
Some(0)
);
}
#[test]
fn test_namespace_resolver_nested_override() {
let mut ns = NamespaceResolver::new().unwrap();
ns.push_scope().unwrap();
ns.bind(Some("p".to_string()), "http://outer".to_string())
.unwrap();
assert_eq!(ns.resolve(Some("p")).unwrap(), Some("http://outer"));
ns.push_scope().unwrap();
ns.bind(Some("p".to_string()), "http://inner".to_string())
.unwrap();
assert_eq!(ns.resolve(Some("p")).unwrap(), Some("http://inner"));
ns.pop_scope().unwrap();
assert_eq!(ns.resolve(Some("p")).unwrap(), Some("http://outer"));
ns.pop_scope().unwrap();
assert_eq!(ns.resolve(Some("p")).unwrap(), None);
}
#[test]
fn test_namespace_resolver_default_ns_override_and_restore() {
let mut ns = NamespaceResolver::new().unwrap();
ns.push_scope().unwrap();
ns.bind(None, "http://a".to_string()).unwrap();
ns.push_scope().unwrap();
ns.bind(None, "http://b".to_string()).unwrap();
assert_eq!(ns.resolve(None).unwrap(), Some("http://b"));
ns.pop_scope().unwrap();
assert_eq!(ns.resolve(None).unwrap(), Some("http://a"));
ns.pop_scope().unwrap();
assert_eq!(ns.resolve(None).unwrap(), None);
}
#[test]
fn test_namespace_resolver_undeclare_default_then_redeclare() {
let mut ns = NamespaceResolver::new().unwrap();
ns.push_scope().unwrap();
ns.bind(None, "http://ns".to_string()).unwrap();
ns.push_scope().unwrap();
ns.bind(None, String::new()).unwrap(); assert_eq!(ns.resolve(None).unwrap(), None);
ns.push_scope().unwrap();
ns.bind(None, "http://new".to_string()).unwrap(); assert_eq!(ns.resolve(None).unwrap(), Some("http://new"));
ns.pop_scope().unwrap();
assert_eq!(ns.resolve(None).unwrap(), None); ns.pop_scope().unwrap();
assert_eq!(ns.resolve(None).unwrap(), Some("http://ns")); ns.pop_scope().unwrap();
assert_eq!(ns.resolve(None).unwrap(), None);
}
#[test]
fn test_namespace_resolver_xml_prefix_always_bound() {
let ns = NamespaceResolver::new().unwrap();
assert_eq!(ns.resolve(Some("xml")).unwrap(), Some(XML_NAMESPACE));
}
#[test]
fn test_namespace_resolver_unbound_prefix() {
let ns = NamespaceResolver::new().unwrap();
assert_eq!(ns.resolve(Some("foo")).unwrap(), None);
assert_eq!(ns.resolve(Some("xmlns")).unwrap(), None);
}
#[test]
fn test_namespace_resolver_many_scopes() {
let mut ns = NamespaceResolver::new().unwrap();
for i in 0..100 {
ns.push_scope().unwrap();
ns.bind(Some("p".to_string()), format!("http://ns/{i}"))
.unwrap();
}
assert_eq!(ns.resolve(Some("p")).unwrap(), Some("http://ns/99"));
for i in (0..100).rev() {
ns.pop_scope().unwrap();
if i > 0 {
let expected = format!("http://ns/{}", i - 1);
assert_eq!(ns.resolve(Some("p")).unwrap(), Some(expected.as_str()));
}
}
assert_eq!(ns.resolve(Some("p")).unwrap(), None);
}
#[test]
fn test_validate_qname_valid() {
assert_eq!(validate_qname("foo"), None);
assert_eq!(validate_qname("ns:local"), None);
assert_eq!(validate_qname("a"), None);
}
#[test]
fn test_validate_qname_multiple_colons() {
let result = validate_qname("a:b:c");
assert!(result.is_some());
assert!(result.unwrap().contains("multiple colons"));
}
#[test]
fn test_validate_qname_empty_prefix() {
let result = validate_qname(":local");
assert!(result.is_some());
assert!(result.unwrap().contains("empty prefix or local part"));
}
#[test]
fn test_validate_qname_empty_local() {
let result = validate_qname("prefix:");
assert!(result.is_some());
assert!(result.unwrap().contains("empty prefix or local part"));
}
#[test]
fn split_name_with_prefix_preserves_source_borrows() {
let source = String::from("ns:elem");
let (prefix, local) = split_name(&source);
assert_eq!(prefix, Some("ns"));
assert_eq!(local, "elem");
assert_eq!(prefix.unwrap().as_ptr(), source.as_ptr());
assert_eq!(local.as_ptr(), source[3..].as_ptr());
}
#[test]
fn split_name_without_prefix_preserves_source_borrow() {
let source = String::from("elem");
let (prefix, local) = split_name(&source);
assert_eq!(prefix, None);
assert_eq!(local, "elem");
assert_eq!(local.as_ptr(), source.as_ptr());
}
#[test]
fn test_validate_pubid_valid() {
ParserInput::new("")
.validate_pubid("-//W3C//DTD XML 1.0//EN")
.unwrap();
}
#[test]
fn test_validate_pubid_invalid_char() {
let result = ParserInput::new("").validate_pubid("bad\x01char");
assert!(result.is_err());
assert!(
result
.unwrap_err()
.syntax_message()
.contains("invalid character")
);
}
#[test]
fn test_xml_decl_invalid_version() {
let mut input = ParserInput::new("<?xml version=\"2.0\"?>");
let result = parse_xml_decl(&mut input);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.syntax_message()
.contains("invalid version number")
);
}
#[test]
fn test_xml_decl_invalid_encoding() {
let mut input = ParserInput::new("<?xml version=\"1.0\" encoding=\"123bad\"?>");
let result = parse_xml_decl(&mut input);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.syntax_message()
.contains("invalid encoding name")
);
}
#[test]
fn test_xml_decl_standalone_invalid() {
let mut input = ParserInput::new("<?xml version=\"1.0\" standalone=\"maybe\"?>");
let result = parse_xml_decl(&mut input);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.syntax_message()
.contains("standalone must be 'yes' or 'no'")
);
}
#[test]
fn test_xml_decl_standalone_no() {
let mut input = ParserInput::new("<?xml version=\"1.0\" standalone=\"no\"?>");
let decl = parse_xml_decl(&mut input).unwrap();
assert_eq!(decl.standalone, Some(false));
}
#[test]
fn test_save_restore_position() {
let mut input = ParserInput::new("abcdef");
input.advance(3).unwrap();
assert_eq!(input.peek(), Some(b'd'));
let saved = input.save_position();
input.advance(2).unwrap();
assert_eq!(input.peek(), Some(b'f'));
input.restore_position(saved);
assert_eq!(input.peek(), Some(b'd'));
assert_eq!(input.location().column, 4); }
#[test]
fn test_depth_limit_via_parse_str_with_options() {
use crate::parser_xmloxide::parser::{ParseOptions, parse_str_with_options};
let xml = "<a><b><c/></b></a>";
let opts = ParseOptions::default().max_depth(3);
assert!(parse_str_with_options(xml, &opts).is_ok());
let xml = "<a><b><c><d/></c></b></a>";
let result = parse_str_with_options(xml, &opts);
assert!(result.is_err());
assert!(result.unwrap_err().syntax_message().contains("depth"));
}
#[test]
fn test_match_builtin_entity_all() {
assert_eq!(match_builtin_entity(b"amp;"), Some(("&", 4)));
assert_eq!(match_builtin_entity(b"lt;"), Some(("<", 3)));
assert_eq!(match_builtin_entity(b"gt;"), Some((">", 3)));
assert_eq!(match_builtin_entity(b"apos;"), Some(("'", 5)));
assert_eq!(match_builtin_entity(b"quot;"), Some(("\"", 5)));
}
#[test]
fn test_match_builtin_entity_partial_no_match() {
assert_eq!(match_builtin_entity(b"am"), None);
assert_eq!(match_builtin_entity(b"l"), None);
assert_eq!(match_builtin_entity(b"apo"), None);
assert_eq!(match_builtin_entity(b"quo"), None);
}
#[test]
fn test_match_builtin_entity_unknown() {
assert_eq!(match_builtin_entity(b"foo;"), None);
assert_eq!(match_builtin_entity(b""), None);
assert_eq!(match_builtin_entity(b"x"), None);
}
#[test]
fn test_expand_entity_text_counts_against_limit() {
let mut input = ParserInput::new("");
Names::insert(
&mut input.entity_map,
"a".to_string(),
EntityDecl {
name: String::new(),
kind: EntityKind::Internal("hello".to_string()),
},
)
.unwrap();
Names::insert(
&mut input.entity_map,
"b".to_string(),
EntityDecl {
name: String::new(),
kind: EntityKind::Internal("&a; &a;".to_string()),
},
)
.unwrap();
input.set_max_entity_expansions(2);
let result = input.expand_entity_text("&b;");
assert!(
result.is_ok() || result.is_err(),
"expansion should either succeed at limit or fail over limit"
);
}
#[test]
fn test_expand_entity_text_no_references() {
let mut input = ParserInput::new("");
let result = input.expand_entity_text("plain text").unwrap();
assert_eq!(result, "plain text");
}
#[test]
fn test_expand_entity_text_builtin_entities() {
let mut input = ParserInput::new("");
let result = input.expand_entity_text("a & b < c").unwrap();
assert_eq!(result, "a & b < c");
}
#[test]
fn test_expand_entity_text_char_refs() {
let mut input = ParserInput::new("");
let result = input.expand_entity_text("A B").unwrap();
assert_eq!(result, "A B");
}
#[test]
fn test_expand_entity_text_unknown_entity_strict() {
let mut input = ParserInput::new("");
let result = input.expand_entity_text("&unknown;");
assert!(result.is_err());
}
#[test]
fn test_expand_entity_text_cdata_not_expanded() {
let mut input = ParserInput::new("");
let result = input
.expand_entity_text("<![CDATA[& not expanded]]>")
.unwrap();
assert_eq!(result, "<![CDATA[& not expanded]]>");
}
#[test]
fn entity_replacement_cdata_keeps_unicode_octets() {
let mut input = ParserInput::new("");
assert_eq!(
input
.expand_entity_text("<![CDATA[\u{e9}\u{540d} &unknown;]]>&")
.unwrap(),
"<![CDATA[\u{e9}\u{540d} &unknown;]]>&"
);
}
#[test]
fn entity_replacement_chain_does_not_use_the_call_stack() {
let mut input = ParserInput::new("");
for index in 0..8192 {
let value = if index == 8191 {
"done".to_owned()
} else {
format!("&e{};", index + 1)
};
Names::insert(
&mut input.entity_map,
format!("e{index}"),
EntityDecl {
name: String::new(),
kind: EntityKind::Internal(value),
},
)
.unwrap();
}
assert_eq!(input.expand_entity_text("&e0;").unwrap(), "done");
assert_eq!(input.entity_expansions, 8192);
input.entity_expansions = 0;
input.set_max_entity_expansions(32);
assert!(
input
.expand_entity_text("&e0;")
.unwrap_err()
.syntax_message()
.contains("entity expansion limit")
);
assert_eq!(input.entity_expansions, 33);
}
#[test]
fn test_find_invalid_xml_char_clean() {
assert_eq!(find_invalid_xml_char("hello world"), None);
assert_eq!(find_invalid_xml_char("tab\there"), None);
assert_eq!(find_invalid_xml_char("newline\nhere"), None);
}
#[test]
fn test_find_invalid_xml_char_with_null() {
assert_eq!(find_invalid_xml_char("bad\x00char"), Some('\x00'));
}
#[test]
fn test_find_invalid_xml_char_with_control() {
assert_eq!(find_invalid_xml_char("bad\x01char"), Some('\x01'));
assert_eq!(find_invalid_xml_char("bad\x08char"), Some('\x08'));
}
#[test]
fn test_may_contain_invalid_xml_chars_fast_check() {
assert!(!may_contain_invalid_xml_chars(b"hello world"));
assert!(!may_contain_invalid_xml_chars(b"tab\there"));
assert!(!may_contain_invalid_xml_chars(b"newline\nhere"));
assert!(may_contain_invalid_xml_chars(b"bad\x00char"));
assert!(may_contain_invalid_xml_chars(b"bad\x01char"));
assert!(may_contain_invalid_xml_chars(b"\x7F")); }
}