use std::collections::BTreeSet;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Scope<T: Ord + Clone> {
All,
Only(BTreeSet<T>),
}
impl<T: Ord + Clone> Scope<T> {
#[must_use]
pub fn top() -> Self {
Self::All
}
#[must_use]
pub fn none() -> Self {
Self::Only(BTreeSet::new())
}
pub fn only<I: IntoIterator<Item = T>>(items: I) -> Self {
Self::Only(items.into_iter().collect())
}
#[must_use]
pub fn leq(&self, other: &Self) -> bool {
match (self, other) {
(_, Self::All) => true,
(Self::All, Self::Only(_)) => false,
(Self::Only(a), Self::Only(b)) => a.is_subset(b),
}
}
#[must_use]
pub fn meet(&self, other: &Self) -> Self {
match (self, other) {
(Self::All, x) | (x, Self::All) => x.clone(),
(Self::Only(a), Self::Only(b)) => Self::Only(a.intersection(b).cloned().collect()),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CountBound {
Unlimited,
AtMost(u64),
}
impl CountBound {
#[must_use]
pub fn top() -> Self {
Self::Unlimited
}
#[must_use]
pub fn leq(&self, other: &Self) -> bool {
match (self, other) {
(_, Self::Unlimited) => true,
(Self::Unlimited, Self::AtMost(_)) => false,
(Self::AtMost(a), Self::AtMost(b)) => a <= b,
}
}
#[must_use]
pub fn meet(&self, other: &Self) -> Self {
match (self, other) {
(Self::Unlimited, x) | (x, Self::Unlimited) => *x,
(Self::AtMost(a), Self::AtMost(b)) => Self::AtMost((*a).min(*b)),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Caveats {
pub fs_read: Scope<String>,
pub fs_write: Scope<String>,
pub exec: Scope<String>,
pub net: Scope<String>,
pub max_calls: CountBound,
pub valid_for_generation: Scope<u64>,
}
impl Caveats {
#[must_use]
pub fn top() -> Self {
Self {
fs_read: Scope::top(),
fs_write: Scope::top(),
exec: Scope::top(),
net: Scope::top(),
max_calls: CountBound::top(),
valid_for_generation: Scope::top(),
}
}
#[must_use]
pub fn leq(&self, other: &Self) -> bool {
self.fs_read.leq(&other.fs_read)
&& self.fs_write.leq(&other.fs_write)
&& self.exec.leq(&other.exec)
&& self.net.leq(&other.net)
&& self.max_calls.leq(&other.max_calls)
&& self.valid_for_generation.leq(&other.valid_for_generation)
}
#[must_use]
pub fn meet(&self, other: &Self) -> Self {
Self {
fs_read: self.fs_read.meet(&other.fs_read),
fs_write: self.fs_write.meet(&other.fs_write),
exec: self.exec.meet(&other.exec),
net: self.net.meet(&other.net),
max_calls: self.max_calls.meet(&other.max_calls),
valid_for_generation: self.valid_for_generation.meet(&other.valid_for_generation),
}
}
}
impl Default for Caveats {
fn default() -> Self {
Self::top()
}
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
#[test]
fn scope_all_is_top() {
let bounded = Scope::only(["/a".to_string()]);
assert!(bounded.leq(&Scope::All));
assert!(!Scope::<String>::All.leq(&bounded));
assert_eq!(Scope::<String>::All.meet(&bounded), bounded);
}
#[test]
fn scope_subset_order() {
let small = Scope::only(["/a".to_string()]);
let big = Scope::only(["/a".to_string(), "/b".to_string()]);
assert!(small.leq(&big));
assert!(!big.leq(&small));
assert_eq!(big.meet(&small), small);
}
#[test]
fn scope_disjoint_meet_is_empty() {
let a = Scope::only(["/a".to_string()]);
let b = Scope::only(["/b".to_string()]);
assert_eq!(a.meet(&b), Scope::none());
assert!(Scope::<String>::none().leq(&a));
}
#[test]
fn count_bound_order_and_meet() {
assert!(CountBound::AtMost(3).leq(&CountBound::AtMost(5)));
assert!(!CountBound::AtMost(5).leq(&CountBound::AtMost(3)));
assert!(CountBound::AtMost(99).leq(&CountBound::Unlimited));
assert!(!CountBound::Unlimited.leq(&CountBound::AtMost(1)));
assert_eq!(
CountBound::AtMost(5).meet(&CountBound::AtMost(3)),
CountBound::AtMost(3)
);
assert_eq!(
CountBound::Unlimited.meet(&CountBound::AtMost(7)),
CountBound::AtMost(7)
);
}
#[test]
fn caveats_top_is_above_everything() {
let restricted = Caveats {
fs_read: Scope::only(["/repo".to_string()]),
fs_write: Scope::none(),
exec: Scope::only(["git".to_string()]),
net: Scope::none(),
max_calls: CountBound::AtMost(10),
valid_for_generation: Scope::only([7u64]),
};
assert!(restricted.leq(&Caveats::top()));
assert!(!Caveats::top().leq(&restricted));
}
#[test]
fn caveats_meet_attenuates_each_axis() {
let a = Caveats {
fs_read: Scope::only(["/repo".to_string(), "/tmp".to_string()]),
max_calls: CountBound::AtMost(10),
..Caveats::top()
};
let b = Caveats {
fs_read: Scope::only(["/repo".to_string()]),
max_calls: CountBound::AtMost(4),
..Caveats::top()
};
let m = a.meet(&b);
assert_eq!(m.fs_read, Scope::only(["/repo".to_string()]));
assert_eq!(m.max_calls, CountBound::AtMost(4));
assert!(m.leq(&a) && m.leq(&b));
}
#[test]
fn caveats_serde_roundtrip() {
let c = Caveats {
exec: Scope::only(["git".to_string(), "cargo".to_string()]),
max_calls: CountBound::AtMost(3),
valid_for_generation: Scope::only([42u64]),
..Caveats::top()
};
let json = serde_json::to_string(&c).unwrap();
let back: Caveats = serde_json::from_str(&json).unwrap();
assert_eq!(c, back);
}
fn scope_str() -> impl Strategy<Value = Scope<String>> {
prop_oneof![
Just(Scope::All),
prop::collection::btree_set("[a-d]", 0..4).prop_map(Scope::Only),
]
}
fn count_bound() -> impl Strategy<Value = CountBound> {
prop_oneof![
Just(CountBound::Unlimited),
(0u64..6).prop_map(CountBound::AtMost)
]
}
fn gen_scope() -> impl Strategy<Value = Scope<u64>> {
prop_oneof![
Just(Scope::All),
prop::collection::btree_set(0u64..4, 0..4).prop_map(Scope::Only),
]
}
prop_compose! {
fn caveats()(
fs_read in scope_str(),
fs_write in scope_str(),
exec in scope_str(),
net in scope_str(),
max_calls in count_bound(),
valid_for_generation in gen_scope(),
) -> Caveats {
Caveats { fs_read, fs_write, exec, net, max_calls, valid_for_generation }
}
}
proptest! {
#[test]
fn leq_reflexive(a in caveats()) {
prop_assert!(a.leq(&a));
}
#[test]
fn leq_antisymmetric(a in caveats(), b in caveats()) {
if a.leq(&b) && b.leq(&a) {
prop_assert_eq!(a, b);
}
}
#[test]
fn leq_transitive(a in caveats(), b in caveats(), c in caveats()) {
if a.leq(&b) && b.leq(&c) {
prop_assert!(a.leq(&c));
}
}
#[test]
fn meet_is_lower_bound(a in caveats(), b in caveats()) {
let m = a.meet(&b);
prop_assert!(m.leq(&a), "meet must be ⊑ left");
prop_assert!(m.leq(&b), "meet must be ⊑ right");
}
#[test]
fn meet_is_greatest_lower_bound(a in caveats(), b in caveats(), c in caveats()) {
if c.leq(&a) && c.leq(&b) {
prop_assert!(c.leq(&a.meet(&b)));
}
}
#[test]
fn meet_commutative(a in caveats(), b in caveats()) {
prop_assert_eq!(a.meet(&b), b.meet(&a));
}
#[test]
fn meet_associative(a in caveats(), b in caveats(), c in caveats()) {
prop_assert_eq!(a.meet(&b).meet(&c), a.meet(&b.meet(&c)));
}
#[test]
fn meet_idempotent(a in caveats()) {
prop_assert_eq!(a.meet(&a), a.clone());
}
#[test]
fn top_is_meet_identity(a in caveats()) {
prop_assert_eq!(a.meet(&Caveats::top()), a.clone());
prop_assert!(a.leq(&Caveats::top()));
}
#[test]
fn meet_never_amplifies(a in caveats(), b in caveats()) {
let m = a.meet(&b);
prop_assert!(m.leq(&a) && m.leq(&b));
if a.leq(&m) {
prop_assert_eq!(&m, &a);
}
}
}
}