use crate::capability::{AccessMode, CapabilitySet, FsCapability};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SandboxState {
pub fs: Vec<FsCapState>,
pub net_blocked: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FsCapState {
pub original: PathBuf,
pub resolved: PathBuf,
pub access: String,
pub is_file: bool,
}
impl SandboxState {
#[must_use]
pub fn from_caps(caps: &CapabilitySet) -> Self {
Self {
fs: caps
.fs_capabilities()
.iter()
.map(|cap| FsCapState {
original: cap.original.clone(),
resolved: cap.resolved.clone(),
access: cap.access.to_string(),
is_file: cap.is_file,
})
.collect(),
net_blocked: caps.is_network_blocked(),
}
}
pub fn to_caps(&self) -> crate::error::Result<CapabilitySet> {
let mut caps = CapabilitySet::new();
for fs_cap in &self.fs {
let access = match fs_cap.access.as_str() {
"read" => AccessMode::Read,
"write" => AccessMode::Write,
"read+write" => AccessMode::ReadWrite,
other => {
return Err(crate::error::NonoError::ConfigParse(format!(
"invalid access mode in sandbox state: {other}"
)));
}
};
let cap = if fs_cap.is_file {
FsCapability::new_file(&fs_cap.original, access)?
} else {
FsCapability::new_dir(&fs_cap.original, access)?
};
caps.add_fs(cap);
}
caps.set_network_blocked(self.net_blocked);
Ok(caps)
}
pub fn to_json(&self) -> crate::error::Result<String> {
serde_json::to_string_pretty(self).map_err(|e| {
crate::error::NonoError::ConfigParse(format!("Failed to serialize sandbox state: {e}"))
})
}
pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str(json)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_state_roundtrip() {
let caps = CapabilitySet::new().block_network();
let state = SandboxState::from_caps(&caps);
assert!(state.net_blocked);
assert!(state.fs.is_empty());
let json = state.to_json().expect("serialize state");
let restored = SandboxState::from_json(&json).expect("deserialize state");
assert!(restored.net_blocked);
}
#[test]
fn test_to_caps_rejects_nonexistent_path() {
let json = r#"{
"fs": [{
"original": "/nonexistent/crafted/path",
"resolved": "/nonexistent/crafted/path",
"access": "read+write",
"is_file": false
}],
"net_blocked": false
}"#;
let state = SandboxState::from_json(json).unwrap();
assert!(
state.to_caps().is_err(),
"to_caps must reject nonexistent paths"
);
}
#[test]
fn test_to_caps_rejects_invalid_access_mode() {
let json = r#"{
"fs": [{
"original": "/tmp",
"resolved": "/tmp",
"access": "root-access",
"is_file": false
}],
"net_blocked": false
}"#;
let state = SandboxState::from_json(json).unwrap();
assert!(
state.to_caps().is_err(),
"to_caps must reject invalid access modes"
);
}
}