use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Default)]
pub struct Allowlist {
entries: BTreeSet<PathBuf>,
}
impl Allowlist {
pub fn empty() -> Self {
Self::default()
}
pub fn from_paths<I, P>(paths: I) -> Self
where
I: IntoIterator<Item = P>,
P: Into<PathBuf>,
{
Self {
entries: paths
.into_iter()
.map(|p| canonical_or_owned(&p.into()))
.collect(),
}
}
pub fn allow(&mut self, path: impl Into<PathBuf>) {
self.entries.insert(canonical_or_owned(&path.into()));
}
pub fn allows(&self, program: &Path) -> bool {
self.entries.contains(&canonical_or_owned(program))
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
fn canonical_or_owned(p: &Path) -> PathBuf {
std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf())
}
pub fn resolve_program(program: &Path) -> PathBuf {
canonical_or_owned(program)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_allowlist_refuses_everything() {
let a = Allowlist::empty();
assert!(a.is_empty());
assert!(!a.allows(Path::new("/usr/bin/env")));
}
#[test]
fn allowlisted_program_matches_through_canonicalization() {
let dir = tempfile::tempdir().unwrap();
let exe = dir.path().join("deploy.sh");
std::fs::write(&exe, b"#!/bin/sh\n").unwrap();
let a = Allowlist::from_paths([&exe]);
assert!(a.allows(&exe));
let other = dir.path().join("evil.sh");
std::fs::write(&other, b"#!/bin/sh\n").unwrap();
assert!(!a.allows(&other));
}
#[test]
fn allow_adds_an_entry() {
let dir = tempfile::tempdir().unwrap();
let exe = dir.path().join("run.sh");
std::fs::write(&exe, b"#!/bin/sh\n").unwrap();
let mut a = Allowlist::empty();
assert!(!a.allows(&exe));
a.allow(&exe);
assert!(a.allows(&exe));
}
}