#![allow(dead_code)]
use crate::abi::callbacks::{xmlInputCloseCallback, xmlInputReadCallback};
use crate::abi::structs::{_xmlParserInput, _xmlParserInputBuffer};
use crate::abi::types::xmlCharEncoding;
use std::fs;
use std::io::Read;
use std::os::raw::{c_char, c_int, c_ulong, c_void};
use std::path::Path;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Encoding {
None,
Utf8,
Utf16Le,
Utf16Be,
Ascii,
Iso8859_1,
Other(String),
}
impl Encoding {
pub(crate) fn to_xml_char_encoding(&self) -> xmlCharEncoding {
match self {
Self::None => xmlCharEncoding::XML_CHAR_ENCODING_NONE,
Self::Utf8 => xmlCharEncoding::XML_CHAR_ENCODING_UTF8,
Self::Utf16Le => xmlCharEncoding::XML_CHAR_ENCODING_UTF16LE,
Self::Utf16Be => xmlCharEncoding::XML_CHAR_ENCODING_UTF16BE,
Self::Ascii => xmlCharEncoding::XML_CHAR_ENCODING_ASCII,
Self::Iso8859_1 => xmlCharEncoding::XML_CHAR_ENCODING_8859_1,
Self::Other(_) => xmlCharEncoding::XML_CHAR_ENCODING_ERROR,
}
}
fn from_name(name: &str) -> Self {
match name.trim().to_ascii_lowercase().as_str() {
"utf-8" | "utf8" => Encoding::Utf8,
"utf-16" | "utf16" => Encoding::Utf16Le, "utf-16le" | "utf16le" => Encoding::Utf16Le,
"utf-16be" | "utf16be" => Encoding::Utf16Be,
"us-ascii" | "ascii" => Encoding::Ascii,
"iso-8859-1" | "iso8859-1" | "latin1" | "latin-1" => Encoding::Iso8859_1,
other => Encoding::Other(other.to_string()),
}
}
}
#[derive(Debug)]
pub(crate) enum InputSource {
Memory(Vec<u8>),
File {
path: String,
file: fs::File,
},
Callback {
read: xmlInputReadCallback,
close: xmlInputCloseCallback,
ctx: *mut c_void,
},
}
impl InputSource {
fn read_all(&mut self) -> Result<Vec<u8>, InputError> {
match self {
InputSource::Memory(data) => Ok(data.clone()),
InputSource::File { file, .. } => {
let mut buf = Vec::new();
file.read_to_end(&mut buf)
.map_err(|e| InputError::Io(e.to_string()))?;
Ok(buf)
}
InputSource::Callback { read, ctx, .. } => {
let mut buf = Vec::new();
let mut tmp = [0u8; 4096];
loop {
let n =
unsafe { read(*ctx, tmp.as_mut_ptr() as *mut c_char, tmp.len() as c_int) };
if n < 0 {
return Err(InputError::Callback("read callback returned error".into()));
}
if n == 0 {
break; }
buf.extend_from_slice(&tmp[..n as usize]);
}
Ok(buf)
}
}
}
fn read_chunk(&mut self, buf: &mut [u8]) -> Result<usize, InputError> {
match self {
InputSource::Memory(_data) => {
Ok(0)
}
InputSource::File { file, .. } => {
file.read(buf).map_err(|e| InputError::Io(e.to_string()))
}
InputSource::Callback { read, ctx, .. } => {
let n = unsafe { read(*ctx, buf.as_mut_ptr() as *mut c_char, buf.len() as c_int) };
if n < 0 {
Err(InputError::Callback("read callback returned error".into()))
} else {
Ok(n as usize)
}
}
}
}
fn filename(&self) -> Option<&str> {
match self {
InputSource::File { path, .. } => Some(path.as_str()),
_ => None,
}
}
}
#[derive(Debug)]
pub(crate) enum InputError {
Io(String),
Callback(String),
InvalidUtf8,
UnexpectedEof,
EmptyInput,
}
impl std::fmt::Display for InputError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
InputError::Io(msg) => write!(f, "I/O error: {msg}"),
InputError::Callback(msg) => write!(f, "callback error: {msg}"),
InputError::InvalidUtf8 => write!(f, "invalid UTF-8 sequence"),
InputError::UnexpectedEof => write!(f, "unexpected end of input"),
InputError::EmptyInput => write!(f, "empty input"),
}
}
}
impl std::error::Error for InputError {}
pub(crate) struct InputBuffer {
source: InputSource,
data: Vec<u8>,
pos: usize,
line: usize,
col: usize,
encoding: Encoding,
filename: Option<String>,
bom_consumed: bool,
}
impl std::fmt::Debug for InputBuffer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("InputBuffer")
.field("source", &self.source)
.field("pos", &self.pos)
.field("line", &self.line)
.field("col", &self.col)
.field("encoding", &self.encoding)
.field("filename", &self.filename)
.field("len", &self.data.len())
.field("bom_consumed", &self.bom_consumed)
.finish()
}
}
impl InputBuffer {
pub fn from_memory(buf: &[u8], uri: Option<&str>) -> Self {
let data = buf.to_vec();
let filename = uri.map(|s| s.to_string());
let mut ib = InputBuffer {
source: InputSource::Memory(data.clone()),
data,
pos: 0,
line: 1,
col: 1,
encoding: Encoding::None,
filename,
bom_consumed: false,
};
ib.detect_bom_and_encoding();
ib
}
pub fn from_file(path: &str) -> Result<Self, InputError> {
let p = Path::new(path);
let file = fs::File::open(p).map_err(|e| InputError::Io(e.to_string()))?;
let filename = Some(path.to_string());
let mut source = InputSource::File {
path: path.to_string(),
file,
};
let data = source.read_all()?;
let mut ib = InputBuffer {
source,
data,
pos: 0,
line: 1,
col: 1,
encoding: Encoding::None,
filename,
bom_consumed: false,
};
ib.detect_bom_and_encoding();
Ok(ib)
}
pub fn from_callback(
read: xmlInputReadCallback,
close: xmlInputCloseCallback,
ctx: *mut c_void,
) -> Result<Self, InputError> {
let mut source = InputSource::Callback { read, close, ctx };
let data = source.read_all()?;
let mut ib = InputBuffer {
source,
data,
pos: 0,
line: 1,
col: 1,
encoding: Encoding::None,
filename: None,
bom_consumed: false,
};
ib.detect_bom_and_encoding();
Ok(ib)
}
fn detect_bom_and_encoding(&mut self) {
if self.data.is_empty() {
self.encoding = Encoding::Utf8;
return;
}
if self.data.len() >= 3
&& self.data[0] == 0xEF
&& self.data[1] == 0xBB
&& self.data[2] == 0xBF
{
self.encoding = Encoding::Utf8;
self.pos = 3;
self.col = 4; self.bom_consumed = true;
self.detect_encoding_from_xml_declaration();
return;
}
if self.data.len() >= 2 && self.data[0] == 0xFF && self.data[1] == 0xFE {
self.encoding = Encoding::Utf16Le;
self.pos = 2;
self.col = 3;
self.bom_consumed = true;
return;
}
if self.data.len() >= 2 && self.data[0] == 0xFE && self.data[1] == 0xFF {
self.encoding = Encoding::Utf16Be;
self.pos = 2;
self.col = 3;
self.bom_consumed = true;
return;
}
self.encoding = Encoding::Utf8;
self.detect_encoding_from_xml_declaration();
}
fn detect_encoding_from_xml_declaration(&mut self) {
let remaining = &self.data[self.pos..];
if remaining.len() < 5 {
return;
}
if &remaining[..5] != b"<?xml" {
return;
}
let pi_end = remaining.windows(2).position(|w| w == b"?>");
let pi_end = match pi_end {
Some(e) => e + 2,
None => return, };
let pi_content = &remaining[..pi_end];
let pi_str = match std::str::from_utf8(pi_content) {
Ok(s) => s,
Err(_) => return,
};
if let Some(enc) = Self::extract_encoding_from_pi(pi_str) {
self.encoding = Encoding::from_name(&enc);
}
}
fn extract_encoding_from_pi(pi: &str) -> Option<String> {
let pi_lower = pi.to_ascii_lowercase();
let kw_pos = pi_lower.find("encoding")?;
let after_kw = &pi[kw_pos + 8..]; let after_kw = after_kw.trim_start();
if !after_kw.starts_with('=') {
return None;
}
let after_eq = after_kw[1..].trim_start();
let quote = after_eq.chars().next()?;
if quote != '"' && quote != '\'' {
return None;
}
let value_start = 1; let value_end = after_eq[value_start..].find(quote)? + value_start;
Some(after_eq[value_start..value_end].to_string())
}
pub fn read_char(&mut self) -> Option<char> {
let c = self.peek_char_inner()?;
self.advance_past_char(c);
Some(c)
}
pub fn peek_char(&self) -> Option<char> {
self.peek_char_inner()
}
fn peek_char_inner(&self) -> Option<char> {
if self.pos >= self.data.len() {
return None;
}
let remaining = &self.data[self.pos..];
Self::decode_utf8_char(remaining)
}
fn decode_utf8_char(bytes: &[u8]) -> Option<char> {
if bytes.is_empty() {
return None;
}
let byte = bytes[0];
let (code_point, _len) = if byte & 0x80 == 0 {
(u32::from(byte), 1)
} else if byte & 0xE0 == 0xC0 {
if bytes.len() < 2 {
return None;
}
let b1 = u32::from(bytes[1]);
if b1 & 0xC0 != 0x80 {
return None;
}
((u32::from(byte & 0x1F) << 6) | (b1 & 0x3F), 2)
} else if byte & 0xF0 == 0xE0 {
if bytes.len() < 3 {
return None;
}
let b1 = u32::from(bytes[1]);
let b2 = u32::from(bytes[2]);
if b1 & 0xC0 != 0x80 || b2 & 0xC0 != 0x80 {
return None;
}
(
(u32::from(byte & 0x0F) << 12) | ((b1 & 0x3F) << 6) | (b2 & 0x3F),
3,
)
} else if byte & 0xF8 == 0xF0 {
if bytes.len() < 4 {
return None;
}
let b1 = u32::from(bytes[1]);
let b2 = u32::from(bytes[2]);
let b3 = u32::from(bytes[3]);
if b1 & 0xC0 != 0x80 || b2 & 0xC0 != 0x80 || b3 & 0xC0 != 0x80 {
return None;
}
(
(u32::from(byte & 0x07) << 18)
| ((b1 & 0x3F) << 12)
| ((b2 & 0x3F) << 6)
| (b3 & 0x3F),
4,
)
} else {
return None;
};
char::from_u32(code_point)
}
fn char_len(&self) -> usize {
if self.pos >= self.data.len() {
return 0;
}
Self::utf8_char_len(self.data[self.pos])
}
fn utf8_char_len(leading: u8) -> usize {
if leading & 0x80 == 0 {
1
} else if leading & 0xE0 == 0xC0 {
2
} else if leading & 0xF0 == 0xE0 {
3
} else if leading & 0xF8 == 0xF0 {
4
} else {
1
}
}
fn advance_past_char(&mut self, c: char) {
let byte_len = self.char_len();
let old_pos = self.pos;
self.pos += byte_len;
if byte_len == 1 && self.data[old_pos] == b'\n' {
self.line += 1;
self.col = 1;
} else if byte_len == 1 && self.data[old_pos] == b'\r' {
if self.pos < self.data.len() && self.data[self.pos] == b'\n' {
self.pos += 1;
}
self.line += 1;
self.col = 1;
} else if c == '\t' {
self.col += 1;
} else {
self.col += 1;
}
}
pub fn read_string(&mut self, max_chars: usize) -> String {
let mut s = String::with_capacity(max_chars.min(256));
for _ in 0..max_chars {
match self.read_char() {
Some(c) => s.push(c),
None => break,
}
}
s
}
pub fn read_all_chars(&mut self) -> String {
let mut s = String::new();
while let Some(c) = self.read_char() {
s.push(c);
}
s
}
pub fn skip(&mut self, n: usize) -> usize {
let end = self.pos.saturating_add(n).min(self.data.len());
let skipped = end - self.pos;
for &byte in &self.data[self.pos..end] {
match byte {
b'\n' => {
self.line += 1;
self.col = 1;
}
b'\r' => {
self.line += 1;
self.col = 1;
}
_ => {
self.col += 1;
}
}
}
self.pos = end;
skipped
}
pub fn pos(&self) -> (usize, usize, usize) {
(self.line, self.col, self.pos)
}
pub fn is_eof(&self) -> bool {
self.pos >= self.data.len()
}
pub fn remaining(&self) -> &[u8] {
&self.data[self.pos..]
}
pub fn consumed(&self) -> &[u8] {
&self.data[..self.pos]
}
pub fn len(&self) -> usize {
self.data.len()
}
pub fn filename(&self) -> Option<&str> {
self.filename.as_deref()
}
pub fn encoding(&self) -> &Encoding {
&self.encoding
}
pub fn bom_was_consumed(&self) -> bool {
self.bom_consumed
}
pub unsafe fn populate_parser_input(&self, input: &mut _xmlParserInput) {
let data_ptr = self.data.as_ptr();
let base = data_ptr as *const crate::abi::types::xmlChar;
let cur = unsafe { data_ptr.add(self.pos) as *const crate::abi::types::xmlChar };
let end = unsafe { data_ptr.add(self.data.len()) as *const crate::abi::types::xmlChar };
input.base = base;
input.cur = cur;
input.end = end;
input.line = self.line as c_int;
input.col = self.col as c_int;
input.length = self.data.len() as c_int;
input.consumed = self.pos as c_ulong;
input.filename = self
.filename
.as_ref()
.map(|s| s.as_ptr() as *const c_char)
.unwrap_or(std::ptr::null());
}
pub unsafe fn populate_parser_input_buffer(&self, buf: &mut _xmlParserInputBuffer) {
match &self.source {
InputSource::Callback { read, close, ctx } => {
buf.readcallback = Some(*read);
buf.closecallback = Some(*close);
buf.context = *ctx;
}
_ => {
buf.readcallback = None;
buf.closecallback = None;
buf.context = std::ptr::null_mut();
}
}
buf.encoder = std::ptr::null_mut();
buf.buffer = std::ptr::null_mut();
buf.raw = std::ptr::null_mut();
buf.compressed = 0;
buf.error = 0;
buf.rawconsumed = 0;
}
pub fn reset(&mut self) {
self.pos = 0;
self.line = 1;
self.col = 1;
self.bom_consumed = false;
self.detect_bom_and_encoding();
}
}
pub(crate) struct InputStack {
inputs: Vec<InputBuffer>,
current: usize,
}
impl std::fmt::Debug for InputStack {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("InputStack")
.field("depth", &self.inputs.len())
.field("current", &self.current)
.finish()
}
}
impl InputStack {
pub fn new(base: InputBuffer) -> Self {
InputStack {
inputs: vec![base],
current: 0,
}
}
pub fn push(&mut self, input: InputBuffer) {
self.inputs.push(input);
self.current = self.inputs.len() - 1;
}
pub fn pop(&mut self) -> Option<InputBuffer> {
if self.inputs.len() <= 1 {
return None;
}
let popped = self.inputs.pop();
self.current = self.inputs.len() - 1;
popped
}
pub fn current(&mut self) -> &mut InputBuffer {
&mut self.inputs[self.current]
}
pub fn current_ref(&self) -> &InputBuffer {
&self.inputs[self.current]
}
pub fn current_pos(&self) -> (usize, usize, usize) {
self.inputs[self.current].pos()
}
pub fn depth(&self) -> usize {
self.inputs.len()
}
pub fn is_eof(&self) -> bool {
self.inputs[self.current].is_eof()
}
pub fn read_char(&mut self) -> Option<char> {
self.inputs[self.current].read_char()
}
pub fn peek_char(&self) -> Option<char> {
self.inputs[self.current].peek_char()
}
}
pub(crate) unsafe fn input_buffer_to_parser_input(buf: &InputBuffer) -> *mut _xmlParserInput {
let input = Box::into_raw(Box::new(_xmlParserInput {
buf: std::ptr::null_mut(),
filename: buf
.filename
.as_ref()
.map(|s| s.as_ptr() as *const c_char)
.unwrap_or(std::ptr::null()),
directory: std::ptr::null(),
base: buf.data.as_ptr() as *const crate::abi::types::xmlChar,
cur: unsafe { buf.data.as_ptr().add(buf.pos) as *const crate::abi::types::xmlChar },
end: unsafe { buf.data.as_ptr().add(buf.data.len()) as *const crate::abi::types::xmlChar },
length: buf.data.len() as c_int,
line: buf.line as c_int,
col: buf.col as c_int,
consumed: buf.pos as c_ulong,
free: None,
encoding: std::ptr::null(),
version: std::ptr::null(),
flags: 0,
id: 0,
parentConsumed: 0,
entity: std::ptr::null_mut(),
}));
input
}
pub(crate) unsafe fn input_buffer_to_parser_input_buffer(
buf: &InputBuffer,
) -> *mut _xmlParserInputBuffer {
let mut raw_buf = Box::new(_xmlParserInputBuffer {
context: std::ptr::null_mut(),
readcallback: None,
closecallback: None,
encoder: std::ptr::null_mut(),
buffer: std::ptr::null_mut(),
raw: std::ptr::null_mut(),
compressed: 0,
error: 0,
rawconsumed: 0,
});
unsafe {
buf.populate_parser_input_buffer(&mut raw_buf);
}
Box::into_raw(raw_buf)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_from_memory_empty() {
let buf = InputBuffer::from_memory(b"", None);
assert!(buf.is_eof());
assert_eq!(buf.pos(), (1, 1, 0));
assert_eq!(buf.encoding(), &Encoding::Utf8);
}
#[test]
fn test_from_memory_basic() {
let buf = InputBuffer::from_memory(b"hello", None);
assert!(!buf.is_eof());
assert_eq!(buf.len(), 5);
assert_eq!(buf.remaining(), b"hello");
assert!(buf.consumed().is_empty());
}
#[test]
fn test_from_memory_with_uri() {
let buf = InputBuffer::from_memory(b"test", Some("http://example.com"));
assert_eq!(buf.filename(), Some("http://example.com"));
}
#[test]
fn test_from_memory_with_encoding_declaration() {
let buf =
InputBuffer::from_memory(b"<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>", None);
assert_eq!(buf.encoding(), &Encoding::Iso8859_1);
}
#[test]
fn test_utf8_bom_detection() {
let mut data = vec![0xEF, 0xBB, 0xBF];
data.extend_from_slice(b"hello");
let buf = InputBuffer::from_memory(&data, None);
assert!(buf.bom_was_consumed());
assert_eq!(buf.pos, 3);
assert_eq!(buf.col, 4);
assert_eq!(buf.remaining(), b"hello");
}
#[test]
fn test_utf16le_bom_detection() {
let data = vec![0xFF, 0xFE, b'h', 0x00, b'i', 0x00];
let buf = InputBuffer::from_memory(&data, None);
assert!(buf.bom_was_consumed());
assert_eq!(buf.encoding(), &Encoding::Utf16Le);
assert_eq!(buf.pos, 2);
}
#[test]
fn test_utf16be_bom_detection() {
let data = vec![0xFE, 0xFF, 0x00, b'h', 0x00, b'i'];
let buf = InputBuffer::from_memory(&data, None);
assert!(buf.bom_was_consumed());
assert_eq!(buf.encoding(), &Encoding::Utf16Be);
assert_eq!(buf.pos, 2);
}
#[test]
fn test_bom_with_encoding_declaration() {
let mut data = vec![0xEF, 0xBB, 0xBF];
data.extend_from_slice(b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
let buf = InputBuffer::from_memory(&data, None);
assert!(buf.bom_was_consumed());
assert_eq!(buf.encoding(), &Encoding::Utf8);
}
#[test]
fn test_encoding_extraction_from_pi() {
let pi = r#"<?xml version="1.0" encoding="UTF-8"?>"#;
assert_eq!(
InputBuffer::extract_encoding_from_pi(pi),
Some("UTF-8".to_string())
);
}
#[test]
fn test_encoding_extraction_single_quotes() {
let pi = r#"<?xml version='1.0' encoding='ISO-8859-1'?>"#;
assert_eq!(
InputBuffer::extract_encoding_from_pi(pi),
Some("ISO-8859-1".to_string())
);
}
#[test]
fn test_encoding_extraction_no_encoding() {
let pi = r#"<?xml version="1.0"?>"#;
assert_eq!(InputBuffer::extract_encoding_from_pi(pi), None);
}
#[test]
fn test_encoding_from_name() {
assert_eq!(Encoding::from_name("UTF-8"), Encoding::Utf8);
assert_eq!(Encoding::from_name("utf8"), Encoding::Utf8);
assert_eq!(Encoding::from_name("UTF-16"), Encoding::Utf16Le);
assert_eq!(Encoding::from_name("utf-16le"), Encoding::Utf16Le);
assert_eq!(Encoding::from_name("UTF-16BE"), Encoding::Utf16Be);
assert_eq!(Encoding::from_name("ASCII"), Encoding::Ascii);
assert_eq!(Encoding::from_name("ISO-8859-1"), Encoding::Iso8859_1);
assert_eq!(
Encoding::from_name("Shift_JIS"),
Encoding::Other("shift_jis".to_string())
);
}
#[test]
fn test_read_char_ascii() {
let mut buf = InputBuffer::from_memory(b"abc", None);
assert_eq!(buf.read_char(), Some('a'));
assert_eq!(buf.read_char(), Some('b'));
assert_eq!(buf.read_char(), Some('c'));
assert_eq!(buf.read_char(), None);
assert!(buf.is_eof());
}
#[test]
fn test_read_char_multibyte_utf8() {
let data = vec![0xC3, 0xA9, 0xE2, 0x82, 0xAC, 0xF0, 0x90, 0x8D, 0x88];
let mut buf = InputBuffer::from_memory(&data, None);
assert_eq!(buf.read_char(), Some('é'));
assert_eq!(buf.read_char(), Some('€'));
assert_eq!(buf.read_char(), Some('𐍈'));
assert_eq!(buf.read_char(), None);
}
#[test]
fn test_peek_char() {
let mut buf = InputBuffer::from_memory(b"abc", None);
assert_eq!(buf.peek_char(), Some('a'));
assert_eq!(buf.peek_char(), Some('a')); assert_eq!(buf.read_char(), Some('a'));
assert_eq!(buf.peek_char(), Some('b'));
}
#[test]
fn test_read_char_tracking() {
let mut buf = InputBuffer::from_memory(b"a\nb\nc", None);
assert_eq!(buf.read_char(), Some('a'));
assert_eq!(buf.pos(), (1, 2, 1));
assert_eq!(buf.read_char(), Some('\n'));
assert_eq!(buf.pos(), (2, 1, 2));
assert_eq!(buf.read_char(), Some('b'));
assert_eq!(buf.pos(), (2, 2, 3));
assert_eq!(buf.read_char(), Some('\n'));
assert_eq!(buf.pos(), (3, 1, 4));
assert_eq!(buf.read_char(), Some('c'));
assert_eq!(buf.pos(), (3, 2, 5));
}
#[test]
fn test_crlf_handling() {
let mut buf = InputBuffer::from_memory(b"a\r\nb", None);
assert_eq!(buf.read_char(), Some('a'));
assert_eq!(buf.pos(), (1, 2, 1));
assert_eq!(buf.read_char(), Some('\r'));
assert_eq!(buf.pos(), (2, 1, 3));
assert_eq!(buf.read_char(), Some('b'));
assert_eq!(buf.pos(), (2, 2, 4));
}
#[test]
fn test_skip() {
let mut buf = InputBuffer::from_memory(b"hello world", None);
assert_eq!(buf.skip(5), 5);
assert_eq!(buf.remaining(), b" world");
assert_eq!(buf.pos(), (1, 6, 5));
}
#[test]
fn test_skip_past_end() {
let mut buf = InputBuffer::from_memory(b"hi", None);
assert_eq!(buf.skip(100), 2);
assert!(buf.is_eof());
}
#[test]
fn test_read_string() {
let mut buf = InputBuffer::from_memory(b"hello world", None);
assert_eq!(buf.read_string(5), "hello");
assert_eq!(buf.read_string(10), " world");
}
#[test]
fn test_read_all_chars() {
let mut buf = InputBuffer::from_memory(b"hello", None);
assert_eq!(buf.read_all_chars(), "hello");
assert!(buf.is_eof());
}
#[test]
fn test_reset() {
let mut buf = InputBuffer::from_memory(b"hello", None);
assert_eq!(buf.read_char(), Some('h'));
assert_eq!(buf.read_char(), Some('e'));
buf.reset();
assert_eq!(buf.read_char(), Some('h'));
assert_eq!(buf.pos(), (1, 2, 1));
}
#[test]
fn test_from_file_not_found() {
let result = InputBuffer::from_file("/nonexistent/file.xml");
assert!(result.is_err());
match result {
Err(InputError::Io(_)) => {} _ => panic!("expected Io error"),
}
}
#[test]
fn test_position_tracking() {
let mut buf = InputBuffer::from_memory(b"line1\nline2\nline3", None);
assert_eq!(buf.pos(), (1, 1, 0));
assert_eq!(buf.read_string(5), "line1");
assert_eq!(buf.pos(), (1, 6, 5));
assert_eq!(buf.read_char(), Some('\n'));
assert_eq!(buf.pos(), (2, 1, 6));
assert_eq!(buf.read_string(5), "line2");
assert_eq!(buf.pos(), (2, 6, 11));
assert_eq!(buf.read_char(), Some('\n'));
assert_eq!(buf.pos(), (3, 1, 12));
assert_eq!(buf.read_string(5), "line3");
assert_eq!(buf.pos(), (3, 6, 17));
assert!(buf.is_eof());
}
#[test]
fn test_input_stack_basic() {
let base = InputBuffer::from_memory(b"base ", None);
let mut stack = InputStack::new(base);
assert_eq!(stack.depth(), 1);
let entity = InputBuffer::from_memory(b"entity", None);
stack.push(entity);
assert_eq!(stack.depth(), 2);
assert_eq!(stack.read_char(), Some('e'));
assert_eq!(stack.current_pos(), (1, 2, 1));
let popped = stack.pop();
assert!(popped.is_some());
assert_eq!(stack.depth(), 1);
assert_eq!(stack.read_char(), Some('b'));
}
#[test]
fn test_input_stack_no_pop_base() {
let base = InputBuffer::from_memory(b"base", None);
let mut stack = InputStack::new(base);
assert!(stack.pop().is_none());
assert_eq!(stack.depth(), 1);
}
#[test]
fn test_input_stack_peek() {
let base = InputBuffer::from_memory(b"abc", None);
let mut stack = InputStack::new(base);
assert_eq!(stack.peek_char(), Some('a'));
assert_eq!(stack.read_char(), Some('a'));
assert_eq!(stack.peek_char(), Some('b'));
}
#[test]
fn test_input_stack_eof() {
let base = InputBuffer::from_memory(b"ab", None);
let mut stack = InputStack::new(base);
assert!(!stack.is_eof());
stack.read_char();
stack.read_char();
assert!(stack.is_eof());
}
#[test]
fn test_decode_utf8_invalid_continuation() {
let data = vec![0xC0, 0x00];
let mut buf = InputBuffer::from_memory(&data, None);
assert!(buf.read_char().is_none());
}
#[test]
fn test_decode_utf8_truncated_sequence() {
let data = vec![0xC3];
let mut buf = InputBuffer::from_memory(&data, None);
assert!(buf.read_char().is_none());
}
#[test]
fn test_encoding_detection_utf8_xml_decl() {
let buf = InputBuffer::from_memory(b"<?xml version='1.0' encoding='UTF-8'?>", None);
assert_eq!(buf.encoding(), &Encoding::Utf8);
}
#[test]
fn test_encoding_detection_latin1_xml_decl() {
let buf = InputBuffer::from_memory(b"<?xml version='1.0' encoding='ISO-8859-1'?>", None);
assert_eq!(buf.encoding(), &Encoding::Iso8859_1);
}
#[test]
fn test_encoding_detection_unknown() {
let buf = InputBuffer::from_memory(b"<?xml version='1.0' encoding='Shift_JIS'?>", None);
assert_eq!(buf.encoding(), &Encoding::Other("shift_jis".to_string()));
}
}