pub(crate) const KEY_SUFFIX: &str = ".localharness.key";
pub(crate) fn key_home_dir_from(
localharness_home: Option<&str>,
userprofile: Option<&str>,
home: Option<&str>,
) -> Option<std::path::PathBuf> {
if let Some(h) = localharness_home.filter(|s| !s.is_empty()) {
return Some(std::path::PathBuf::from(h));
}
let h = userprofile
.filter(|s| !s.is_empty())
.or_else(|| home.filter(|s| !s.is_empty()))?;
Some(std::path::Path::new(h).join(".localharness").join("keys"))
}
pub(crate) fn key_home_dir() -> Option<std::path::PathBuf> {
let lh = std::env::var("LOCALHARNESS_HOME").ok();
let up = std::env::var("USERPROFILE").ok();
let home = std::env::var("HOME").ok();
key_home_dir_from(lh.as_deref(), up.as_deref(), home.as_deref())
}
pub(crate) fn home_key_path(name: &str) -> Option<std::path::PathBuf> {
key_home_dir().map(|d| d.join(format!("{name}{KEY_SUFFIX}")))
}
pub(crate) fn cwd_key_path(name: &str) -> String {
format!("{name}{KEY_SUFFIX}")
}
pub(crate) fn pick_key_read_path(
cwd: String,
cwd_exists: bool,
home: Option<String>,
home_exists: bool,
) -> Option<String> {
if cwd_exists {
return Some(cwd);
}
match home {
Some(h) if home_exists => Some(h),
_ => None,
}
}
pub(crate) fn resolve_key_read_path(name: &str) -> Option<String> {
let cwd = cwd_key_path(name);
let cwd_exists = std::path::Path::new(&cwd).exists();
let home = home_key_path(name);
let home_exists = home.as_ref().map(|p| p.exists()).unwrap_or(false);
pick_key_read_path(
cwd,
cwd_exists,
home.map(|p| p.to_string_lossy().into_owned()),
home_exists,
)
}
pub(crate) fn key_write_path(name: &str) -> String {
if let Some(home) = home_key_path(name) {
if let Some(dir) = home.parent() {
if std::fs::create_dir_all(dir).is_ok() {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700));
}
return home.to_string_lossy().into_owned();
}
}
}
cwd_key_path(name)
}
pub(crate) fn identity_key_files() -> Result<Vec<String>, String> {
use std::collections::BTreeMap;
let mut by_name: BTreeMap<String, String> = BTreeMap::new();
let mut scan = |dir: &std::path::Path, absolute: bool| {
if let Ok(rd) = std::fs::read_dir(dir) {
for e in rd.flatten() {
if let Ok(f) = e.file_name().into_string() {
if let Some(stem) = f.strip_suffix(KEY_SUFFIX) {
let path = if absolute {
dir.join(&f).to_string_lossy().into_owned()
} else {
f.clone()
};
by_name.insert(stem.to_string(), path);
}
}
}
}
};
if let Some(home) = key_home_dir() {
scan(&home, true);
}
scan(std::path::Path::new("."), false);
Ok(by_name.into_values().collect())
}
pub(crate) fn gitignore_already_covers(existing: &str, key_file: &str) -> bool {
existing.lines().any(|l| {
let t = l.trim();
t == "*.localharness.key" || t == key_file
})
}
pub(crate) fn key_is_in_cwd(key_file: &str) -> bool {
!key_file.contains('/') && !key_file.contains('\\')
}
pub(crate) fn secure_key_file(key_file: &str) -> bool {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(key_file, std::fs::Permissions::from_mode(0o600));
}
if !key_is_in_cwd(key_file) {
return false;
}
match std::fs::read_to_string(".gitignore") {
Ok(existing) => {
if gitignore_already_covers(&existing, key_file) {
false
} else {
let sep = if existing.is_empty() || existing.ends_with('\n') { "" } else { "\n" };
std::fs::write(".gitignore", format!("{existing}{sep}*.localharness.key\n")).is_ok()
}
}
Err(_) => std::fs::write(".gitignore", "*.localharness.key\n").is_ok(),
}
}
pub(crate) fn resolve_caller_file(name: Option<&str>) -> Result<String, String> {
if let Some(n) = name {
return Ok(resolve_key_read_path(n).unwrap_or_else(|| cwd_key_path(n)));
}
let mut found = identity_key_files()?;
match found.len() {
0 => Err(
"no identity key — run `localharness create <yourname>` first, \
or pass --as <name>"
.to_string(),
),
1 => Ok(found.remove(0)),
_ => Err(format!(
"multiple identities ({}) — pick one with --as <name>",
found.join(", ")
)),
}
}
pub(crate) fn resolve_caller_label(name: Option<&str>) -> Result<String, String> {
if let Some(n) = name {
return Ok(n.to_string());
}
let file = resolve_caller_file(None)?;
let base = std::path::Path::new(&file)
.file_name()
.and_then(|s| s.to_str())
.unwrap_or(&file);
Ok(base.strip_suffix(KEY_SUFFIX).unwrap_or(base).to_string())
}
pub(crate) fn resolve_caller_key(name: Option<&str>) -> Result<(String, String), String> {
let file = resolve_caller_file(name)?;
let key_hex = std::fs::read_to_string(&file)
.map_err(|_| match name {
Some(n) => format!("no identity key at {file} — run `localharness create {n}` first"),
None => format!("cannot read {file}"),
})?
.trim()
.to_string();
if key_hex.is_empty() {
return Err(format!(
"{file} is empty — recreate it with `localharness create <name>`"
));
}
Ok((file, key_hex))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn gitignore_already_covers_detects_wildcard_and_exact() {
assert!(gitignore_already_covers("target/\n*.localharness.key\n", "alice.localharness.key"));
assert!(gitignore_already_covers("alice.localharness.key\n", "alice.localharness.key"));
assert!(gitignore_already_covers(" *.localharness.key \n", "x.localharness.key"));
assert!(!gitignore_already_covers("target/\nnode_modules/\n", "alice.localharness.key"));
assert!(!gitignore_already_covers("bob.localharness.key\n", "alice.localharness.key"));
assert!(!gitignore_already_covers("", "alice.localharness.key"));
}
#[test]
fn pick_key_read_path_prefers_cwd_then_home() {
let cwd = "alice.localharness.key".to_string();
let home = Some("/home/me/.localharness/keys/alice.localharness.key".to_string());
assert_eq!(
pick_key_read_path(cwd.clone(), true, home.clone(), true),
Some(cwd.clone())
);
assert_eq!(
pick_key_read_path(cwd.clone(), true, None, false),
Some(cwd.clone())
);
assert_eq!(
pick_key_read_path(cwd.clone(), false, home.clone(), true),
home.clone()
);
assert_eq!(pick_key_read_path(cwd.clone(), false, home.clone(), false), None);
assert_eq!(pick_key_read_path(cwd, false, None, false), None);
}
#[test]
fn key_is_in_cwd_distinguishes_bare_from_pathful() {
assert!(key_is_in_cwd("alice.localharness.key"));
assert!(!key_is_in_cwd("/home/me/.localharness/keys/alice.localharness.key"));
assert!(!key_is_in_cwd("C:\\Users\\me\\.localharness\\keys\\a.localharness.key"));
}
#[test]
fn key_home_dir_from_honors_override_and_falls_back() {
use std::path::PathBuf;
assert_eq!(
key_home_dir_from(Some("/custom/keys"), Some("/u/prof"), Some("/u/home")),
Some(PathBuf::from("/custom/keys"))
);
assert_eq!(
key_home_dir_from(None, Some("/u/prof"), None),
Some(PathBuf::from("/u/prof").join(".localharness").join("keys"))
);
assert_eq!(
key_home_dir_from(None, None, Some("/u/home")),
Some(PathBuf::from("/u/home").join(".localharness").join("keys"))
);
assert_eq!(
key_home_dir_from(Some(""), Some(""), Some("/u/home")),
Some(PathBuf::from("/u/home").join(".localharness").join("keys"))
);
assert_eq!(key_home_dir_from(None, None, None), None);
}
}