use std::borrow::Cow;
use std::marker::PhantomData;
use memchr::{memchr_iter, memrchr};
use crate::charsets::{
ASCII_XML_NAME as ASCII_NAME, ASCII_XML_NAME_CHAR, NS,
is_name_char_4e, is_name_char_unicode, is_name_start_char, is_name_start_char_4e,
};
use crate::error::{ErrorCode, ErrorDomain, ErrorLevel, Result, XmlError};
use crate::options::ParseOptions;
pub(crate) struct InputStream<'src> {
pub bytes: Cow<'src, [u8]>,
pub saved_parent_pos: usize,
pub entity_name: String,
pub depth_at_push: u32,
pub base_uri: Option<String>,
}
pub(crate) struct Scanner<'src, 'opt> {
cur_ptr: *const u8,
cur_len: usize,
cur_pos: usize,
src_ptr: *const u8,
src_orig_len: usize,
in_place_mut: Option<std::ptr::NonNull<u8>>,
entity_streams: Vec<InputStream<'src>>,
_marker: PhantomData<&'src [u8]>,
pub opts: Cow<'opt, ParseOptions>,
}
impl<'src, 'opt> Scanner<'src, 'opt> {
pub fn new(src: &'src [u8], opts: Cow<'opt, ParseOptions>) -> Self {
Self {
cur_ptr: src.as_ptr(),
cur_len: src.len(),
cur_pos: 0,
src_ptr: src.as_ptr(),
src_orig_len: src.len(),
in_place_mut: None,
entity_streams: Vec::new(),
_marker: PhantomData,
opts,
}
}
pub fn new_in_place(src: &'src mut [u8], opts: Cow<'opt, ParseOptions>) -> Self {
let mut_ptr = src.as_mut_ptr();
let len = src.len();
Self {
cur_ptr: mut_ptr as *const u8,
cur_len: len,
cur_pos: 0,
src_ptr: mut_ptr as *const u8,
src_orig_len: len,
in_place_mut: std::ptr::NonNull::new(mut_ptr),
entity_streams: Vec::new(),
_marker: PhantomData,
opts,
}
}
#[inline]
fn src(&self) -> &[u8] {
unsafe { std::slice::from_raw_parts(self.src_ptr, self.src_orig_len) }
}
#[inline]
pub fn is_in_place(&self) -> bool { self.in_place_mut.is_some() }
#[inline]
#[allow(dead_code)] pub fn write_byte_at(&mut self, offset: usize, byte: u8) {
let mut_base = self.in_place_mut.expect(
"Scanner::write_byte_at requires in-place mode",
);
debug_assert!(offset < self.src_orig_len);
unsafe { *mut_base.as_ptr().add(offset) = byte; }
}
#[inline]
#[allow(dead_code)] pub fn write_bytes_at(&mut self, offset: usize, bytes: &[u8]) {
if bytes.is_empty() { return; }
let mut_base = self.in_place_mut.expect(
"Scanner::write_bytes_at requires in-place mode",
);
debug_assert!(offset + bytes.len() <= self.src_orig_len);
unsafe {
std::ptr::copy(
bytes.as_ptr(),
mut_base.as_ptr().add(offset),
bytes.len(),
);
}
}
#[inline]
pub fn src_slice(&self, start: usize, end: usize) -> &'src [u8] {
&self.src_with_src_lifetime()[start..end]
}
pub fn compact_at(&mut self, start: usize, end: usize, new_bytes: &[u8]) {
let mut_base = self.in_place_mut.expect(
"Scanner::compact_at requires in-place mode (constructed via new_in_place)",
);
debug_assert!(start <= end);
debug_assert!(end <= self.src_orig_len);
debug_assert!(new_bytes.len() <= end - start);
unsafe {
let dst = mut_base.as_ptr().add(start);
std::ptr::copy_nonoverlapping(new_bytes.as_ptr(), dst, new_bytes.len());
}
}
#[inline]
pub(crate) unsafe fn rebind(&mut self, ptr: *const u8, len: usize, new_cur_pos: usize) {
debug_assert!(new_cur_pos <= len, "rebind: cur_pos out of bounds");
debug_assert!(self.entity_streams.is_empty(),
"rebind: cannot rebind while an entity-replacement stream is active");
self.cur_ptr = ptr;
self.cur_len = len;
self.cur_pos = new_cur_pos;
self.src_ptr = ptr;
self.src_orig_len = len;
}
#[inline]
pub fn in_entity(&self) -> bool {
!self.entity_streams.is_empty()
}
#[inline]
pub fn stream_depth(&self) -> usize {
1 + self.entity_streams.len()
}
pub fn push_entity_stream(
&mut self,
name: String,
bytes: String,
element_depth: u32,
base_uri: Option<String>,
) -> Result<()> {
if self.entity_streams.iter().any(|s| s.entity_name == name) {
return Err(self.err(format!(
"recursive entity reference: &{name}; — XML 1.0 WFC 'No Recursion' forbids \
an entity from being expanded inside its own replacement text"
)));
}
let saved_parent_pos = self.cur_pos;
let bytes_vec: Vec<u8> = bytes.into_bytes();
let new_ptr = bytes_vec.as_ptr();
let new_len = bytes_vec.len();
self.entity_streams.push(InputStream {
bytes: Cow::Owned(bytes_vec),
saved_parent_pos,
entity_name: name,
depth_at_push: element_depth,
base_uri,
});
self.cur_ptr = new_ptr;
self.cur_len = new_len;
self.cur_pos = 0;
Ok(())
}
pub fn current_base_uri(&self) -> Option<&str> {
self.entity_streams.iter().rev()
.find_map(|s| s.base_uri.as_deref())
}
pub fn top_entity_info(&self) -> Option<(&str, u32)> {
self.entity_streams.last().map(|s| (s.entity_name.as_str(), s.depth_at_push))
}
#[inline]
pub fn entity_stream_depth(&self) -> usize {
self.entity_streams.len()
}
#[inline]
pub fn cur_bytes(&self) -> &[u8] {
unsafe { std::slice::from_raw_parts(self.cur_ptr, self.cur_len) }
}
#[inline]
pub fn cur_tail(&self) -> &[u8] {
unsafe {
std::slice::from_raw_parts(
self.cur_ptr.add(self.cur_pos),
self.cur_len - self.cur_pos,
)
}
}
#[inline]
pub fn cur_pos(&self) -> usize { self.cur_pos }
#[inline]
pub fn cur_advance_pos(&mut self, n: usize) { self.cur_pos += n; }
#[inline]
pub fn cur_set_pos(&mut self, p: usize) { self.cur_pos = p; }
#[inline]
pub fn cur_len(&self) -> usize { self.cur_len }
#[inline]
pub fn current_borrowed_bytes(&self) -> Option<&'src [u8]> {
if self.entity_streams.is_empty() { Some(self.src_with_src_lifetime()) } else { None }
}
#[inline]
pub fn src_bytes(&self) -> &'src [u8] { self.src_with_src_lifetime() }
#[inline]
fn src_with_src_lifetime(&self) -> &'src [u8] {
unsafe { std::slice::from_raw_parts(self.src_ptr, self.src_orig_len) }
}
#[inline]
pub fn on_original_source(&self) -> bool {
self.entity_streams.is_empty()
}
pub fn original_slice(&self, start: usize, end: usize) -> &str {
let s = start.min(self.src_orig_len);
let e = end.min(self.src_orig_len).max(s);
unsafe {
let bytes = std::slice::from_raw_parts(self.src_ptr.add(s), e - s);
std::str::from_utf8_unchecked(bytes)
}
}
#[inline]
pub fn peek(&self) -> Option<u8> {
if self.cur_pos < self.cur_len {
Some(unsafe { *self.cur_ptr.add(self.cur_pos) })
} else {
None
}
}
#[inline]
pub fn peek_at(&self, off: usize) -> Option<u8> {
let p = self.cur_pos.checked_add(off)?;
if p < self.cur_len {
Some(unsafe { *self.cur_ptr.add(p) })
} else {
None
}
}
#[inline]
pub fn is_eof(&self) -> bool {
self.cur_pos >= self.cur_len && self.entity_streams.is_empty()
}
#[inline]
pub fn starts_with(&self, pat: &[u8]) -> bool {
self.cur_tail().starts_with(pat)
}
pub fn advance(&mut self) -> Option<u8> {
if self.cur_pos >= self.cur_len { return None; }
let b = unsafe { *self.cur_ptr.add(self.cur_pos) };
self.cur_pos += 1;
Some(b)
}
pub fn skip_n(&mut self, n: usize) {
for _ in 0..n { self.advance(); }
}
pub fn skip_ws(&mut self) {
while matches!(self.peek(), Some(b' ' | b'\t' | b'\r' | b'\n')) {
self.advance();
}
}
pub fn src_offset(&self) -> usize {
if let Some(bottom) = self.entity_streams.first() {
bottom.saved_parent_pos
} else {
self.cur_pos
}
}
pub fn err(&self, msg: impl Into<String>) -> XmlError {
self.err_with_level(ErrorLevel::Fatal, msg)
}
pub fn err_with_level(&self, level: ErrorLevel, msg: impl Into<String>) -> XmlError {
let offset = self.src_offset();
let (line, col) = compute_line_col(self.src(), offset);
let source = self.opts.base_url.as_deref().unwrap_or("<input>");
XmlError::new(ErrorDomain::Parser, level, msg)
.at(source, line, col, offset as u64)
}
pub fn expect(&mut self, b: u8) -> Result<()> {
match self.advance() {
Some(c) if c == b => Ok(()),
Some(c) => Err(self.err(format!("expected '{}', got '{}'", b as char, c as char))),
None => Err(self.err(format!("expected '{}', got EOF", b as char))),
}
}
pub fn expect_str(&mut self, pat: &[u8]) -> Result<()> {
for &b in pat { self.expect(b)?; }
Ok(())
}
pub fn expect_ws(&mut self) -> Result<()> {
if !matches!(self.peek(), Some(b' ' | b'\t' | b'\r' | b'\n')) {
return Err(self.err("expected whitespace"));
}
Ok(())
}
pub fn cur_str(&self, start: usize, end: usize) -> Cow<'src, str> {
if self.entity_streams.is_empty() {
let s = unsafe { std::str::from_utf8_unchecked(&self.src_with_src_lifetime()[start..end]) };
Cow::Borrowed(s)
} else {
let bytes = unsafe { std::slice::from_raw_parts(self.cur_ptr, self.cur_len) };
let s = unsafe { std::str::from_utf8_unchecked(&bytes[start..end]) };
Cow::Owned(s.to_string())
}
}
pub fn cur_slice(&self, start: usize, end: usize) -> Cow<'src, [u8]> {
if self.entity_streams.is_empty() {
Cow::Borrowed(&self.src_with_src_lifetime()[start..end])
} else {
let bytes = unsafe { std::slice::from_raw_parts(self.cur_ptr, self.cur_len) };
Cow::Owned(bytes[start..end].to_vec())
}
}
#[allow(dead_code)] pub fn append_cur_str(&self, start: usize, end: usize, buf: &mut String) {
let bytes = self.cur_bytes();
let s = unsafe { std::str::from_utf8_unchecked(&bytes[start..end]) };
buf.push_str(s);
}
#[allow(dead_code)]
pub fn append_cur_bytes(&self, start: usize, end: usize, buf: &mut Vec<u8>) {
let bytes = self.cur_bytes();
buf.extend_from_slice(&bytes[start..end]);
}
pub fn try_pop_entity_stream(&mut self) -> bool {
let Some(popped) = self.entity_streams.pop() else { return false; };
self.cur_pos = popped.saved_parent_pos;
if let Some(new_top) = self.entity_streams.last() {
self.cur_ptr = new_top.bytes.as_ptr();
self.cur_len = new_top.bytes.len();
} else {
self.cur_ptr = self.src_ptr;
self.cur_len = self.src_orig_len;
}
true
}
#[allow(dead_code)] pub fn peek_char(&self) -> Option<(char, usize)> {
if self.cur_pos >= self.cur_len { return None; }
let bytes = self.cur_tail();
let b = bytes[0];
if b < 0x80 { return Some((b as char, 1)); }
let len = utf8_seq_len(b)?;
let slice = bytes.get(..len)?;
let s = std::str::from_utf8(slice).ok()?;
s.chars().next().map(|c| (c, len))
}
pub fn scan_name_raw(&mut self) -> Result<(usize, usize)> {
let lax = self.opts.skip_name_validation;
let start = self.cur_pos;
let bytes: &[u8] = unsafe { std::slice::from_raw_parts(self.cur_ptr, self.cur_len) };
let end = self.cur_len;
let mut i = start;
if i >= end {
return Err(self.err("expected XML name, got EOF"));
}
let b0 = bytes[i];
if b0 < 0x80 {
if !lax && ASCII_NAME[b0 as usize] & NS == 0 {
return Err(self.err(format!("invalid name-start character {:?}", b0 as char))
.with_code(crate::error::ErrorCode::NameRequired));
}
i += 1;
} else {
let len = match utf8_seq_len(b0) {
Some(l) if i + l <= end => l,
_ => return Err(self.err("invalid UTF-8 in name")),
};
let slice = &bytes[i..i + len];
let c = std::str::from_utf8(slice).ok()
.and_then(|s| s.chars().next())
.ok_or_else(|| self.err("invalid UTF-8 in name"))?;
if !lax && !self.name_start_non_ascii(c) {
return Err(self.err(format!("invalid name-start character {:?}", c))
.with_code(crate::error::ErrorCode::NameRequired));
}
i += len;
}
let _ = lax; while i < end && ASCII_XML_NAME_CHAR[unsafe { *bytes.get_unchecked(i) } as usize] != 0 {
i += 1;
}
while i < end {
let b = unsafe { *bytes.get_unchecked(i) };
if b < 0x80 { break; } let len = match utf8_seq_len(b) {
Some(l) if i + l <= end => l,
_ => break,
};
if lax {
i += len;
continue;
}
let slice = &bytes[i..i + len];
let c_opt = std::str::from_utf8(slice).ok().and_then(|s| s.chars().next());
match c_opt {
Some(c) if self.name_char_non_ascii(c) => i += len,
_ => break,
}
while i < end && ASCII_XML_NAME_CHAR[unsafe { *bytes.get_unchecked(i) } as usize] != 0 {
i += 1;
}
}
self.cur_pos = i;
Ok((start, i))
}
pub fn scan_name(&mut self) -> Result<Cow<'src, str>> {
let (s, e) = self.scan_name_raw()?;
Ok(self.cur_str(s, e))
}
pub fn scan_nmtoken(&mut self) -> Result<Cow<'src, str>> {
let lax = self.opts.skip_name_validation;
let start = self.cur_pos;
let bytes: &[u8] = unsafe { std::slice::from_raw_parts(self.cur_ptr, self.cur_len) };
let end = self.cur_len;
let mut i = start;
while i < end && ASCII_XML_NAME_CHAR[unsafe { *bytes.get_unchecked(i) } as usize] != 0 {
i += 1;
}
while i < end {
let b = unsafe { *bytes.get_unchecked(i) };
if b < 0x80 { break; }
let len = match utf8_seq_len(b) {
Some(l) if i + l <= end => l,
_ => break,
};
if lax {
i += len;
continue;
}
let slice = &bytes[i..i + len];
let c_opt = std::str::from_utf8(slice).ok().and_then(|s| s.chars().next());
match c_opt {
Some(c) if self.name_char_non_ascii(c) => i += len,
_ => break,
}
while i < end && ASCII_XML_NAME_CHAR[unsafe { *bytes.get_unchecked(i) } as usize] != 0 {
i += 1;
}
}
if i == start {
return Err(self.err("expected Nmtoken (one or more NameChar)"));
}
self.cur_pos = i;
Ok(self.cur_str(start, i))
}
pub fn scan_name_bytes(&mut self) -> Result<Cow<'src, [u8]>> {
let (s, e) = self.scan_name_raw()?;
Ok(self.cur_slice(s, e))
}
pub fn skip_name(&mut self) -> Result<()> {
self.scan_name_raw().map(|_| ())
}
#[inline]
fn name_start_non_ascii(&self, c: char) -> bool {
if self.opts.xml10_fourth_edition { is_name_start_char_4e(c) } else { is_name_start_char(c) }
}
#[inline]
fn name_char_non_ascii(&self, c: char) -> bool {
if self.opts.xml10_fourth_edition { is_name_char_4e(c) } else { is_name_char_unicode(c) }
}
}
pub fn compute_line_col(src: &[u8], offset: usize) -> (u32, u32) {
let end = offset.min(src.len());
let prefix = &src[..end];
let line = memchr_iter(b'\n', prefix).count() as u32 + 1;
let col_start = match memrchr(b'\n', prefix) {
Some(n) => n + 1, None => 0,
};
let col = (end - col_start) as u32 + 1;
(line, col)
}
pub fn validate_xml_chars(bytes: &[u8]) -> Result<()> {
const LSB: u64 = 0x0101_0101_0101_0101;
const MSB: u64 = 0x8080_8080_8080_8080;
const HI3: u64 = 0xE0E0_E0E0_E0E0_E0E0;
const LO5: u64 = 0x1F1F_1F1F_1F1F_1F1F;
const TAB: u64 = 0x0909_0909_0909_0909;
const LF: u64 = 0x0A0A_0A0A_0A0A_0A0A;
const CR: u64 = 0x0D0D_0D0D_0D0D_0D0D;
const EF: u64 = 0xEFEF_EFEF_EFEF_EFEF;
#[inline(always)]
fn has_zero(x: u64) -> u64 {
x.wrapping_sub(LSB) & !x & MSB
}
let mut i = 0;
while i + 8 <= bytes.len() {
let chunk = u64::from_le_bytes(bytes[i..i + 8].try_into().unwrap());
let any_lt20 = has_zero(chunk & HI3);
let any_ef = has_zero(chunk ^ EF);
if (any_lt20 | any_ef) == 0 {
i += 8;
continue;
}
let bad_ctrl = if any_lt20 != 0 {
let low5 = chunk & LO5;
let allowed = has_zero(low5 ^ TAB)
| has_zero(low5 ^ LF)
| has_zero(low5 ^ CR);
any_lt20 & !allowed
} else {
0
};
if (bad_ctrl | any_ef) != 0 {
validate_xml_chars_slow(bytes, i, i + 8)?;
}
i += 8;
}
validate_xml_chars_slow(bytes, i, bytes.len())
}
#[cold]
#[inline(never)]
fn validate_xml_chars_slow(bytes: &[u8], from: usize, to: usize) -> Result<()> {
let mut i = from;
while i < to {
let b = bytes[i];
if b < 0x20 {
if !matches!(b, 0x09 | 0x0A | 0x0D) {
return Err(invalid_char_at(bytes, i, format!("U+{b:04X}")));
}
} else if b == 0xEF && i + 2 < bytes.len() && bytes[i + 1] == 0xBF {
match bytes[i + 2] {
0xBE => return Err(invalid_char_at(bytes, i, "U+FFFE".into())),
0xBF => return Err(invalid_char_at(bytes, i, "U+FFFF".into())),
_ => {}
}
}
i += 1;
}
Ok(())
}
fn invalid_char_at(bytes: &[u8], i: usize, codepoint: String) -> XmlError {
let (line, col) = compute_line_col(bytes, i);
XmlError::new(
ErrorDomain::Parser,
ErrorLevel::Fatal,
format!("invalid XML character {codepoint} at byte {i} (XML 1.0 § 2.2)"),
)
.with_code(ErrorCode::InvalidChar)
.at("<input>", line, col, i as u64)
}
pub fn is_xml_char(c: char) -> bool {
matches!(c as u32,
0x09 | 0x0A | 0x0D |
0x20..=0xD7FF |
0xE000..=0xFFFD |
0x10000..=0x10FFFF
)
}
pub fn is_xml_11_char(c: char) -> bool {
matches!(c as u32,
0x01..=0xD7FF |
0xE000..=0xFFFD |
0x10000..=0x10FFFF
)
}
pub fn is_pubid_char(b: u8) -> bool {
matches!(b,
0x20 | 0x0D | 0x0A |
b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' |
b'-' | b'\'' | b'(' | b')' | b'+' | b',' | b'.' | b'/' |
b':' | b'=' | b'?' | b';' | b'!' | b'*' | b'#' | b'@' | b'$' | b'_' | b'%'
)
}
pub fn utf8_seq_len(lead: u8) -> Option<usize> {
match lead {
0x00..=0x7F => Some(1),
0xC0..=0xDF => Some(2),
0xE0..=0xEF => Some(3),
0xF0..=0xF7 => Some(4),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::options::ParseOptions;
fn fresh<'src>(src: &'src [u8]) -> Scanner<'src, 'static> {
Scanner::new(src, Cow::Owned(ParseOptions::default()))
}
#[test]
fn current_base_uri_none_on_original_source() {
let s = fresh(b"<r/>");
assert_eq!(s.current_base_uri(), None);
}
#[test]
fn current_base_uri_returns_top_frames_uri() {
let mut s = fresh(b"<r/>");
s.push_entity_stream(
"outer".to_string(),
"abc".to_string(),
0,
Some("file:///docs/outer.ent".to_string()),
).unwrap();
assert_eq!(s.current_base_uri(), Some("file:///docs/outer.ent"));
}
#[test]
fn current_base_uri_falls_through_a_none_frame() {
let mut s = fresh(b"<r/>");
s.push_entity_stream(
"outer".to_string(),
"abc".to_string(),
0,
Some("file:///docs/outer.ent".to_string()),
).unwrap();
s.push_entity_stream(
"inner".to_string(),
"def".to_string(),
0,
None,
).unwrap();
assert_eq!(s.current_base_uri(), Some("file:///docs/outer.ent"));
}
#[test]
fn current_base_uri_uses_innermost_external_frame() {
let mut s = fresh(b"<r/>");
s.push_entity_stream(
"outer".to_string(),
"abc".to_string(),
0,
Some("file:///outer.ent".to_string()),
).unwrap();
s.push_entity_stream(
"inner".to_string(),
"def".to_string(),
0,
Some("file:///docs/sub/inner.ent".to_string()),
).unwrap();
assert_eq!(s.current_base_uri(), Some("file:///docs/sub/inner.ent"));
}
#[test]
fn rebind_swings_cursor_to_new_buffer() {
let a = b"<a/>";
let b = b"<bbbb/>";
let mut s = fresh(a);
s.advance(); s.advance();
assert_eq!(s.cur_pos(), 2);
unsafe { s.rebind(b.as_ptr(), b.len(), 1); }
assert_eq!(s.cur_pos(), 1);
assert_eq!(s.cur_len(), b.len());
assert_eq!(s.peek(), Some(b'b'));
}
#[test]
fn rebind_lets_subsequent_advances_walk_new_buffer() {
let a = b"xxxx";
let b = b"yyy";
let mut s = fresh(a);
while s.advance().is_some() {}
unsafe { s.rebind(b.as_ptr(), b.len(), 0); }
let mut bytes = Vec::new();
while let Some(byte) = s.advance() { bytes.push(byte); }
assert_eq!(&bytes[..], b"yyy");
}
}