use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum FsAccess {
#[default]
None,
ReadOnly(Vec<PathBuf>),
ReadWrite(Vec<PathBuf>),
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum NetAccess {
#[default]
None,
OutboundHttp(Option<Vec<String>>),
OutboundFull(Option<Vec<String>>),
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum ListenAccess {
#[default]
None,
Ports(Vec<u16>),
PortRange(u16, u16),
Any,
}
impl ListenAccess {
#[must_use]
pub fn allows(&self, port: u16) -> bool {
match self {
Self::None => false,
Self::Ports(ports) => ports.contains(&port),
Self::PortRange(lo, hi) => (*lo..=*hi).contains(&port),
Self::Any => true,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum EnvAccess {
#[default]
None,
AllowList(Vec<String>),
Full,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct Manifold {
pub fs: FsAccess,
pub net: NetAccess,
pub crypto: bool,
pub child_process: bool,
pub env: EnvAccess,
pub allow_exit: bool,
pub http_timeout_ms: Option<u64>,
#[serde(default)]
pub listen: ListenAccess,
}
impl Manifold {
pub const fn sealed() -> Self {
Self {
fs: FsAccess::None,
net: NetAccess::None,
crypto: false,
child_process: false,
env: EnvAccess::None,
allow_exit: false,
http_timeout_ms: None,
listen: ListenAccess::None,
}
}
pub fn open() -> Self {
Self {
fs: FsAccess::ReadWrite(Vec::new()),
net: NetAccess::OutboundFull(None),
crypto: true,
child_process: true,
env: EnvAccess::Full,
allow_exit: true,
http_timeout_ms: None,
listen: ListenAccess::Any,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sealed_is_the_default() {
assert_eq!(Manifold::default(), Manifold::sealed());
}
#[test]
fn sealed_has_no_capabilities() {
let m = Manifold::sealed();
assert!(matches!(m.fs, FsAccess::None));
assert!(matches!(m.net, NetAccess::None));
assert!(!m.crypto);
assert!(!m.child_process);
assert!(matches!(m.env, EnvAccess::None));
assert!(!m.allow_exit);
assert!(matches!(m.listen, ListenAccess::None));
}
#[test]
fn open_grants_everything() {
let m = Manifold::open();
assert!(matches!(m.fs, FsAccess::ReadWrite(_)));
assert!(matches!(m.net, NetAccess::OutboundFull(_)));
assert!(m.crypto);
assert!(m.child_process);
assert!(matches!(m.env, EnvAccess::Full));
assert!(m.allow_exit);
assert!(matches!(m.listen, ListenAccess::Any));
}
#[test]
fn listen_allows_matches_grant_shapes() {
assert!(!ListenAccess::None.allows(80));
let ports = ListenAccess::Ports(vec![8080, 9090]);
assert!(ports.allows(8080));
assert!(ports.allows(9090));
assert!(!ports.allows(8081));
let range = ListenAccess::PortRange(9000, 9100);
assert!(range.allows(9000));
assert!(range.allows(9100));
assert!(!range.allows(8999));
assert!(!range.allows(9101));
assert!(ListenAccess::Any.allows(1));
assert!(ListenAccess::Any.allows(65535));
}
#[test]
fn listen_serde_roundtrips_every_variant() {
for v in [
ListenAccess::None,
ListenAccess::Ports(vec![9090]),
ListenAccess::Ports(vec![80, 443, 8080]),
ListenAccess::PortRange(9000, 9100),
ListenAccess::Any,
] {
let json = serde_json::to_string(&v).expect("serialize");
let back: ListenAccess = serde_json::from_str(&json).expect("deserialize");
assert_eq!(v, back, "roundtrip drift for {json}");
}
}
#[test]
fn listen_serde_is_externally_tagged_like_the_other_axes() {
assert_eq!(
serde_json::to_string(&ListenAccess::None).expect("ser"),
r#""None""#
);
assert_eq!(
serde_json::to_string(&ListenAccess::Ports(vec![9090])).expect("ser"),
r#"{"Ports":[9090]}"#
);
assert_eq!(
serde_json::to_string(&ListenAccess::PortRange(9000, 9100)).expect("ser"),
r#"{"PortRange":[9000,9100]}"#
);
assert_eq!(
serde_json::to_string(&ListenAccess::Any).expect("ser"),
r#""Any""#
);
}
#[test]
fn manifold_without_listen_field_parses_as_none() {
let legacy = r#"{
"fs": "None",
"net": "None",
"crypto": false,
"child_process": false,
"env": "None",
"allow_exit": false,
"http_timeout_ms": null
}"#;
let m: Manifold = serde_json::from_str(legacy).expect("legacy manifold parses");
assert_eq!(m, Manifold::sealed());
assert!(matches!(m.listen, ListenAccess::None));
}
#[test]
fn manifold_listen_roundtrips_through_json() {
let mut m = Manifold::sealed();
m.listen = ListenAccess::PortRange(18200, 18210);
let json = serde_json::to_string(&m).expect("serialize");
let back: Manifold = serde_json::from_str(&json).expect("deserialize");
assert_eq!(m, back);
}
}