Accept a list of profiles on the CLI and in the config

This commit is contained in:
2026-10-01 19:56:39 +02:00
parent 1e7e632a4a
commit 8457372a0f
5 changed files with 330 additions and 73 deletions
+96 -45
View File
@@ -11,13 +11,13 @@ use crate::{BindSpec, EnvEntry, SandboxConfig, SandboxError, SandboxMode};
pub fn build(args: Args, file_config: Option<FileConfig>) -> Result<SandboxConfig, SandboxError> {
let (mut globals, mut profile) = match file_config {
Some(c) => {
let profile = c.resolve_profile(args.profile.as_deref())?;
let profile = c.resolve_profile(&args.profile)?;
(c.options, profile)
}
None => (
Options::default(),
ResolvedProfile {
name: args.profile.clone(),
name: join_profile_names(&args.profile),
options: Options::default(),
},
),
@@ -419,64 +419,89 @@ impl FileConfig {
Ok(config)
}
fn resolve_profile(&self, selected: Option<&str>) -> Result<ResolvedProfile, SandboxError> {
let Some(name) = selected
.map(String::from)
.or_else(|| self.options.profile.clone())
else {
return Ok(ResolvedProfile::default());
fn resolve_profile(&self, selected: &[String]) -> Result<ResolvedProfile, SandboxError> {
let names = if selected.is_empty() {
self.options
.profile
.clone()
.map(|p| p.0)
.unwrap_or_default()
} else {
selected.to_vec()
};
let options = self.resolve_chain(&name)?;
if names.is_empty() {
return Ok(ResolvedProfile::default());
}
let merged = self
.linearize_profiles(&names)?
.into_iter()
.map(|name| self.profiles[&name].clone())
.fold(Options::default(), |base, next| base.merge_with(next));
Ok(ResolvedProfile {
name: Some(name),
options,
name: join_profile_names(&names),
options: merged,
})
}
fn resolve_chain(&self, name: &str) -> Result<Options, SandboxError> {
let chain = self.collect_chain(name)?;
let merged = chain
.into_iter()
.rev()
.fold(Options::default(), |base, child| base.merge_with(child));
Ok(merged)
fn linearize_profiles(&self, names: &[String]) -> Result<Vec<String>, SandboxError> {
let mut walk = ProfileWalk::new(&self.profiles);
for name in names {
walk.visit(name)?;
}
Ok(walk.ordered)
}
}
fn collect_chain(&self, name: &str) -> Result<Vec<Options>, SandboxError> {
let mut visited: Vec<String> = Vec::new();
let mut chain: Vec<Options> = Vec::new();
let mut current = name.to_string();
struct ProfileWalk<'a> {
profiles: &'a HashMap<String, Options>,
path: Vec<String>,
ordered: Vec<String>,
}
loop {
if visited.iter().any(|seen| seen == &current) {
visited.push(current);
return Err(SandboxError::ProfileCycle(visited));
}
let options = self
.profiles
.get(&current)
.cloned()
.ok_or_else(|| SandboxError::ProfileNotFound(current.clone()))?;
visited.push(current);
let parent = options.profile.clone();
chain.push(options);
match parent {
Some(p) => current = p,
None => return Ok(chain),
}
impl<'a> ProfileWalk<'a> {
fn new(profiles: &'a HashMap<String, Options>) -> Self {
Self {
profiles,
path: Vec::new(),
ordered: Vec::new(),
}
}
fn visit(&mut self, name: &str) -> Result<(), SandboxError> {
if self.path.iter().any(|seen| seen == name) {
self.path.push(name.to_string());
return Err(SandboxError::ProfileCycle(self.path.clone()));
}
if self.ordered.iter().any(|done| done == name) {
return Ok(());
}
let options = self
.profiles
.get(name)
.ok_or_else(|| SandboxError::ProfileNotFound(name.to_string()))?;
self.path.push(name.to_string());
for parent in options.profile.iter().flat_map(|p| &p.0) {
self.visit(parent)?;
}
self.path.pop();
self.ordered.push(name.to_string());
Ok(())
}
}
fn join_profile_names(names: &[String]) -> Option<String> {
(!names.is_empty()).then(|| names.join(","))
}
#[derive(Deserialize, Default, Clone)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct Options {
pub profile: Option<String>,
pub profile: Option<ProfileNames>,
pub blacklist: Option<bool>,
pub whitelist: Option<bool>,
pub hardened: Option<bool>,
@@ -567,6 +592,32 @@ fn expand_and_require_absolute(path: &Path) -> Result<PathBuf, SandboxError> {
Ok(expanded)
}
#[derive(Deserialize, Clone, Debug, PartialEq)]
#[serde(from = "OneOrMany")]
pub struct ProfileNames(pub Vec<String>);
#[derive(Deserialize)]
#[serde(untagged)]
enum OneOrMany {
One(String),
Many(Vec<String>),
}
impl From<OneOrMany> for ProfileNames {
fn from(value: OneOrMany) -> Self {
match value {
OneOrMany::One(name) => Self(vec![name]),
OneOrMany::Many(names) => Self(names),
}
}
}
impl From<&str> for ProfileNames {
fn from(name: &str) -> Self {
Self(vec![name.to_string()])
}
}
#[derive(Deserialize, Clone)]
#[serde(untagged)]
pub enum CommandValue {