use crate::{EnvAccess, FsAccess, ListenAccess, Manifold, NetAccess};
use std::path::PathBuf;
use super::args::Cli;
pub fn build_manifold(cli: &Cli) -> Manifold {
if cli.allow_all {
return Manifold::open();
}
let any_allow = cli.allow_net.is_some()
|| cli.allow_listen.is_some()
|| cli.allow_fs.is_some()
|| cli.allow_env.is_some()
|| cli.allow_fs_read.is_some()
|| cli.allow_fs_write.is_some()
|| cli.allow_child_process
|| cli.allow_worker
|| cli.allow_crypto
|| cli.permission;
let explicit_sandbox = cli.sandbox || any_allow;
if !explicit_sandbox {
return Manifold::open();
}
let mut m = Manifold::sealed();
if let Some(s) = cli.allow_net.as_deref() {
let hosts = parse_allow_list(s);
m.net = if hosts.is_empty() || has_wildcard(&hosts) {
NetAccess::OutboundFull(None)
} else {
NetAccess::OutboundFull(Some(hosts))
};
}
let fs_paths: Option<Vec<String>> =
if cli.allow_fs.is_some() || cli.allow_fs_read.is_some() || cli.allow_fs_write.is_some() {
let mut combined: Vec<String> = Vec::new();
for src in [
cli.allow_fs.as_deref(),
cli.allow_fs_read.as_deref(),
cli.allow_fs_write.as_deref(),
]
.iter()
.flatten()
{
combined.extend(parse_allow_list(src));
}
let mut seen = std::collections::BTreeSet::new();
combined.retain(|p| seen.insert(p.clone()));
Some(combined)
} else {
None
};
if let Some(paths) = fs_paths {
let roots: Vec<PathBuf> = if paths.is_empty() || has_wildcard(&paths) {
vec![PathBuf::from("/")]
} else {
paths.into_iter().map(PathBuf::from).collect()
};
m.fs = FsAccess::ReadWrite(roots);
}
if let Some(s) = cli.allow_env.as_deref() {
let vars = parse_allow_list(s);
m.env = if vars.is_empty() || has_wildcard(&vars) {
EnvAccess::Full
} else {
EnvAccess::AllowList(vars)
};
}
if let Some(s) = cli.allow_listen.as_deref() {
m.listen = listen_from_arg(s);
}
if cli.allow_crypto {
m.crypto = true;
}
m
}
fn listen_from_arg(s: &str) -> ListenAccess {
let entries = parse_allow_list(s);
if entries.is_empty() || has_wildcard(&entries) {
return ListenAccess::Any;
}
if let [only] = entries.as_slice()
&& let Some((lo, hi)) = parse_port_range(only)
{
return ListenAccess::PortRange(lo, hi);
}
ListenAccess::Ports(
entries
.iter()
.filter_map(|e| e.parse::<u16>().ok())
.collect(),
)
}
fn parse_port_range(entry: &str) -> Option<(u16, u16)> {
let (lo, hi) = entry.split_once('-')?;
Some((lo.trim().parse().ok()?, hi.trim().parse().ok()?))
}
pub fn parse_allow_listen_arg(s: &str) -> Result<String, String> {
let entries = parse_allow_list(s);
if has_wildcard(&entries) {
return Ok(s.to_string());
}
if let [only] = entries.as_slice()
&& only.contains('-')
{
let (lo, hi) = parse_port_range(only).ok_or_else(|| {
format!("invalid --allow-listen range '{only}': expected LO-HI with ports in 1-65535")
})?;
if lo == 0 || lo > hi {
return Err(format!(
"invalid --allow-listen range '{only}': expected 1 <= LO <= HI <= 65535"
));
}
return Ok(s.to_string());
}
for entry in &entries {
match entry.parse::<u16>() {
Ok(0) | Err(_) => {
return Err(format!(
"invalid --allow-listen entry '{entry}': expected a port (1-65535), \
a comma-separated port list, a single LO-HI range, or *"
));
}
Ok(_) => {}
}
}
Ok(s.to_string())
}
pub fn parse_allow_net_arg(s: &str) -> Result<String, String> {
for entry in parse_allow_list(s) {
let port_part = if let Some(v6) = entry.strip_prefix('[') {
match v6.split_once(']') {
Some((_, after)) => after.strip_prefix(':'),
None => {
return Err(format!(
"invalid --allow-net entry '{entry}': unterminated '[' in IPv6 host"
));
}
}
} else if entry.matches(':').count() == 1 {
entry.split_once(':').map(|(_, p)| p)
} else {
None
};
if let Some(p) = port_part
&& p.parse::<u16>().is_err()
{
return Err(format!(
"invalid --allow-net entry '{entry}': '{p}' is not a valid port \
(use host, host:port, *.suffix[:port], or [v6][:port])"
));
}
}
Ok(s.to_string())
}
pub fn parse_allow_list(s: &str) -> Vec<String> {
s.split(',')
.map(str::trim)
.filter(|p| !p.is_empty())
.map(String::from)
.collect()
}
pub fn has_wildcard(list: &[String]) -> bool {
list.iter().any(|s| s == "*")
}
pub fn is_implicit_open(cli: &Cli) -> bool {
if cli.allow_all {
return false;
}
let any_allow = cli.allow_net.is_some()
|| cli.allow_listen.is_some()
|| cli.allow_fs.is_some()
|| cli.allow_env.is_some()
|| cli.allow_fs_read.is_some()
|| cli.allow_fs_write.is_some()
|| cli.allow_child_process
|| cli.allow_worker
|| cli.allow_crypto
|| cli.permission;
!(cli.sandbox || any_allow)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn allow_net_accepts_hosts_and_host_port_entries() {
for ok in [
"api.example.com",
"*.trusted.io",
"*",
"127.0.0.1:9000",
"*.trusted.io:8443",
"a.com,b.com:81, c.com",
"[::1]:9000",
"[::1]",
"::1", ] {
assert!(
parse_allow_net_arg(ok).is_ok(),
"expected '{ok}' to be accepted"
);
}
}
#[test]
fn allow_listen_accepts_ports_ranges_and_wildcard() {
for ok in [
"8080",
"8080,9090",
" 80 , 443 ",
"9000-9100",
"1-65535",
"*",
] {
assert!(
parse_allow_listen_arg(ok).is_ok(),
"expected '{ok}' to be accepted"
);
}
}
#[test]
fn allow_listen_rejects_invalid_entries() {
for bad in [
"0",
"65536",
"http",
"8o80",
"8080,nope",
"9100-9000", "0-100", "80-90,100", "-1",
] {
assert!(
parse_allow_listen_arg(bad).is_err(),
"expected '{bad}' to be rejected"
);
}
}
#[test]
fn allow_net_rejects_invalid_port_suffixes() {
for bad in [
"127.0.0.1:90o0",
"host:",
"host:-1",
"host:65536",
"good.com,host:nope",
"[::1]:abc",
"[::1",
] {
assert!(
parse_allow_net_arg(bad).is_err(),
"expected '{bad}' to be rejected"
);
}
}
}