use std::time::Duration;
use thiserror::Error;
#[derive(Error, Debug, Clone, PartialEq)]
pub enum LimitViolation {
#[error("execution time limit exceeded: {limit:?} (actual: {actual:?})")]
TimeExceeded {
limit: Duration,
actual: Duration,
},
#[error("memory limit exceeded: {limit} bytes (actual: {actual} bytes)")]
MemoryExceeded {
limit: usize,
actual: usize,
},
#[error("instruction limit exceeded: {limit} (actual: {actual})")]
InstructionsExceeded {
limit: u64,
actual: u64,
},
#[error("stack depth limit exceeded: {limit} (actual: {actual})")]
StackDepthExceeded {
limit: usize,
actual: usize,
},
#[error("output size limit exceeded: {limit} bytes (actual: {actual} bytes)")]
OutputSizeExceeded {
limit: usize,
actual: usize,
},
#[error("filesystem operation limit exceeded: {limit} operations")]
FsOpsExceeded {
limit: usize,
},
#[error("network operation limit exceeded: {limit} operations")]
NetOpsExceeded {
limit: usize,
},
}
#[derive(Debug, Clone, PartialEq)]
pub struct Limits {
pub timeout: Option<Duration>,
pub memory_bytes: Option<usize>,
pub max_instructions: Option<u64>,
pub max_stack_depth: Option<usize>,
pub max_output_bytes: Option<usize>,
pub max_fs_ops: Option<usize>,
pub max_net_ops: Option<usize>,
pub max_concurrent_tasks: Option<usize>,
}
impl Default for Limits {
fn default() -> Self {
Self {
timeout: Some(Duration::from_secs(30)),
memory_bytes: Some(64 * 1024 * 1024), max_instructions: Some(10_000_000), max_stack_depth: Some(1000),
max_output_bytes: Some(1024 * 1024), max_fs_ops: Some(100),
max_net_ops: Some(10),
max_concurrent_tasks: Some(16),
}
}
}
impl Limits {
pub fn unlimited() -> Self {
Self {
timeout: None,
memory_bytes: None,
max_instructions: None,
max_stack_depth: None,
max_output_bytes: None,
max_fs_ops: None,
max_net_ops: None,
max_concurrent_tasks: None,
}
}
pub fn strict() -> Self {
Self {
timeout: Some(Duration::from_secs(5)),
memory_bytes: Some(16 * 1024 * 1024), max_instructions: Some(1_000_000), max_stack_depth: Some(100),
max_output_bytes: Some(64 * 1024), max_fs_ops: Some(0), max_net_ops: Some(0), max_concurrent_tasks: Some(4),
}
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub fn with_memory_bytes(mut self, bytes: usize) -> Self {
self.memory_bytes = Some(bytes);
self
}
pub fn with_memory_mb(mut self, mb: usize) -> Self {
self.memory_bytes = Some(mb * 1024 * 1024);
self
}
pub fn with_max_instructions(mut self, count: u64) -> Self {
self.max_instructions = Some(count);
self
}
pub fn with_max_stack_depth(mut self, depth: usize) -> Self {
self.max_stack_depth = Some(depth);
self
}
pub fn with_max_output_bytes(mut self, bytes: usize) -> Self {
self.max_output_bytes = Some(bytes);
self
}
pub fn with_max_fs_ops(mut self, ops: usize) -> Self {
self.max_fs_ops = Some(ops);
self
}
pub fn with_max_net_ops(mut self, ops: usize) -> Self {
self.max_net_ops = Some(ops);
self
}
pub fn with_max_concurrent_tasks(mut self, tasks: usize) -> Self {
self.max_concurrent_tasks = Some(tasks);
self
}
pub fn no_timeout(mut self) -> Self {
self.timeout = None;
self
}
pub fn check_time(&self, elapsed: Duration) -> Result<(), LimitViolation> {
if let Some(limit) = self.timeout {
if elapsed > limit {
return Err(LimitViolation::TimeExceeded {
limit,
actual: elapsed,
});
}
}
Ok(())
}
pub fn check_memory(&self, used: usize) -> Result<(), LimitViolation> {
if let Some(limit) = self.memory_bytes {
if used > limit {
return Err(LimitViolation::MemoryExceeded {
limit,
actual: used,
});
}
}
Ok(())
}
pub fn check_instructions(&self, count: u64) -> Result<(), LimitViolation> {
if let Some(limit) = self.max_instructions {
if count > limit {
return Err(LimitViolation::InstructionsExceeded {
limit,
actual: count,
});
}
}
Ok(())
}
pub fn check_stack_depth(&self, depth: usize) -> Result<(), LimitViolation> {
if let Some(limit) = self.max_stack_depth {
if depth > limit {
return Err(LimitViolation::StackDepthExceeded {
limit,
actual: depth,
});
}
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct LimitTracker {
limits: Limits,
start_time: std::time::Instant,
instructions_executed: u64,
memory_used: usize,
current_stack_depth: usize,
output_bytes: usize,
fs_ops: usize,
net_ops: usize,
}
impl LimitTracker {
pub fn new(limits: Limits) -> Self {
Self {
limits,
start_time: std::time::Instant::now(),
instructions_executed: 0,
memory_used: 0,
current_stack_depth: 0,
output_bytes: 0,
fs_ops: 0,
net_ops: 0,
}
}
pub fn reset(&mut self) {
self.start_time = std::time::Instant::now();
self.instructions_executed = 0;
self.memory_used = 0;
self.current_stack_depth = 0;
self.output_bytes = 0;
self.fs_ops = 0;
self.net_ops = 0;
}
pub fn check_timeout(&self) -> Result<(), LimitViolation> {
self.limits.check_time(self.start_time.elapsed())
}
pub fn record_instructions(&mut self, count: u64) -> Result<(), LimitViolation> {
self.instructions_executed += count;
self.limits.check_instructions(self.instructions_executed)
}
pub fn record_memory(&mut self, bytes: usize) -> Result<(), LimitViolation> {
self.memory_used = bytes;
self.limits.check_memory(self.memory_used)
}
pub fn push_stack(&mut self) -> Result<(), LimitViolation> {
self.current_stack_depth += 1;
self.limits.check_stack_depth(self.current_stack_depth)
}
pub fn pop_stack(&mut self) {
self.current_stack_depth = self.current_stack_depth.saturating_sub(1);
}
pub fn record_output(&mut self, bytes: usize) -> Result<(), LimitViolation> {
self.output_bytes += bytes;
if let Some(limit) = self.limits.max_output_bytes {
if self.output_bytes > limit {
return Err(LimitViolation::OutputSizeExceeded {
limit,
actual: self.output_bytes,
});
}
}
Ok(())
}
pub fn record_fs_op(&mut self) -> Result<(), LimitViolation> {
self.fs_ops += 1;
if let Some(limit) = self.limits.max_fs_ops {
if self.fs_ops > limit {
return Err(LimitViolation::FsOpsExceeded { limit });
}
}
Ok(())
}
pub fn record_net_op(&mut self) -> Result<(), LimitViolation> {
self.net_ops += 1;
if let Some(limit) = self.limits.max_net_ops {
if self.net_ops > limit {
return Err(LimitViolation::NetOpsExceeded { limit });
}
}
Ok(())
}
pub fn elapsed(&self) -> Duration {
self.start_time.elapsed()
}
pub fn memory_used(&self) -> usize {
self.memory_used
}
pub fn instructions_executed(&self) -> u64 {
self.instructions_executed
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_limits() {
let limits = Limits::default();
assert!(limits.timeout.is_some());
assert!(limits.memory_bytes.is_some());
assert!(limits.max_instructions.is_some());
}
#[test]
fn test_unlimited() {
let limits = Limits::unlimited();
assert!(limits.timeout.is_none());
assert!(limits.memory_bytes.is_none());
}
#[test]
fn test_strict_limits() {
let limits = Limits::strict();
assert_eq!(limits.max_fs_ops, Some(0));
assert_eq!(limits.max_net_ops, Some(0));
}
#[test]
fn test_builder_pattern() {
let limits = Limits::default()
.with_timeout(Duration::from_secs(10))
.with_memory_mb(32);
assert_eq!(limits.timeout, Some(Duration::from_secs(10)));
assert_eq!(limits.memory_bytes, Some(32 * 1024 * 1024));
}
#[test]
fn test_limit_checks() {
let limits = Limits::default().with_memory_bytes(1000);
assert!(limits.check_memory(500).is_ok());
assert!(limits.check_memory(1500).is_err());
}
#[test]
fn test_limit_tracker() {
let limits = Limits::default().with_max_instructions(100);
let mut tracker = LimitTracker::new(limits);
assert!(tracker.record_instructions(50).is_ok());
assert!(tracker.record_instructions(60).is_err());
}
}