use std::path::{Path, PathBuf};
use super::key_file::create_dir_owner_only;
use super::{KeyFileError, NodeKey, Purpose};
use crate::profile::Profile;
pub const DEFAULT_IDENTITY_NAME: &str = "default";
const OLD_KEY_FILE: &str = "identity.key";
const MAX_NAME_BYTES: usize = 64;
pub fn default_identity_dir() -> Option<PathBuf> {
let set = |var: &str| std::env::var_os(var).filter(|v| !v.is_empty());
let home = || set("HOME").map(PathBuf::from);
let data = if cfg!(target_os = "macos") {
home()?.join("Library/Application Support")
} else {
set("XDG_DATA_HOME")
.map(PathBuf::from)
.or_else(|| home().map(|h| h.join(".local/share")))?
};
Some(data.join("macula").join("identity"))
}
pub fn identity_path(dir: &Path, name: &str, profile: Profile) -> Result<PathBuf, KeyFileError> {
if !valid_name(name) {
return Err(KeyFileError::IdentityName(name.to_string()));
}
Ok(dir.join(format!("{name}.{}.key", profile.name())))
}
fn valid_name(name: &str) -> bool {
let named = |c: &u8| c.is_ascii_lowercase() || c.is_ascii_digit();
let bytes = name.as_bytes();
bytes.first().is_some_and(named)
&& bytes.len() <= MAX_NAME_BYTES
&& bytes.iter().all(|c| named(c) || *c == b'-' || *c == b'_')
}
impl NodeKey {
pub fn stored_identity(
dir: &Path,
name: &str,
profile: Profile,
) -> Result<(NodeKey, PathBuf), KeyFileError> {
let path = identity_path(dir, name, profile)?;
move_old_key(dir)?;
match NodeKey::load_or_create(&path, profile) {
Ok(key) => Ok((key, path)),
Err(reason) => Err(KeyFileError::StoredKey {
path,
reason: Box::new(reason),
}),
}
}
}
fn move_old_key(dir: &Path) -> Result<(), KeyFileError> {
let old = dir.parent().unwrap_or(Path::new("")).join(OLD_KEY_FILE);
match std::fs::symlink_metadata(&old) {
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
Err(e) => return Err(old_key(&old, e.into())),
Ok(_) => {}
}
let profile = old_key_profile(&old).map_err(|e| old_key(&old, e))?;
let to = identity_path(dir, DEFAULT_IDENTITY_NAME, profile)?;
create_dir_owner_only(dir, true).map_err(|e| old_key(&old, e))?;
match std::fs::hard_link(&old, &to) {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => same_key(&old, &to)?,
Err(e) => return Err(old_key(&old, e.into())),
}
std::fs::remove_file(&old).map_err(|e| old_key(&old, e.into()))
}
fn old_key_profile(old: &Path) -> Result<Profile, KeyFileError> {
match NodeKey::load(old, Purpose::Identity, Profile::PqPure) {
Ok(_) => Ok(Profile::PqPure),
Err(KeyFileError::WrongProfile(found)) => {
NodeKey::load(old, Purpose::Identity, found).map(|_| found)
}
Err(e) => Err(e),
}
}
fn same_key(old: &Path, to: &Path) -> Result<(), KeyFileError> {
let read = |p: &Path| std::fs::read(p).map_err(|e| old_key(old, e.into()));
if read(old)? == read(to)? {
return Ok(());
}
Err(KeyFileError::OldKeyPlaceTaken {
from: old.to_path_buf(),
to: to.to_path_buf(),
})
}
fn old_key(from: &Path, reason: KeyFileError) -> KeyFileError {
KeyFileError::OldKey {
from: from.to_path_buf(),
reason: Box::new(reason),
}
}