const WASM_PAGE_SIZE_BYTES: u64 = 65536;
const BYTES_PER_MB: u64 = 1024 * 1024;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResourceLimits {
pub max_memory_pages: Option<u32>,
pub max_fuel: Option<u64>,
pub max_output_bytes: Option<usize>,
pub max_file_size: Option<u64>,
pub max_disk_bytes: Option<u64>,
}
impl Default for ResourceLimits {
fn default() -> Self {
ResourceLimits {
max_memory_pages: Some(mb_to_pages(256)), max_fuel: None,
max_output_bytes: Some((10 * BYTES_PER_MB) as usize), max_file_size: Some(50 * BYTES_PER_MB), max_disk_bytes: Some(100 * BYTES_PER_MB), }
}
}
impl ResourceLimits {
pub fn from_cli(
max_memory_mb: u32,
max_fuel: u64,
max_output_mb: u32,
max_file_size_mb: u32,
max_disk_mb: u32,
) -> Self {
ResourceLimits {
max_memory_pages: zero_is_none_u32(max_memory_mb).map(mb_to_pages),
max_fuel: zero_is_none_u64(max_fuel),
max_output_bytes: zero_is_none_u32(max_output_mb).map(|mb| mb_to_bytes(mb) as usize),
max_file_size: zero_is_none_u32(max_file_size_mb).map(mb_to_bytes),
max_disk_bytes: zero_is_none_u32(max_disk_mb).map(mb_to_bytes),
}
}
pub fn with_overrides(&self, overrides: &LimitsOverride) -> Self {
let mut merged = self.clone();
if let Some(mb) = overrides.max_memory_mb {
merged.max_memory_pages = zero_is_none_u32(mb).map(mb_to_pages);
}
if let Some(fuel) = overrides.max_fuel {
merged.max_fuel = zero_is_none_u64(fuel);
}
if let Some(mb) = overrides.max_output_mb {
merged.max_output_bytes = zero_is_none_u32(mb).map(|mb| mb_to_bytes(mb) as usize);
}
if let Some(mb) = overrides.max_file_size_mb {
merged.max_file_size = zero_is_none_u32(mb).map(mb_to_bytes);
}
if let Some(mb) = overrides.max_disk_mb {
merged.max_disk_bytes = zero_is_none_u32(mb).map(mb_to_bytes);
}
merged
}
pub fn clamp_to(&self, ceiling: &ResourceLimits) -> Self {
ResourceLimits {
max_memory_pages: clamp_opt(self.max_memory_pages, ceiling.max_memory_pages),
max_fuel: clamp_opt(self.max_fuel, ceiling.max_fuel),
max_output_bytes: clamp_opt(self.max_output_bytes, ceiling.max_output_bytes),
max_file_size: clamp_opt(self.max_file_size, ceiling.max_file_size),
max_disk_bytes: clamp_opt(self.max_disk_bytes, ceiling.max_disk_bytes),
}
}
pub fn check_write(
&self,
new_len: u64,
existing_len: u64,
current_disk_usage: u64,
) -> Result<(), String> {
if let Some(max) = self.max_file_size {
if new_len > max {
return Err(format!(
"File size limit exceeded: {new_len} bytes > {max} byte limit"
));
}
}
if let Some(max) = self.max_disk_bytes {
let projected = current_disk_usage.saturating_sub(existing_len) + new_len;
if projected > max {
return Err(format!(
"Disk usage limit exceeded: {projected} bytes > {max} byte limit"
));
}
}
Ok(())
}
}
#[derive(Debug, Clone, Default, serde::Deserialize)]
pub struct LimitsOverride {
pub max_memory_mb: Option<u32>,
pub max_fuel: Option<u64>,
pub max_output_mb: Option<u32>,
pub max_file_size_mb: Option<u32>,
pub max_disk_mb: Option<u32>,
}
pub fn dir_size(dir: &std::path::Path) -> u64 {
let mut total = 0u64;
let Ok(entries) = std::fs::read_dir(dir) else {
return 0;
};
for entry in entries.flatten() {
let Ok(meta) = entry.metadata() else {
continue;
};
if meta.is_dir() {
total += dir_size(&entry.path());
} else if meta.is_file() {
total += meta.len();
}
}
total
}
fn clamp_opt<T: Ord + Copy>(val: Option<T>, ceiling: Option<T>) -> Option<T> {
match (val, ceiling) {
(_, None) => val,
(None, Some(c)) => Some(c),
(Some(v), Some(c)) => Some(v.min(c)),
}
}
fn mb_to_pages(mb: u32) -> u32 {
((mb as u64 * BYTES_PER_MB) / WASM_PAGE_SIZE_BYTES) as u32
}
fn mb_to_bytes(mb: u32) -> u64 {
mb as u64 * BYTES_PER_MB
}
fn zero_is_none_u32(v: u32) -> Option<u32> {
if v == 0 {
None
} else {
Some(v)
}
}
fn zero_is_none_u64(v: u64) -> Option<u64> {
if v == 0 {
None
} else {
Some(v)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mb_to_pages() {
assert_eq!(mb_to_pages(1), 16);
assert_eq!(mb_to_pages(256), 4096);
}
#[test]
fn test_from_cli_units() {
let l = ResourceLimits::from_cli(256, 1_000_000, 10, 50, 100);
assert_eq!(l.max_memory_pages, Some(4096));
assert_eq!(l.max_fuel, Some(1_000_000));
assert_eq!(l.max_output_bytes, Some(10 * 1024 * 1024));
assert_eq!(l.max_file_size, Some(50 * 1024 * 1024));
assert_eq!(l.max_disk_bytes, Some(100 * 1024 * 1024));
}
#[test]
fn test_from_cli_zero_means_unlimited() {
let l = ResourceLimits::from_cli(0, 0, 0, 0, 0);
assert_eq!(l.max_memory_pages, None);
assert_eq!(l.max_fuel, None);
assert_eq!(l.max_output_bytes, None);
assert_eq!(l.max_file_size, None);
assert_eq!(l.max_disk_bytes, None);
}
#[test]
fn test_default_has_no_fuel_cap() {
assert_eq!(ResourceLimits::default().max_fuel, None);
assert!(ResourceLimits::default().max_memory_pages.is_some());
}
#[test]
fn test_with_overrides_partial() {
let base = ResourceLimits::from_cli(256, 0, 10, 50, 100);
let ov = LimitsOverride {
max_fuel: Some(5000),
max_file_size_mb: Some(1),
..Default::default()
};
let merged = base.with_overrides(&ov);
assert_eq!(merged.max_fuel, Some(5000));
assert_eq!(merged.max_file_size, Some(1024 * 1024));
assert_eq!(merged.max_memory_pages, base.max_memory_pages);
assert_eq!(merged.max_output_bytes, base.max_output_bytes);
}
#[test]
fn test_with_overrides_zero_disables() {
let base = ResourceLimits::from_cli(256, 1000, 10, 50, 100);
let ov = LimitsOverride {
max_fuel: Some(0),
..Default::default()
};
let merged = base.with_overrides(&ov);
assert_eq!(merged.max_fuel, None);
}
#[test]
fn test_clamp_to_tightens_only() {
let ceiling = ResourceLimits {
max_memory_pages: Some(100),
max_fuel: Some(1000),
max_output_bytes: Some(50),
max_file_size: Some(500),
max_disk_bytes: Some(5000),
};
let below = ResourceLimits {
max_memory_pages: Some(10),
max_fuel: Some(100),
max_output_bytes: Some(5),
max_file_size: Some(50),
max_disk_bytes: Some(500),
};
assert_eq!(below.clamp_to(&ceiling), below);
let above = ResourceLimits {
max_memory_pages: Some(999),
max_fuel: Some(99999),
max_output_bytes: Some(999),
max_file_size: Some(99999),
max_disk_bytes: Some(99999),
};
assert_eq!(above.clamp_to(&ceiling), ceiling);
}
#[test]
fn test_clamp_to_none_semantics() {
let ceiling = ResourceLimits {
max_memory_pages: Some(64),
max_fuel: Some(10),
max_output_bytes: Some(1),
max_file_size: Some(2),
max_disk_bytes: Some(3),
};
let unlimited = ResourceLimits {
max_memory_pages: None,
max_fuel: None,
max_output_bytes: None,
max_file_size: None,
max_disk_bytes: None,
};
assert_eq!(unlimited.clamp_to(&ceiling), ceiling);
let val = ResourceLimits {
max_memory_pages: Some(7),
max_fuel: None,
max_output_bytes: Some(9),
max_file_size: None,
max_disk_bytes: Some(11),
};
assert_eq!(val.clamp_to(&unlimited), val);
}
#[test]
fn test_check_write_file_size() {
let l = ResourceLimits {
max_file_size: Some(100),
max_disk_bytes: None,
..ResourceLimits::default()
};
assert!(l.check_write(100, 0, 0).is_ok());
let err = l.check_write(101, 0, 0).unwrap_err();
assert!(err.contains("File size limit"));
}
#[test]
fn test_check_write_disk_usage() {
let l = ResourceLimits {
max_file_size: None,
max_disk_bytes: Some(1000),
..ResourceLimits::default()
};
assert!(l.check_write(100, 0, 900).is_ok());
let err = l.check_write(101, 0, 900).unwrap_err();
assert!(err.contains("Disk usage limit"));
}
#[test]
fn test_check_write_disk_counts_replacement() {
let l = ResourceLimits {
max_file_size: None,
max_disk_bytes: Some(1000),
..ResourceLimits::default()
};
assert!(l.check_write(600, 500, 900).is_ok());
}
#[test]
fn test_check_write_unlimited() {
let l = ResourceLimits {
max_file_size: None,
max_disk_bytes: None,
..ResourceLimits::default()
};
assert!(l.check_write(u64::MAX, 0, u64::MAX).is_ok());
}
#[test]
fn test_dir_size_missing_is_zero() {
let p = std::path::Path::new("/nonexistent/wasmrun/path/xyz");
assert_eq!(dir_size(p), 0);
}
#[test]
fn test_dir_size_sums_files() {
let tmp = std::env::temp_dir().join(format!("wasmrun_dirsize_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&tmp);
std::fs::create_dir_all(tmp.join("sub")).unwrap();
std::fs::write(tmp.join("a.txt"), b"hello").unwrap(); std::fs::write(tmp.join("sub/b.txt"), b"world!").unwrap(); assert_eq!(dir_size(&tmp), 11);
let _ = std::fs::remove_dir_all(&tmp);
}
}