use crate::config;
use std::path::{Path, PathBuf};
fn hash_tag(s: &str) -> String {
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for b in s.bytes() {
h ^= b as u64;
h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
format!("{:012x}", h & 0xffff_ffff_ffff)
}
pub fn hub_tag(hub_key: &str) -> String {
hash_tag(hub_key)
}
fn key_material(pubkey: &str) -> String {
let mut it = pubkey.split_whitespace();
match (it.next(), it.next()) {
(Some(algo), Some(body)) => format!("{algo} {body}"),
_ => pubkey.trim().to_string(),
}
}
pub fn key_tag(pubkey: &str) -> String {
hash_tag(&key_material(pubkey))
}
pub fn slug(label: &str) -> String {
let mut out = String::new();
let mut dash = false;
for c in label.chars() {
if c.is_ascii_alphanumeric() || c == '_' {
out.push(c.to_ascii_lowercase());
dash = false;
} else if !dash {
out.push('-');
dash = true;
}
}
let s: String = out.trim_matches('-').chars().take(24).collect();
let s = s.trim_matches('-').to_string();
if s.is_empty() {
"x".into()
} else {
s
}
}
pub fn root() -> anyhow::Result<PathBuf> {
Ok(config::home()?.join(".confer").join("clones"))
}
pub fn hub_dir_name(hub_slug: &str, hub_key: &str) -> String {
format!("{}-{}", slug(hub_slug), hub_tag(hub_key))
}
pub fn role_dir_name(role_id: &str, pubkey: &str) -> String {
format!("{}-{}", slug(role_id), key_tag(pubkey))
}
pub fn split_tag(dir_name: &str) -> Option<(&str, &str)> {
dir_name.rsplit_once('-')
}
pub fn clone_dir(
hub_slug: &str,
hub_key: &str,
role_id: &str,
pubkey: &str,
) -> anyhow::Result<PathBuf> {
Ok(root()?
.join(hub_dir_name(hub_slug, hub_key))
.join(role_dir_name(role_id, pubkey)))
}
pub fn resolve_by_tags(
root: &Path,
hub_tag: &str,
key_tag: &str,
expect_pubkey: Option<&str>,
) -> Option<PathBuf> {
let Ok(hubs) = std::fs::read_dir(root) else {
return None;
};
let mut found: Option<PathBuf> = None;
for h in hubs.flatten() {
if !h.file_type().map(|t| t.is_dir()).unwrap_or(false) {
continue;
}
if !tag_matches(&h.file_name().to_string_lossy(), hub_tag) {
continue;
}
let Ok(roles) = std::fs::read_dir(h.path()) else {
continue;
};
for r in roles.flatten() {
if !r.file_type().map(|t| t.is_dir()).unwrap_or(false) {
continue;
}
if !tag_matches(&r.file_name().to_string_lossy(), key_tag) {
continue;
}
let idf = r.path().join(".confer").join("identity.json");
if !std::fs::symlink_metadata(&idf).map(|m| m.is_file()).unwrap_or(false) {
continue;
}
if let Some(want) = expect_pubkey {
match identity_pubkey(&r.path()) {
Some(have) if same_key(&have, want) => {}
_ => continue,
}
}
if found.is_some() {
return None;
}
found = Some(r.path());
}
}
found
}
pub fn same_key(a: &str, b: &str) -> bool {
key_material(a) == key_material(b)
}
pub fn identity_pubkey(clone: &Path) -> Option<String> {
let txt = std::fs::read_to_string(clone.join(".confer").join("identity.json")).ok()?;
let v: serde_json::Value = serde_json::from_str(&txt).ok()?;
if let Some(pk) = v.get("pubkey").and_then(|x| x.as_str()).filter(|s| !s.is_empty()) {
return Some(pk.to_string());
}
let sk = v.get("signing_key").and_then(|x| x.as_str()).filter(|s| !s.is_empty())?;
pubkey_of_key(Path::new(sk))
}
fn pubkey_of_key(key: &Path) -> Option<String> {
let pubpath = if key.extension().and_then(|e| e.to_str()) == Some("pub") {
key.to_path_buf()
} else {
let mut s = key.as_os_str().to_os_string();
s.push(".pub");
PathBuf::from(s)
};
let pk = std::fs::read_to_string(&pubpath).ok()?.trim().to_string();
(!pk.is_empty()).then_some(pk)
}
pub fn backfill_pubkey(clone: &Path, pubkey: &str) {
let idf = clone.join(".confer").join("identity.json");
let Ok(txt) = std::fs::read_to_string(&idf) else { return };
let Ok(mut v) = serde_json::from_str::<serde_json::Value>(&txt) else { return };
if v.get("pubkey").and_then(|x| x.as_str()).filter(|s| !s.is_empty()).is_some() {
return;
}
if let Some(obj) = v.as_object_mut() {
obj.insert("pubkey".into(), serde_json::Value::String(pubkey.to_string()));
if let Ok(s) = serde_json::to_string_pretty(&v) {
let _ = std::fs::write(&idf, s);
}
}
}
fn tag_matches(dir_name: &str, want: &str) -> bool {
matches!(split_tag(dir_name), Some((_, t)) if t == want && is_tag(t))
}
fn is_tag(s: &str) -> bool {
s.len() == 12 && s.bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
}
pub fn resolve(hub_key: &str, pubkey: &str) -> anyhow::Result<Option<PathBuf>> {
Ok(resolve_by_tags(&root()?, &hub_tag(hub_key), &key_tag(pubkey), Some(pubkey)))
}
pub struct ManagedClone {
pub path: PathBuf,
pub role: String,
pub hub_slug: String,
}
pub fn list() -> Vec<ManagedClone> {
let mut out = Vec::new();
let Ok(root) = root() else {
return out;
};
let hubs = match std::fs::read_dir(&root) {
Ok(rd) => rd,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return out,
Err(e) => {
eprintln!(
"confer: ⚠ cannot read the managed-clone home {} ({e}) — the managed-clone list may be INCOMPLETE.",
root.display()
);
return out;
}
};
for h in hubs.flatten() {
if !h.file_type().map(|t| t.is_dir()).unwrap_or(false) {
continue;
}
let hname = h.file_name().to_string_lossy().to_string();
let hub_slug = split_tag(&hname).map(|(s, _)| s.to_string()).unwrap_or(hname);
let roles = match std::fs::read_dir(h.path()) {
Ok(rd) => rd,
Err(e) => {
eprintln!(
"confer: ⚠ cannot read managed hub dir {} ({e}) — its clones are OMITTED from the list.",
h.path().display()
);
continue;
}
};
for r in roles.flatten() {
if !r.file_type().map(|t| t.is_dir()).unwrap_or(false) {
continue;
}
if !r.path().join(".confer").join("identity.json").is_file() {
continue;
}
let rname = r.file_name().to_string_lossy().to_string();
let role = std::fs::read_to_string(r.path().join(".confer").join("identity.json"))
.ok()
.and_then(|t| serde_json::from_str::<serde_json::Value>(&t).ok())
.and_then(|v| v.get("role").and_then(|x| x.as_str()).map(String::from))
.unwrap_or_else(|| split_tag(&rname).map(|(s, _)| s.to_string()).unwrap_or(rname));
out.push(ManagedClone { path: r.path(), role, hub_slug: hub_slug.clone() });
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
use std::sync::atomic::{AtomicU32, Ordering};
fn tmp() -> PathBuf {
static N: AtomicU32 = AtomicU32::new(0);
let n = N.fetch_add(1, Ordering::Relaxed);
let d = std::env::temp_dir().join(format!("confer-clonehome-{}-{n}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
d
}
#[test]
fn slug_is_readable_but_path_safe() {
assert_eq!(slug("alice"), "alice");
assert_eq!(slug("carol-role"), "carol-role"); assert_eq!(slug("Carol_Role"), "carol_role"); assert_eq!(slug("../../etc/passwd"), "etc-passwd"); assert_eq!(slug("a b\tc"), "a-b-c"); assert_eq!(slug("héllo"), "h-llo"); assert_eq!(slug("--weird--"), "weird"); assert_eq!(slug(""), "x"); assert_eq!(slug("!!!"), "x"); assert!(slug(&"z".repeat(200)).len() <= 24); assert!(!slug("../x").contains('/'));
}
#[test]
fn tag_is_deterministic_hyphen_free_lowercase_hex() {
let a = hub_tag("root-commit-sha-abc");
assert_eq!(a, hub_tag("root-commit-sha-abc")); assert_ne!(a, hub_tag("root-commit-sha-xyz")); assert_eq!(a.len(), 12);
assert!(!a.contains('-')); assert!(a.chars().all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase()));
}
#[test]
fn split_tag_grabs_the_last_part_even_with_hyphenated_slug() {
assert_eq!(split_tag("carol-role-1a2b3c4d5e6f"), Some(("carol-role", "1a2b3c4d5e6f")));
assert_eq!(split_tag("alice-aabbccddeeff"), Some(("alice", "aabbccddeeff")));
assert_eq!(split_tag("x-000000000000"), Some(("x", "000000000000")));
assert_eq!(split_tag("notagseparator"), None); }
#[test]
fn resolve_matches_by_tag_despite_a_relabeled_prefix() {
let root = tmp();
let hub_t = "3f9a1c024b8e";
let key_t = "1a2b3c4d5e6f";
let hub_dir = root.join(format!("team-hub-{hub_t}"));
let role_dir = hub_dir.join(format!("helper-renamed-{key_t}")); std::fs::create_dir_all(role_dir.join(".confer")).unwrap();
std::fs::write(role_dir.join(".confer").join("identity.json"), "{}").unwrap();
std::fs::create_dir_all(hub_dir.join("carol-999999999999")).unwrap();
assert_eq!(resolve_by_tags(&root, hub_t, key_t, None), Some(role_dir));
assert_eq!(resolve_by_tags(&root, hub_t, "ffffffffffff", None), None);
assert_eq!(resolve_by_tags(&root, "ffffffffffff", key_t, None), None);
assert_eq!(resolve_by_tags(&tmp(), hub_t, key_t, None), None);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn resolve_fails_closed_when_two_clones_share_a_key_tag() {
let root = tmp();
let hub_t = "3f9a1c024b8e";
let key_t = "1a2b3c4d5e6f";
let hub_dir = root.join(format!("team-hub-{hub_t}"));
for slug in ["real", "imposter"] {
let d = hub_dir.join(format!("{slug}-{key_t}"));
std::fs::create_dir_all(d.join(".confer")).unwrap();
std::fs::write(d.join(".confer").join("identity.json"), "{}").unwrap();
}
assert_eq!(resolve_by_tags(&root, hub_t, key_t, None), None, "ambiguous tag must fail closed");
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn resolve_verifies_the_pubkey_not_just_the_tag() {
let root = tmp();
let hub_t = "aaaaaaaaaaaa";
let key_t = "bbbbbbbbbbbb";
let hub_dir = root.join(format!("h-{hub_t}"));
for (slug, pk) in [("real", "ssh-ed25519 REALKEY x"), ("evil", "ssh-ed25519 EVILKEY y")] {
let d = hub_dir.join(format!("{slug}-{key_t}"));
std::fs::create_dir_all(d.join(".confer")).unwrap();
std::fs::write(d.join(".confer").join("identity.json"), format!("{{\"pubkey\":\"{pk}\"}}")).unwrap();
}
let hit = resolve_by_tags(&root, hub_t, key_t, Some("ssh-ed25519 REALKEY other-comment"));
assert_eq!(hit, Some(hub_dir.join(format!("real-{key_t}"))));
assert_eq!(resolve_by_tags(&root, hub_t, key_t, Some("ssh-ed25519 STRANGER z")), None);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn resolve_rejects_a_pubkeyless_clone_when_a_key_is_expected() {
let root = tmp();
let hub_t = "cccccccccccc";
let key_t = "dddddddddddd";
let d = root.join(format!("h-{hub_t}")).join(format!("planted-{key_t}"));
std::fs::create_dir_all(d.join(".confer")).unwrap();
std::fs::write(d.join(".confer").join("identity.json"), "{}").unwrap(); assert_eq!(resolve_by_tags(&root, hub_t, key_t, Some("ssh-ed25519 SOMEKEY x")), None);
assert!(resolve_by_tags(&root, hub_t, key_t, None).is_some());
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn resolve_recovers_pubkey_from_signing_key_for_a_legacy_clone() {
let root = tmp();
let hub_t = "eeeeeeeeeeee";
let key_t = "ffffffffffff";
let keydir = tmp();
std::fs::create_dir_all(&keydir).unwrap();
let keypath = keydir.join("agentkey");
std::fs::write(keydir.join("agentkey.pub"), "ssh-ed25519 LEGACYKEY host\n").unwrap();
let d = root.join(format!("h-{hub_t}")).join(format!("legacy-{key_t}"));
std::fs::create_dir_all(d.join(".confer")).unwrap();
std::fs::write(
d.join(".confer").join("identity.json"),
format!("{{\"role\":\"x\",\"signing_key\":\"{}\"}}", keypath.display()),
)
.unwrap();
assert_eq!(identity_pubkey(&d).as_deref(), Some("ssh-ed25519 LEGACYKEY host"));
assert_eq!(resolve_by_tags(&root, hub_t, key_t, Some("ssh-ed25519 LEGACYKEY other")), Some(d));
assert_eq!(resolve_by_tags(&root, hub_t, key_t, Some("ssh-ed25519 OTHERKEY z")), None);
let _ = std::fs::remove_dir_all(&root);
let _ = std::fs::remove_dir_all(&keydir);
}
#[test]
fn backfill_pubkey_writes_missing_and_preserves_present() {
let root = tmp();
let d = root.join("c");
std::fs::create_dir_all(d.join(".confer")).unwrap();
let idf = d.join(".confer").join("identity.json");
std::fs::write(&idf, "{\"role\":\"x\"}").unwrap();
backfill_pubkey(&d, "ssh-ed25519 KKK x");
let v: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&idf).unwrap()).unwrap();
assert_eq!(v.get("pubkey").and_then(|x| x.as_str()), Some("ssh-ed25519 KKK x"));
assert_eq!(v.get("role").and_then(|x| x.as_str()), Some("x"), "existing fields preserved");
backfill_pubkey(&d, "ssh-ed25519 DIFFERENT y");
let v2: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&idf).unwrap()).unwrap();
assert_eq!(v2.get("pubkey").and_then(|x| x.as_str()), Some("ssh-ed25519 KKK x"));
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn resolve_ignores_a_foreign_dir_with_a_non_tag_suffix() {
let root = tmp();
let hub_dir = root.join("backup-2024");
std::fs::create_dir_all(hub_dir.join("stuff-notahextag")).unwrap();
assert_eq!(resolve_by_tags(&root, "2024", "notahextag", None), None);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn resolve_skips_a_bare_tag_colliding_dir_that_is_not_a_clone() {
let root = tmp();
let hub_t = "aaaaaaaaaaaa";
let key_t = "bbbbbbbbbbbb";
let hub_dir = root.join(format!("h-{hub_t}"));
std::fs::create_dir_all(hub_dir.join(format!("imposter-{key_t}"))).unwrap(); assert_eq!(resolve_by_tags(&root, hub_t, key_t, None), None);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn key_tag_is_environment_stable_and_ignores_the_comment() {
let a = key_tag("ssh-ed25519 AAAAC3NzBODYMATERIAL helper@host-a");
let b = key_tag("ssh-ed25519 AAAAC3NzBODYMATERIAL someone@else");
assert_eq!(a, b); assert_ne!(a, key_tag("ssh-ed25519 DIFFERENTBODYMATERIAL x")); assert!(!a.contains('-') && a.len() == 12);
}
#[test]
fn dir_names_compose_and_round_trip() {
let name = hub_dir_name("Team Hub!", "some-root-sha");
let (prefix, tag) = split_tag(&name).unwrap();
assert_eq!(prefix, "team-hub"); assert_eq!(tag, hub_tag("some-root-sha")); }
}