#[cfg(target_os = "macos")]
pub mod apfs;
#[cfg(target_os = "linux")]
pub mod overlayfs;
use std::path::{Path, PathBuf};
use std::process::ExitStatus;
use anyhow::Result;
use crate::session::SessionRecord;
pub struct Sandbox {
pub target: PathBuf,
pub session_dir: PathBuf,
pub layers: Layers,
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
pub enum Layers {
Overlay {
upper: PathBuf,
work: PathBuf,
},
Snapshot {
snapshot: PathBuf,
parent_dev: u64,
parent_ino: u64,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ChangeKind {
Added,
Modified,
Deleted,
}
impl ChangeKind {
pub fn letter(self) -> char {
match self {
ChangeKind::Added => 'A',
ChangeKind::Modified => 'M',
ChangeKind::Deleted => 'D',
}
}
}
#[derive(Debug)]
pub struct Change {
pub kind: ChangeKind,
pub path: PathBuf,
pub is_dir: bool,
}
pub trait SnapshotBackend {
fn exec(&self, sandbox: &Sandbox, command: &str) -> Result<ExitStatus>;
fn changes(&self, sandbox: &Sandbox) -> Result<Vec<Change>>;
fn restore(&self, sandbox: &Sandbox) -> Result<()>;
fn merge(&self, sandbox: &Sandbox) -> Result<()>;
fn is_stale(&self, sandbox: &Sandbox) -> bool;
fn kind(&self) -> crate::session::BackendKind;
}
pub fn select() -> Result<Box<dyn SnapshotBackend>> {
#[cfg(target_os = "linux")]
{
Ok(Box::new(overlayfs::OverlayFs))
}
#[cfg(target_os = "macos")]
{
Ok(Box::new(apfs::Apfs))
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
anyhow::bail!(
"no snapshot backend supports this platform yet (OverlayFS on Linux, \
APFS on macOS).\nRefusing to run the command unsandboxed."
)
}
}
pub fn for_record(record: &SessionRecord) -> Result<Box<dyn SnapshotBackend>> {
match record.backend.as_str() {
#[cfg(target_os = "linux")]
"overlayfs" => Ok(Box::new(overlayfs::OverlayFs)),
#[cfg(target_os = "macos")]
"apfs" => Ok(Box::new(apfs::Apfs)),
other => anyhow::bail!(
"session was created by the `{other}` backend, which is not available on this platform"
),
}
}
pub fn sandbox_of(session_dir: &Path, record: &SessionRecord) -> Result<Sandbox> {
let layers = match record.backend.as_str() {
"overlayfs" => Layers::Overlay {
upper: record
.upper
.clone()
.ok_or_else(|| anyhow::anyhow!("overlayfs session record missing upper path"))?,
work: record
.work
.clone()
.ok_or_else(|| anyhow::anyhow!("overlayfs session record missing work path"))?,
},
"apfs" => Layers::Snapshot {
snapshot: record
.snapshot
.clone()
.ok_or_else(|| anyhow::anyhow!("apfs session record missing snapshot path"))?,
parent_dev: record
.parent_dev
.ok_or_else(|| anyhow::anyhow!("apfs session record missing parent identity"))?,
parent_ino: record
.parent_ino
.ok_or_else(|| anyhow::anyhow!("apfs session record missing parent identity"))?,
},
other => anyhow::bail!("unknown backend `{other}` in session record"),
};
Ok(Sandbox {
target: record.target.clone(),
session_dir: session_dir.to_path_buf(),
layers,
})
}
pub fn sort_changes(changes: &mut [Change]) {
changes.sort_by(|a, b| {
a.path
.as_os_str()
.as_encoded_bytes()
.cmp(b.path.as_os_str().as_encoded_bytes())
});
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
pub fn validate_mount_path(path: &Path) -> Result<()> {
let s = path.to_string_lossy();
if s.contains(':') || s.contains(',') || s.contains('\\') {
anyhow::bail!(
"path {} contains characters unsupported in overlay mount options (: , \\)",
path.display()
);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sort_is_raw_byte_order_not_component_order() {
let mut changes = vec![
Change {
kind: ChangeKind::Added,
path: PathBuf::from("a/c"),
is_dir: false,
},
Change {
kind: ChangeKind::Added,
path: PathBuf::from("a-b"),
is_dir: false,
},
];
sort_changes(&mut changes);
assert_eq!(changes[0].path, PathBuf::from("a-b"));
assert_eq!(changes[1].path, PathBuf::from("a/c"));
}
}