use super::error::Result;
use super::roots::RootIdentity;
use super::schema::SafetyLockConfig;
use super::util::PathProbe;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TrustedRootEntry {
pub identity: RootIdentity,
pub exists: bool,
}
impl TrustedRootEntry {
pub fn spelling(&self) -> String {
self.identity.spelling()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct TrustedRootListing {
pub entries: Vec<TrustedRootEntry>,
}
impl TrustedRootListing {
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
pub fn list_roots(config: &SafetyLockConfig, probe: &dyn PathProbe) -> Result<TrustedRootListing> {
config.validate()?;
Ok(TrustedRootListing {
entries: config
.roots
.approved
.iter()
.map(|identity| TrustedRootEntry {
identity: identity.clone(),
exists: probe.is_readable_dir(identity.as_path()),
})
.collect(),
})
}
#[cfg(test)]
mod tests {
use std::ffi::OsString;
use std::os::unix::ffi::OsStringExt;
use std::path::PathBuf;
use super::super::error::SafetyLockError;
use super::super::schema::TrustedRootsSection;
use super::super::test_probe::{Entry, FakeProbe};
use super::super::util::OsPathProbe;
use super::*;
fn identity(path: &str) -> RootIdentity {
RootIdentity::new(path).unwrap()
}
fn non_unicode_identity(suffix: &[u8]) -> RootIdentity {
let mut bytes = b"/tmp/".to_vec();
bytes.extend_from_slice(suffix);
RootIdentity::new(PathBuf::from(OsString::from_vec(bytes))).unwrap()
}
fn approved(roots: impl IntoIterator<Item = RootIdentity>) -> SafetyLockConfig {
SafetyLockConfig {
roots: TrustedRootsSection {
approved: roots.into_iter().collect(),
},
}
}
#[test]
fn every_approved_root_lists_once_in_stored_order() {
let config = approved([
identity("/home/alice/work-dotfiles"),
identity("/home/alice/dotfiles"),
identity("/home/alice/archive/dotfiles"),
]);
let probe = FakeProbe::dir("/home/alice/dotfiles")
.add("/home/alice/work-dotfiles", Entry::UnreadableDir);
let listing = list_roots(&config, &probe).unwrap();
assert!(!listing.is_empty());
assert_eq!(
listing
.entries
.iter()
.map(|entry| (entry.spelling(), entry.exists))
.collect::<Vec<_>>(),
vec![
("/home/alice/work-dotfiles".to_owned(), false),
("/home/alice/dotfiles".to_owned(), true),
("/home/alice/archive/dotfiles".to_owned(), false),
]
);
}
#[test]
fn nothing_approved_lists_nothing() {
let listing = list_roots(&SafetyLockConfig::default(), &FakeProbe::default()).unwrap();
assert!(listing.is_empty());
}
#[test]
fn every_listed_spelling_reads_back_as_the_root_it_names() {
let config = approved([
identity("/home/alice/dotfiles"),
non_unicode_identity(b"\x80dots"),
]);
let listing = list_roots(&config, &FakeProbe::default()).unwrap();
for entry in &listing.entries {
assert_eq!(
RootIdentity::parse(&entry.spelling()).unwrap(),
entry.identity,
"`{}` did not read back as the root it names",
entry.spelling()
);
}
}
#[test]
fn lossy_colliding_roots_list_as_two_distinguishable_entries() {
let one = non_unicode_identity(b"\x80");
let other = non_unicode_identity(b"\x81");
assert_eq!(
one.as_path().to_string_lossy(),
other.as_path().to_string_lossy(),
"test premise: these two roots render identically when lossy"
);
let listing = list_roots(&approved([one, other]), &FakeProbe::default()).unwrap();
let spellings: Vec<String> = listing
.entries
.iter()
.map(TrustedRootEntry::spelling)
.collect();
assert_eq!(
spellings,
vec!["os-bytes:2f746d702f80", "os-bytes:2f746d702f81"]
);
}
#[test]
fn an_unusable_collection_is_reported_rather_than_listed_empty() {
let root = identity("/home/alice/dotfiles");
let error = list_roots(&approved([root.clone(), root]), &FakeProbe::default()).unwrap_err();
assert!(
matches!(error, SafetyLockError::DuplicateApprovedRoot { .. }),
"unexpected error: {error}"
);
}
#[test]
fn existence_reflects_the_real_filesystem() {
let home = tempfile::tempdir().unwrap();
let present = std::fs::canonicalize(home.path()).unwrap().join("dotfiles");
std::fs::create_dir(&present).unwrap();
let removed = present.parent().unwrap().join("gone");
let config = approved([
RootIdentity::new(&present).unwrap(),
RootIdentity::new(&removed).unwrap(),
]);
let listing = list_roots(&config, &OsPathProbe).unwrap();
assert!(listing.entries[0].exists);
assert!(!listing.entries[1].exists);
assert_eq!(listing.entries[1].identity.as_path(), removed);
}
#[test]
fn an_entry_prints_the_spelling_forget_accepts() {
let entry = TrustedRootEntry {
identity: RootIdentity::new("/home/alice/dotfiles").unwrap(),
exists: false,
};
assert_eq!(entry.spelling(), "/home/alice/dotfiles");
assert_eq!(
RootIdentity::parse(&entry.spelling()).unwrap(),
entry.identity
);
}
#[test]
fn an_empty_listing_is_a_state_not_an_error() {
assert!(TrustedRootListing::default().is_empty());
}
}