Accept SRC:DST remap syntax in --ro/--rw

This commit is contained in:
2026-04-22 21:51:00 +02:00
parent 06bb638737
commit 305ac9d927
7 changed files with 250 additions and 59 deletions
+190 -40
View File
@@ -5,7 +5,7 @@ use std::path::{Path, PathBuf};
use serde::Deserialize;
use crate::cli::Args;
use crate::{EnvEntry, SandboxConfig, SandboxError, SandboxMode};
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 {
@@ -64,8 +64,8 @@ pub fn build(args: Args, file_config: Option<FileConfig>) -> Result<SandboxConfi
globals.dry_run,
),
chdir: resolve_chdir(args.chdir, profile.chdir, globals.chdir)?,
extra_rw: merge_paths(args.extra_rw, &profile.rw, &globals.rw)?,
extra_ro: merge_paths(args.extra_ro, &profile.ro, &globals.ro)?,
extra_rw: merge_bind_specs(args.extra_rw, &profile.rw, &globals.rw)?,
extra_ro: merge_bind_specs(args.extra_ro, &profile.ro, &globals.ro)?,
mask: merge_vecs(args.mask, &profile.mask, &globals.mask),
env,
unsetenv,
@@ -138,22 +138,55 @@ fn resolve_chdir(
}
}
fn merge_paths(
cli: Vec<PathBuf>,
profile: &[PathBuf],
globals: &[PathBuf],
) -> Result<Vec<PathBuf>, SandboxError> {
let config_paths = globals.iter().chain(profile).cloned();
let cli_paths = cli
.into_iter()
.map(prepare_cli_bind_path)
.collect::<Result<Vec<_>, _>>()?;
Ok(config_paths.chain(cli_paths).collect())
fn merge_bind_specs(
cli: Vec<String>,
profile: &[String],
globals: &[String],
) -> Result<Vec<BindSpec>, SandboxError> {
let config_specs = globals
.iter()
.chain(profile)
.map(|raw| resolve_config_bind_spec(raw));
let cli_specs = cli.iter().map(|raw| resolve_cli_bind_spec(raw));
config_specs.chain(cli_specs).collect()
}
fn prepare_cli_bind_path(path: PathBuf) -> Result<PathBuf, SandboxError> {
fn resolve_cli_bind_spec(raw: &str) -> Result<BindSpec, SandboxError> {
let (src, dst) = split_bind_spec(raw)?;
let source = prepare_cli_bind_path(Path::new(src))?;
let target = match dst {
Some(d) => {
let p = PathBuf::from(d);
require_absolute(&p)?;
p
}
None => source.clone(),
};
Ok(BindSpec { source, target })
}
fn resolve_config_bind_spec(raw: &str) -> Result<BindSpec, SandboxError> {
let (src, dst) = split_bind_spec(raw)?;
let source = expand_and_require_existing(Path::new(src))?;
let target = match dst {
Some(d) => expand_and_require_absolute(Path::new(d))?,
None => source.clone(),
};
Ok(BindSpec { source, target })
}
fn split_bind_spec(raw: &str) -> Result<(&str, Option<&str>), SandboxError> {
match raw.split_once(':') {
Some((src, dst)) if !src.is_empty() && !dst.is_empty() => Ok((src, Some(dst))),
Some(_) => Err(SandboxError::InvalidBindSpec(raw.to_string())),
None if raw.is_empty() => Err(SandboxError::InvalidBindSpec(raw.to_string())),
None => Ok((raw, None)),
}
}
fn prepare_cli_bind_path(path: &Path) -> Result<PathBuf, SandboxError> {
let absolute =
std::path::absolute(&path).map_err(|_| SandboxError::PathMissing(path.clone()))?;
std::path::absolute(path).map_err(|_| SandboxError::PathMissing(path.to_path_buf()))?;
if !absolute.exists() {
return Err(SandboxError::PathMissing(absolute));
}
@@ -314,9 +347,9 @@ pub struct Options {
pub dry_run: Option<bool>,
pub chdir: Option<PathBuf>,
#[serde(default)]
pub rw: Vec<PathBuf>,
pub rw: Vec<String>,
#[serde(default)]
pub ro: Vec<PathBuf>,
pub ro: Vec<String>,
#[serde(default)]
pub mask: Vec<PathBuf>,
#[serde(default)]
@@ -329,12 +362,6 @@ pub struct Options {
impl Options {
fn validate_paths(&mut self) -> Result<(), SandboxError> {
for p in &mut self.rw {
*p = expand_and_require_existing(p)?;
}
for p in &mut self.ro {
*p = expand_and_require_existing(p)?;
}
if let Some(ref chdir) = self.chdir {
self.chdir = Some(expand_and_canonicalize(chdir)?);
}
@@ -722,13 +749,13 @@ mod tests {
fn build_rw_paths_accumulate() {
let file_config = FileConfig {
options: Options {
rw: vec![PathBuf::from("/tmp")],
rw: vec!["/tmp".into()],
..Options::default()
},
profile: HashMap::from([(
"extra".into(),
Options {
rw: vec![PathBuf::from("/usr")],
rw: vec!["/usr".into()],
..Options::default()
},
)]),
@@ -736,7 +763,7 @@ mod tests {
};
let args = Args {
profile: Some("extra".into()),
extra_rw: vec![PathBuf::from("/var")],
extra_rw: vec!["/var".into()],
..args_with_command()
};
let config = build(args, Some(file_config)).unwrap();
@@ -885,13 +912,13 @@ mod tests {
let home = std::env::var("HOME").unwrap();
let file_config = FileConfig {
options: Options {
rw: vec![PathBuf::from("~/")],
rw: vec!["~/".into()],
..Options::default()
},
..FileConfig::default()
};
let config = build(args_with_command(), Some(file_config)).unwrap();
assert_eq!(config.extra_rw, vec![PathBuf::from(&home)]);
assert_eq!(config.extra_rw, vec![same_bind(Path::new(&home))]);
}
#[test]
@@ -902,17 +929,18 @@ mod tests {
std::fs::write(&target, "x").unwrap();
std::os::unix::fs::symlink(&target, &link).unwrap();
let link_str = link.to_str().unwrap().to_string();
let file_config = FileConfig {
options: Options {
ro: vec![link.clone()],
rw: vec![link.clone()],
ro: vec![link_str.clone()],
rw: vec![link_str],
..Options::default()
},
..FileConfig::default()
};
let config = build(args_with_command(), Some(file_config)).unwrap();
assert_eq!(config.extra_ro, vec![link.clone()]);
assert_eq!(config.extra_rw, vec![link]);
assert_eq!(config.extra_ro, vec![same_bind(&link)]);
assert_eq!(config.extra_rw, vec![same_bind(&link)]);
}
#[test]
@@ -923,21 +951,22 @@ mod tests {
std::fs::write(&target, "x").unwrap();
std::os::unix::fs::symlink(&target, &link).unwrap();
let link_str = link.to_str().unwrap().to_string();
let args = Args {
extra_ro: vec![link.clone()],
extra_rw: vec![link.clone()],
extra_ro: vec![link_str.clone()],
extra_rw: vec![link_str],
..args_with_command()
};
let config = build(args, None).unwrap();
assert_eq!(config.extra_ro, vec![link.clone()]);
assert_eq!(config.extra_rw, vec![link]);
assert_eq!(config.extra_ro, vec![same_bind(&link)]);
assert_eq!(config.extra_rw, vec![same_bind(&link)]);
}
#[test]
fn build_relative_config_path_rejected() {
let file_config = FileConfig {
options: Options {
rw: vec![PathBuf::from("relative/path")],
rw: vec!["relative/path".into()],
..Options::default()
},
..FileConfig::default()
@@ -948,6 +977,120 @@ mod tests {
));
}
#[test]
fn build_cli_remap_target() {
let dir = TempDir::new().unwrap();
let src = dir.path().join("host");
std::fs::create_dir(&src).unwrap();
let src_str = src.to_str().unwrap();
let args = Args {
extra_ro: vec![format!("{src_str}:/inside/elsewhere")],
..args_with_command()
};
let config = build(args, None).unwrap();
assert_eq!(
config.extra_ro,
vec![BindSpec {
source: src,
target: PathBuf::from("/inside/elsewhere"),
}]
);
}
#[test]
fn build_config_remap_target() {
let dir = TempDir::new().unwrap();
let src = dir.path().join("host");
std::fs::create_dir(&src).unwrap();
let src_str = src.to_str().unwrap();
let file_config = FileConfig {
options: Options {
rw: vec![format!("{src_str}:/inside/elsewhere")],
..Options::default()
},
..FileConfig::default()
};
let config = build(args_with_command(), Some(file_config)).unwrap();
assert_eq!(
config.extra_rw,
vec![BindSpec {
source: src,
target: PathBuf::from("/inside/elsewhere"),
}]
);
}
#[test]
fn build_config_tilde_expands_in_target() {
let home = std::env::var("HOME").unwrap();
let file_config = FileConfig {
options: Options {
rw: vec!["~/:~/mounted".into()],
..Options::default()
},
..FileConfig::default()
};
let config = build(args_with_command(), Some(file_config)).unwrap();
assert_eq!(
config.extra_rw,
vec![BindSpec {
source: PathBuf::from(&home),
target: PathBuf::from(format!("{home}/mounted")),
}]
);
}
#[test]
fn build_relative_target_rejected() {
let dir = TempDir::new().unwrap();
let src_str = dir.path().to_str().unwrap();
let args = Args {
extra_ro: vec![format!("{src_str}:relative/target")],
..args_with_command()
};
assert!(matches!(
build(args, None),
Err(SandboxError::ConfigPathNotAbsolute(_))
));
}
#[test]
fn build_empty_source_half_rejected() {
let args = Args {
extra_ro: vec![":/target".into()],
..args_with_command()
};
assert!(matches!(
build(args, None),
Err(SandboxError::InvalidBindSpec(_))
));
}
#[test]
fn build_empty_target_half_rejected() {
let dir = TempDir::new().unwrap();
let src_str = dir.path().to_str().unwrap();
let args = Args {
extra_ro: vec![format!("{src_str}:")],
..args_with_command()
};
assert!(matches!(
build(args, None),
Err(SandboxError::InvalidBindSpec(_))
));
}
#[test]
fn build_chdir_from_config() {
let file_config = FileConfig {
@@ -1281,11 +1424,18 @@ mod tests {
assert!(matches!(result, Err(SandboxError::NoCommand)));
}
fn assert_paths(actual: &[PathBuf], expected: &[&str]) {
let expected: Vec<PathBuf> = expected.iter().map(PathBuf::from).collect();
fn assert_paths(actual: &[String], expected: &[&str]) {
let expected: Vec<String> = expected.iter().map(|s| s.to_string()).collect();
assert_eq!(actual, &expected);
}
fn same_bind(path: &Path) -> BindSpec {
BindSpec {
source: path.to_path_buf(),
target: path.to_path_buf(),
}
}
fn assert_command(cmd: &Option<CommandValue>, expected: &[&str]) {
let actual = cmd.clone().unwrap().into_vec();
let expected: Vec<String> = expected.iter().map(|s| s.to_string()).collect();