use std::any::Any;
use std::error::Error;
use std::fmt::{Display, Formatter};
use std::panic::{AssertUnwindSafe, catch_unwind, panic_any, resume_unwind};
use std::sync::{Mutex, OnceLock};
use std::time::Instant;
use super::parsing_locator_util::ParsingLocatorError;
use super::{
ChainedTextHandlerRuntimeError, CommentProcessorTextHandler,
CommentProcessorTextHandlerRuntimeError, EventProcessorTextHandler,
EventProcessorTextHandlerRuntimeError, ITextHandler, ParsingLocatorUtil, TextParseCause,
TextParseException, TextParseStatus, TextParsingCommentError, TextParsingCommentUtil,
TextParsingElementError, TextParsingElementUtil, TextParsingLiteralUtil, TextParsingUtil,
TextParsingUtilError,
};
use crate::util::Utf16String;
const DOCUMENT_NULL_MESSAGE: &str = "Document cannot be null";
const READER_NULL_MESSAGE: &str = "Reader cannot be null";
const HANDLER_NULL_MESSAGE: &str = "Handler cannot be null";
const NULL_PARSE_DOCUMENT_READER_MESSAGE: &str =
"Cannot invoke \"java.io.Reader.read(char[])\" because \"reader\" is null";
const NULL_PARSE_DOCUMENT_HANDLER_MESSAGE: &str = "Cannot invoke \"org.thymeleaf.templateparser.text.ITextHandler.handleDocumentStart(long, int, int)\" because \"handler\" is null";
const MAX_UNCONSUMED_BUFFER_SIZE: i32 = 16 * 1024 * 1024;
const MAX_CONSECUTIVE_ZERO_READS: usize = 1024;
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct TextParserRuntimeError {
class_name: &'static str,
message: Option<Utf16String>,
}
impl TextParserRuntimeError {
fn illegal_argument(message: &'static str) -> Self {
Self {
class_name: "java.lang.IllegalArgumentException",
message: Some(Utf16String::from_rust_str(message)),
}
}
fn negative_array_size(size: i32) -> Self {
Self {
class_name: "java.lang.NegativeArraySizeException",
message: Some(Utf16String::from_rust_str(&size.to_string())),
}
}
fn array_index(index: i32, length: usize) -> Self {
Self {
class_name: "java.lang.ArrayIndexOutOfBoundsException",
message: Some(Utf16String::from_rust_str(&format!(
"Index {index} out of bounds for length {length}"
))),
}
}
fn string_range(offset: i32, len: i32, length: usize) -> Self {
Self {
class_name: "java.lang.StringIndexOutOfBoundsException",
message: Some(Utf16String::from_rust_str(&format!(
"Range [{offset}, {offset} + {len}) out of bounds for length {length}"
))),
}
}
fn arraycopy_negative_length(length: i32) -> Self {
Self::with_java_metadata(
"java.lang.ArrayIndexOutOfBoundsException",
Some(Utf16String::from_rust_str(&format!(
"arraycopy: length {length} is negative"
))),
)
}
fn arraycopy_source_index(index: i32, length: usize) -> Self {
Self::with_java_metadata(
"java.lang.ArrayIndexOutOfBoundsException",
Some(Utf16String::from_rust_str(&format!(
"arraycopy: source index {index} out of bounds for char[{length}]"
))),
)
}
fn arraycopy_destination_index(index: i32, length: usize) -> Self {
Self::with_java_metadata(
"java.lang.ArrayIndexOutOfBoundsException",
Some(Utf16String::from_rust_str(&format!(
"arraycopy: destination index {index} out of bounds for char[{length}]"
))),
)
}
fn arraycopy_last_source(index: i64, length: usize) -> Self {
Self::with_java_metadata(
"java.lang.ArrayIndexOutOfBoundsException",
Some(Utf16String::from_rust_str(&format!(
"arraycopy: last source index {index} out of bounds for char[{length}]"
))),
)
}
fn arraycopy_last_destination(index: i64, length: usize) -> Self {
Self::with_java_metadata(
"java.lang.ArrayIndexOutOfBoundsException",
Some(Utf16String::from_rust_str(&format!(
"arraycopy: last destination index {index} out of bounds for char[{length}]"
))),
)
}
fn null_reader() -> Self {
Self {
class_name: "java.lang.NullPointerException",
message: Some(Utf16String::from_rust_str(
NULL_PARSE_DOCUMENT_READER_MESSAGE,
)),
}
}
fn null_handler() -> Self {
Self {
class_name: "java.lang.NullPointerException",
message: Some(Utf16String::from_rust_str(
NULL_PARSE_DOCUMENT_HANDLER_MESSAGE,
)),
}
}
#[must_use]
pub(crate) fn with_java_metadata(
class_name: &'static str,
message: Option<Utf16String>,
) -> Self {
Self {
class_name,
message,
}
}
#[must_use]
pub(crate) const fn class_name(&self) -> &'static str {
self.class_name
}
#[must_use]
pub(crate) fn message(&self) -> Option<Utf16String> {
self.message.clone()
}
}
impl Display for TextParserRuntimeError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter.write_str(
&self
.message
.as_ref()
.map_or_else(|| "null".to_owned(), Utf16String::to_string_lossy),
)
}
}
impl Error for TextParserRuntimeError {}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TextParserReaderError {
class_name: String,
message: Option<Utf16String>,
}
impl TextParserReaderError {
#[must_use]
pub fn new(class_name: &str, message: Option<Utf16String>) -> Self {
Self {
class_name: class_name.to_owned(),
message,
}
}
#[must_use]
pub fn io(message: &str) -> Self {
Self::new(
"java.io.IOException",
Some(Utf16String::from_rust_str(message)),
)
}
#[must_use]
pub fn class_name(&self) -> &str {
&self.class_name
}
#[must_use]
pub fn message(&self) -> Option<Utf16String> {
self.message.clone()
}
}
impl Display for TextParserReaderError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter.write_str(
&self
.message
.as_ref()
.map_or_else(|| "null".to_owned(), Utf16String::to_string_lossy),
)
}
}
impl Error for TextParserReaderError {}
pub trait TextParserReader {
fn read_buffer(&mut self, buffer: &mut [u16]) -> Result<i32, TextParserReaderError> {
self.read_range(buffer, 0, buffer.len() as i32)
}
fn read_range(
&mut self,
buffer: &mut [u16],
offset: i32,
len: i32,
) -> Result<i32, TextParserReaderError>;
fn close(&mut self) -> Result<(), TextParserReaderError> {
Ok(())
}
}
#[derive(Debug)]
struct StringTextParserReader {
input: Vec<u16>,
position: usize,
}
impl StringTextParserReader {
fn new(document: &Utf16String) -> Self {
Self {
input: document.as_utf16().to_vec(),
position: 0,
}
}
}
impl TextParserReader for StringTextParserReader {
fn read_range(
&mut self,
buffer: &mut [u16],
offset: i32,
len: i32,
) -> Result<i32, TextParserReaderError> {
if len == 0 {
return Ok(0);
}
if self.position >= self.input.len() {
return Ok(-1);
}
let copied = (len as usize).min(self.input.len() - self.position);
let destination = offset as usize;
buffer[destination..destination + copied]
.copy_from_slice(&self.input[self.position..self.position + copied]);
self.position += copied;
Ok(copied as i32)
}
}
#[derive(Debug)]
struct BufferPool {
state: Mutex<BufferPoolState>,
pool_buffer_size: i32,
}
#[derive(Debug)]
struct BufferPoolState {
buffers: Vec<Option<Vec<u16>>>,
}
#[derive(Debug)]
struct AllocatedBuffer {
buffer: Vec<u16>,
pool_index: Option<usize>,
}
impl BufferPool {
fn new(pool_size: i32, pool_buffer_size: i32) -> Self {
if pool_size < 0 {
panic_runtime(TextParserRuntimeError::negative_array_size(pool_size));
}
let mut buffers = Vec::with_capacity(pool_size as usize);
for _ in 0..pool_size {
buffers.push(Some(char_array(pool_buffer_size)));
}
Self {
state: Mutex::new(BufferPoolState { buffers }),
pool_buffer_size,
}
}
#[inline(always)]
fn allocate_buffer(&self, buffer_size: i32) -> AllocatedBuffer {
if buffer_size != self.pool_buffer_size {
return AllocatedBuffer {
buffer: char_array(buffer_size),
pool_index: None,
};
}
let mut state = self.state.lock().unwrap_or_else(|error| error.into_inner());
for (pool_index, slot) in state.buffers.iter_mut().enumerate() {
if let Some(buffer) = slot.take() {
return AllocatedBuffer {
buffer,
pool_index: Some(pool_index),
};
}
}
AllocatedBuffer {
buffer: char_array(buffer_size),
pool_index: None,
}
}
#[inline(always)]
fn release_buffer(&self, allocated: Option<AllocatedBuffer>) {
let Some(allocated) = allocated else {
return;
};
if allocated.buffer.len() as i32 != self.pool_buffer_size {
return;
}
let Some(pool_index) = allocated.pool_index else {
return;
};
let mut state = self.state.lock().unwrap_or_else(|error| error.into_inner());
state.buffers[pool_index] = Some(allocated.buffer);
}
}
pub struct TextParser {
pool: BufferPool,
process_comments_and_literals: bool,
standard_dialect_present: bool,
}
impl TextParser {
#[must_use]
pub fn new(
pool_size: i32,
buffer_size: i32,
process_comments_and_literals: bool,
standard_dialect_present: bool,
) -> Self {
Self {
pool: BufferPool::new(pool_size, buffer_size),
process_comments_and_literals,
standard_dialect_present,
}
}
pub fn parse(
&self,
document: Option<&Utf16String>,
handler: Option<Box<dyn ITextHandler>>,
) -> Result<(), Box<TextParseException>> {
let Some(document) = document else {
panic_runtime(TextParserRuntimeError::illegal_argument(
DOCUMENT_NULL_MESSAGE,
));
};
self.parse_reader(
Some(Box::new(StringTextParserReader::new(document))),
handler,
)
}
pub fn parse_reader(
&self,
reader: Option<Box<dyn TextParserReader>>,
handler: Option<Box<dyn ITextHandler>>,
) -> Result<(), Box<TextParseException>> {
let Some(reader) = reader else {
panic_runtime(TextParserRuntimeError::illegal_argument(
READER_NULL_MESSAGE,
));
};
if handler.is_none() {
panic_runtime(TextParserRuntimeError::illegal_argument(
HANDLER_NULL_MESSAGE,
));
}
let mut handler_chain: Box<dyn ITextHandler> =
Box::new(EventProcessorTextHandler::new(handler));
if self.process_comments_and_literals {
handler_chain = Box::new(CommentProcessorTextHandler::new(
self.standard_dialect_present,
Some(handler_chain),
));
}
self.parse_document(
Some(reader),
self.pool.pool_buffer_size,
Some(handler_chain),
)
}
#[inline(always)]
pub(crate) fn parse_document(
&self,
mut reader: Option<Box<dyn TextParserReader>>,
suggested_buffer_size: i32,
mut handler: Option<Box<dyn ITextHandler>>,
) -> Result<(), Box<TextParseException>> {
let mut allocated_buffer = None;
let outcome = catch_unwind(AssertUnwindSafe(|| {
self.parse_document_body(
reader.as_deref_mut(),
suggested_buffer_size,
handler.as_deref_mut(),
&mut allocated_buffer,
)
}));
self.pool.release_buffer(allocated_buffer.take());
if let Some(reader) = reader.as_deref_mut() {
let _ = catch_unwind(AssertUnwindSafe(|| {
let _ = reader.close();
}));
}
match outcome {
Ok(result) => result,
Err(payload) => match panic_payload_to_cause(payload) {
Ok(cause) => Err(Box::new(TextParseException::with_cause(Some(cause)))),
Err(payload) => resume_unwind(payload),
},
}
}
fn parse_document_body(
&self,
reader: Option<&mut (dyn TextParserReader + 'static)>,
suggested_buffer_size: i32,
handler: Option<&mut (dyn ITextHandler + 'static)>,
allocated_buffer: &mut Option<AllocatedBuffer>,
) -> Result<(), Box<TextParseException>> {
let parsing_start_time_nanos = nano_time();
let Some(handler) = handler else {
panic_runtime(TextParserRuntimeError::null_handler());
};
let mut status = TextParseStatus::new();
handler.handle_document_start(parsing_start_time_nanos, 1, 1)?;
let mut buffer_size = suggested_buffer_size;
*allocated_buffer = Some(self.pool.allocate_buffer(buffer_size));
let Some(reader) = reader else {
panic_runtime(TextParserRuntimeError::null_reader());
};
let mut buffer_content_size = reader
.read_buffer(
&mut allocated_buffer
.as_mut()
.expect("buffer allocated before first read")
.buffer,
)
.map_err(reader_error_as_text_parse)?;
let mut cont = buffer_content_size != -1;
let mut consecutive_zero_reads = usize::from(buffer_content_size == 0);
status.offset = -1;
status.line = 1;
status.col = 1;
status.in_structure = false;
status.in_comment_line = false;
status.literal_marker = 0;
while cont {
self.parse_buffer(
&mut allocated_buffer.as_mut().expect("active buffer").buffer,
0,
buffer_content_size,
handler,
&mut status,
)?;
let mut read_offset = 0;
let mut read_len = buffer_size;
if status.offset == 0 {
if buffer_content_size == buffer_size {
self.ensure_buffer_growth_is_safe(buffer_size, status.line, status.col)?;
self.grow_buffer(&mut buffer_size, buffer_content_size, allocated_buffer);
}
read_offset = buffer_content_size;
read_len = buffer_size.wrapping_sub(read_offset);
} else if status.offset < buffer_content_size {
let content_to_move = buffer_content_size.wrapping_sub(status.offset);
let active = &mut allocated_buffer.as_mut().expect("active buffer").buffer;
array_copy_within(active, status.offset, 0, content_to_move);
read_offset = content_to_move;
read_len = buffer_size.wrapping_sub(read_offset);
status.offset = 0;
buffer_content_size = read_offset;
}
let read = reader
.read_range(
&mut allocated_buffer.as_mut().expect("active buffer").buffer,
read_offset,
read_len,
)
.map_err(reader_error_as_text_parse)?;
if read != -1 {
buffer_content_size = read_offset.wrapping_add(read);
if read == 0 {
consecutive_zero_reads = consecutive_zero_reads.saturating_add(1);
if consecutive_zero_reads > MAX_CONSECUTIVE_ZERO_READS {
return Err(Box::new(TextParseException::with_message_at(
Some(&Utf16String::from_rust_str(&format!(
"Text parser reader made no progress after {MAX_CONSECUTIVE_ZERO_READS} consecutive zero-length reads"
))),
status.line,
status.col,
)));
}
} else {
consecutive_zero_reads = 0;
}
} else {
cont = false;
}
}
let mut last_line = status.line;
let mut last_col = status.col;
let last_start = status.offset;
let last_len = buffer_content_size.wrapping_sub(last_start);
if last_len > 0 {
let active = &mut allocated_buffer.as_mut().expect("active buffer").buffer;
if status.in_structure && !status.in_comment_line {
let source = utf16_string_from_range(active, last_start, last_len);
let mut message = "Incomplete structure: \""
.encode_utf16()
.collect::<Vec<_>>();
message.extend_from_slice(source.as_utf16());
message.push(u16::from(b'"'));
return Err(Box::new(TextParseException::with_message_at(
Some(&Utf16String::from_utf16(message)),
status.line,
status.col,
)));
}
handler.handle_text(Some(active), last_start, last_len, status.line, status.col)?;
let maxi = last_start.wrapping_add(last_len);
let mut index = last_start;
while index < maxi {
let character = array_unit(active, index);
if character == u16::from(b'\n') {
last_line = last_line.wrapping_add(1);
last_col = 1;
} else {
last_col = last_col.wrapping_add(1);
}
index = index.wrapping_add(1);
}
}
let parsing_end_time_nanos = nano_time();
handler.handle_document_end(
parsing_end_time_nanos,
parsing_end_time_nanos.wrapping_sub(parsing_start_time_nanos),
last_line,
last_col,
)
}
#[inline(always)]
fn ensure_buffer_growth_is_safe(
&self,
buffer_size: i32,
line: i32,
col: i32,
) -> Result<(), Box<TextParseException>> {
let next_buffer_size = i64::from(buffer_size).saturating_mul(2);
if buffer_size <= 0 || next_buffer_size > i64::from(MAX_UNCONSUMED_BUFFER_SIZE) {
return Err(Box::new(TextParseException::with_message_at(
Some(&Utf16String::from_rust_str(&format!(
"Text parser retained an unconsumed structure beyond {MAX_UNCONSUMED_BUFFER_SIZE} UTF-16 code units"
))),
line,
col,
)));
}
Ok(())
}
#[inline(always)]
fn grow_buffer(
&self,
buffer_size: &mut i32,
buffer_content_size: i32,
allocated_buffer: &mut Option<AllocatedBuffer>,
) {
*buffer_size = buffer_size.wrapping_mul(2);
let mut new_buffer = None;
let growth = catch_unwind(AssertUnwindSafe(|| {
new_buffer = Some(self.pool.allocate_buffer(*buffer_size));
array_copy(
&allocated_buffer.as_ref().expect("old buffer").buffer,
0,
&mut new_buffer.as_mut().expect("new buffer").buffer,
0,
buffer_content_size,
);
}));
match growth {
Ok(()) => {
let old_buffer =
allocated_buffer.replace(new_buffer.take().expect("new buffer allocated"));
self.pool.release_buffer(old_buffer);
}
Err(payload) if payload.is::<TextParserRuntimeError>() => {
self.pool.release_buffer(new_buffer.take());
}
Err(payload) => {
self.pool.release_buffer(new_buffer.take());
resume_unwind(payload);
}
}
}
fn parse_buffer(
&self,
buffer: &mut [u16],
offset: i32,
len: i32,
handler: &mut dyn ITextHandler,
status: &mut TextParseStatus,
) -> Result<(), Box<TextParseException>> {
let mut locator = [status.line, status.col];
let mut current_line = locator[0];
let mut current_col = locator[1];
let maxi = offset.wrapping_add(len);
let mut index = offset;
let mut current = index;
let mut in_open_element = false;
let mut in_close_element = false;
let mut in_comment_block = false;
let mut in_comment_line = false;
let mut in_literal = false;
let mut position;
let mut tag_start = index;
let mut tag_end = index;
while index < maxi {
let mut in_structure = in_open_element
|| in_close_element
|| in_comment_block
|| in_comment_line
|| in_literal;
if !in_structure {
position = util_i32_value(
TextParsingUtil::find_next_structure_start_or_literal_marker(
Some(buffer),
index,
maxi,
Some(&mut locator),
self.process_comments_and_literals,
),
);
if position == -1 {
set_terminal_status(status, current, current_line, current_col);
return Ok(());
}
let (
mut character,
classified_open,
classified_close,
classified_comment_block,
classified_comment_line,
classified_literal,
literal_marker,
) = classify_structure_start(
buffer,
position,
maxi,
self.process_comments_and_literals,
);
in_open_element = classified_open;
in_close_element = classified_close;
in_comment_block = classified_comment_block;
in_comment_line = classified_comment_line;
in_literal = classified_literal;
if let Some(literal_marker) = literal_marker {
status.literal_marker = literal_marker;
}
in_structure = in_open_element
|| in_close_element
|| in_comment_block
|| in_comment_line
|| in_literal;
if in_structure && !in_literal {
tag_start = position;
}
while !in_structure {
count_locator(&mut locator, character);
position = util_i32_value(
TextParsingUtil::find_next_structure_start_or_literal_marker(
Some(buffer),
position.wrapping_add(1),
maxi,
Some(&mut locator),
self.process_comments_and_literals,
),
);
if position == -1 {
set_terminal_status(status, current, current_line, current_col);
return Ok(());
}
let classification = classify_structure_start(
buffer,
position,
maxi,
self.process_comments_and_literals,
);
character = classification.0;
in_open_element = classification.1;
in_close_element = classification.2;
in_comment_block = classification.3;
in_comment_line = classification.4;
in_literal = classification.5;
if let Some(literal_marker) = classification.6 {
status.literal_marker = literal_marker;
}
in_structure = in_open_element
|| in_close_element
|| in_comment_block
|| in_comment_line
|| in_literal;
if in_structure && !in_literal {
tag_start = position;
}
}
if tag_start > current {
handler.handle_text(
Some(buffer),
current,
tag_start.wrapping_sub(current),
current_line,
current_col,
)?;
}
if tag_start == position {
current = tag_start;
current_line = locator[0];
current_col = locator[1];
}
index = position;
} else {
position = if in_literal {
util_i32_value(TextParsingUtil::find_next_literal_end(
Some(buffer),
index,
maxi,
Some(&mut locator),
status.literal_marker,
))
} else if in_comment_block {
util_i32_value(TextParsingUtil::find_next_comment_block_end(
Some(buffer),
index,
maxi,
Some(&mut locator),
))
} else if in_comment_line {
util_i32_value(TextParsingUtil::find_next_comment_line_end(
Some(buffer),
index,
maxi,
Some(&mut locator),
))
} else {
util_i32_value(TextParsingUtil::find_next_structure_end_avoid_quotes(
Some(buffer),
index,
maxi,
Some(&mut locator),
))
};
if position < 0 {
status.offset = current;
status.line = current_line;
status.col = current_col;
status.in_structure = true;
status.in_comment_line = in_comment_line;
status.literal_marker = 0;
return Ok(());
}
if in_open_element {
tag_end = position;
if array_unit(buffer, tag_end.wrapping_sub(1)) == u16::from(b'/') {
element_parse(TextParsingElementUtil::parse_standalone_element(
Some(buffer),
current,
tag_end.wrapping_sub(current).wrapping_add(1),
current_line,
current_col,
Some(handler),
))?;
} else {
element_parse(TextParsingElementUtil::parse_open_element(
Some(buffer),
current,
tag_end.wrapping_sub(current).wrapping_add(1),
current_line,
current_col,
Some(handler),
))?;
}
in_open_element = false;
} else if in_close_element {
tag_end = position;
element_parse(TextParsingElementUtil::parse_close_element(
Some(buffer),
current,
tag_end.wrapping_sub(current).wrapping_add(1),
current_line,
current_col,
Some(handler),
))?;
in_close_element = false;
} else if in_comment_block {
tag_end = position;
comment_parse(TextParsingCommentUtil::parse_comment(
Some(buffer),
current,
tag_end.wrapping_sub(current).wrapping_add(1),
current_line,
current_col,
handler,
))?;
in_comment_block = false;
} else if in_comment_line {
tag_end = position;
handler.handle_text(
Some(buffer),
current,
tag_end.wrapping_sub(current).wrapping_add(1),
current_line,
current_col,
)?;
in_comment_line = false;
} else {
in_literal = false;
status.literal_marker = 0;
}
count_locator(&mut locator, array_unit(buffer, position));
if tag_end == position {
current = tag_end.wrapping_add(1);
current_line = locator[0];
current_col = locator[1];
}
index = position.wrapping_add(1);
}
}
set_terminal_status(status, current, current_line, current_col);
Ok(())
}
}
type StructureStartClassification = (u16, bool, bool, bool, bool, bool, Option<u16>);
fn classify_structure_start(
buffer: &[u16],
position: i32,
maxi: i32,
process_comments_and_literals: bool,
) -> StructureStartClassification {
let character = array_unit(buffer, position);
let in_open_element = element_bool_value(TextParsingElementUtil::is_open_element_start(
Some(buffer),
position,
maxi,
));
if in_open_element {
return (character, true, false, false, false, false, None);
}
let in_close_element = element_bool_value(TextParsingElementUtil::is_close_element_start(
Some(buffer),
position,
maxi,
));
if in_close_element {
return (character, false, true, false, false, false, None);
}
if !process_comments_and_literals {
return (character, false, false, false, false, false, None);
}
let in_comment_block = comment_bool_value(TextParsingCommentUtil::is_comment_block_start(
Some(buffer),
position,
maxi,
));
if in_comment_block {
return (character, false, false, true, false, false, None);
}
let in_comment_line = comment_bool_value(TextParsingCommentUtil::is_comment_line_start(
Some(buffer),
position,
maxi,
));
if in_comment_line {
return (character, false, false, false, true, false, None);
}
let in_literal = matches!(character, 0x0027 | 0x0022 | 0x0060)
|| comment_bool_value(TextParsingLiteralUtil::is_regex_literal_start(
Some(buffer),
position,
maxi,
));
(
character,
false,
false,
false,
false,
in_literal,
Some(if in_literal { character } else { 0 }),
)
}
fn set_terminal_status(
status: &mut TextParseStatus,
current: i32,
current_line: i32,
current_col: i32,
) {
status.offset = current;
status.line = current_line;
status.col = current_col;
status.in_structure = false;
status.in_comment_line = false;
status.literal_marker = 0;
}
fn nano_time() -> i64 {
static ORIGIN: OnceLock<Instant> = OnceLock::new();
let elapsed = ORIGIN.get_or_init(Instant::now).elapsed().as_nanos();
elapsed as i64
}
fn char_array(size: i32) -> Vec<u16> {
if size < 0 {
panic_runtime(TextParserRuntimeError::negative_array_size(size));
}
vec![0; size as usize]
}
fn utf16_string_from_range(buffer: &[u16], offset: i32, len: i32) -> Utf16String {
if offset < 0 || len < 0 {
panic_runtime(TextParserRuntimeError::string_range(
offset,
len,
buffer.len(),
));
}
let end = i64::from(offset) + i64::from(len);
if end > buffer.len() as i64 {
panic_runtime(TextParserRuntimeError::string_range(
offset,
len,
buffer.len(),
));
}
Utf16String::from_utf16(buffer[offset as usize..end as usize].to_vec())
}
fn array_unit(buffer: &[u16], index: i32) -> u16 {
if index < 0 || index as usize >= buffer.len() {
panic_runtime(TextParserRuntimeError::array_index(index, buffer.len()));
}
buffer[index as usize]
}
fn array_copy(
source: &[u16],
source_offset: i32,
destination: &mut [u16],
destination_offset: i32,
len: i32,
) {
if len < 0 {
panic_runtime(TextParserRuntimeError::arraycopy_negative_length(len));
}
if source_offset < 0 {
panic_runtime(TextParserRuntimeError::arraycopy_source_index(
source_offset,
source.len(),
));
}
if destination_offset < 0 {
panic_runtime(TextParserRuntimeError::arraycopy_destination_index(
destination_offset,
destination.len(),
));
}
let source_end = i64::from(source_offset) + i64::from(len);
if source_end > source.len() as i64 {
panic_runtime(TextParserRuntimeError::arraycopy_last_source(
source_end,
source.len(),
));
}
let destination_end = i64::from(destination_offset) + i64::from(len);
if destination_end > destination.len() as i64 {
panic_runtime(TextParserRuntimeError::arraycopy_last_destination(
destination_end,
destination.len(),
));
}
destination[destination_offset as usize..destination_end as usize]
.copy_from_slice(&source[source_offset as usize..source_end as usize]);
}
fn array_copy_within(buffer: &mut [u16], source_offset: i32, destination_offset: i32, len: i32) {
if len < 0 {
panic_runtime(TextParserRuntimeError::arraycopy_negative_length(len));
}
if source_offset < 0 {
panic_runtime(TextParserRuntimeError::arraycopy_source_index(
source_offset,
buffer.len(),
));
}
if destination_offset < 0 {
panic_runtime(TextParserRuntimeError::arraycopy_destination_index(
destination_offset,
buffer.len(),
));
}
let source_end = i64::from(source_offset) + i64::from(len);
if source_end > buffer.len() as i64 {
panic_runtime(TextParserRuntimeError::arraycopy_last_source(
source_end,
buffer.len(),
));
}
let destination_end = i64::from(destination_offset) + i64::from(len);
if destination_end > buffer.len() as i64 {
panic_runtime(TextParserRuntimeError::arraycopy_last_destination(
destination_end,
buffer.len(),
));
}
buffer.copy_within(
source_offset as usize..source_end as usize,
destination_offset as usize,
);
}
fn util_i32_value(result: Result<i32, TextParsingUtilError>) -> i32 {
match result {
Ok(value) => value,
Err(error) => panic_any(error),
}
}
fn element_bool_value(result: Result<bool, TextParsingElementError>) -> bool {
match result {
Ok(value) => value,
Err(error) => panic_any(error),
}
}
fn comment_bool_value(result: Result<bool, TextParsingCommentError>) -> bool {
match result {
Ok(value) => value,
Err(error) => panic_any(error),
}
}
fn element_parse(
result: Result<(), TextParsingElementError>,
) -> Result<(), Box<TextParseException>> {
match result {
Ok(()) => Ok(()),
Err(TextParsingElementError::TextParse(exception)) => Err(exception),
Err(error) => panic_any(error),
}
}
fn comment_parse(
result: Result<(), TextParsingCommentError>,
) -> Result<(), Box<TextParseException>> {
match result {
Ok(()) => Ok(()),
Err(TextParsingCommentError::TextParse(exception)) => Err(exception),
Err(error) => panic_any(error),
}
}
fn count_locator(locator: &mut [i32], character: u16) {
if let Err(error) = ParsingLocatorUtil::count_char(Some(locator), character) {
panic_any(error);
}
}
fn reader_error_as_text_parse(error: TextParserReaderError) -> Box<TextParseException> {
let class_name = error.class_name.clone();
let message = error.message.clone();
Box::new(TextParseException::with_cause(Some(
TextParseCause::with_java_metadata(Box::new(error), class_name, message),
)))
}
fn panic_payload_to_cause(
payload: Box<dyn Any + Send>,
) -> Result<TextParseCause, Box<dyn Any + Send>> {
let payload = match payload.downcast::<TextParserRuntimeError>() {
Ok(error) => {
let class_name = error.class_name();
let message = error.message();
return Ok(TextParseCause::with_java_metadata(
error, class_name, message,
));
}
Err(payload) => payload,
};
let payload = match payload.downcast::<TextParsingUtilError>() {
Ok(error) => {
let class_name = error.class_name();
let message = error.message();
return Ok(TextParseCause::with_java_metadata(
error, class_name, message,
));
}
Err(payload) => payload,
};
let payload = match payload.downcast::<TextParsingElementError>() {
Ok(error) => {
let class_name = error.class_name();
let message = Some(error.message());
return Ok(TextParseCause::with_java_metadata(
error, class_name, message,
));
}
Err(payload) => payload,
};
let payload = match payload.downcast::<TextParsingCommentError>() {
Ok(error) => {
let class_name = error.class_name();
let message = Some(error.message());
return Ok(TextParseCause::with_java_metadata(
error, class_name, message,
));
}
Err(payload) => payload,
};
let payload = match payload.downcast::<ParsingLocatorError>() {
Ok(error) => {
let class_name = error.class_name();
let message = Some(error.message());
return Ok(TextParseCause::with_java_metadata(
error, class_name, message,
));
}
Err(payload) => payload,
};
let payload = match payload.downcast::<EventProcessorTextHandlerRuntimeError>() {
Ok(error) => {
let class_name = error.class_name();
let message = Some(error.message().clone());
return Ok(TextParseCause::with_java_metadata(
error, class_name, message,
));
}
Err(payload) => payload,
};
let payload = match payload.downcast::<CommentProcessorTextHandlerRuntimeError>() {
Ok(error) => {
let class_name = error.class_name();
let message = error.message();
return Ok(TextParseCause::with_java_metadata(
error, class_name, message,
));
}
Err(payload) => payload,
};
match payload.downcast::<ChainedTextHandlerRuntimeError>() {
Ok(error) => {
let class_name = error.class_name();
let message = Some(error.message());
Ok(TextParseCause::with_java_metadata(
error, class_name, message,
))
}
Err(payload) => Err(payload),
}
}
fn panic_runtime(error: TextParserRuntimeError) -> ! {
panic_any(error)
}
#[cfg(test)]
mod tests {
use std::cell::RefCell;
use std::error::Error;
use std::fmt::{Display, Write};
use std::panic::{AssertUnwindSafe, catch_unwind, panic_any};
use std::rc::Rc;
use super::{
AllocatedBuffer, BufferPool, ITextHandler, MAX_CONSECUTIVE_ZERO_READS,
MAX_UNCONSUMED_BUFFER_SIZE, ParsingLocatorError, StringTextParserReader,
TextParseException, TextParser, TextParserReader, TextParserReaderError,
TextParserRuntimeError, Utf16String, array_copy, array_copy_within, array_unit,
comment_bool_value, comment_parse, count_locator, element_bool_value, element_parse,
panic_payload_to_cause, utf16_string_from_range, util_i32_value,
};
use crate::text::{
AbstractChainedTextHandler, AbstractTextHandler, CommentProcessorTextHandlerRuntimeError,
EventProcessorTextHandler, TextParsingCommentError, TextParsingElementError,
TextParsingUtilError,
};
const JAVA_GOLDEN: &str = include_str!("../../tests/fixtures/text_parser_golden.txt");
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum CloseMode {
None,
Io,
Assertion,
}
#[derive(Debug)]
struct ScriptedReaderState {
input: Vec<u16>,
max_chunk: usize,
zero_reads: i32,
fail_call: i32,
close_mode: CloseMode,
position: usize,
read_calls: i32,
close_count: i32,
requests: Vec<String>,
}
struct ScriptedReader {
state: Rc<RefCell<ScriptedReaderState>>,
}
impl ScriptedReader {
fn new(
input: Utf16String,
max_chunk: usize,
zero_reads: i32,
fail_call: i32,
close_mode: CloseMode,
) -> (Self, Rc<RefCell<ScriptedReaderState>>) {
let state = Rc::new(RefCell::new(ScriptedReaderState {
input: input.as_utf16().to_vec(),
max_chunk,
zero_reads,
fail_call,
close_mode,
position: 0,
read_calls: 0,
close_count: 0,
requests: Vec::new(),
}));
(
Self {
state: Rc::clone(&state),
},
state,
)
}
}
impl TextParserReader for ScriptedReader {
fn read_range(
&mut self,
buffer: &mut [u16],
offset: i32,
len: i32,
) -> Result<i32, TextParserReaderError> {
let mut state = self.state.borrow_mut();
state.requests.push(format!("{offset}:{len}"));
state.read_calls += 1;
if state.fail_call == state.read_calls {
let message = format!("reader-boom-{}", state.read_calls);
return Err(TextParserReaderError::io(&message));
}
if state.zero_reads > 0 {
state.zero_reads -= 1;
return Ok(0);
}
if state.position >= state.input.len() {
return Ok(-1);
}
let copied = (len as usize)
.min(state.max_chunk)
.min(state.input.len() - state.position);
let input_start = state.position;
let input_end = input_start + copied;
buffer[offset as usize..offset as usize + copied]
.copy_from_slice(&state.input[input_start..input_end]);
state.position = input_end;
Ok(copied as i32)
}
fn close(&mut self) -> Result<(), TextParserReaderError> {
let mut state = self.state.borrow_mut();
state.close_count += 1;
match state.close_mode {
CloseMode::None => Ok(()),
CloseMode::Io => Err(TextParserReaderError::io("close-boom")),
CloseMode::Assertion => panic_any("close-error"),
}
}
}
#[derive(Default)]
struct RecordingState {
events: String,
semantic: bool,
fail_event: Option<&'static str>,
runtime_fail_event: Option<&'static str>,
unknown_runtime_fail_event: Option<&'static str>,
}
struct RecordingHandler {
state: Rc<RefCell<RecordingState>>,
}
impl RecordingHandler {
fn new(semantic: bool) -> (Self, Rc<RefCell<RecordingState>>) {
let state = Rc::new(RefCell::new(RecordingState {
semantic,
..RecordingState::default()
}));
(
Self {
state: Rc::clone(&state),
},
state,
)
}
fn record(
&self,
event: &'static str,
buffer: Option<&mut [u16]>,
offset: i32,
len: i32,
arguments: String,
) -> Result<(), Box<TextParseException>> {
let mut state = self.state.borrow_mut();
if !state.events.is_empty() {
state.events.push('|');
}
write!(state.events, "{event}({arguments})@").unwrap();
match buffer.as_deref() {
None => state.events.push_str("null"),
Some(value)
if offset >= 0
&& len >= 0
&& i64::from(offset) + i64::from(len) <= value.len() as i64 =>
{
state.events.push_str(&hex(
&value[offset as usize..offset.wrapping_add(len) as usize]
));
}
Some(_) => write!(state.events, "range({offset},{len})").unwrap(),
}
if state.fail_event == Some(event) {
return Err(Box::new(TextParseException::with_message_at(
Some(&Utf16String::from_rust_str(&format!("checked-{event}"))),
71,
72,
)));
}
if state.runtime_fail_event == Some(event) {
panic_any(TextParserRuntimeError::with_java_metadata(
"java.lang.IllegalStateException",
Some(Utf16String::from_rust_str(&format!("runtime-{event}"))),
));
}
if state.unknown_runtime_fail_event == Some(event) {
panic_any("unknown-handler-panic");
}
Ok(())
}
fn arguments(&self, detailed: String, semantic: String) -> String {
if self.state.borrow().semantic {
semantic
} else {
detailed
}
}
}
impl ITextHandler for RecordingHandler {
fn handle_document_start(
&mut self,
_start_time_nanos: i64,
line: i32,
col: i32,
) -> Result<(), Box<TextParseException>> {
self.record("documentStart", None, 0, 0, format!("{line},{col}"))
}
fn handle_document_end(
&mut self,
_end_time_nanos: i64,
total_time_nanos: i64,
line: i32,
col: i32,
) -> Result<(), Box<TextParseException>> {
self.record(
"documentEnd",
None,
0,
0,
format!("{},{line},{col}", total_time_nanos >= 0),
)
}
fn handle_text(
&mut self,
buffer: Option<&mut [u16]>,
offset: i32,
len: i32,
line: i32,
col: i32,
) -> Result<(), Box<TextParseException>> {
let arguments = self.arguments(
format!("{offset},{len},{line},{col}"),
format!("{line},{col}"),
);
self.record("text", buffer, offset, len, arguments)
}
fn handle_comment(
&mut self,
buffer: Option<&mut [u16]>,
content_offset: i32,
content_len: i32,
outer_offset: i32,
outer_len: i32,
line: i32,
col: i32,
) -> Result<(), Box<TextParseException>> {
let arguments = self.arguments(
format!("{content_offset},{content_len},{outer_offset},{outer_len},{line},{col}"),
format!("{line},{col}"),
);
self.record("comment", buffer, outer_offset, outer_len, arguments)
}
fn handle_standalone_element_start(
&mut self,
buffer: Option<&mut [u16]>,
name_offset: i32,
name_len: i32,
minimized: bool,
line: i32,
col: i32,
) -> Result<(), Box<TextParseException>> {
let arguments = self.arguments(
format!("{name_offset},{name_len},{minimized},{line},{col}"),
format!("{minimized},{line},{col}"),
);
self.record("standaloneStart", buffer, name_offset, name_len, arguments)
}
fn handle_standalone_element_end(
&mut self,
buffer: Option<&mut [u16]>,
name_offset: i32,
name_len: i32,
minimized: bool,
line: i32,
col: i32,
) -> Result<(), Box<TextParseException>> {
let arguments = self.arguments(
format!("{name_offset},{name_len},{minimized},{line},{col}"),
format!("{minimized},{line},{col}"),
);
self.record("standaloneEnd", buffer, name_offset, name_len, arguments)
}
fn handle_open_element_start(
&mut self,
buffer: Option<&mut [u16]>,
name_offset: i32,
name_len: i32,
line: i32,
col: i32,
) -> Result<(), Box<TextParseException>> {
let arguments = self.arguments(
format!("{name_offset},{name_len},{line},{col}"),
format!("{line},{col}"),
);
self.record("openStart", buffer, name_offset, name_len, arguments)
}
fn handle_open_element_end(
&mut self,
buffer: Option<&mut [u16]>,
name_offset: i32,
name_len: i32,
line: i32,
col: i32,
) -> Result<(), Box<TextParseException>> {
let arguments = self.arguments(
format!("{name_offset},{name_len},{line},{col}"),
format!("{line},{col}"),
);
self.record("openEnd", buffer, name_offset, name_len, arguments)
}
fn handle_close_element_start(
&mut self,
buffer: Option<&mut [u16]>,
name_offset: i32,
name_len: i32,
line: i32,
col: i32,
) -> Result<(), Box<TextParseException>> {
let arguments = self.arguments(
format!("{name_offset},{name_len},{line},{col}"),
format!("{line},{col}"),
);
self.record("closeStart", buffer, name_offset, name_len, arguments)
}
fn handle_close_element_end(
&mut self,
buffer: Option<&mut [u16]>,
name_offset: i32,
name_len: i32,
line: i32,
col: i32,
) -> Result<(), Box<TextParseException>> {
let arguments = self.arguments(
format!("{name_offset},{name_len},{line},{col}"),
format!("{line},{col}"),
);
self.record("closeEnd", buffer, name_offset, name_len, arguments)
}
fn handle_attribute(
&mut self,
buffer: Option<&mut [u16]>,
name_offset: i32,
name_len: i32,
name_line: i32,
name_col: i32,
operator_offset: i32,
operator_len: i32,
operator_line: i32,
operator_col: i32,
value_content_offset: i32,
value_content_len: i32,
value_outer_offset: i32,
value_outer_len: i32,
value_line: i32,
value_col: i32,
) -> Result<(), Box<TextParseException>> {
let arguments = self.arguments(
format!(
"{name_offset},{name_len},{name_line},{name_col},{operator_offset},{operator_len},{operator_line},{operator_col},{value_content_offset},{value_content_len},{value_outer_offset},{value_outer_len},{value_line},{value_col}"
),
format!(
"{name_line},{name_col},{operator_line},{operator_col},{value_line},{value_col}"
),
);
self.record("attribute", buffer, name_offset, name_len, arguments)
}
}
fn handler(semantic: bool) -> (Box<dyn ITextHandler>, Rc<RefCell<RecordingState>>) {
let (handler, state) = RecordingHandler::new(semantic);
(Box::new(handler), state)
}
fn scripted_reader(
input: &Utf16String,
max_chunk: usize,
zero_reads: i32,
fail_call: i32,
close_mode: CloseMode,
) -> (Box<dyn TextParserReader>, Rc<RefCell<ScriptedReaderState>>) {
let (reader, state) =
ScriptedReader::new(input.clone(), max_chunk, zero_reads, fail_call, close_mode);
(Box::new(reader), state)
}
fn generate_golden() -> String {
let mut output = String::new();
emit(
&mut output,
"baseline",
"10f9dd2eb8cbd98515ce14b149d115e0287d0add",
);
validation_cases(&mut output);
document_cases(&mut output);
split_matrix_cases(&mut output);
reader_cases(&mut output);
handler_failure_cases(&mut output);
incomplete_cases(&mut output);
buffer_pool_cases(&mut output);
constructor_cases(&mut output);
output
}
fn validation_cases(output: &mut String) {
let parser = TextParser::new(1, 4, true, true);
emit(
output,
"validation.nullDocument",
throwable(|| {
let (handler, _) = handler(false);
parser.parse(None, Some(handler))
}),
);
emit(
output,
"validation.nullStringHandler",
throwable(|| parser.parse(Some(&java("x")), None)),
);
emit(
output,
"validation.nullReader",
throwable(|| {
let (handler, _) = handler(false);
parser.parse_reader(None, Some(handler))
}),
);
emit(
output,
"validation.nullReaderHandler",
throwable(|| {
let (reader, _) = scripted_reader(&java("x"), 1, 0, -1, CloseMode::None);
parser.parse_reader(Some(reader), None)
}),
);
}
fn document_cases(output: &mut String) {
let mut documents = vec![
java(""),
java("plain"),
java("a\nb\r\nc"),
java("[#root]x[/root]"),
java("[#img src=\"hello\" alt='x'/]"),
java("[# one]"),
java("[[#hello]]...[[/hello]]"),
java("/*hello*/"),
java("/*[#hello/]*/tail"),
java("/*[(hello)]*/something;"),
java("a//line\n[#x/]b"),
java("\"[#not]\" [#yes/]"),
java("'a\\'[#not]' [#yes/]"),
java("`[#not]` [#yes/]"),
java("/[#not]/ [#yes/]"),
];
documents.push(Utf16String::from_utf16(vec![
0xd800,
u16::from(b'['),
u16::from(b'#'),
u16::from(b'x'),
u16::from(b'/'),
u16::from(b']'),
0xdc00,
]));
for process_comments in [false, true] {
for standard_dialect in [false, true] {
for (index, document) in documents.iter().enumerate() {
let (handler, state) = handler(false);
let parser = TextParser::new(2, 3, process_comments, standard_dialect);
emit(
output,
&format!(
"document.{process_comments}.{standard_dialect}.{index}.throwable"
),
throwable(|| parser.parse(Some(document), Some(handler))),
);
emit(
output,
&format!("document.{process_comments}.{standard_dialect}.{index}.events"),
state.borrow().events.clone(),
);
}
}
}
}
fn split_matrix_cases(output: &mut String) {
let documents = [
java("before[#root a=\"x]y\"]line\n[#single/][/root]after"),
java("/*[(hello)]*/ [1,\n 2,3] tail;"),
java("a//comment\nb/*[#x/]*/c"),
java("\"quoted\\\\\\\" [#no]\" [#yes/]"),
java("[#template a='zero' b='one']\n\naaaaa\n\n[/template]"),
];
for (index, document) in documents.iter().enumerate() {
for process_comments in [false, true] {
let expected = parse_with_buffer(document, 64, process_comments);
let mut digest = String::new();
for buffer_size in 1..=96 {
let actual = parse_with_buffer(document, buffer_size, process_comments);
assert_eq!(
actual, expected,
"split mismatch document={index}, process={process_comments}, buffer={buffer_size}"
);
write!(
digest,
"{}:{};",
utf16_string_hash(&actual),
actual.encode_utf16().count()
)
.unwrap();
}
emit(
output,
&format!("split.{index}.{process_comments}"),
format!("{expected};matrixHash={}", utf16_string_hash(&digest)),
);
}
}
}
fn parse_with_buffer(
document: &Utf16String,
buffer_size: i32,
process_comments: bool,
) -> String {
let parser = TextParser::new(2, buffer_size, process_comments, true);
let (reader, _) = scripted_reader(document, usize::MAX, 0, -1, CloseMode::None);
let (handler, state) = handler(true);
let mut chain: Box<dyn ITextHandler> =
Box::new(super::EventProcessorTextHandler::new(Some(handler)));
if process_comments {
chain = Box::new(super::CommentProcessorTextHandler::new(true, Some(chain)));
}
match parser.parse_document(Some(reader), buffer_size, Some(chain)) {
Ok(()) => state.borrow().events.clone(),
Err(error) => describe_text_parse(&error),
}
}
fn reader_cases(output: &mut String) {
run_reader(
output,
"reader.chunk1",
&java("[#x/]tail"),
1,
0,
-1,
CloseMode::None,
3,
);
run_reader(
output,
"reader.chunk2",
&java("[#x/]tail"),
2,
0,
-1,
CloseMode::None,
3,
);
run_reader(
output,
"reader.zeroThenData",
&java("[#x/]"),
2,
2,
-1,
CloseMode::None,
3,
);
run_reader(
output,
"reader.readFailure",
&java("[#x/]tail"),
2,
0,
3,
CloseMode::None,
3,
);
run_reader(
output,
"reader.closeIOException",
&java("plain"),
2,
0,
-1,
CloseMode::Io,
3,
);
run_reader(
output,
"reader.closeAssertion",
&java("plain"),
2,
0,
-1,
CloseMode::Assertion,
3,
);
run_reader(
output,
"reader.empty",
&java(""),
2,
0,
-1,
CloseMode::None,
3,
);
}
#[allow(clippy::too_many_arguments)]
fn run_reader(
output: &mut String,
key: &str,
input: &Utf16String,
max_chunk: usize,
zero_reads: i32,
fail_call: i32,
close_mode: CloseMode,
buffer_size: i32,
) {
let parser = TextParser::new(1, buffer_size, false, true);
let (reader, reader_state) =
scripted_reader(input, max_chunk, zero_reads, fail_call, close_mode);
let (handler, handler_state) = handler(false);
emit(
output,
&format!("{key}.throwable"),
throwable(|| parser.parse_document(Some(reader), buffer_size, Some(handler))),
);
emit(
output,
&format!("{key}.events"),
handler_state.borrow().events.clone(),
);
emit(
output,
&format!("{key}.requests"),
reader_state.borrow().requests.join(","),
);
emit(
output,
&format!("{key}.closeCount"),
reader_state.borrow().close_count,
);
}
fn handler_failure_cases(output: &mut String) {
for event in ["documentStart", "text", "openStart", "documentEnd"] {
let parser = TextParser::new(1, 3, false, true);
let (reader, reader_state) =
scripted_reader(&java("[#x]text[/x]"), 2, 0, -1, CloseMode::None);
let (handler, handler_state) = handler(false);
handler_state.borrow_mut().fail_event = Some(event);
emit(
output,
&format!("handler.checked.{event}"),
throwable(|| parser.parse_document(Some(reader), 3, Some(handler))),
);
emit(
output,
&format!("handler.checked.{event}.events"),
handler_state.borrow().events.clone(),
);
emit(
output,
&format!("handler.checked.{event}.closeCount"),
reader_state.borrow().close_count,
);
}
for event in ["documentStart", "text", "standaloneStart", "documentEnd"] {
let document = if event == "standaloneStart" {
java("[#x/]")
} else {
java("text")
};
let parser = TextParser::new(1, 3, false, true);
let (reader, reader_state) = scripted_reader(&document, 2, 0, -1, CloseMode::None);
let (handler, handler_state) = handler(false);
handler_state.borrow_mut().runtime_fail_event = Some(event);
emit(
output,
&format!("handler.runtime.{event}"),
throwable(|| parser.parse_document(Some(reader), 3, Some(handler))),
);
emit(
output,
&format!("handler.runtime.{event}.events"),
handler_state.borrow().events.clone(),
);
emit(
output,
&format!("handler.runtime.{event}.closeCount"),
reader_state.borrow().close_count,
);
}
}
fn incomplete_cases(output: &mut String) {
let documents = [
java("[#open"),
java("[/close"),
java("/*block"),
java("//line"),
java("\"literal"),
java("'literal"),
java("`literal"),
java("/regex"),
];
for process_comments in [false, true] {
for (index, document) in documents.iter().enumerate() {
let parser = TextParser::new(1, 2, process_comments, true);
let (reader, _) = scripted_reader(document, 1, 0, -1, CloseMode::None);
let (handler, state) = handler(false);
emit(
output,
&format!("incomplete.{process_comments}.{index}.throwable"),
throwable(|| parser.parse_document(Some(reader), 2, Some(handler))),
);
emit(
output,
&format!("incomplete.{process_comments}.{index}.events"),
state.borrow().events.clone(),
);
}
}
}
fn buffer_pool_cases(output: &mut String) {
let pool = BufferPool::new(2, 4);
let first = pool.allocate_buffer(4);
let first_pointer = first.buffer.as_ptr();
let second = pool.allocate_buffer(4);
let second_pointer = second.buffer.as_ptr();
let overflow = pool.allocate_buffer(4);
let overflow_pointer = overflow.buffer.as_ptr();
emit(
output,
"pool.distinct",
format!(
"{},{},{}",
first_pointer != second_pointer,
first_pointer != overflow_pointer,
second_pointer != overflow_pointer
),
);
pool.release_buffer(Some(first));
let reused_first = pool.allocate_buffer(4);
emit(
output,
"pool.reusedFirst",
reused_first.buffer.as_ptr() == first_pointer,
);
pool.release_buffer(Some(AllocatedBuffer {
buffer: vec![0; 4],
pool_index: None,
}));
let still_overflow = pool.allocate_buffer(4);
emit(
output,
"pool.foreignIgnored",
still_overflow.buffer.as_ptr() != first_pointer
&& still_overflow.buffer.as_ptr() != second_pointer,
);
pool.release_buffer(Some(second));
let reused_second = pool.allocate_buffer(4);
emit(
output,
"pool.reusedSecond",
reused_second.buffer.as_ptr() == second_pointer,
);
let different_one = pool.allocate_buffer(3);
let different_two = pool.allocate_buffer(3);
emit(
output,
"pool.differentSize",
format!(
"{},{},{}",
different_one.buffer.len(),
different_two.buffer.len(),
different_one.buffer.as_ptr() != different_two.buffer.as_ptr()
),
);
pool.release_buffer(None);
emit(output, "pool.releaseNull", "NO_ERROR");
emit(
output,
"pool.negativeAllocate",
panic_throwable(|| {
let _ = pool.allocate_buffer(-1);
}),
);
emit(
output,
"pool.negativePoolSize",
panic_throwable(|| {
let _ = BufferPool::new(-1, 4);
}),
);
emit(
output,
"pool.negativeBufferSize",
panic_throwable(|| {
let _ = BufferPool::new(1, -1);
}),
);
let zero_negative = BufferPool::new(0, -1);
emit(output, "pool.zeroPoolNegativeBuffer", true);
emit(
output,
"pool.zeroPoolNegativeAllocate",
panic_throwable(|| {
let _ = zero_negative.allocate_buffer(-1);
}),
);
}
fn constructor_cases(output: &mut String) {
emit(
output,
"constructor.negativePool",
panic_throwable(|| {
let _ = TextParser::new(-1, 4, false, true);
}),
);
emit(
output,
"constructor.negativeBuffer",
panic_throwable(|| {
let _ = TextParser::new(1, -1, false, true);
}),
);
emit(
output,
"constructor.zeroPoolNegativeBuffer",
panic_throwable(|| {
let _ = TextParser::new(0, -1, false, true);
}),
);
let parser = TextParser::new(0, 1, false, true);
let (first_handler, first_state) = handler(false);
let (second_handler, second_state) = handler(false);
emit(
output,
"constructor.zeroPool.first",
throwable(|| parser.parse(Some(&java("a")), Some(first_handler))),
);
emit(
output,
"constructor.zeroPool.second",
throwable(|| parser.parse(Some(&java("b")), Some(second_handler))),
);
emit(
output,
"constructor.zeroPool.events",
format!(
"{}|{}",
first_state.borrow().events,
second_state.borrow().events
),
);
}
fn throwable(operation: impl FnOnce() -> Result<(), Box<TextParseException>>) -> String {
throwable_boxed(Box::new(operation))
}
fn throwable_boxed(
operation: Box<dyn FnOnce() -> Result<(), Box<TextParseException>> + '_>,
) -> String {
match catch_unwind(AssertUnwindSafe(operation)) {
Ok(Ok(())) => "NO_ERROR".to_owned(),
Ok(Err(error)) => describe_text_parse(&error),
Err(payload) => describe_panic(payload),
}
}
fn panic_throwable(operation: impl FnOnce()) -> String {
panic_throwable_boxed(Box::new(operation))
}
fn panic_throwable_boxed(operation: Box<dyn FnOnce() + '_>) -> String {
match catch_unwind(AssertUnwindSafe(operation)) {
Ok(()) => "NO_ERROR".to_owned(),
Err(payload) => describe_panic(payload),
}
}
fn describe_text_parse(error: &TextParseException) -> String {
let mut result = format!(
"org.thymeleaf.templateparser.text.TextParseException;message={};line={};col={}",
error
.get_message()
.map_or_else(|| "null".to_owned(), |message| hex(message.as_utf16())),
error
.get_line()
.map_or_else(|| "null".to_owned(), |line| line.to_string()),
error
.get_col()
.map_or_else(|| "null".to_owned(), |col| col.to_string())
);
if let Some(cause) = error.get_cause() {
write!(
result,
";causeClass={};causeMessage={}",
cause.class_name(),
hex(&error
.source()
.expect("TextParseCause always owns its source")
.to_string()
.encode_utf16()
.collect::<Vec<_>>())
)
.unwrap();
}
result
}
fn describe_panic(payload: Box<dyn std::any::Any + Send>) -> String {
match payload.downcast::<TextParserRuntimeError>() {
Ok(error) => format!(
"{};message={}",
error.class_name(),
error
.message()
.map_or_else(|| "null".to_owned(), |message| hex(message.as_utf16()))
),
Err(_) => panic!("unknown panic payload"),
}
}
fn java(value: &str) -> Utf16String {
Utf16String::from_rust_str(value)
}
fn utf16_string_hash(value: &str) -> i32 {
value.encode_utf16().fold(0_i32, |hash, unit| {
hash.wrapping_mul(31).wrapping_add(i32::from(unit))
})
}
fn hex(value: &[u16]) -> String {
value
.iter()
.map(|unit| format!("{unit:04x}"))
.collect::<Vec<_>>()
.join(",")
}
fn emit(output: &mut String, key: &str, value: impl Display) {
writeln!(output, "{key}={value}").unwrap();
}
#[test]
fn golden_matches_streaming_parser_pool_and_failure_semantics() {
assert_eq!(generate_golden(), JAVA_GOLDEN);
}
#[test]
fn repeated_zero_length_reads_are_bounded() {
let parser = TextParser::new(0, 8, false, false);
let (reader, reader_state) = ScriptedReader::new(
java("x"),
1,
MAX_CONSECUTIVE_ZERO_READS as i32 + 1,
-1,
CloseMode::None,
);
let (handler, _) = RecordingHandler::new(false);
let error = parser
.parse_document(Some(Box::new(reader)), 8, Some(Box::new(handler)))
.expect_err("repeated zero-length reads must terminate");
assert!(error.to_string().contains("reader made no progress"));
assert_eq!(reader_state.borrow().close_count, 1);
}
#[test]
fn unconsumed_buffer_growth_has_hard_cap() {
let parser = TextParser::new(0, 8, false, false);
let error = parser
.ensure_buffer_growth_is_safe(MAX_UNCONSUMED_BUFFER_SIZE / 2 + 1, 7, 9)
.expect_err("next growth exceeds configured cap");
assert!(error.to_string().contains("unconsumed structure"));
assert_eq!(error.get_line(), Some(7));
assert_eq!(error.get_col(), Some(9));
}
#[test]
fn buffer_growth_runtime_exception_is_ignored_after_java_int_wrap() {
let parser = TextParser::new(0, 1, false, false);
let mut buffer_size = i32::MAX;
let mut allocated_buffer = Some(AllocatedBuffer {
buffer: vec![u16::from(b'x')],
pool_index: None,
});
parser.grow_buffer(&mut buffer_size, 1, &mut allocated_buffer);
assert_eq!(buffer_size, -2);
assert_eq!(
allocated_buffer
.as_ref()
.expect("old buffer remains active")
.buffer,
vec![u16::from(b'x')]
);
}
#[test]
fn runtime_adapters_preserve_distinct_jvm_failure_contracts() {
let error = TextParserReaderError::new("example.ReaderError", None);
assert_eq!(error.class_name(), "example.ReaderError");
assert_eq!(error.message(), None);
assert_eq!(error.to_string(), "null");
let io_error = TextParserReaderError::io("reader-message");
assert_eq!(io_error.class_name(), "java.io.IOException");
assert_eq!(
io_error
.message()
.expect("IOException has a message")
.to_string_lossy(),
"reader-message"
);
let document = java("xy");
let mut reader = StringTextParserReader::new(&document);
let mut destination = [0_u16; 2];
assert_eq!(reader.read_range(&mut destination, 0, 0), Ok(0));
let cases = [
(
TextParserRuntimeError::array_index(-1, 2),
"java.lang.ArrayIndexOutOfBoundsException",
"Index -1 out of bounds for length 2",
),
(
TextParserRuntimeError::string_range(-1, 1, 2),
"java.lang.StringIndexOutOfBoundsException",
"Range [-1, -1 + 1) out of bounds for length 2",
),
(
TextParserRuntimeError::arraycopy_negative_length(-1),
"java.lang.ArrayIndexOutOfBoundsException",
"arraycopy: length -1 is negative",
),
(
TextParserRuntimeError::arraycopy_source_index(-1, 2),
"java.lang.ArrayIndexOutOfBoundsException",
"arraycopy: source index -1 out of bounds for char[2]",
),
(
TextParserRuntimeError::arraycopy_destination_index(-1, 2),
"java.lang.ArrayIndexOutOfBoundsException",
"arraycopy: destination index -1 out of bounds for char[2]",
),
(
TextParserRuntimeError::arraycopy_last_source(3, 2),
"java.lang.ArrayIndexOutOfBoundsException",
"arraycopy: last source index 3 out of bounds for char[2]",
),
(
TextParserRuntimeError::arraycopy_last_destination(3, 2),
"java.lang.ArrayIndexOutOfBoundsException",
"arraycopy: last destination index 3 out of bounds for char[2]",
),
];
for (error, expected_class, expected_message) in cases {
assert_eq!(error.class_name(), expected_class);
assert_eq!(error.to_string(), expected_message);
}
assert_runtime_error(|| {
let _ = array_unit(&[1], -1);
});
assert_runtime_error(|| {
let _ = utf16_string_from_range(&[1], -1, 1);
});
assert_runtime_error(|| {
let _ = utf16_string_from_range(&[1], 0, 2);
});
let mut destination = [0_u16; 2];
assert_runtime_error(|| array_copy(&[1, 2], 0, &mut destination, 0, -1));
assert_runtime_error(|| array_copy(&[1, 2], -1, &mut destination, 0, 1));
assert_runtime_error(|| array_copy(&[1, 2], 0, &mut destination, -1, 1));
assert_runtime_error(|| array_copy(&[1, 2], 1, &mut destination, 0, 2));
assert_runtime_error(|| array_copy(&[1, 2], 0, &mut destination, 1, 2));
array_copy(&[1, 2], 0, &mut destination, 0, 2);
assert_eq!(destination, [1, 2]);
assert_runtime_error(|| array_copy_within(&mut [1, 2], 0, 0, -1));
assert_runtime_error(|| array_copy_within(&mut [1, 2], -1, 0, 1));
assert_runtime_error(|| array_copy_within(&mut [1, 2], 0, -1, 1));
assert_runtime_error(|| array_copy_within(&mut [1, 2], 1, 0, 2));
assert_runtime_error(|| array_copy_within(&mut [1, 2], 0, 1, 2));
let mut overlapping = [1, 2, 3];
array_copy_within(&mut overlapping, 0, 1, 2);
assert_eq!(overlapping, [1, 1, 2]);
}
#[test]
fn internal_defaults_null_formatting_and_poison_recovery_are_deterministic() {
let runtime = TextParserRuntimeError::with_java_metadata("example.Runtime", None);
assert_eq!(runtime.to_string(), "null");
assert_eq!(
describe_panic(Box::new(runtime)),
"example.Runtime;message=null"
);
assert_eq!(
describe_text_parse(&TextParseException::new()),
"org.thymeleaf.templateparser.text.TextParseException;message=null;line=null;col=null"
);
let mut reader = StringTextParserReader::new(&java("xy"));
let mut buffer = [0_u16; 2];
assert_eq!(reader.read_buffer(&mut buffer), Ok(2));
assert_eq!(buffer, [u16::from(b'x'), u16::from(b'y')]);
assert_eq!(reader.close(), Ok(()));
let pool = BufferPool::new(1, 2);
let poison = catch_unwind(AssertUnwindSafe(|| {
let _guard = pool.state.lock().expect("fresh mutex");
panic!("poison buffer pool");
}));
assert!(poison.is_err());
let allocated = pool.allocate_buffer(2);
assert_eq!(allocated.pool_index, Some(0));
pool.release_buffer(Some(allocated));
assert!(
pool.state
.lock()
.unwrap_or_else(|error| error.into_inner())
.buffers[0]
.is_some()
);
}
#[test]
fn parse_document_normalizes_known_causes_and_cleans_up_unknown_panics() {
let parser = TextParser::new(1, 2, false, false);
let (reader, reader_state) = ScriptedReader::new(java("x"), 1, 0, 1, CloseMode::None);
let (handler, _) = RecordingHandler::new(false);
let initial_read = parser
.parse_document(Some(Box::new(reader)), 2, Some(Box::new(handler)))
.expect_err("initial Reader failure is preserved");
assert_eq!(
initial_read.get_cause().expect("reader cause").class_name(),
"java.io.IOException"
);
assert_eq!(reader_state.borrow().close_count, 1);
let (reader, reader_state) = ScriptedReader::new(java("plain"), 5, 0, -1, CloseMode::None);
let (handler, handler_state) = RecordingHandler::new(false);
handler_state.borrow_mut().fail_event = Some("text");
let final_text = parser
.parse_document(Some(Box::new(reader)), 5, Some(Box::new(handler)))
.expect_err("terminal text checked failure is preserved");
assert_eq!(final_text.get_line(), Some(71));
assert_eq!(final_text.get_col(), Some(72));
assert_eq!(reader_state.borrow().close_count, 1);
let (handler, _) = RecordingHandler::new(false);
let null_reader = parser
.parse_document(None, 2, Some(Box::new(handler)))
.expect_err("null reader is wrapped");
assert_eq!(
null_reader.get_cause().expect("cause").class_name(),
"java.lang.NullPointerException"
);
let (reader, reader_state) = ScriptedReader::new(java("x"), 1, 0, -1, CloseMode::None);
let null_handler = parser
.parse_document(Some(Box::new(reader)), 2, None)
.expect_err("null handler is wrapped");
assert_eq!(
null_handler.get_cause().expect("cause").class_name(),
"java.lang.NullPointerException"
);
assert_eq!(reader_state.borrow().close_count, 1);
let (reader, reader_state) = ScriptedReader::new(java("x"), 1, 0, -1, CloseMode::None);
let (handler, handler_state) = RecordingHandler::new(false);
handler_state.borrow_mut().unknown_runtime_fail_event = Some("documentStart");
let payload = catch_unwind(AssertUnwindSafe(|| {
let _ = parser.parse_document(Some(Box::new(reader)), 2, Some(Box::new(handler)));
}))
.expect_err("unknown panic must resume");
assert_eq!(
payload
.downcast_ref::<&'static str>()
.copied()
.expect("original payload"),
"unknown-handler-panic"
);
assert_eq!(reader_state.borrow().close_count, 1);
}
#[test]
fn panic_taxonomy_preserves_every_java_runtime_category() {
let variants: Vec<Box<dyn std::any::Any + Send>> = vec![
Box::new(TextParserRuntimeError::negative_array_size(-1)),
Box::new(TextParsingUtilError::NullText),
Box::new(TextParsingElementError::NullArrayLoad),
Box::new(TextParsingCommentError::NullArrayLoad),
Box::new(ParsingLocatorError::NullLocator),
Box::new(CommentProcessorTextHandlerRuntimeError::NullCharArrayLoad),
];
for variant in variants {
let cause = panic_payload_to_cause(variant).expect("known Java exception");
assert!(cause.class_name().starts_with("java."));
}
let event_payload = catch_unwind(AssertUnwindSafe(|| {
let mut handler =
EventProcessorTextHandler::new(Some(Box::new(AbstractTextHandler::new())));
let _ = handler.handle_open_element_start(None, 0, 1, 1, 1);
}))
.expect_err("null text reaches EventProcessor runtime adapter");
assert_eq!(
panic_payload_to_cause(event_payload)
.expect("event runtime adapter")
.class_name(),
"java.lang.IllegalArgumentException"
);
let chained_payload = catch_unwind(AssertUnwindSafe(|| {
let mut handler = AbstractChainedTextHandler::new(None);
let _ = handler.handle_text(None, 0, 0, 1, 1);
}))
.expect_err("null next reaches chained runtime adapter");
assert_eq!(
panic_payload_to_cause(chained_payload)
.expect("chained runtime adapter")
.class_name(),
"java.lang.NullPointerException"
);
assert!(panic_payload_to_cause(Box::new("rust-error")).is_err());
assert_runtime_payload::<&'static str>(|| {
let _ = describe_panic(Box::new("unknown"));
});
}
#[test]
fn parser_helpers_keep_checked_and_runtime_channels_separate() {
assert_eq!(util_i32_value(Ok(7)), 7);
assert!(element_bool_value(Ok(true)));
assert!(comment_bool_value(Ok(true)));
assert_runtime_payload::<TextParsingUtilError>(|| {
let _ = util_i32_value(Err(TextParsingUtilError::NullText));
});
assert_runtime_payload::<TextParsingElementError>(|| {
let _ = element_bool_value(Err(TextParsingElementError::NullArrayLoad));
});
assert_runtime_payload::<TextParsingCommentError>(|| {
let _ = comment_bool_value(Err(TextParsingCommentError::NullArrayLoad));
});
let checked = Box::new(TextParseException::with_message(Some(
Utf16String::from_rust_str("checked"),
)));
assert!(element_parse(Err(TextParsingElementError::TextParse(checked))).is_err());
assert_runtime_payload::<TextParsingElementError>(|| {
let _ = element_parse(Err(TextParsingElementError::NullArrayLoad));
});
let checked = Box::new(TextParseException::with_message(Some(
Utf16String::from_rust_str("checked"),
)));
assert!(comment_parse(Err(TextParsingCommentError::TextParse(checked))).is_err());
assert_runtime_payload::<TextParsingCommentError>(|| {
let _ = comment_parse(Err(TextParsingCommentError::NullArrayLoad));
});
assert!(element_parse(Ok(())).is_ok());
assert!(comment_parse(Ok(())).is_ok());
assert_runtime_payload::<ParsingLocatorError>(|| count_locator(&mut [], u16::from(b'x')));
}
#[test]
fn raw_parse_document_emits_each_terminal_structure_event() {
for document in [
"[#x/]", "[#x]", "[/x]", "/*x*/", "//x\n", "a/", "a/b/", "'x'", "\"x\"",
] {
let parser = TextParser::new(0, 64, true, false);
let (reader, _) = ScriptedReader::new(java(document), 64, 0, -1, CloseMode::None);
let (handler, state) = RecordingHandler::new(false);
parser
.parse_document(Some(Box::new(reader)), 64, Some(Box::new(handler)))
.expect("raw structure parses");
assert!(
state.borrow().events.contains("documentEnd"),
"missing terminal event for {document:?}"
);
}
let (handler, _) = RecordingHandler::new(false);
handler
.record("manual", Some(&mut [1]), 2, 1, "invalid".to_owned())
.expect("recording harness accepts invalid ranges");
let mut handler = handler;
handler
.handle_comment(Some(&mut [1, 2, 3, 4]), 1, 1, 0, 4, 1, 1)
.expect("comment callback is recorded");
}
#[test]
fn raw_structure_endpoints_propagate_checked_handler_errors() {
for (document, fail_event) in [
("[#x/]", "standaloneStart"),
("[/x]", "closeStart"),
("/*x*/", "comment"),
("//x\n", "text"),
] {
let parser = TextParser::new(0, 8, true, false);
let (reader, _) = ScriptedReader::new(java(document), 8, 0, -1, CloseMode::None);
let (handler, state) = RecordingHandler::new(false);
state.borrow_mut().fail_event = Some(fail_event);
let error = parser
.parse_document(Some(Box::new(reader)), 8, Some(Box::new(handler)))
.expect_err("handler checked failure propagates");
assert_eq!(error.get_line(), Some(71));
assert_eq!(error.get_col(), Some(72));
}
}
#[test]
fn buffer_growth_resumes_unknown_panic_after_releasing_candidate() {
let parser = TextParser::new(0, 1, false, false);
let mut buffer_size = 1;
let mut allocated_buffer = None;
let payload = catch_unwind(AssertUnwindSafe(|| {
parser.grow_buffer(&mut buffer_size, 1, &mut allocated_buffer);
}))
.expect_err("missing old buffer is an unknown Rust invariant panic");
assert!(payload.downcast_ref::<TextParserRuntimeError>().is_none());
assert_eq!(buffer_size, 2);
assert!(allocated_buffer.is_none());
}
fn assert_runtime_error(operation: impl FnOnce()) {
assert_runtime_payload::<TextParserRuntimeError>(operation);
}
fn assert_runtime_payload<T: std::any::Any + Send>(operation: impl FnOnce()) {
let payload = catch_unwind(AssertUnwindSafe(operation)).expect_err("operation must panic");
assert!(payload.downcast::<T>().is_ok());
}
}