use crate::context::Context;
use sha2::{Digest, Sha256};
use unicode_normalization::UnicodeNormalization;
pub const LIVE_SERVICE: &str = "Claude Code-credentials";
pub const CRED_FILE: &str = ".credentials.json";
const FALLBACK_ACCOUNT: &str = "claude-code-user";
pub fn account_name(ctx: &Context) -> String {
let from_env = ctx.user.as_deref().filter(|u| !u.is_empty());
match from_env.map(str::to_owned).or_else(login_name) {
Some(name) if is_accepted_account(&name) => name,
_ => FALLBACK_ACCOUNT.to_string(),
}
}
fn login_name() -> Option<String> {
let mut buffer = [0_i8; 1024];
let mut passwd: libc::passwd = unsafe { std::mem::zeroed() };
let mut found: *mut libc::passwd = std::ptr::null_mut();
let code = unsafe {
libc::getpwuid_r(
libc::getuid(),
&raw mut passwd,
buffer.as_mut_ptr().cast(),
buffer.len(),
&raw mut found,
)
};
if code != 0 || found.is_null() || passwd.pw_name.is_null() {
return None;
}
let name = unsafe { std::ffi::CStr::from_ptr(passwd.pw_name) };
name.to_str().ok().map(str::to_owned)
}
fn is_accepted_account(s: &str) -> bool {
!s.is_empty()
&& s.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b'-'))
}
pub fn dir_hash(dir: &str) -> String {
let normalised: String = dir.nfc().collect();
hex::encode(&Sha256::digest(normalised.as_bytes())[..4])
}
pub fn service_for_dir(dir: &str) -> String {
format!("{LIVE_SERVICE}-{}", dir_hash(dir))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_absent_user_falls_back_to_the_passwd_name() {
let Some(expected) = login_name() else {
return; };
let home = std::path::PathBuf::from("/home/x");
assert_eq!(account_name(&Context::new(home.clone())), expected);
assert_eq!(
account_name(&Context::new(home).with_user(String::new())),
expected
);
}
#[test]
fn matches_measured_vectors() {
let cases = [
("/Users/ngoquocdat/.claude", "e80beed8"),
(
"/private/tmp/claude-501/-Users-ngoquocdat/32ac806a-a8b6-4258-8ba1-548995f8d827/scratchpad/m0/s1",
"a4bc5512",
),
(
"/private/tmp/claude-501/-Users-ngoquocdat/32ac806a-a8b6-4258-8ba1-548995f8d827/scratchpad/m0/s2",
"79d45fb9",
),
];
for (path, expected) in cases {
assert_eq!(dir_hash(path), expected, "derivation drifted for {path}");
}
}
#[test]
fn builds_the_full_service_name() {
assert_eq!(
service_for_dir("/Users/ngoquocdat/.claude"),
"Claude Code-credentials-e80beed8"
);
}
#[test]
fn a_trailing_slash_is_a_different_slot() {
assert_ne!(dir_hash("/Users/x/.claude"), dir_hash("/Users/x/.claude/"));
}
#[test]
fn account_names_are_screened_like_claude_code_screens_them() {
for good in ["ngoquocdat", "a.b_c-1", "X"] {
assert!(is_accepted_account(good), "{good} should be accepted");
}
for bad in [
"",
"has space",
"quote\"",
"sl/ash",
"semi;colon",
"dollar$",
] {
assert!(!is_accepted_account(bad), "{bad:?} should be rejected");
}
}
}