use std::path::{Path, PathBuf};
use auths_verifier::IdentityDID;
use crate::ports::{ConfigStore, ConfigStoreError};
const ROOTS_FILE: &str = "roots";
fn roots_path(auths_dir: &Path) -> PathBuf {
auths_dir.join(ROOTS_FILE)
}
pub fn load_pinned_roots(
store: &dyn ConfigStore,
auths_dir: &Path,
) -> Result<Vec<String>, ConfigStoreError> {
match store.read(&roots_path(auths_dir))? {
Some(content) => Ok(parse_roots(&content)),
None => Ok(Vec::new()),
}
}
pub fn parse_roots(content: &str) -> Vec<String> {
content
.lines()
.map(str::trim)
.filter(|line| !line.is_empty() && !line.starts_with('#'))
.map(str::to_string)
.collect()
}
pub fn is_pinned_root(
store: &dyn ConfigStore,
auths_dir: &Path,
did: &str,
) -> Result<bool, ConfigStoreError> {
Ok(load_pinned_roots(store, auths_dir)?
.iter()
.any(|root| root == did))
}
pub fn add_pinned_root(
store: &dyn ConfigStore,
auths_dir: &Path,
did: &str,
) -> Result<(), ConfigStoreError> {
let mut roots = load_pinned_roots(store, auths_dir)?;
if roots.iter().any(|root| root == did) {
return Ok(());
}
roots.push(did.to_string());
store.write(&roots_path(auths_dir), &format!("{}\n", roots.join("\n")))
}
#[derive(Debug, thiserror::Error)]
pub enum RootsError {
#[error("could not read roots pin file: {0}")]
Store(#[from] ConfigStoreError),
#[error("roots pin line {line} ({value:?}) is not a valid did:keri: root: {source}")]
MalformedRoot {
line: usize,
value: String,
#[source]
source: auths_verifier::DidParseError,
},
}
pub fn parse_roots_typed(content: &str) -> Result<Vec<IdentityDID>, RootsError> {
content
.lines()
.enumerate()
.map(|(idx, raw)| (idx + 1, raw.trim()))
.filter(|(_, line)| !line.is_empty() && !line.starts_with('#'))
.map(|(line, value)| {
IdentityDID::parse(value).map_err(|source| RootsError::MalformedRoot {
line,
value: value.to_string(),
source,
})
})
.collect()
}
pub fn load_pinned_roots_typed(
store: &dyn ConfigStore,
auths_dir: &Path,
) -> Result<Vec<IdentityDID>, RootsError> {
match store.read(&roots_path(auths_dir))? {
Some(content) => parse_roots_typed(&content),
None => Ok(Vec::new()),
}
}
#[cfg(test)]
mod tests {
use super::*;
struct FsStore;
impl ConfigStore for FsStore {
fn read(&self, path: &Path) -> Result<Option<String>, ConfigStoreError> {
match std::fs::read_to_string(path) {
Ok(content) => Ok(Some(content)),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(ConfigStoreError::Read {
path: path.to_path_buf(),
source: e,
}),
}
}
fn write(&self, path: &Path, content: &str) -> Result<(), ConfigStoreError> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|e| ConfigStoreError::Write {
path: path.to_path_buf(),
source: e,
})?;
}
std::fs::write(path, content).map_err(|e| ConfigStoreError::Write {
path: path.to_path_buf(),
source: e,
})
}
}
#[test]
fn parse_roots_ignores_blanks_and_comments() {
let content = "did:keri:Eaaa\n\n# a comment\n did:keri:Ebbb \n";
assert_eq!(
parse_roots(content),
vec!["did:keri:Eaaa".to_string(), "did:keri:Ebbb".to_string()]
);
}
#[test]
fn typed_roots_parse_valid_did_keri() {
let roots = parse_roots_typed("did:keri:Eaaa\n# c\n did:keri:Ebbb \n").expect("parse");
assert_eq!(
roots.iter().map(|r| r.as_str()).collect::<Vec<_>>(),
["did:keri:Eaaa", "did:keri:Ebbb"]
);
}
#[test]
fn typed_roots_reject_malformed_line_fail_closed() {
let err = parse_roots_typed("did:keri:Eaaa\nnot-a-did\n").expect_err("must fail closed");
assert!(
matches!(err, RootsError::MalformedRoot { line: 2, .. }),
"expected MalformedRoot at line 2, got {err:?}"
);
}
#[test]
fn typed_roots_absent_file_is_empty() {
let tmp = tempfile::TempDir::new().expect("temp dir");
assert!(
load_pinned_roots_typed(&FsStore, tmp.path())
.expect("load")
.is_empty()
);
}
#[test]
fn add_and_membership_roundtrip() {
let tmp = tempfile::TempDir::new().expect("temp dir");
let dir = tmp.path();
let store = FsStore;
assert!(load_pinned_roots(&store, dir).expect("load").is_empty());
assert!(!is_pinned_root(&store, dir, "did:keri:Eroot").expect("check"));
add_pinned_root(&store, dir, "did:keri:Eroot").expect("add");
assert!(is_pinned_root(&store, dir, "did:keri:Eroot").expect("check pinned"));
assert!(!is_pinned_root(&store, dir, "did:keri:Eother").expect("check unpinned"));
add_pinned_root(&store, dir, "did:keri:Eroot").expect("add again");
assert_eq!(load_pinned_roots(&store, dir).expect("load").len(), 1);
add_pinned_root(&store, dir, "did:keri:Esecond").expect("add second");
assert_eq!(load_pinned_roots(&store, dir).expect("load").len(), 2);
}
}