Accept a list of profiles on the CLI and in the config
This commit is contained in:
+96
-45
@@ -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 == ¤t) {
|
||||
visited.push(current);
|
||||
return Err(SandboxError::ProfileCycle(visited));
|
||||
}
|
||||
|
||||
let options = self
|
||||
.profiles
|
||||
.get(¤t)
|
||||
.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 {
|
||||
|
||||
Reference in New Issue
Block a user