HGDC5V5PNOXYZ4NM66GEQ2AQZ7FHQ4FYWTSFV3Z5Y3MTFVKAJXGQC
// Copyright © 2023 Kim Altintop <kim@eagain.io>
// SPDX-License-Identifier: GPL-2.0-only
use 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-only
use 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-only
pub mod pijul;
pub enum Identity {
Pijul(pijul::Identity),
Did,
}
[package]
name = "nestje-identities"
version = "0.1.0"
authors.workspace = true
license.workspace = true
rust-version.workspace = true
edition.workspace = true
[dependencies]
anyhow.workspace = true
chrono.workspace = true
dirs.workspace = true
log.workspace = true
serde.workspace = true
toml.workspace = true
[dev-dependencies]
env_logger = "0.10"
once_cell.workspace = true
tempfile = "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"