Accept SRC:DST remap syntax in --ro/--rw
This commit is contained in:
+190
-40
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user