use std::ffi::{OsStr, OsString};
use std::path::{Path, PathBuf};
use std::process::Command;
use crate::discover::path;
use crate::error::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Namespace {
Pid,
Ipc,
Uts,
User,
Cgroup,
Net,
}
impl Namespace {
#[must_use]
pub const fn flag(self) -> &'static str {
match self {
Self::Pid => "--unshare-pid",
Self::Ipc => "--unshare-ipc",
Self::Uts => "--unshare-uts",
Self::User => "--unshare-user",
Self::Cgroup => "--unshare-cgroup",
Self::Net => "--unshare-net",
}
}
}
#[derive(Debug, Clone)]
#[must_use = "BwrapCommand does nothing until you call `.into_command()`"]
pub struct BwrapCommand {
bwrap: PathBuf,
bwrap_args: Vec<OsString>,
program: Option<OsString>,
program_args: Vec<OsString>,
}
impl BwrapCommand {
pub fn new() -> Result<Self> {
Self::from_path(path().ok_or(Error::NotFound)?)
}
pub fn from_path(bwrap: impl AsRef<Path>) -> Result<Self> {
let bwrap = bwrap.as_ref();
if !bwrap.is_file() {
return Err(Error::NotFound);
}
Ok(Self {
bwrap: bwrap.to_path_buf(),
bwrap_args: Vec::new(),
program: None,
program_args: Vec::new(),
})
}
pub fn unshare<I>(mut self, namespaces: I) -> Self
where
I: IntoIterator<Item = Namespace>,
{
for ns in namespaces {
self.bwrap_args.push(OsString::from(ns.flag()));
}
self
}
pub fn die_with_parent(mut self) -> Self {
self.bwrap_args.push(OsString::from("--die-with-parent"));
self
}
pub fn ro_bind<H, G>(mut self, host: H, guest: G) -> Self
where
H: AsRef<OsStr>,
G: AsRef<OsStr>,
{
self.bwrap_args.push(OsString::from("--ro-bind"));
self.bwrap_args.push(host.as_ref().to_os_string());
self.bwrap_args.push(guest.as_ref().to_os_string());
self
}
pub fn bind<H, G>(mut self, host: H, guest: G) -> Self
where
H: AsRef<OsStr>,
G: AsRef<OsStr>,
{
self.bwrap_args.push(OsString::from("--bind"));
self.bwrap_args.push(host.as_ref().to_os_string());
self.bwrap_args.push(guest.as_ref().to_os_string());
self
}
pub fn dev_bind<H, G>(mut self, host: H, guest: G) -> Self
where
H: AsRef<OsStr>,
G: AsRef<OsStr>,
{
self.bwrap_args.push(OsString::from("--dev-bind"));
self.bwrap_args.push(host.as_ref().to_os_string());
self.bwrap_args.push(guest.as_ref().to_os_string());
self
}
pub fn tmpfs<P: AsRef<OsStr>>(mut self, mount_point: P) -> Self {
self.bwrap_args.push(OsString::from("--tmpfs"));
self.bwrap_args.push(mount_point.as_ref().to_os_string());
self
}
pub fn raw_arg<A: AsRef<OsStr>>(mut self, arg: A) -> Self {
self.bwrap_args.push(arg.as_ref().to_os_string());
self
}
pub fn program<P: AsRef<OsStr>>(mut self, program: P) -> Self {
self.program = Some(program.as_ref().to_os_string());
self
}
pub fn arg<A: AsRef<OsStr>>(mut self, arg: A) -> Self {
self.program_args.push(arg.as_ref().to_os_string());
self
}
pub fn args<I, A>(mut self, args: I) -> Self
where
I: IntoIterator<Item = A>,
A: AsRef<OsStr>,
{
for a in args {
self.program_args.push(a.as_ref().to_os_string());
}
self
}
#[must_use]
pub fn into_command(self) -> Command {
let mut cmd = Command::new(&self.bwrap);
cmd.args(&self.bwrap_args);
if let Some(ref program) = self.program {
cmd.arg("--").arg(program).args(&self.program_args);
}
cmd
}
#[must_use]
pub fn bwrap_path(&self) -> &Path {
&self.bwrap
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::missing_docs_in_private_items,
reason = "tests are allowed to use unwrap and omit docs"
)]
mod tests {
use super::*;
#[test]
fn namespace_flags_match_bwrap_cli() {
assert_eq!(Namespace::Pid.flag(), "--unshare-pid");
assert_eq!(Namespace::Ipc.flag(), "--unshare-ipc");
assert_eq!(Namespace::Uts.flag(), "--unshare-uts");
assert_eq!(Namespace::User.flag(), "--unshare-user");
assert_eq!(Namespace::Cgroup.flag(), "--unshare-cgroup");
assert_eq!(Namespace::Net.flag(), "--unshare-net");
}
#[test]
fn from_path_missing_is_not_found() {
let err = BwrapCommand::from_path("/no/such/bux-bwrap-binary").unwrap_err();
assert!(
matches!(err, Error::NotFound),
"missing path must be NotFound: {err}"
);
}
#[test]
fn from_path_existing_file_records_path() {
let dir = std::env::temp_dir().join(format!("bux-bwrap-from-path-{}", std::process::id()));
drop(std::fs::remove_dir_all(&dir));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("bwrap");
std::fs::write(&path, b"dummy-bwrap").unwrap();
let cmd = BwrapCommand::from_path(&path).unwrap();
assert_eq!(cmd.bwrap_path(), path.as_path());
drop(std::fs::remove_dir_all(&dir));
}
}