use std::{
env,
fs,
path::PathBuf,
};
use anyhow::{
anyhow,
ensure,
Context as _,
};
use chrono::{
DateTime,
Utc,
};
use tracing::debug;
#[cfg(test)]
mod tests;
#[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize)]
#[cfg_attr(test, derive(serde::Serialize))]
pub struct Identity {
pub display_name: String,
pub email: String,
pub last_modified: DateTime<Utc>,
pub public_key: PublicKey,
}
impl Identity {
pub fn load(name: &str) -> anyhow::Result<Self> {
ensure!(!name.is_empty(), "identity name cannot be empty");
let path = config_dir()
.context("could not find global config dir")?
.join("identities")
.join(name)
.join("identity.toml");
let src = fs::read_to_string(&path)
.with_context(|| format!("failed to read identity from {}", path.display()))?;
Ok(toml::from_str(&src)?)
}
pub fn load_all() -> LoadAll {
LoadAll { dir: None }
}
pub fn list() -> List {
List { dir: None }
}
}
#[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize)]
#[cfg_attr(test, derive(serde::Serialize))]
pub struct PublicKey {
pub version: u64,
pub algorithm: Algorithm,
#[serde(default)]
pub expires: Option<DateTime<Utc>>,
pub signature: String,
pub key: String,
}
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, serde::Deserialize)]
#[cfg_attr(test, derive(serde::Serialize))]
#[repr(u8)]
pub enum Algorithm {
Ed25519,
}
pub fn config_dir() -> Option<PathBuf> {
env::var("PIJUL_CONFIG_DIR")
.map(PathBuf::from)
.ok()
.or_else(|| {
dirs::config_dir().map(|mut cfg| {
cfg.push("pijul");
cfg
})
})
}
#[must_use = "iterators are lazy and do nothing unless consumed"]
pub struct List {
dir: Option<fs::ReadDir>,
}
impl Iterator for List {
type Item = anyhow::Result<String>;
fn next(&mut self) -> Option<Self::Item> {
if let Some(dir) = self.dir.as_mut() {
let entry = dir.next()?;
let item = entry.map_err(Into::into).and_then(|entry| {
let name = entry
.file_name()
.into_string()
.map_err(|os| anyhow!("invalid name for identity: `{os:?}`"))?;
let path = config_dir()
.expect("config dir must be valid")
.join("identities")
.join(&name)
.join("identity.toml");
debug!("probe: {}", path.display());
if path.try_exists()? {
Ok(Some(name))
} else {
Ok(None)
}
});
item.transpose()
} else {
match read_identities_dir() {
Ok(dir) => {
self.dir = Some(dir);
self.next()
},
Err(e) => Some(Err(e)),
}
}
}
}
#[must_use = "iterators are lazy and do nothing unless consumed"]
pub struct LoadAll {
dir: Option<fs::ReadDir>,
}
impl Iterator for LoadAll {
type Item = anyhow::Result<(String, Identity)>;
fn next(&mut self) -> Option<Self::Item> {
if let Some(dir) = self.dir.as_mut() {
let entry = dir.next()?;
let item = entry.map_err(Into::into).and_then(|entry| {
let name = entry
.file_name()
.into_string()
.map_err(|os| anyhow!("invalid name for identity: `{os:?}`"))?;
let id = Identity::load(&name)
.with_context(|| format!("failed to load identity `{name}`"))?;
Ok((name, id))
});
Some(item)
} else {
match read_identities_dir() {
Ok(dir) => {
self.dir = Some(dir);
self.next()
},
Err(e) => Some(Err(e)),
}
}
}
}
fn read_identities_dir() -> anyhow::Result<fs::ReadDir> {
let path = config_dir()
.context("could not find global config dir")?
.join("identities");
fs::read_dir(&path).with_context(|| format!("failed to read directory {}", path.display()))
}