use crate::error::{HedlError, HedlResult};
#[cfg(not(target_arch = "wasm32"))]
use std::time::{Duration, Instant};
#[cfg(target_arch = "wasm32")]
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct Limits {
pub max_file_size: usize,
pub max_line_length: usize,
pub max_indent_depth: usize,
pub max_nodes: usize,
pub max_aliases: usize,
pub max_columns: usize,
pub max_nest_depth: usize,
pub max_block_string_size: usize,
pub max_object_keys: usize,
pub max_total_keys: usize,
pub max_total_ids: usize,
pub timeout: Option<Duration>,
}
impl Default for Limits {
fn default() -> Self {
Self {
max_file_size: 1024 * 1024 * 1024, max_line_length: 1024 * 1024, max_indent_depth: 50,
max_nodes: 10_000_000,
max_aliases: 10_000,
max_columns: 100,
max_nest_depth: 100,
max_block_string_size: 10 * 1024 * 1024, max_object_keys: 10_000,
max_total_keys: 10_000_000, max_total_ids: 10_000_000, timeout: Some(Duration::from_secs(30)), }
}
}
impl Limits {
pub fn unlimited() -> Self {
Self {
max_file_size: usize::MAX,
max_line_length: usize::MAX,
max_indent_depth: usize::MAX,
max_nodes: usize::MAX,
max_aliases: usize::MAX,
max_columns: usize::MAX,
max_nest_depth: usize::MAX,
max_block_string_size: usize::MAX,
max_object_keys: usize::MAX,
max_total_keys: usize::MAX,
max_total_ids: usize::MAX,
timeout: None,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct TimeoutContext {
#[cfg(not(target_arch = "wasm32"))]
start: Instant,
#[cfg(not(target_arch = "wasm32"))]
timeout: Option<Duration>,
}
impl TimeoutContext {
#[cfg(not(target_arch = "wasm32"))]
pub fn new(timeout: Option<Duration>) -> Self {
Self {
start: Instant::now(),
timeout,
}
}
#[cfg(target_arch = "wasm32")]
pub fn new(_timeout: Option<Duration>) -> Self {
Self {}
}
#[cfg(not(target_arch = "wasm32"))]
pub fn check_timeout(&self, line_num: usize) -> HedlResult<()> {
if let Some(timeout) = self.timeout {
let elapsed = self.start.elapsed();
if elapsed > timeout {
return Err(HedlError::security(
format!(
"parsing timeout exceeded: {}ms > {}ms",
elapsed.as_millis(),
timeout.as_millis()
),
line_num,
));
}
}
Ok(())
}
#[cfg(target_arch = "wasm32")]
#[inline(always)]
pub fn check_timeout(&self, _line_num: usize) -> HedlResult<()> {
Ok(())
}
}
pub const DEFAULT_TIMEOUT_CHECK_INTERVAL: usize = 10_000;
pub struct TimeoutCheckIterator<'a, I>
where
I: Iterator,
{
inner: I,
timeout_ctx: &'a TimeoutContext,
check_interval: usize,
iteration_count: usize,
}
impl<'a, I> TimeoutCheckIterator<'a, I>
where
I: Iterator,
{
pub fn new(inner: I, timeout_ctx: &'a TimeoutContext) -> Self {
Self::with_interval(inner, timeout_ctx, DEFAULT_TIMEOUT_CHECK_INTERVAL)
}
pub fn with_interval(inner: I, timeout_ctx: &'a TimeoutContext, check_interval: usize) -> Self {
Self {
inner,
timeout_ctx,
check_interval,
iteration_count: 0,
}
}
}
impl<'a, I> Iterator for TimeoutCheckIterator<'a, I>
where
I: Iterator<Item = (usize, &'a str)>,
{
type Item = Result<(usize, &'a str), HedlError>;
fn next(&mut self) -> Option<Self::Item> {
let item = self.inner.next()?;
let (line_num, _line) = item;
self.iteration_count += 1;
if self.iteration_count % self.check_interval == 0 {
if let Err(e) = self.timeout_ctx.check_timeout(line_num) {
return Some(Err(e));
}
}
Some(Ok(item))
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
pub trait TimeoutCheckExt<'a>: Iterator<Item = (usize, &'a str)> + Sized {
fn with_timeout_check(self, timeout_ctx: &'a TimeoutContext) -> TimeoutCheckIterator<'a, Self> {
TimeoutCheckIterator::new(self, timeout_ctx)
}
}
impl<'a, I> TimeoutCheckExt<'a> for I where I: Iterator<Item = (usize, &'a str)> {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_max_file_size() {
let limits = Limits::default();
assert_eq!(limits.max_file_size, 1024 * 1024 * 1024); }
#[test]
fn test_default_max_line_length() {
let limits = Limits::default();
assert_eq!(limits.max_line_length, 1024 * 1024); }
#[test]
fn test_default_max_indent_depth() {
let limits = Limits::default();
assert_eq!(limits.max_indent_depth, 50);
}
#[test]
fn test_default_max_nodes() {
let limits = Limits::default();
assert_eq!(limits.max_nodes, 10_000_000); }
#[test]
fn test_default_max_aliases() {
let limits = Limits::default();
assert_eq!(limits.max_aliases, 10_000); }
#[test]
fn test_default_max_columns() {
let limits = Limits::default();
assert_eq!(limits.max_columns, 100);
}
#[test]
fn test_unlimited_max_file_size() {
let limits = Limits::unlimited();
assert_eq!(limits.max_file_size, usize::MAX);
}
#[test]
fn test_unlimited_max_line_length() {
let limits = Limits::unlimited();
assert_eq!(limits.max_line_length, usize::MAX);
}
#[test]
fn test_unlimited_max_indent_depth() {
let limits = Limits::unlimited();
assert_eq!(limits.max_indent_depth, usize::MAX);
}
#[test]
fn test_unlimited_max_nodes() {
let limits = Limits::unlimited();
assert_eq!(limits.max_nodes, usize::MAX);
}
#[test]
fn test_unlimited_max_aliases() {
let limits = Limits::unlimited();
assert_eq!(limits.max_aliases, usize::MAX);
}
#[test]
fn test_unlimited_max_columns() {
let limits = Limits::unlimited();
assert_eq!(limits.max_columns, usize::MAX);
}
#[test]
fn test_limits_clone() {
let original = Limits::default();
let cloned = original.clone();
assert_eq!(original.max_file_size, cloned.max_file_size);
assert_eq!(original.max_line_length, cloned.max_line_length);
assert_eq!(original.max_indent_depth, cloned.max_indent_depth);
assert_eq!(original.max_nodes, cloned.max_nodes);
assert_eq!(original.max_aliases, cloned.max_aliases);
assert_eq!(original.max_columns, cloned.max_columns);
}
#[test]
fn test_limits_debug() {
let limits = Limits::default();
let debug = format!("{:?}", limits);
assert!(debug.contains("max_file_size"));
assert!(debug.contains("max_line_length"));
assert!(debug.contains("max_indent_depth"));
assert!(debug.contains("max_nodes"));
assert!(debug.contains("max_aliases"));
assert!(debug.contains("max_columns"));
}
#[test]
fn test_custom_limits() {
let limits = Limits {
max_file_size: 100,
max_line_length: 200,
max_indent_depth: 5,
max_nodes: 1000,
max_aliases: 50,
max_columns: 10,
max_nest_depth: 20,
max_block_string_size: 5000,
max_object_keys: 100,
max_total_keys: 500,
max_total_ids: 1000,
timeout: Some(Duration::from_secs(5)),
};
assert_eq!(limits.max_file_size, 100);
assert_eq!(limits.max_line_length, 200);
assert_eq!(limits.max_indent_depth, 5);
assert_eq!(limits.max_nodes, 1000);
assert_eq!(limits.max_aliases, 50);
assert_eq!(limits.max_columns, 10);
assert_eq!(limits.max_nest_depth, 20);
assert_eq!(limits.max_block_string_size, 5000);
assert_eq!(limits.max_object_keys, 100);
assert_eq!(limits.max_total_keys, 500);
assert_eq!(limits.max_total_ids, 1000);
assert_eq!(limits.timeout, Some(Duration::from_secs(5)));
}
#[test]
fn test_limits_zero_values() {
let limits = Limits {
max_file_size: 0,
max_line_length: 0,
max_indent_depth: 0,
max_nodes: 0,
max_aliases: 0,
max_columns: 0,
max_nest_depth: 0,
max_block_string_size: 0,
max_object_keys: 0,
max_total_keys: 0,
max_total_ids: 0,
timeout: Some(Duration::from_secs(0)),
};
assert_eq!(limits.max_file_size, 0);
assert_eq!(limits.max_columns, 0);
assert_eq!(limits.max_nest_depth, 0);
assert_eq!(limits.max_block_string_size, 0);
assert_eq!(limits.max_object_keys, 0);
assert_eq!(limits.max_total_keys, 0);
}
#[test]
fn test_default_max_nest_depth() {
let limits = Limits::default();
assert_eq!(limits.max_nest_depth, 100);
}
#[test]
fn test_default_max_block_string_size() {
let limits = Limits::default();
assert_eq!(limits.max_block_string_size, 10 * 1024 * 1024); }
#[test]
fn test_unlimited_max_nest_depth() {
let limits = Limits::unlimited();
assert_eq!(limits.max_nest_depth, usize::MAX);
}
#[test]
fn test_unlimited_max_block_string_size() {
let limits = Limits::unlimited();
assert_eq!(limits.max_block_string_size, usize::MAX);
}
#[test]
fn test_default_max_total_keys() {
let limits = Limits::default();
assert_eq!(limits.max_total_keys, 10_000_000);
}
#[test]
fn test_unlimited_max_total_keys() {
let limits = Limits::unlimited();
assert_eq!(limits.max_total_keys, usize::MAX);
}
#[test]
fn test_max_total_keys_greater_than_max_object_keys() {
let limits = Limits::default();
assert!(
limits.max_total_keys > limits.max_object_keys,
"max_total_keys ({}) should be greater than max_object_keys ({})",
limits.max_total_keys,
limits.max_object_keys
);
}
#[test]
fn test_default_max_total_ids() {
let limits = Limits::default();
assert_eq!(limits.max_total_ids, 10_000_000);
}
#[test]
fn test_unlimited_max_total_ids() {
let limits = Limits::unlimited();
assert_eq!(limits.max_total_ids, usize::MAX);
}
#[test]
fn test_max_total_ids_matches_max_total_keys() {
let limits = Limits::default();
assert_eq!(
limits.max_total_ids, limits.max_total_keys,
"max_total_ids ({}) should match max_total_keys ({}) for consistency",
limits.max_total_ids, limits.max_total_keys
);
}
#[test]
fn test_default_timeout() {
let limits = Limits::default();
assert_eq!(limits.timeout, Some(Duration::from_secs(30)));
}
#[test]
fn test_unlimited_no_timeout() {
let limits = Limits::unlimited();
assert_eq!(limits.timeout, None);
}
#[test]
fn test_custom_timeout() {
let limits = Limits {
timeout: Some(Duration::from_secs(60)),
..Limits::default()
};
assert_eq!(limits.timeout, Some(Duration::from_secs(60)));
}
#[test]
fn test_disabled_timeout() {
let limits = Limits {
timeout: None,
..Limits::default()
};
assert_eq!(limits.timeout, None);
}
#[test]
fn test_timeout_context_no_timeout() {
let ctx = TimeoutContext::new(None);
assert!(ctx.check_timeout(1).is_ok());
assert!(ctx.check_timeout(1000).is_ok());
}
#[test]
fn test_timeout_context_with_generous_timeout() {
let ctx = TimeoutContext::new(Some(Duration::from_secs(10)));
assert!(ctx.check_timeout(1).is_ok());
}
#[test]
fn test_timeout_context_with_zero_timeout() {
let ctx = TimeoutContext::new(Some(Duration::from_micros(1)));
std::thread::sleep(Duration::from_micros(10));
let result = ctx.check_timeout(42);
assert!(result.is_err());
if let Err(e) = result {
let msg = e.to_string();
assert!(msg.contains("timeout exceeded") || msg.contains("Timeout"));
}
}
#[test]
fn test_timeout_context_error_message() {
let ctx = TimeoutContext::new(Some(Duration::from_nanos(1)));
std::thread::sleep(Duration::from_millis(1));
let result = ctx.check_timeout(123);
assert!(result.is_err());
if let Err(e) = result {
let msg = e.to_string();
assert!(msg.contains("123")); }
}
#[test]
fn test_timeout_iterator_basic() {
let lines = [(1, "line1"), (2, "line2"), (3, "line3")];
let timeout_ctx = TimeoutContext::new(Some(Duration::from_secs(60)));
let mut count = 0;
for result in lines.iter().copied().with_timeout_check(&timeout_ctx) {
let (_line_num, _line) = result.unwrap();
count += 1;
}
assert_eq!(count, 3);
}
#[test]
fn test_timeout_iterator_no_timeout() {
let lines = vec![(1, "a"); 1000];
let timeout_ctx = TimeoutContext::new(Some(Duration::from_secs(60)));
let count = lines
.iter()
.copied()
.with_timeout_check(&timeout_ctx)
.filter_map(Result::ok)
.count();
assert_eq!(count, 1000);
}
#[test]
fn test_timeout_iterator_triggers_timeout() {
let lines: Vec<(usize, &str)> = (1..=100_000).map(|i| (i, "line")).collect();
let timeout_ctx = TimeoutContext::new(Some(Duration::from_micros(1)));
let mut hit_timeout = false;
for result in lines.iter().copied().with_timeout_check(&timeout_ctx) {
if result.is_err() {
hit_timeout = true;
break;
}
}
let _ = hit_timeout; }
#[test]
fn test_timeout_iterator_custom_interval() {
let lines = vec![(1, "a"); 100];
let timeout_ctx = TimeoutContext::new(Some(Duration::from_secs(60)));
let count = TimeoutCheckIterator::with_interval(lines.iter().copied(), &timeout_ctx, 1)
.filter_map(Result::ok)
.count();
assert_eq!(count, 100);
}
#[test]
fn test_timeout_iterator_size_hint() {
let lines = [(1, "a"), (2, "b"), (3, "c")];
let timeout_ctx = TimeoutContext::new(Some(Duration::from_secs(60)));
let iter = lines.iter().copied().with_timeout_check(&timeout_ctx);
let (lower, upper) = iter.size_hint();
assert_eq!(lower, 3);
assert_eq!(upper, Some(3));
}
#[test]
fn test_timeout_iterator_empty() {
let lines: Vec<(usize, &str)> = vec![];
let timeout_ctx = TimeoutContext::new(Some(Duration::from_secs(60)));
let count = lines
.iter()
.copied()
.with_timeout_check(&timeout_ctx)
.filter_map(Result::ok)
.count();
assert_eq!(count, 0);
}
#[test]
fn test_timeout_iterator_single_item() {
let lines = [(1, "line")];
let timeout_ctx = TimeoutContext::new(Some(Duration::from_secs(60)));
let items: Vec<_> = lines
.iter()
.copied()
.with_timeout_check(&timeout_ctx)
.collect();
assert_eq!(items.len(), 1);
assert!(items[0].is_ok());
}
#[test]
fn test_timeout_iterator_no_timeout_configured() {
let lines = vec![(1, "a"); 1000];
let timeout_ctx = TimeoutContext::new(None);
let count = lines
.iter()
.copied()
.with_timeout_check(&timeout_ctx)
.filter_map(Result::ok)
.count();
assert_eq!(count, 1000);
}
#[test]
fn test_default_timeout_check_interval() {
assert_eq!(DEFAULT_TIMEOUT_CHECK_INTERVAL, 10_000);
}
#[test]
fn test_timeout_check_interval_performance_characteristic() {
let interval = DEFAULT_TIMEOUT_CHECK_INTERVAL;
assert!(
interval >= 1000,
"Check interval too small, may impact performance"
);
assert!(
interval <= 100_000,
"Check interval too large, slow timeout detection"
);
}
}