use std::collections::HashSet;
use std::fs;
use std::path::{Path, PathBuf};
use crate::error::BranchError;
const O_WRONLY: u64 = 0o1;
const O_RDWR: u64 = 0o2;
const O_CREAT: u64 = 0o100;
const O_TRUNC: u64 = 0o1000;
const O_APPEND: u64 = 0o2000;
const O_DIRECTORY: u64 = 0o200000;
const WRITE_FLAGS: u64 = O_WRONLY | O_RDWR | O_CREAT | O_TRUNC | O_APPEND;
fn dir_size(dir: &Path) -> u64 {
let mut total = 0u64;
if let Ok(entries) = fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
total += dir_size(&path);
} else if let Ok(meta) = path.symlink_metadata() {
total += meta.len();
}
}
}
total
}
pub struct SeccompCowBranch {
workdir: PathBuf,
workdir_str: String,
upper: PathBuf,
storage_dir: PathBuf,
deleted: HashSet<String>,
has_changes: bool,
finished: bool,
max_disk_bytes: u64,
disk_used: u64,
}
impl SeccompCowBranch {
pub fn create(workdir: &Path, storage: Option<&Path>, max_disk_bytes: u64) -> Result<Self, BranchError> {
let storage_base = storage
.map(|p| p.to_path_buf())
.unwrap_or_else(|| std::env::temp_dir().join(format!("sandlock-cow-{}", std::process::id())));
let branch_id = uuid::Uuid::new_v4().to_string();
let branch_dir = storage_base.join(&branch_id);
let upper = branch_dir.join("upper");
fs::create_dir_all(&upper)
.map_err(|e| BranchError::Operation(format!("create upper: {}", e)))?;
let workdir = workdir.canonicalize()
.map_err(|e| BranchError::Operation(format!("canonicalize workdir: {}", e)))?;
Ok(Self {
workdir_str: workdir.to_string_lossy().into_owned(),
workdir,
upper,
storage_dir: branch_dir,
deleted: HashSet::new(),
has_changes: false,
finished: false,
max_disk_bytes,
disk_used: 0,
})
}
pub fn upper_dir(&self) -> &Path {
&self.upper
}
pub fn workdir(&self) -> &Path {
&self.workdir
}
pub fn workdir_str(&self) -> &str {
&self.workdir_str
}
pub fn has_changes(&self) -> bool {
self.has_changes
}
pub fn matches(&self, path: &str) -> bool {
path == self.workdir_str || path.starts_with(&format!("{}/", self.workdir_str))
}
fn safe_rel(&self, path: &str) -> Option<String> {
let rel = pathdiff::diff_paths(path, &self.workdir)?;
let rel_str = rel.to_string_lossy().into_owned();
if rel_str == ".." || rel_str.starts_with("../") {
return None;
}
Some(rel_str)
}
pub fn is_deleted(&self, rel_path: &str) -> bool {
self.deleted.contains(rel_path)
}
pub fn mark_deleted(&mut self, rel_path: &str) {
self.deleted.insert(rel_path.to_string());
self.has_changes = true;
}
fn check_quota(&self, additional: u64) -> Result<(), BranchError> {
if self.max_disk_bytes > 0 {
if additional == 0 {
if self.disk_used >= self.max_disk_bytes {
return Err(BranchError::QuotaExceeded);
}
} else if self.disk_used + additional > self.max_disk_bytes {
return Err(BranchError::QuotaExceeded);
}
}
Ok(())
}
fn recalc_disk_used(&mut self) {
self.disk_used = dir_size(&self.upper);
}
pub fn ensure_cow_copy(&mut self, rel_path: &str) -> Result<PathBuf, BranchError> {
self.deleted.remove(rel_path);
self.has_changes = true;
let upper_file = self.upper.join(rel_path);
let lower_file = self.workdir.join(rel_path);
if upper_file.exists() || upper_file.is_symlink() {
return Ok(upper_file);
}
if let Some(parent) = upper_file.parent() {
fs::create_dir_all(parent)
.map_err(|e| BranchError::Operation(format!("create parent: {}", e)))?;
}
if lower_file.is_symlink() {
self.check_quota(256)?; let target = fs::read_link(&lower_file)
.map_err(|e| BranchError::Operation(format!("readlink: {}", e)))?;
std::os::unix::fs::symlink(&target, &upper_file)
.map_err(|e| BranchError::Operation(format!("symlink: {}", e)))?;
self.disk_used += 256;
} else if lower_file.exists() {
let meta = lower_file.metadata()
.map_err(|e| BranchError::Operation(format!("metadata: {}", e)))?;
let file_size = meta.len();
self.check_quota(file_size)?;
fs::copy(&lower_file, &upper_file)
.map_err(|e| BranchError::Operation(format!("copy: {}", e)))?;
fs::set_permissions(&upper_file, meta.permissions())
.map_err(|e| BranchError::Operation(format!("set permissions: {}", e)))?;
self.disk_used += file_size;
} else {
self.check_quota(0)?;
}
Ok(upper_file)
}
pub fn resolve_read(&self, rel_path: &str) -> PathBuf {
let upper_file = self.upper.join(rel_path);
if upper_file.exists() || upper_file.is_symlink() {
upper_file
} else {
self.workdir.join(rel_path)
}
}
pub fn handle_open(&mut self, path: &str, flags: u64) -> Result<Option<PathBuf>, BranchError> {
if flags & O_DIRECTORY != 0 {
return Ok(None);
}
let rel = match self.safe_rel(path) {
Some(r) => r,
None => return Ok(None),
};
let is_write = flags & WRITE_FLAGS != 0;
if is_write && self.max_disk_bytes > 0 {
self.recalc_disk_used();
self.check_quota(0)?;
}
if self.is_deleted(&rel) {
if flags & O_CREAT != 0 {
return self.ensure_cow_copy(&rel).map(Some);
}
return Ok(None);
}
if is_write {
self.ensure_cow_copy(&rel).map(Some)
} else {
let resolved = self.resolve_read(&rel);
if resolved.exists() || resolved.is_symlink() {
Ok(Some(resolved))
} else {
Ok(None)
}
}
}
pub fn handle_unlink(&mut self, path: &str, is_dir: bool) -> bool {
let rel = match self.safe_rel(path) {
Some(r) => r,
None => return false,
};
let upper_file = self.upper.join(&rel);
let lower_file = self.workdir.join(&rel);
if upper_file.exists() || upper_file.is_symlink() {
if is_dir {
let _ = fs::remove_dir_all(&upper_file);
} else {
let _ = fs::remove_file(&upper_file);
}
self.recalc_disk_used();
}
if lower_file.exists() || lower_file.is_symlink() {
self.mark_deleted(&rel);
} else {
self.has_changes = true;
}
true
}
pub fn handle_mkdir(&mut self, path: &str) -> Result<bool, BranchError> {
let rel = match self.safe_rel(path) {
Some(r) => r,
None => return Ok(false),
};
self.check_quota(4096)?; self.deleted.remove(&rel);
self.has_changes = true;
let upper_dir = self.upper.join(&rel);
let ok = fs::create_dir_all(&upper_dir).is_ok();
if ok {
self.disk_used += 4096;
}
Ok(ok)
}
pub fn handle_rename(&mut self, old_path: &str, new_path: &str) -> Result<bool, BranchError> {
let old_rel = match self.safe_rel(old_path) {
Some(r) => r,
None => return Ok(false),
};
let new_rel = match self.safe_rel(new_path) {
Some(r) => r,
None => return Ok(false),
};
let old_upper = self.ensure_cow_copy(&old_rel)?;
let new_upper = self.upper.join(&new_rel);
if let Some(parent) = new_upper.parent() {
let _ = fs::create_dir_all(parent);
}
if fs::rename(&old_upper, &new_upper).is_err() {
return Ok(false);
}
let lower_old = self.workdir.join(&old_rel);
if lower_old.exists() || lower_old.is_symlink() {
self.mark_deleted(&old_rel);
}
Ok(true)
}
pub fn handle_stat(&self, path: &str) -> Option<PathBuf> {
let rel = self.safe_rel(path)?;
if self.is_deleted(&rel) {
return None;
}
let resolved = self.resolve_read(&rel);
if resolved.exists() || resolved.is_symlink() {
Some(resolved)
} else {
None
}
}
pub fn handle_symlink(&mut self, target: &str, linkpath: &str) -> Result<bool, BranchError> {
let rel = match self.safe_rel(linkpath) {
Some(r) => r,
None => return Ok(false),
};
if std::path::Path::new(target).is_absolute() || target.split('/').any(|c| c == "..") {
return Ok(false);
}
self.check_quota(256)?;
self.deleted.remove(&rel);
self.has_changes = true;
let upper_link = self.upper.join(&rel);
if let Some(parent) = upper_link.parent() {
let _ = fs::create_dir_all(parent);
}
let ok = std::os::unix::fs::symlink(target, &upper_link).is_ok();
if ok {
self.disk_used += 256;
}
Ok(ok)
}
pub fn handle_link(&mut self, oldpath: &str, newpath: &str) -> Result<bool, BranchError> {
let old_rel = match self.safe_rel(oldpath) {
Some(r) => r,
None => return Ok(false),
};
let new_rel = match self.safe_rel(newpath) {
Some(r) => r,
None => return Ok(false),
};
let old_upper = self.ensure_cow_copy(&old_rel)?;
let new_upper = self.upper.join(&new_rel);
if let Some(parent) = new_upper.parent() {
let _ = fs::create_dir_all(parent);
}
Ok(fs::hard_link(&old_upper, &new_upper).is_ok())
}
pub fn handle_chmod(&mut self, path: &str, mode: u32) -> Result<bool, BranchError> {
let rel = match self.safe_rel(path) {
Some(r) => r,
None => return Ok(false),
};
let upper = self.ensure_cow_copy(&rel)?;
use std::os::unix::fs::PermissionsExt;
Ok(fs::set_permissions(&upper, fs::Permissions::from_mode(mode)).is_ok())
}
pub fn handle_chown(&mut self, path: &str, uid: u32, gid: u32) -> Result<bool, BranchError> {
let rel = match self.safe_rel(path) {
Some(r) => r,
None => return Ok(false),
};
let upper = self.ensure_cow_copy(&rel)?;
let ok = unsafe {
let c_path = std::ffi::CString::new(upper.to_str().unwrap_or("")).unwrap();
libc::chown(c_path.as_ptr(), uid, gid) == 0
};
Ok(ok)
}
pub fn handle_truncate(&mut self, path: &str, length: i64) -> Result<bool, BranchError> {
let rel = match self.safe_rel(path) {
Some(r) => r,
None => return Ok(false),
};
let new_len = length as u64;
let upper = self.ensure_cow_copy(&rel)?;
let old_len = upper.metadata().map(|m| m.len()).unwrap_or(0);
if new_len > old_len {
self.check_quota(new_len - old_len)?;
}
let file = match fs::OpenOptions::new().write(true).open(&upper) {
Ok(f) => f,
Err(_) => return Ok(false),
};
let ok = file.set_len(new_len).is_ok();
if ok {
if new_len > old_len {
self.disk_used += new_len - old_len;
} else {
self.disk_used = self.disk_used.saturating_sub(old_len - new_len);
}
}
Ok(ok)
}
pub fn handle_readlink(&self, path: &str) -> Option<String> {
let rel = self.safe_rel(path)?;
if self.is_deleted(&rel) {
return None;
}
let upper = self.upper.join(&rel);
let lower = self.workdir.join(&rel);
if upper.is_symlink() {
fs::read_link(&upper).ok().map(|p| p.to_string_lossy().into_owned())
} else if lower.is_symlink() {
fs::read_link(&lower).ok().map(|p| p.to_string_lossy().into_owned())
} else {
None
}
}
pub fn changes(&self) -> Result<Vec<crate::dry_run::Change>, BranchError> {
use crate::dry_run::{Change, ChangeKind};
let mut result = Vec::new();
for entry in walkdir::WalkDir::new(&self.upper).min_depth(1) {
let entry = entry.map_err(|e| BranchError::Operation(format!("walk: {}", e)))?;
if entry.file_type().is_dir() {
continue;
}
let rel = entry.path().strip_prefix(&self.upper).unwrap();
let lower = self.workdir.join(rel);
let kind = if lower.exists() {
ChangeKind::Modified
} else {
ChangeKind::Added
};
result.push(Change { kind, path: rel.to_path_buf() });
}
for rel_path in &self.deleted {
result.push(Change {
kind: ChangeKind::Deleted,
path: std::path::PathBuf::from(rel_path),
});
}
Ok(result)
}
pub fn list_merged_dir(&self, rel_path: &str) -> Vec<String> {
let lower_dir = self.workdir.join(rel_path);
let upper_dir = self.upper.join(rel_path);
let mut entries = std::collections::BTreeSet::new();
if let Ok(rd) = fs::read_dir(&upper_dir) {
for e in rd.flatten() {
entries.insert(e.file_name().to_string_lossy().into_owned());
}
}
if let Ok(rd) = fs::read_dir(&lower_dir) {
for e in rd.flatten() {
let name = e.file_name().to_string_lossy().into_owned();
let child_rel = if rel_path == "." || rel_path.is_empty() {
name.clone()
} else {
format!("{}/{}", rel_path, name)
};
if !self.is_deleted(&child_rel) {
entries.insert(name);
}
}
}
entries.into_iter().collect()
}
pub fn commit(&mut self) -> Result<(), BranchError> {
if self.finished { return Ok(()); }
for rel_path in &self.deleted {
let dest = self.workdir.join(rel_path);
if dest.is_dir() {
let _ = fs::remove_dir_all(&dest);
} else if dest.exists() || dest.is_symlink() {
let _ = fs::remove_file(&dest);
}
}
let mut synced_dirs = HashSet::new();
for entry in walkdir::WalkDir::new(&self.upper).min_depth(1) {
let entry = entry.map_err(|e| BranchError::Operation(format!("walk: {}", e)))?;
let rel = entry.path().strip_prefix(&self.upper).unwrap();
let dest = self.workdir.join(rel);
if entry.file_type().is_dir() {
fs::create_dir_all(&dest)
.map_err(|e| BranchError::Operation(format!("mkdir: {}", e)))?;
} else {
if let Some(parent) = dest.parent() {
let _ = fs::create_dir_all(parent);
}
fs::copy(entry.path(), &dest)
.map_err(|e| BranchError::Operation(format!("copy: {}", e)))?;
synced_dirs.insert(dest.parent().unwrap().to_path_buf());
}
}
for d in &synced_dirs {
if let Ok(fd) = fs::OpenOptions::new().read(true).open(d) {
let _ = fd.sync_all();
}
}
self.cleanup();
self.finished = true;
Ok(())
}
pub fn abort(&mut self) -> Result<(), BranchError> {
if self.finished { return Ok(()); }
self.cleanup();
self.finished = true;
Ok(())
}
fn cleanup(&self) {
let _ = fs::remove_dir_all(&self.storage_dir);
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
fn setup_workdir() -> (tempfile::TempDir, tempfile::TempDir) {
let workdir = tempfile::tempdir().unwrap();
let storage = tempfile::tempdir().unwrap();
fs::write(workdir.path().join("existing.txt"), "hello").unwrap();
fs::create_dir(workdir.path().join("subdir")).unwrap();
fs::write(workdir.path().join("subdir/nested.txt"), "nested").unwrap();
(workdir, storage)
}
#[test]
fn test_create_branch() {
let (workdir, storage) = setup_workdir();
let branch = SeccompCowBranch::create(workdir.path(), Some(storage.path()), 0).unwrap();
assert!(branch.upper_dir().exists());
assert!(!branch.has_changes());
}
#[test]
fn test_matches() {
let (workdir, storage) = setup_workdir();
let branch = SeccompCowBranch::create(workdir.path(), Some(storage.path()), 0).unwrap();
let wdstr = workdir.path().canonicalize().unwrap();
let wdstr = wdstr.to_str().unwrap();
assert!(branch.matches(&format!("{}/foo.txt", wdstr)));
assert!(branch.matches(wdstr));
assert!(!branch.matches("/tmp/other"));
}
#[test]
fn test_ensure_cow_copy() {
let (workdir, storage) = setup_workdir();
let mut branch = SeccompCowBranch::create(workdir.path(), Some(storage.path()), 0).unwrap();
let upper = branch.ensure_cow_copy("existing.txt").unwrap();
assert!(upper.exists());
assert_eq!(fs::read_to_string(&upper).unwrap(), "hello");
assert!(branch.has_changes());
}
#[test]
fn test_resolve_read_prefers_upper() {
let (workdir, storage) = setup_workdir();
let mut branch = SeccompCowBranch::create(workdir.path(), Some(storage.path()), 0).unwrap();
let upper = branch.ensure_cow_copy("existing.txt").unwrap();
fs::write(&upper, "modified").unwrap();
let resolved = branch.resolve_read("existing.txt");
assert_eq!(fs::read_to_string(&resolved).unwrap(), "modified");
}
#[test]
fn test_is_deleted() {
let (workdir, storage) = setup_workdir();
let mut branch = SeccompCowBranch::create(workdir.path(), Some(storage.path()), 0).unwrap();
assert!(!branch.is_deleted("existing.txt"));
branch.mark_deleted("existing.txt");
assert!(branch.is_deleted("existing.txt"));
}
#[test]
fn test_commit_merges_upper() {
let (workdir, storage) = setup_workdir();
let mut branch = SeccompCowBranch::create(workdir.path(), Some(storage.path()), 0).unwrap();
let upper = branch.ensure_cow_copy("new.txt").unwrap();
fs::write(&upper, "new content").unwrap();
branch.commit().unwrap();
assert_eq!(fs::read_to_string(workdir.path().join("new.txt")).unwrap(), "new content");
}
#[test]
fn test_commit_applies_deletions() {
let (workdir, storage) = setup_workdir();
let mut branch = SeccompCowBranch::create(workdir.path(), Some(storage.path()), 0).unwrap();
branch.mark_deleted("existing.txt");
branch.commit().unwrap();
assert!(!workdir.path().join("existing.txt").exists());
}
#[test]
fn test_abort_discards_changes() {
let (workdir, storage) = setup_workdir();
let mut branch = SeccompCowBranch::create(workdir.path(), Some(storage.path()), 0).unwrap();
let upper = branch.ensure_cow_copy("new.txt").unwrap();
fs::write(&upper, "should be discarded").unwrap();
branch.abort().unwrap();
assert!(!workdir.path().join("new.txt").exists());
}
#[test]
fn test_changes_added_file() {
let (workdir, storage) = setup_workdir();
let mut branch = SeccompCowBranch::create(workdir.path(), Some(storage.path()), 0).unwrap();
let upper = branch.ensure_cow_copy("brand_new.txt").unwrap();
fs::write(&upper, "new content").unwrap();
let changes = branch.changes().unwrap();
assert_eq!(changes.len(), 1);
assert_eq!(changes[0].kind, crate::dry_run::ChangeKind::Added);
assert_eq!(changes[0].path, std::path::PathBuf::from("brand_new.txt"));
}
#[test]
fn test_changes_modified_file() {
let (workdir, storage) = setup_workdir();
let mut branch = SeccompCowBranch::create(workdir.path(), Some(storage.path()), 0).unwrap();
let upper = branch.ensure_cow_copy("existing.txt").unwrap();
fs::write(&upper, "modified content").unwrap();
let changes = branch.changes().unwrap();
assert_eq!(changes.len(), 1);
assert_eq!(changes[0].kind, crate::dry_run::ChangeKind::Modified);
assert_eq!(changes[0].path, std::path::PathBuf::from("existing.txt"));
}
#[test]
fn test_changes_deleted_file() {
let (workdir, storage) = setup_workdir();
let mut branch = SeccompCowBranch::create(workdir.path(), Some(storage.path()), 0).unwrap();
branch.mark_deleted("existing.txt");
let changes = branch.changes().unwrap();
assert_eq!(changes.len(), 1);
assert_eq!(changes[0].kind, crate::dry_run::ChangeKind::Deleted);
assert_eq!(changes[0].path, std::path::PathBuf::from("existing.txt"));
}
#[test]
fn test_changes_no_changes() {
let (workdir, storage) = setup_workdir();
let branch = SeccompCowBranch::create(workdir.path(), Some(storage.path()), 0).unwrap();
let changes = branch.changes().unwrap();
assert!(changes.is_empty());
}
#[test]
fn test_changes_mixed() {
let (workdir, storage) = setup_workdir();
let mut branch = SeccompCowBranch::create(workdir.path(), Some(storage.path()), 0).unwrap();
let upper = branch.ensure_cow_copy("new.txt").unwrap();
fs::write(&upper, "new").unwrap();
let upper2 = branch.ensure_cow_copy("existing.txt").unwrap();
fs::write(&upper2, "changed").unwrap();
branch.mark_deleted("subdir/nested.txt");
let mut changes = branch.changes().unwrap();
changes.sort_by(|a, b| a.path.cmp(&b.path));
assert_eq!(changes.len(), 3);
assert_eq!(changes[0].kind, crate::dry_run::ChangeKind::Modified);
assert_eq!(changes[0].path, std::path::PathBuf::from("existing.txt"));
assert_eq!(changes[1].kind, crate::dry_run::ChangeKind::Added);
assert_eq!(changes[1].path, std::path::PathBuf::from("new.txt"));
assert_eq!(changes[2].kind, crate::dry_run::ChangeKind::Deleted);
assert_eq!(changes[2].path, std::path::PathBuf::from("subdir/nested.txt"));
}
fn abs(branch: &SeccompCowBranch, rel: &str) -> String {
format!("{}/{}", branch.workdir_str(), rel)
}
#[test]
fn test_quota_exceeded_on_cow_copy() {
let (workdir, storage) = setup_workdir();
let mut branch = SeccompCowBranch::create(workdir.path(), Some(storage.path()), 4).unwrap();
let err = branch.ensure_cow_copy("existing.txt").unwrap_err();
assert!(matches!(err, BranchError::QuotaExceeded));
}
#[test]
fn test_quota_allows_within_limit() {
let (workdir, storage) = setup_workdir();
let mut branch = SeccompCowBranch::create(workdir.path(), Some(storage.path()), 100).unwrap();
assert!(branch.ensure_cow_copy("existing.txt").is_ok());
}
#[test]
fn test_quota_unlimited() {
let (workdir, storage) = setup_workdir();
let mut branch = SeccompCowBranch::create(workdir.path(), Some(storage.path()), 0).unwrap();
assert!(branch.ensure_cow_copy("existing.txt").is_ok());
}
#[test]
fn test_quota_cumulative_exhaustion() {
let (workdir, storage) = setup_workdir();
let mut branch = SeccompCowBranch::create(workdir.path(), Some(storage.path()), 10).unwrap();
assert!(branch.ensure_cow_copy("existing.txt").is_ok());
let err = branch.ensure_cow_copy("subdir/nested.txt").unwrap_err();
assert!(matches!(err, BranchError::QuotaExceeded));
}
#[test]
fn test_quota_exact_boundary() {
let (workdir, storage) = setup_workdir();
let mut branch = SeccompCowBranch::create(workdir.path(), Some(storage.path()), 5).unwrap();
assert!(branch.ensure_cow_copy("existing.txt").is_ok());
}
#[test]
fn test_quota_handle_open_write_denied() {
let (workdir, storage) = setup_workdir();
let mut branch = SeccompCowBranch::create(workdir.path(), Some(storage.path()), 4).unwrap();
let path = abs(&branch, "existing.txt");
let err = branch.handle_open(&path, O_WRONLY).unwrap_err();
assert!(matches!(err, BranchError::QuotaExceeded));
}
#[test]
fn test_quota_handle_open_read_allowed() {
let (workdir, storage) = setup_workdir();
let mut branch = SeccompCowBranch::create(workdir.path(), Some(storage.path()), 1).unwrap();
let path = abs(&branch, "existing.txt");
let result = branch.handle_open(&path, 0).unwrap(); assert!(result.is_some());
}
#[test]
fn test_quota_handle_open_create_denied() {
let (workdir, storage) = setup_workdir();
let mut branch = SeccompCowBranch::create(workdir.path(), Some(storage.path()), 4).unwrap();
let path = abs(&branch, "existing.txt");
branch.mark_deleted("existing.txt");
let err = branch.handle_open(&path, O_CREAT).unwrap_err();
assert!(matches!(err, BranchError::QuotaExceeded));
}
#[test]
fn test_quota_handle_mkdir_denied() {
let (workdir, storage) = setup_workdir();
let mut branch = SeccompCowBranch::create(workdir.path(), Some(storage.path()), 100).unwrap();
let path = abs(&branch, "newdir");
let err = branch.handle_mkdir(&path).unwrap_err();
assert!(matches!(err, BranchError::QuotaExceeded));
}
#[test]
fn test_quota_handle_mkdir_allowed() {
let (workdir, storage) = setup_workdir();
let mut branch = SeccompCowBranch::create(workdir.path(), Some(storage.path()), 5000).unwrap();
let path = abs(&branch, "newdir");
assert!(matches!(branch.handle_mkdir(&path), Ok(true)));
}
#[test]
fn test_quota_handle_symlink_denied() {
let (workdir, storage) = setup_workdir();
let mut branch = SeccompCowBranch::create(workdir.path(), Some(storage.path()), 100).unwrap();
let linkpath = abs(&branch, "mylink");
let err = branch.handle_symlink("existing.txt", &linkpath).unwrap_err();
assert!(matches!(err, BranchError::QuotaExceeded));
}
#[test]
fn test_quota_handle_symlink_allowed() {
let (workdir, storage) = setup_workdir();
let mut branch = SeccompCowBranch::create(workdir.path(), Some(storage.path()), 500).unwrap();
let linkpath = abs(&branch, "mylink");
assert!(matches!(branch.handle_symlink("existing.txt", &linkpath), Ok(true)));
}
#[test]
fn test_quota_handle_rename_denied() {
let (workdir, storage) = setup_workdir();
let mut branch = SeccompCowBranch::create(workdir.path(), Some(storage.path()), 4).unwrap();
let old = abs(&branch, "existing.txt");
let new = abs(&branch, "renamed.txt");
let err = branch.handle_rename(&old, &new).unwrap_err();
assert!(matches!(err, BranchError::QuotaExceeded));
}
#[test]
fn test_quota_handle_link_denied() {
let (workdir, storage) = setup_workdir();
let mut branch = SeccompCowBranch::create(workdir.path(), Some(storage.path()), 4).unwrap();
let old = abs(&branch, "existing.txt");
let new = abs(&branch, "hardlink.txt");
let err = branch.handle_link(&old, &new).unwrap_err();
assert!(matches!(err, BranchError::QuotaExceeded));
}
#[test]
fn test_quota_handle_chmod_denied() {
let (workdir, storage) = setup_workdir();
let mut branch = SeccompCowBranch::create(workdir.path(), Some(storage.path()), 4).unwrap();
let path = abs(&branch, "existing.txt");
let err = branch.handle_chmod(&path, 0o644).unwrap_err();
assert!(matches!(err, BranchError::QuotaExceeded));
}
#[test]
fn test_quota_handle_chown_denied() {
let (workdir, storage) = setup_workdir();
let mut branch = SeccompCowBranch::create(workdir.path(), Some(storage.path()), 4).unwrap();
let path = abs(&branch, "existing.txt");
let err = branch.handle_chown(&path, 1000, 1000).unwrap_err();
assert!(matches!(err, BranchError::QuotaExceeded));
}
#[test]
fn test_quota_handle_truncate_grow_denied() {
let (workdir, storage) = setup_workdir();
let mut branch = SeccompCowBranch::create(workdir.path(), Some(storage.path()), 10).unwrap();
let path = abs(&branch, "existing.txt");
let err = branch.handle_truncate(&path, 20).unwrap_err();
assert!(matches!(err, BranchError::QuotaExceeded));
}
#[test]
fn test_quota_handle_truncate_shrink_allowed() {
let (workdir, storage) = setup_workdir();
let mut branch = SeccompCowBranch::create(workdir.path(), Some(storage.path()), 10).unwrap();
let path = abs(&branch, "existing.txt");
assert!(matches!(branch.handle_truncate(&path, 2), Ok(true)));
assert_eq!(branch.disk_used, 2);
}
#[test]
fn test_quota_freed_after_unlink() {
let (workdir, storage) = setup_workdir();
let mut branch = SeccompCowBranch::create(workdir.path(), Some(storage.path()), 11).unwrap();
assert!(branch.ensure_cow_copy("existing.txt").is_ok());
assert!(branch.ensure_cow_copy("subdir/nested.txt").is_ok());
let path = abs(&branch, "existing.txt");
assert!(branch.handle_unlink(&path, false));
assert_eq!(branch.disk_used, 6);
assert!(branch.ensure_cow_copy("existing.txt").is_ok());
}
#[test]
fn test_quota_second_cow_copy_is_free() {
let (workdir, storage) = setup_workdir();
let mut branch = SeccompCowBranch::create(workdir.path(), Some(storage.path()), 5).unwrap();
assert!(branch.ensure_cow_copy("existing.txt").is_ok());
assert!(branch.ensure_cow_copy("existing.txt").is_ok());
}
#[test]
fn test_quota_disk_used_tracking() {
let (workdir, storage) = setup_workdir();
let mut branch = SeccompCowBranch::create(workdir.path(), Some(storage.path()), 1000).unwrap();
assert_eq!(branch.disk_used, 0);
branch.ensure_cow_copy("existing.txt").unwrap(); assert_eq!(branch.disk_used, 5);
branch.ensure_cow_copy("subdir/nested.txt").unwrap(); assert_eq!(branch.disk_used, 11);
}
#[test]
fn test_quota_new_file_blocked_when_exhausted() {
let (workdir, storage) = setup_workdir();
let mut branch = SeccompCowBranch::create(workdir.path(), Some(storage.path()), 5).unwrap();
assert!(branch.ensure_cow_copy("existing.txt").is_ok());
let err = branch.ensure_cow_copy("brand_new.txt").unwrap_err();
assert!(matches!(err, BranchError::QuotaExceeded));
}
#[test]
fn test_quota_new_file_allowed_when_space_remains() {
let (workdir, storage) = setup_workdir();
let mut branch = SeccompCowBranch::create(workdir.path(), Some(storage.path()), 100).unwrap();
assert!(branch.ensure_cow_copy("brand_new.txt").is_ok());
}
#[test]
fn test_quota_resync_on_write_open() {
let (workdir, storage) = setup_workdir();
let mut branch = SeccompCowBranch::create(workdir.path(), Some(storage.path()), 50).unwrap();
let path = abs(&branch, "existing.txt");
let upper = branch.handle_open(&path, O_WRONLY).unwrap().unwrap();
fs::write(&upper, vec![0u8; 50]).unwrap();
assert_eq!(branch.disk_used, 5);
let path2 = abs(&branch, "subdir/nested.txt");
let err = branch.handle_open(&path2, O_WRONLY).unwrap_err();
assert!(matches!(err, BranchError::QuotaExceeded));
assert!(branch.disk_used >= 50);
}
#[test]
fn test_quota_resync_not_triggered_on_read() {
let (workdir, storage) = setup_workdir();
let mut branch = SeccompCowBranch::create(workdir.path(), Some(storage.path()), 10).unwrap();
let write_path = abs(&branch, "existing.txt");
let upper = branch.handle_open(&write_path, O_WRONLY).unwrap().unwrap();
fs::write(&upper, vec![0u8; 50]).unwrap();
let read_path = abs(&branch, "existing.txt");
let result = branch.handle_open(&read_path, 0).unwrap(); assert!(result.is_some());
assert_eq!(branch.disk_used, 5);
}
}