Skip to main content

shape_runtime/project/
sandbox.rs

1//! Sandbox configuration for shape.toml `[sandbox]`.
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6/// [sandbox] section — isolation settings for deterministic/testing modes.
7#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq)]
8pub struct SandboxSection {
9    /// Whether sandbox mode is enabled.
10    #[serde(default)]
11    pub enabled: bool,
12    /// Use a deterministic runtime (fixed time, seeded RNG).
13    #[serde(default)]
14    pub deterministic: bool,
15    /// RNG seed for deterministic mode.
16    #[serde(default)]
17    pub seed: Option<u64>,
18    /// Memory limit (human-readable, e.g. "64MB").
19    #[serde(default)]
20    pub memory_limit: Option<String>,
21    /// Execution time limit (human-readable, e.g. "10s").
22    #[serde(default)]
23    pub time_limit: Option<String>,
24    /// Use a virtual filesystem instead of real I/O.
25    #[serde(default)]
26    pub virtual_fs: bool,
27    /// Seed files for the virtual filesystem: vfs_path -> real_path.
28    #[serde(default)]
29    pub seed_files: HashMap<String, String>,
30}
31
32impl SandboxSection {
33    /// Parse the memory_limit string (e.g. "64MB") into bytes.
34    pub fn memory_limit_bytes(&self) -> Option<u64> {
35        self.memory_limit.as_ref().and_then(|s| parse_byte_size(s))
36    }
37
38    /// Parse the time_limit string (e.g. "10s") into milliseconds.
39    pub fn time_limit_ms(&self) -> Option<u64> {
40        self.time_limit.as_ref().and_then(|s| parse_duration_ms(s))
41    }
42}
43
44/// Parse a human-readable byte size like "64MB", "1GB", "512KB".
45pub(crate) fn parse_byte_size(s: &str) -> Option<u64> {
46    let s = s.trim();
47    let (num_part, suffix) = split_numeric_suffix(s)?;
48    let value: u64 = num_part.parse().ok()?;
49    let multiplier = match suffix.to_uppercase().as_str() {
50        "B" | "" => 1,
51        "KB" | "K" => 1024,
52        "MB" | "M" => 1024 * 1024,
53        "GB" | "G" => 1024 * 1024 * 1024,
54        _ => return None,
55    };
56    Some(value * multiplier)
57}
58
59/// Parse a human-readable duration like "10s", "500ms", "2m".
60pub(crate) fn parse_duration_ms(s: &str) -> Option<u64> {
61    let s = s.trim();
62    let (num_part, suffix) = split_numeric_suffix(s)?;
63    let value: u64 = num_part.parse().ok()?;
64    let multiplier = match suffix.to_lowercase().as_str() {
65        "ms" => 1,
66        "s" | "" => 1000,
67        "m" | "min" => 60_000,
68        _ => return None,
69    };
70    Some(value * multiplier)
71}
72
73/// Split "64MB" into ("64", "MB").
74fn split_numeric_suffix(s: &str) -> Option<(&str, &str)> {
75    let idx = s
76        .find(|c: char| !c.is_ascii_digit() && c != '.')
77        .unwrap_or(s.len());
78    if idx == 0 {
79        return None;
80    }
81    Some((&s[..idx], &s[idx..]))
82}