use std::collections::HashMap;
use std::io::Write;
use std::sync::atomic::{AtomicI32, Ordering};
use std::sync::{Mutex, OnceLock};
use crate::ported::zsh_h::{nameddir, ND_USERNAME, PRINT_LIST, PRINT_NAMEONLY};
#[allow(non_upper_case_globals)]
pub static allusersadded: AtomicI32 = AtomicI32::new(0);
pub fn createnameddirtable() { let _ = nameddirtab();
allusersadded.store(0, Ordering::Relaxed); }
pub fn emptynameddirtable() { if let Ok(mut t) = nameddirtab().lock() { t.clear(); }
allusersadded.store(0, Ordering::Relaxed); }
pub fn fillnameddirtable() { if allusersadded.load(Ordering::Relaxed) != 0 { return;
}
#[cfg(unix)]
unsafe {
libc::setpwent(); loop {
let pw = libc::getpwent();
if pw.is_null() {
break;
}
if crate::ported::utils::errflag.load(Ordering::Relaxed) != 0 {
break;
}
let name = std::ffi::CStr::from_ptr((*pw).pw_name)
.to_string_lossy()
.into_owned();
let dir = std::ffi::CStr::from_ptr((*pw).pw_dir)
.to_string_lossy()
.into_owned();
crate::ported::utils::adduserdir(&name, &dir, ND_USERNAME, true);
}
libc::endpwent(); }
allusersadded.store(1, Ordering::Relaxed); }
pub fn addnameddirnode(nam: &str, mut nd: nameddir) { nd.diff = nd.dir.len() as i32 - nam.len() as i32;
if let Ok(mut t) = nameddirtab().lock() {
nd.node.nam = nam.to_string();
t.insert(nam.to_string(), nd);
}
}
pub fn removenameddirnode(nam: &str) -> Option<nameddir> { let removed = nameddirtab().lock().ok().and_then(|mut t| t.remove(nam));
if removed.is_some() { }
removed }
pub fn freenameddirnode(hn: nameddir) { }
pub fn printnameddirnode(hn: &nameddir, printflags: i32) { let stdout = std::io::stdout();
let mut out = stdout.lock();
if (printflags & PRINT_NAMEONLY) != 0 { let _ = writeln!(out, "{}", hn.node.nam); return;
}
if (printflags & PRINT_LIST) != 0 { let _ = write!(out, "hash -d "); if hn.node.nam.starts_with('-') { let _ = write!(out, "-- "); }
}
let _ = write!(
out,
"{}={}",
crate::ported::utils::quotedzputs(&hn.node.nam), crate::ported::utils::quotedzputs(&hn.dir), );
let _ = writeln!(out); }
static NAMEDDIRTAB_INNER: OnceLock<Mutex<HashMap<String, nameddir>>> = OnceLock::new();
#[allow(non_snake_case)]
pub fn nameddirtab() -> &'static Mutex<HashMap<String, nameddir>> { NAMEDDIRTAB_INNER.get_or_init(|| Mutex::new(HashMap::new()))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ported::zsh_h::{hashnode, nameddir};
static NAMEDDIR_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn fresh_table() {
if let Ok(mut t) = nameddirtab().lock() {
t.clear();
}
allusersadded.store(0, Ordering::Relaxed);
}
fn make_nd(name: &str, dir: &str, flags: i32) -> nameddir {
nameddir {
node: hashnode {
next: None,
nam: name.to_string(),
flags,
},
dir: dir.to_string(),
diff: 0,
}
}
#[test]
fn addnameddirnode_sets_diff_and_inserts() {
let _g = NAMEDDIR_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
fresh_table();
addnameddirnode("p", make_nd("p", "/home/user/projects", 0));
let t = nameddirtab().lock().unwrap();
let nd = t.get("p").expect("entry inserted");
assert_eq!(nd.dir, "/home/user/projects");
assert_eq!(nd.diff, 18);
assert_eq!(nd.node.nam, "p");
}
#[test]
fn removenameddirnode_returns_node_and_clears_entry() {
let _g = NAMEDDIR_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
fresh_table();
addnameddirnode("k", make_nd("k", "/tmp/k", 0));
assert!(nameddirtab().lock().unwrap().contains_key("k"));
let dropped = removenameddirnode("k");
assert!(dropped.is_some());
assert_eq!(dropped.unwrap().dir, "/tmp/k");
assert!(!nameddirtab().lock().unwrap().contains_key("k"));
}
#[test]
fn removenameddirnode_missing_returns_none() {
let _g = NAMEDDIR_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
fresh_table();
assert!(removenameddirnode("absent").is_none());
}
#[test]
fn emptynameddirtable_clears_and_resets_allusersadded() {
let _g = NAMEDDIR_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
fresh_table();
addnameddirnode("a", make_nd("a", "/a", 0));
addnameddirnode("b", make_nd("b", "/b", 0));
allusersadded.store(1, Ordering::Relaxed);
emptynameddirtable();
assert!(nameddirtab().lock().unwrap().is_empty());
assert_eq!(allusersadded.load(Ordering::Relaxed), 0);
}
#[test]
fn createnameddirtable_resets_allusersadded() {
let _g = NAMEDDIR_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
allusersadded.store(1, Ordering::Relaxed);
createnameddirtable();
assert_eq!(allusersadded.load(Ordering::Relaxed), 0);
}
#[test]
fn nd_username_value_matches_zsh_h() {
assert_eq!(ND_USERNAME, 1 << 1);
}
#[test]
fn fillnameddirtable_short_circuits_when_allusersadded_set() {
let _g = NAMEDDIR_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
allusersadded.store(1, Ordering::Relaxed);
fillnameddirtable();
assert_eq!(allusersadded.load(Ordering::Relaxed), 1,
"c:98 conditional must early-exit; flag remains 1 unchanged");
}
#[test]
fn addnameddirnode_diff_can_be_negative() {
let _g = NAMEDDIR_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
fresh_table();
addnameddirnode("longname", make_nd("longname", "/x", 0));
let t = nameddirtab().lock().unwrap();
let nd = t.get("longname").expect("entry inserted");
assert_eq!(nd.diff, -6, "c:125 — signed subtraction may underflow zero");
}
#[test]
fn addnameddirnode_overwrites_existing_entry() {
let _g = NAMEDDIR_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
fresh_table();
addnameddirnode("p", make_nd("p", "/old", 0));
addnameddirnode("p", make_nd("p", "/new/longer/path", 0));
let t = nameddirtab().lock().unwrap();
let nd = t.get("p").expect("entry present");
assert_eq!(nd.dir, "/new/longer/path", "c:127 — addhashnode replaces");
assert_eq!(nd.diff, "/new/longer/path".len() as i32 - 1);
assert_eq!(t.len(), 1, "must not accumulate duplicate keys");
}
#[test]
fn addnameddirnode_diff_zero_when_lengths_equal() {
let _g = NAMEDDIR_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
fresh_table();
addnameddirnode("abcde", make_nd("abcde", "/etc/", 0));
let t = nameddirtab().lock().unwrap();
let nd = t.get("abcde").expect("entry inserted");
assert_eq!(nd.diff, 0,
"len(\"/etc/\") == len(\"abcde\") == 5 → diff = 0");
}
#[test]
fn emptynameddirtable_resets_table_and_flag() {
let _g = NAMEDDIR_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
fresh_table();
addnameddirnode("a", make_nd("a", "/x", 0));
addnameddirnode("b", make_nd("b", "/y", 0));
allusersadded.store(1, Ordering::Relaxed);
emptynameddirtable();
assert!(nameddirtab().lock().unwrap().is_empty(),
"emptynameddirtable must clear the table");
assert_eq!(allusersadded.load(Ordering::Relaxed), 0,
"emptynameddirtable must reset allusersadded to 0");
}
#[test]
fn removenameddirnode_returns_some_for_present_and_none_for_absent() {
let _g = NAMEDDIR_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
fresh_table();
addnameddirnode("here", make_nd("here", "/tmp/here", 0));
let removed = removenameddirnode("here");
assert!(removed.is_some(), "present entry must return Some");
assert_eq!(removed.unwrap().dir, "/tmp/here");
assert!(removenameddirnode("here").is_none(),
"second remove must return None");
assert!(removenameddirnode("never_was").is_none());
}
#[test]
fn removenameddirnode_actually_removes_from_table() {
let _g = NAMEDDIR_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
fresh_table();
addnameddirnode("a", make_nd("a", "/a", 0));
addnameddirnode("b", make_nd("b", "/b", 0));
assert_eq!(nameddirtab().lock().unwrap().len(), 2);
removenameddirnode("a");
let t = nameddirtab().lock().unwrap();
assert_eq!(t.len(), 1);
assert!(!t.contains_key("a"));
assert!(t.contains_key("b"), "removing 'a' must NOT touch 'b'");
}
#[test]
fn createnameddirtable_is_idempotent() {
let _g = NAMEDDIR_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
fresh_table();
createnameddirtable();
addnameddirnode("preserved", make_nd("preserved", "/x", 0));
createnameddirtable(); let t = nameddirtab().lock().unwrap();
assert!(t.contains_key("preserved"),
"second createnameddirtable must NOT wipe existing entries");
}
#[test]
fn freenameddirnode_consumes_node() {
let nd = make_nd("doomed", "/tmp", 0);
freenameddirnode(nd);
}
}