HGDC5V5PNOXYZ4NM66GEQ2AQZ7FHQ4FYWTSFV3Z5Y3MTFVKAJXGQC // Copyright © 2023 Kim Altintop <kim@eagain.io>// SPDX-License-Identifier: GPL-2.0-onlyuse std::{env,fs,path::PathBuf,};use anyhow::{anyhow,ensure,Context as _,};use chrono::{DateTime,Utc,};use log::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().ok_or_else(|| anyhow!("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,}// TODO: Move to some shim for the pijul crate (i.e. stuff not in libpijul)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})})}/// Iterator for [`Identity::list`].#[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)),}}}}/// Iterator for [`Identity::load_all`].#[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().ok_or_else(|| anyhow!("could not find global config dir"))?.join("identities");fs::read_dir(&path).with_context(|| format!("failed to read directory {}", path.display()))}
// Copyright © 2023 Kim Altintop <kim@eagain.io>// SPDX-License-Identifier: GPL-2.0-onlyuse std::{collections::HashMap,ffi::OsString,fs::File,io::Write,};use chrono::{TimeZone as _,Utc,};use once_cell::sync::Lazy;use tempfile::{tempdir,TempDir,};use test_log::test;use super::*;static ALICE: Lazy<Identity> = Lazy::new(|| {Identity {display_name: "Alice".into(),email: "alice@example.com".into(),last_modified: Utc.with_ymd_and_hms(2023, 10, 4, 13, 33, 53).unwrap(),public_key: PublicKey {version: 0,algorithm: Algorithm::Ed25519,expires: None,signature: "2gZ2P25vNVgCPim8XY6GJNJWbBm4vzqZ5g9ti9VffojUUhLybhhV3QycNhyaCa6pPogjsNj9sdgQ2rjgeSnyghz8".into(),key: "FYAsb1jhNwopkb38rrLxK1Ka2UFkJw6mQhXy8iD9bkBx".into()}}});static BOB: Lazy<Identity> = Lazy::new(|| {Identity {display_name: "Bob".into(),email: "bob@example.com".into(),last_modified: Utc.with_ymd_and_hms(2023, 10, 4, 13, 33, 38).unwrap(),public_key: PublicKey {version: 0,algorithm: Algorithm::Ed25519,expires: None,signature: "21GFwPQppUzXaBcfShBYwqrYxJAYgyJrfShS6fmcQ2bcJXasqaQtxYPgzZQy64Dq1rxAPmwzEYTLjEeCDDsLgfrE".into(),key: "D7qbdVjnf4GjH4q6uqVerFWjaypL7wQ7arCfaEcPQDqG".into()}}});struct ConfigDir {#[allow(unused)]tmp: TempDir,env: Option<OsString>,}impl ConfigDir {fn setup() -> anyhow::Result<Self> {let tmp = tempdir()?;let env = env::var_os("PIJUL_CONFIG_DIR");let identities = tmp.path().join("identities");let a = identities.join("a");let b = identities.join("b");for (mut dir, id) in [(a, &*ALICE), (b, &*BOB)] {fs::create_dir_all(&dir)?;let toml = toml::to_string(id)?;dir.push("identity.toml");File::create(dir)?.write_all(toml.as_bytes())?;}env::set_var("PIJUL_CONFIG_DIR", tmp.path());Ok(Self { tmp, env })}}impl Drop for ConfigDir {fn drop(&mut self) {if let Some(prev) = self.env.take() {env::set_var("PIJUL_CONFIG_DIR", prev)}}}#[test]fn list() {let _cfg = ConfigDir::setup().unwrap();let mut ids = Identity::list().collect::<Result<Vec<_>, _>>().unwrap();ids.sort();assert_eq!(vec!["a", "b"], ids)}#[test]fn load_all() {let _cfg = ConfigDir::setup().unwrap();let actual = Identity::load_all().collect::<Result<HashMap<_, _>, _>>().unwrap();let expected = HashMap::from([("a".to_owned(), ALICE.clone()),("b".to_owned(), BOB.clone()),]);assert_eq!(expected, actual);}
// Copyright © 2023 Kim Altintop <kim@eagain.io>// SPDX-License-Identifier: GPL-2.0-onlypub mod pijul;pub enum Identity {Pijul(pijul::Identity),Did,}
[package]name = "nestje-identities"version = "0.1.0"authors.workspace = truelicense.workspace = truerust-version.workspace = trueedition.workspace = true[dependencies]anyhow.workspace = truechrono.workspace = truedirs.workspace = truelog.workspace = trueserde.workspace = truetoml.workspace = true[dev-dependencies]env_logger = "0.10"once_cell.workspace = truetempfile = "3.8"test-log = "0.2.12"
/target
toml = "0.8.2"
][[package]]name = "dirs"version = "5.0.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"dependencies = ["dirs-sys",][[package]]name = "dirs-sys"version = "0.4.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"dependencies = ["libc","option-ext","redox_users","windows-sys",
][[package]]name = "termcolor"version = "1.3.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64"dependencies = ["winapi-util",][[package]]name = "test-log"version = "0.2.12"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "d9601d162c1d77e62c1ea0bc8116cd1caf143ce3af947536c3c9052a1677fe0c"dependencies = ["proc-macro2","quote","syn 1.0.109",
dependencies = ["serde",][[package]]name = "toml"version = "0.8.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d"dependencies = ["serde","serde_spanned","toml_datetime","toml_edit",][[package]]name = "toml_datetime"version = "0.6.3"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"