yoi/crates/pod-registry/src/conflict.rs

204 lines
6.2 KiB
Rust

//! Pure functions that decide whether scope rules collide.
//!
//! These helpers are read-only over [`LockFile`]; they never touch the
//! file or the lock itself. The mutating operations in [`crate::mutate`]
//! call them under the [`crate::LockFileGuard`].
use manifest::{Permission, ScopeRule};
use crate::table::{Allocation, LockFile};
/// Whether `a` and `b` claim any overlapping concrete path.
///
/// Recursive rules cover `target/**`; non-recursive rules cover the
/// target itself and its direct children. The four cases enumerate
/// when those coverage sets intersect.
pub(crate) fn rules_overlap(a: &ScopeRule, b: &ScopeRule) -> bool {
match (a.recursive, b.recursive) {
(true, true) => a.target.starts_with(&b.target) || b.target.starts_with(&a.target),
(true, false) => {
// a covers a.target/**; b covers {b.target, b.target/*}.
b.target.starts_with(&a.target) || a.target.parent() == Some(b.target.as_path())
}
(false, true) => {
a.target.starts_with(&b.target) || b.target.parent() == Some(a.target.as_path())
}
(false, false) => {
a.target == b.target
|| a.target.parent() == Some(b.target.as_path())
|| b.target.parent() == Some(a.target.as_path())
}
}
}
/// Does `cover` fully contain `inner`'s claimed paths?
fn covers_fully(cover: &ScopeRule, inner: &ScopeRule) -> bool {
if cover.permission < inner.permission {
return false;
}
if cover.recursive {
inner.target.starts_with(&cover.target)
} else {
inner.target == cover.target && !inner.recursive
}
}
/// Check whether `rule` is contained in `parent`'s effective write
/// scope: its allow set covers `rule`, and no child of `parent` has
/// already taken a piece that would overlap `rule`.
pub fn is_within_effective_write(lock: &LockFile, parent: &str, rule: &ScopeRule) -> bool {
let Some(alloc) = lock.find(parent) else {
return false;
};
if rule.permission != Permission::Write {
return alloc.scope_allow.iter().any(|r| covers_fully(r, rule));
}
let covered = alloc
.scope_allow
.iter()
.filter(|r| r.permission == Permission::Write)
.any(|r| covers_fully(r, rule));
if !covered {
return false;
}
let child_conflict = lock
.allocations
.iter()
.filter(|a| a.delegated_from.as_deref() == Some(parent))
.flat_map(|a| a.scope_allow.iter())
.filter(|r| r.permission == Permission::Write)
.any(|r| rules_overlap(r, rule));
!child_conflict
}
/// Find the Pod that actually owns a write scope overlapping `rule`.
///
/// Walks the delegation tree: if an allocation overlaps `rule`, we
/// descend into its children and return the deepest overlapping node
/// as the true owner. `exempt` names a Pod whose ownership is
/// permitted (used during delegation: the spawner itself is allowed
/// to still own the rule's region because it is handing it down).
pub fn find_conflict_owner(
lock: &LockFile,
rule: &ScopeRule,
exempt: Option<&str>,
) -> Option<String> {
if rule.permission != Permission::Write {
return None;
}
for alloc in lock
.allocations
.iter()
.filter(|a| a.delegated_from.is_none())
{
if let Some(owner) = find_conflict_in_subtree(lock, alloc, rule) {
if Some(owner.as_str()) == exempt {
continue;
}
return Some(owner);
}
}
None
}
fn find_conflict_in_subtree(
lock: &LockFile,
alloc: &Allocation,
rule: &ScopeRule,
) -> Option<String> {
let overlaps_here = alloc
.scope_allow
.iter()
.filter(|r| r.permission == Permission::Write)
.any(|r| rules_overlap(r, rule));
if !overlaps_here {
return None;
}
for child in lock
.allocations
.iter()
.filter(|a| a.delegated_from.as_deref() == Some(alloc.pod_name.as_str()))
{
if let Some(owner) = find_conflict_in_subtree(lock, child, rule) {
return Some(owner);
}
}
Some(alloc.pod_name.clone())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_util::*;
use crate::{ScopeLockError, delegate_scope, register_pod};
use tempfile::TempDir;
#[test]
fn rules_overlap_prefix_relation() {
assert!(rules_overlap(
&write_rule("/src", true),
&write_rule("/src/core", true)
));
assert!(rules_overlap(
&write_rule("/src/core", true),
&write_rule("/src", true),
));
assert!(!rules_overlap(
&write_rule("/src", true),
&write_rule("/docs", true),
));
}
#[test]
fn rules_overlap_non_recursive() {
assert!(!rules_overlap(
&write_rule("/src", false),
&write_rule("/src/a/b", true),
));
assert!(rules_overlap(
&write_rule("/src", false),
&write_rule("/src/child", false),
));
}
#[test]
fn conflict_detection_descends_to_real_owner() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("pods.json");
let mut g = open_empty(&path);
register_pod(
&mut g,
"a".into(),
std::process::id(),
sock("a"),
vec![write_rule("/src", true)],
sid(),
)
.unwrap();
delegate_scope(
&mut g,
"a",
"b".into(),
std::process::id(),
sock("b"),
vec![write_rule("/src/core", true)],
)
.unwrap();
// A different top-level Pod trying to register /src/core/x
// should be blamed on B (deepest owner), not A.
let err = register_pod(
&mut g,
"x".into(),
std::process::id(),
sock("x"),
vec![write_rule("/src/core/x", true)],
sid(),
)
.unwrap_err();
match err {
ScopeLockError::WriteConflict { competitor, .. } => assert_eq!(competitor, "b"),
other => panic!("expected WriteConflict, got {other:?}"),
}
}
}