#![allow(non_camel_case_types)]
use crate::ported::zsh_h::{alias, BANG_TOK, CASE, COPROC, DINBRACK, DOLOOP, DONE, ELIF, ELSE, ESAC, FI, FOR, FOREACH, FUNC, IF, INBRACE_TOK, NOCORRECT, OUTBRACE_TOK, REPEAT, SELECT, THEN, TIME, TYPESET, UNTIL, WHILE, ZEND};
use std::collections::HashMap;
use std::fs;
use std::io;
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
use std::sync::atomic::Ordering;
use crate::compat::zgetcwd;
use crate::hist::{hashchar, hist_ring};
use crate::jobs::getsigidx;
use crate::signals::{settrap, unsettrap};
use crate::text::{getpermtext, zoutputtab};
use crate::utils::{nicezputs, quotedzputs, xsymlink, zputs, ztrcmp, zwarn};
use crate::zsh_h::{cmdnam, hashnode, hashtable, reswd, shfunc, ALIAS_GLOBAL, ALIAS_SUFFIX, DISABLED, EF_RUN, HASHED, HIST_DUP, HIST_TMPSTORE, PM_CUR_FPATH, PM_KSHSTORED, PM_LOADDIR, PM_TAGGED, PM_TAGGED_LOCAL, PM_UNALIASED, PM_UNDEFINED, PM_ZSHSTORED, PRINT_LIST, PRINT_NAMEONLY, PRINT_WHENCE_CSH, PRINT_WHENCE_FUNCDEF, PRINT_WHENCE_SIMPLE, PRINT_WHENCE_VERBOSE, PRINT_WHENCE_WORD, ZSIG_FUNC};
pub fn hasher(str: &str) -> u32 {
let mut hashval: u32 = 0;
for c in str.bytes() {
hashval = hashval.wrapping_add(hashval.wrapping_shl(5).wrapping_add(c as u32));
}
hashval
}
pub fn newhashtable(size: i32, name: &str) -> (String, i32) {
(name.to_string(), size)
}
pub fn deletehashtable<T>(ht: &mut HashMap<String, T>) {
ht.clear();
}
pub fn addhashnode<T>(ht: &mut HashMap<String, T>, nam: &str, value: T) {
ht.insert(nam.to_string(), value);
}
pub fn addhashnode2<T>(ht: &mut HashMap<String, T>, nam: &str, nodeptr: T) -> Option<T> {
ht.insert(nam.to_string(), nodeptr)
}
pub fn gethashnode<'a, T: HashNodeFlags>(
ht: &'a HashMap<String, T>,
nam: &str,
) -> Option<&'a T> {
ht.get(nam).filter(|t| !t.is_disabled())
}
impl cmdnam_table {
pub fn new() -> Self {
Self {
table: HashMap::new(),
path_checked_index: 0,
path: Vec::new(),
hash_executables_only: false,
}
}
pub fn set_path(&mut self, path: Vec<String>) {
self.path = path;
self.path_checked_index = 0;
}
pub fn set_hash_executables_only(&mut self, value: bool) {
self.hash_executables_only = value;
}
pub fn add(&mut self, cmd: cmdnam) {
self.table.insert(cmd.node.nam.clone(), cmd);
}
pub fn get(&self, name: &str) -> Option<&cmdnam> {
self.table
.get(name)
.filter(|c| (c.node.flags & DISABLED as i32) == 0)
}
pub fn get_including_disabled(&self, name: &str) -> Option<&cmdnam> {
self.table.get(name)
}
pub fn remove(&mut self, name: &str) -> Option<cmdnam> {
self.table.remove(name)
}
pub fn clear(&mut self) {
self.table.clear();
self.path_checked_index = 0;
}
pub fn len(&self) -> usize {
self.table.len()
}
pub fn is_empty(&self) -> bool {
self.table.is_empty()
}
pub fn hash_dir(&mut self, dir: &str, dir_index: usize) {
if dir.starts_with('.') || dir.is_empty() {
return;
}
let Ok(entries) = fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let Ok(name) = entry.file_name().into_string() else {
continue;
};
if self.table.contains_key(&name) {
continue;
}
let path = entry.path();
let should_add = if self.hash_executables_only {
#[cfg(unix)]
{
path.metadata()
.map(|m| m.is_file() && m.permissions().mode() & 0o111 != 0)
.unwrap_or(false)
}
#[cfg(not(unix))]
{
path.is_file()
}
} else {
true
};
if should_add {
let segment = self
.path
.get(dir_index)
.cloned()
.unwrap_or_else(|| dir.to_string());
self.table
.insert(name.clone(), cmdnam_unhashed(&name, vec![segment]));
}
}
}
pub fn fill(&mut self) {
for i in self.path_checked_index..self.path.len() {
let dir = self.path[i].clone();
self.hash_dir(&dir, i);
}
self.path_checked_index = self.path.len();
}
pub fn iter(&self) -> impl Iterator<Item = (&String, &cmdnam)> {
self.table.iter()
}
pub fn get_full_path(&self, name: &str) -> Option<PathBuf> {
let cmd = self.table.get(name)?;
if (cmd.node.flags & DISABLED as i32) != 0 {
return None;
}
if (cmd.node.flags & HASHED as i32) != 0 {
if let Some(ref s) = cmd.cmd {
return Some(PathBuf::from(s));
}
}
if let Some(ref segs) = cmd.name {
if let Some(seg) = segs.first() {
let mut path = PathBuf::from(seg);
path.push(name);
return Some(path);
}
}
None
}
}
impl Default for cmdnam_table {
fn default() -> Self {
Self::new()
}
}
pub fn gethashnode2<'a, T>(ht: &'a HashMap<String, T>, nam: &str) -> Option<&'a T> {
ht.get(nam)
}
pub fn removehashnode<T>(ht: &mut HashMap<String, T>, nam: &str) -> Option<T> {
ht.remove(nam)
}
pub fn disablehashnode<T: HashNodeFlags>(hn: &mut HashMap<String, T>, flags: &str) -> bool {
hn.get_mut(flags)
.map(|node| {
node.set_disabled(true);
true
})
.unwrap_or(false) }
impl shfunc_table {
pub fn new() -> Self {
Self {
table: HashMap::new(),
}
}
pub fn add(&mut self, func: shfunc) -> Option<shfunc> {
self.table
.insert(func.node.nam.clone(), Box::new(func))
.map(|b| *b)
}
pub fn get(&self, name: &str) -> Option<&shfunc> {
self.table
.get(name)
.map(|b| b.as_ref())
.filter(|f| (f.node.flags & DISABLED as i32) == 0)
}
pub fn get_including_disabled(&self, name: &str) -> Option<&shfunc> {
self.table.get(name).map(|b| b.as_ref())
}
pub fn get_mut(&mut self, name: &str) -> Option<&mut shfunc> {
self.table
.get_mut(name)
.map(|b| b.as_mut())
.filter(|f| (f.node.flags & DISABLED as i32) == 0)
}
pub fn remove(&mut self, name: &str) -> Option<shfunc> {
self.table.remove(name).map(|b| *b)
}
pub fn contains_key(&self, name: &str) -> bool {
self.table.contains_key(name)
}
pub fn addnode(&mut self, shf: *mut shfunc) {
if shf.is_null() {
return;
}
let boxed = unsafe { Box::from_raw(shf) };
let name = boxed.node.nam.clone();
let _ = self.table.insert(name, boxed);
}
pub fn getnode(&self, name: &str) -> *mut shfunc {
self.table
.get(name)
.filter(|b| (b.node.flags & DISABLED as i32) == 0)
.map(|b| b.as_ref() as *const shfunc as *mut shfunc)
.unwrap_or(std::ptr::null_mut())
}
pub fn getnode2(&self, name: &str) -> *mut shfunc {
self.table
.get(name)
.map(|b| b.as_ref() as *const shfunc as *mut shfunc)
.unwrap_or(std::ptr::null_mut())
}
pub fn disable(&mut self, name: &str) -> bool {
if let Some(func) = self.table.get_mut(name) {
func.node.flags |= DISABLED as i32;
true
} else {
false
}
}
pub fn enable(&mut self, name: &str) -> bool {
if let Some(func) = self.table.get_mut(name) {
func.node.flags &= !(DISABLED as i32);
true
} else {
false
}
}
pub fn len(&self) -> usize {
self.table.len()
}
pub fn is_empty(&self) -> bool {
self.table.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = (&String, &shfunc)> {
self.table.iter().map(|(k, b)| (k, b.as_ref()))
}
pub fn iter_sorted(&self) -> Vec<(&String, &shfunc)> {
let mut entries: Vec<(&String, &shfunc)> =
self.table.iter().map(|(k, b)| (k, b.as_ref())).collect();
entries.sort_by(|a, b| a.0.cmp(b.0));
entries
}
pub fn clear(&mut self) {
self.table.clear();
}
}
impl Default for shfunc_table {
fn default() -> Self {
Self::new()
}
}
pub const RESWDS: &[(&str, i32)] = &[
("!", BANG_TOK),
("[[", DINBRACK),
("{", INBRACE_TOK),
("}", OUTBRACE_TOK),
("case", CASE),
("coproc", COPROC),
("declare", TYPESET),
("do", DOLOOP),
("done", DONE),
("elif", ELIF),
("else", ELSE),
("end", ZEND),
("esac", ESAC),
("export", TYPESET),
("fi", FI),
("float", TYPESET),
("for", FOR),
("foreach", FOREACH),
("function", FUNC),
("if", IF),
("integer", TYPESET),
("local", TYPESET),
("nocorrect", NOCORRECT),
("readonly", TYPESET),
("repeat", REPEAT),
("select", SELECT),
("then", THEN),
("time", TIME),
("typeset", TYPESET),
("until", UNTIL),
("while", WHILE),
];
pub fn enablehashnode<T: HashNodeFlags>(hn: &mut HashMap<String, T>, flags: &str) -> bool {
hn.get_mut(flags)
.map(|node| {
node.set_disabled(false);
true
})
.unwrap_or(false) }
impl reswd_table {
pub fn new() -> Self {
let mut table = HashMap::new();
let words: [(&str, i32); 31] = [
("!", BANG_TOK), ("[[", DINBRACK), ("{", INBRACE_TOK), ("}", OUTBRACE_TOK), ("case", CASE), ("coproc", COPROC), ("declare", TYPESET), ("do", DOLOOP), ("done", DONE), ("elif", ELIF), ("else", ELSE), ("end", ZEND), ("esac", ESAC), ("export", TYPESET), ("fi", FI), ("float", TYPESET), ("for", FOR), ("foreach", FOREACH), ("function", FUNC), ("if", IF), ("integer", TYPESET), ("local", TYPESET), ("nocorrect", NOCORRECT), ("readonly", TYPESET), ("repeat", REPEAT), ("select", SELECT), ("then", THEN), ("time", TIME), ("typeset", TYPESET), ("until", UNTIL), ("while", WHILE), ];
debug_assert_eq!(words.len(), RESWDS.len());
for (name, token) in words {
table.insert(
name.to_string(),
reswd {
node: hashnode {
next: None,
nam: name.to_string(),
flags: 0,
},
token,
},
);
}
Self { table }
}
pub fn get(&self, name: &str) -> Option<&reswd> {
self.table
.get(name)
.filter(|r| (r.node.flags & DISABLED as i32) == 0)
}
pub fn get_including_disabled(&self, name: &str) -> Option<&reswd> {
self.table.get(name)
}
pub fn disable(&mut self, name: &str) -> bool {
if let Some(rw) = self.table.get_mut(name) {
rw.node.flags |= DISABLED as i32;
true
} else {
false
}
}
pub fn enable(&mut self, name: &str) -> bool {
if let Some(rw) = self.table.get_mut(name) {
rw.node.flags &= !(DISABLED as i32);
true
} else {
false
}
}
pub fn is_reserved(&self, name: &str) -> bool {
self.get(name).is_some()
}
pub fn iter(&self) -> impl Iterator<Item = (&String, &reswd)> {
self.table.iter()
}
}
impl Default for reswd_table {
fn default() -> Self {
Self::new()
}
}
pub fn hnamcmp(ap: &str, bp: &str) -> std::cmp::Ordering {
ztrcmp(ap, bp) }
pub fn scanmatchtable<T: HashNodeFlags, F: FnMut(&str, &T)>(
ht: &HashMap<String, T>,
pattern: Option<&str>,
sorted: bool,
flags1: u32,
flags2: u32,
mut func: F,
) -> i32 {
let mut entries: Vec<(&String, &T)> = ht.iter().collect();
if sorted {
entries.sort_by(|a, b| hnamcmp(a.0, b.0)); }
let mut match_count = 0;
for (name, node) in entries {
if let Some(p) = pattern {
if !simple_glob_match(p, name) {
continue;
}
}
let f = node.flags();
if flags1 != 0 && (f & flags1) == 0 {
continue;
}
if flags2 != 0 && (f & flags2) != 0 {
continue;
}
func(name, node);
match_count += 1;
}
match_count
}
impl alias_table {
pub fn new() -> Self {
Self {
table: indexmap::IndexMap::new(),
}
}
pub fn with_defaults() -> Self {
let mut table = Self::new();
table.add(createaliasnode("run-help", "man", 0)); table.add(createaliasnode("which-command", "whence", 0)); table
}
pub fn add(&mut self, alias: alias) -> Option<alias> {
self.table.insert(alias.node.nam.clone(), alias)
}
pub fn get(&self, name: &str) -> Option<&alias> {
self.table
.get(name)
.filter(|a| (a.node.flags & DISABLED as i32) == 0)
}
pub fn get_including_disabled(&self, name: &str) -> Option<&alias> {
self.table.get(name)
}
pub fn get_mut(&mut self, name: &str) -> Option<&mut alias> {
self.table
.get_mut(name)
.filter(|a| (a.node.flags & DISABLED as i32) == 0)
}
pub fn remove(&mut self, name: &str) -> Option<alias> {
self.table.remove(name)
}
pub fn disable(&mut self, name: &str) -> bool {
if let Some(alias) = self.table.get_mut(name) {
alias.node.flags |= DISABLED as i32;
true
} else {
false
}
}
pub fn enable(&mut self, name: &str) -> bool {
if let Some(alias) = self.table.get_mut(name) {
alias.node.flags &= !(DISABLED as i32);
true
} else {
false
}
}
pub fn len(&self) -> usize {
self.table.len()
}
pub fn is_empty(&self) -> bool {
self.table.is_empty()
}
pub fn clear(&mut self) {
self.table.clear();
}
pub fn iter(&self) -> impl Iterator<Item = (&String, &alias)> {
self.table.iter()
}
pub fn iter_sorted(&self) -> Vec<(&String, &alias)> {
let mut entries: Vec<_> = self.table.iter().collect();
entries.sort_by(|a, b| a.0.cmp(b.0));
entries
}
}
impl Default for alias_table {
fn default() -> Self {
Self::new()
}
}
pub fn scanhashtable<T: HashNodeFlags, F: FnMut(&str, &T)>(
ht: &HashMap<String, T>,
sorted: bool,
flags1: u32,
flags2: u32,
func: F,
) -> i32 {
scanmatchtable(ht, None, sorted, flags1, flags2, func)
}
pub fn expandhashtable<T>(ht: &mut HashMap<String, T>) {
let want = ht.len() * 2;
ht.reserve(want.saturating_sub(ht.capacity()));
}
pub fn resizehashtable<T>(ht: &mut HashMap<String, T>, newsize: i32) {
let need = newsize.max(0) as usize;
if need > ht.capacity() {
ht.reserve(need - ht.capacity());
}
}
pub fn emptyhashtable<T>(ht: &mut HashMap<String, T>) {
ht.clear();
}
pub fn printhashtabinfo<T>(name: &str, ht: &HashMap<String, T>) -> String {
format!(
"name of table : {}\nsize of nodes[] : {}\nnumber of nodes : {}",
name,
ht.capacity(),
ht.len()
)
}
pub fn bin_hashinfo(
_nam: &str,
_args: &[String], _ops: &crate::ported::zsh_h::options,
_func: i32,
) -> i32 {
let banner = "----------------------------------------------------";
println!("{}", banner);
{
let tab = cmdnamtab_lock().read().expect("cmdnamtab poisoned");
println!("name of table : cmdnamtab");
println!("number of nodes : {}", tab.len());
}
println!("{}", banner);
{
let tab = shfunctab_lock().read().expect("shfunctab poisoned");
println!("name of table : shfunctab");
println!("number of nodes : {}", tab.len());
}
println!("{}", banner);
{
let tab = aliastab_lock().read().expect("aliastab poisoned");
println!("name of table : aliastab");
println!("number of nodes : {}", tab.len());
}
println!("{}", banner);
0
}
pub fn createcmdnamtable() {
let _ = cmdnamtab_lock();
}
pub fn emptycmdnamtable() {
cmdnamtab_lock()
.write()
.expect("cmdnamtab poisoned")
.clear();
}
pub fn hashdir(dir: &str, dir_index: usize) {
cmdnamtab_lock()
.write()
.expect("cmdnamtab poisoned")
.hash_dir(dir, dir_index);
}
pub fn fillcmdnamtable(path: &[String]) {
let mut tab = cmdnamtab_lock().write().expect("cmdnamtab poisoned");
for (idx, dir) in path.iter().enumerate() {
tab.hash_dir(dir, idx);
}
}
pub fn freecmdnamnode(hn: &str) {
cmdnamtab_lock()
.write()
.expect("cmdnamtab poisoned")
.remove(hn);
}
pub fn printcmdnamnode(hn: &cmdnam, printflags: i32) {
if (printflags & PRINT_WHENCE_WORD) != 0 {
let kind = if (hn.node.flags & HASHED as i32) != 0 {
"hashed"
} else {
"command"
};
println!("{}: {}", hn.node.nam, kind); return; }
if (printflags & (PRINT_WHENCE_CSH | PRINT_WHENCE_SIMPLE)) != 0 {
let mut so = std::io::stdout();
if (hn.node.flags & HASHED as i32) != 0 {
if let Some(cmd) = &hn.cmd {
let _ = zputs(cmd, &mut so); }
println!(); } else {
if let Some(name_arr) = &hn.name {
if let Some(first) = name_arr.first() {
let _ = zputs(first, &mut so); }
}
print!("/"); let _ = zputs(&hn.node.nam, &mut so); println!(); }
return; }
if (printflags & PRINT_WHENCE_VERBOSE) != 0 {
let mut so = std::io::stdout();
if (hn.node.flags & HASHED as i32) != 0 {
let _ = nicezputs(&hn.node.nam, &mut so); print!(" is hashed to "); if let Some(cmd) = &hn.cmd {
let _ = nicezputs(cmd, &mut so); }
println!(); } else {
let _ = nicezputs(&hn.node.nam, &mut so); print!(" is "); if let Some(name_arr) = &hn.name {
if let Some(first) = name_arr.first() {
let _ = nicezputs(first, &mut so); }
}
print!("/"); let _ = nicezputs(&hn.node.nam, &mut so); println!(); }
return; }
if (printflags & PRINT_LIST) != 0 {
print!("hash "); if hn.node.nam.starts_with('-') {
print!("-- "); }
}
if (hn.node.flags & HASHED as i32) != 0 {
print!("{}", quotedzputs(&hn.node.nam)); print!("="); if let Some(cmd) = &hn.cmd {
print!("{}", quotedzputs(cmd)); }
println!(); } else {
print!("{}", quotedzputs(&hn.node.nam)); print!("="); if let Some(name_arr) = &hn.name {
if let Some(first) = name_arr.first() {
print!("{}", quotedzputs(first)); }
}
print!("/"); print!("{}", quotedzputs(&hn.node.nam)); println!(); }
}
pub fn createshfunctable() {
let _ = shfunctab_lock();
}
pub fn removeshfuncnode(nam: &str) -> Option<shfunc> {
if let Some(sig_part) = nam.strip_prefix("TRAP") {
if let Some(sig) = getsigidx(sig_part) {
crate::ported::signals::removetrap(sig);
}
}
shfunctab_lock()
.write()
.expect("shfunctab poisoned")
.remove(nam)
}
pub fn disableshfuncnode(hn: &str) {
{
let mut tab = shfunctab_lock().write().expect("shfunctab poisoned");
tab.disable(hn);
}
if let Some(sig_part) = hn.strip_prefix("TRAP") {
if let Some(sig) = getsigidx(sig_part) {
unsettrap(sig);
}
}
}
pub fn enableshfuncnode(hn: &str) {
{
let mut tab = shfunctab_lock().write().expect("shfunctab poisoned");
tab.enable(hn);
}
if let Some(sig_part) = hn.strip_prefix("TRAP") {
if let Some(sig) = getsigidx(sig_part) {
let _ = settrap(sig, None,ZSIG_FUNC);
}
}
}
pub fn freeshfuncnode(hn: &str) {
shfunctab_lock()
.write()
.expect("shfunctab poisoned")
.remove(hn);
}
pub fn printshfuncnode(hn: &shfunc, printflags: i32) {
if (printflags & PRINT_NAMEONLY) != 0
|| ((printflags & PRINT_WHENCE_SIMPLE) != 0
&& (printflags & PRINT_WHENCE_FUNCDEF) == 0)
{
let mut so = std::io::stdout();
let _ = zputs(&hn.node.nam, &mut so); println!(); return; }
if (printflags & (PRINT_WHENCE_VERBOSE | PRINT_WHENCE_WORD)) != 0
&& (printflags & PRINT_WHENCE_FUNCDEF) == 0
{
let mut so = std::io::stdout();
let _ = nicezputs(&hn.node.nam, &mut so); let msg = if (printflags & PRINT_WHENCE_WORD) != 0 {
": function" } else if (hn.node.flags & PM_UNDEFINED as i32) != 0 {
" is an autoload shell function" } else {
" is a shell function" };
print!("{}", msg);
if (printflags & PRINT_WHENCE_VERBOSE) != 0 {
if let Some(filename) = &hn.filename {
print!(" from "); print!("{}", quotedzputs(filename)); if (hn.node.flags & PM_LOADDIR as i32) != 0 {
print!("/"); print!("{}", quotedzputs(&hn.node.nam)); }
}
}
println!(); return; }
print!("{}", quotedzputs(&hn.node.nam));
let has_body_source = hn.body.as_deref().is_some_and(|b| !b.is_empty());
if hn.funcdef.is_some() || has_body_source || (hn.node.flags & PM_UNDEFINED as i32) != 0 {
print!(" () {{\n"); let _ = zoutputtab(&mut io::stdout()); let mut t: Option<String>;
if (hn.node.flags & PM_UNDEFINED as i32) != 0 {
println!("{} undefined", hashchar.load(Ordering::Relaxed) as u8 as char); let _ = zoutputtab(&mut io::stdout()); t = None;
} else if let Some(fd) = hn.funcdef.as_ref() {
t = Some(getpermtext(fd.clone(), None, 1)); } else {
t = hn.body.clone().map(|s| {
let mut s = s.trim().to_string();
if s.starts_with('{') {
s = s[1..].trim_start().to_string();
}
if s.ends_with('}') {
s.pop();
s = s.trim_end().to_string();
}
if s.ends_with(';') {
s.pop();
s = s.trim_end().to_string();
}
s
});
}
if (hn.node.flags & (PM_TAGGED | PM_TAGGED_LOCAL) as i32) != 0 {
println!("{} traced", hashchar.load(Ordering::Relaxed) as u8 as char); let _ = zoutputtab(&mut io::stdout()); }
if t.is_none() {
let fopt: &[u8] = b"UtTkzc"; let flgs: [u32; 6] = [
PM_UNALIASED,
PM_TAGGED,
PM_TAGGED_LOCAL,
PM_KSHSTORED,
PM_ZSHSTORED,
PM_CUR_FPATH,
];
let mut so = std::io::stdout();
let _ = zputs("builtin autoload -X", &mut so); for fl in 0..fopt.len() {
if (hn.node.flags & flgs[fl] as i32) != 0 {
print!("{}", fopt[fl] as char); }
}
if let Some(filename) = &hn.filename {
if (hn.node.flags & PM_LOADDIR as i32) != 0 {
print!(" "); let _ = zputs(filename, &mut so); }
}
} else {
let body = t.take().unwrap();
let mut so = std::io::stdout();
let _ = zputs(&body, &mut so); let ef_run = hn
.funcdef
.as_ref()
.map(|fd| (fd.flags & EF_RUN) != 0)
.unwrap_or(false);
if ef_run {
println!(); let _ = zoutputtab(&mut io::stdout()); print!("{}", quotedzputs(&hn.node.nam)); print!(" \"$@\""); }
}
print!("\n}}"); } else {
print!(" () {{ }}"); }
if let Some(redir) = &hn.redir {
let t = getpermtext(redir.clone(), None, 1); if !t.is_empty() {
let mut so = std::io::stdout();
let _ = zputs(&t, &mut so); }
}
println!(); }
pub fn scanmatchshfunc<F>(pattern: Option<&str>, mut func: F) -> i32
where
F: FnMut(&str, &shfunc),
{
let tab = shfunctab_lock().read().expect("shfunctab poisoned");
let mut count = 0;
for (name, entry) in tab.iter() {
let matches = match pattern {
None => true,
Some(p) => simple_glob_match(p, name),
};
if matches {
func(name, entry);
count += 1;
}
}
count
}
pub fn scanshfunc<F>(func: F) -> i32
where
F: FnMut(&str, &shfunc),
{
scanmatchshfunc(None, func)
}
pub fn printshfuncexpand(hn: &shfunc, printflags: i32, expand: i32) {
let save_expand: i32; save_expand = crate::text::TEXT_EXPAND_TABS.load(Ordering::Relaxed); crate::text::TEXT_EXPAND_TABS.store(expand, Ordering::Relaxed); printshfuncnode(hn, printflags); crate::text::TEXT_EXPAND_TABS.store(save_expand, Ordering::Relaxed); }
pub fn getshfuncfile(shf: &str) -> Option<String> {
let tab = shfunctab_lock().read().expect("shfunctab poisoned");
tab.get_including_disabled(shf)
.and_then(|f| f.filename.clone())
}
pub fn createreswdtable() {
let _ = reswdtab_lock();
}
pub fn printreswdnode(hn: &reswd, printflags: i32) {
if (printflags & PRINT_WHENCE_WORD) != 0 {
println!("{}: reserved", hn.node.nam); return; }
if (printflags & PRINT_WHENCE_CSH) != 0 {
println!("{}: shell reserved word", hn.node.nam); return; }
if (printflags & PRINT_WHENCE_VERBOSE) != 0 {
println!("{} is a reserved word", hn.node.nam); return; }
println!("{}", hn.node.nam); }
pub fn createaliastable(ht: &mut hashtable) {
fn cmpnodes_strcmp(a: &str, b: &str) -> i32 {
a.cmp(b) as i32
}
ht.hash = Some(hasher); ht.emptytable = None; ht.filltable = None; ht.cmpnodes = Some(cmpnodes_strcmp); ht.addnode = None; ht.getnode = None; ht.getnode2 = None; ht.removenode = None; ht.disablenode = None; ht.enablenode = None; ht.freenode = None; ht.printnode = None; }
pub trait HashNodeFlags {
fn flags(&self) -> u32;
fn set_disabled(&mut self, disabled: bool);
fn is_disabled(&self) -> bool {
self.flags() & (DISABLED as u32) != 0
}
}
impl HashNodeFlags for alias {
fn flags(&self) -> u32 {
self.node.flags as u32
}
fn set_disabled(&mut self, disabled: bool) {
if disabled {
self.node.flags |= DISABLED as i32;
} else {
self.node.flags &= !(DISABLED as i32);
}
}
}
impl HashNodeFlags for shfunc {
fn flags(&self) -> u32 {
self.node.flags as u32
}
fn set_disabled(&mut self, disabled: bool) {
if disabled {
self.node.flags |= DISABLED as i32;
} else {
self.node.flags &= !(DISABLED as i32);
}
}
}
impl HashNodeFlags for cmdnam {
fn flags(&self) -> u32 {
self.node.flags as u32
}
fn set_disabled(&mut self, disabled: bool) {
if disabled {
self.node.flags |= DISABLED as i32;
} else {
self.node.flags &= !(DISABLED as i32);
}
}
}
impl HashNodeFlags for reswd {
fn flags(&self) -> u32 {
self.node.flags as u32
}
fn set_disabled(&mut self, disabled: bool) {
if disabled {
self.node.flags |= DISABLED as i32;
} else {
self.node.flags &= !(DISABLED as i32);
}
}
}
pub fn createaliastables() {
let mut tab = aliastab_lock().write().expect("aliastab poisoned");
tab.add(createaliasnode("run-help", "man", 0)); tab.add(createaliasnode("which-command", "whence", 0)); drop(tab);
let _ = sufaliastab_lock();
}
pub fn createaliasnode(name: &str, text: &str, flags: u32) -> alias {
alias {
node: hashnode {
next: None,
nam: name.to_string(),
flags: flags as i32,
},
text: text.to_string(),
inuse: 0,
}
}
pub fn freealiasnode(hn: &str) {
let mut tab = aliastab_lock().write().expect("aliastab poisoned");
tab.remove(hn);
}
pub fn printaliasnode(hn: &alias, printflags: i32) {
if (printflags & PRINT_NAMEONLY) != 0 {
let mut so = std::io::stdout();
let _ = zputs(&hn.node.nam, &mut so); println!(); return; }
if (printflags & PRINT_WHENCE_WORD) != 0 {
if (hn.node.flags & ALIAS_SUFFIX as i32) != 0 {
println!("{}: suffix alias", hn.node.nam); } else if (hn.node.flags & ALIAS_GLOBAL as i32) != 0 {
println!("{}: global alias", hn.node.nam); } else {
println!("{}: alias", hn.node.nam); }
return; }
if (printflags & PRINT_WHENCE_SIMPLE) != 0 {
let mut so = std::io::stdout();
let _ = zputs(&hn.text, &mut so); println!(); return; }
if (printflags & PRINT_WHENCE_CSH) != 0 {
let mut so = std::io::stdout();
let _ = nicezputs(&hn.node.nam, &mut so); print!(": "); if (hn.node.flags & ALIAS_SUFFIX as i32) != 0 {
print!("suffix "); } else if (hn.node.flags & ALIAS_GLOBAL as i32) != 0 {
print!("globally "); }
print!("aliased to "); let _ = nicezputs(&hn.text, &mut so); println!(); return; }
if (printflags & PRINT_WHENCE_VERBOSE) != 0 {
let mut so = std::io::stdout();
let _ = nicezputs(&hn.node.nam, &mut so); print!(" is a"); if (hn.node.flags & ALIAS_SUFFIX as i32) != 0 {
print!(" suffix"); } else if (hn.node.flags & ALIAS_GLOBAL as i32) != 0 {
print!(" global"); } else {
print!("n"); }
print!(" alias for "); let _ = nicezputs(&hn.text, &mut so); println!(); return; }
if (printflags & PRINT_LIST) != 0 {
if hn.node.nam.contains('=') {
zwarn(&format!(
"invalid alias '{}' encountered while printing aliases",
hn.node.nam
));
return; }
print!("alias "); if (hn.node.flags & ALIAS_SUFFIX as i32) != 0 {
print!("-s "); } else if (hn.node.flags & ALIAS_GLOBAL as i32) != 0 {
print!("-g "); }
if hn.node.nam.starts_with('-') || hn.node.nam.starts_with('+') {
print!("-- "); }
}
print!("{}", quotedzputs(&hn.node.nam)); print!("="); print!("{}", quotedzputs(&hn.text)); println!(); }
pub fn createhisttable() {
let _ = histtab_lock();
}
pub fn histhasher(s: &str) -> u32 {
#[inline]
fn is_inblank_narrow(c: char) -> bool {
c == ' ' || c == '\t'
}
let mut hashval: u32 = 0;
let mut chars = s.chars().peekable();
while let Some(&c) = chars.peek() {
if is_inblank_narrow(c) {
chars.next();
} else {
break;
}
}
while let Some(c) = chars.next() {
if is_inblank_narrow(c) {
while let Some(&next) = chars.peek() {
if is_inblank_narrow(next) {
chars.next();
} else {
break;
}
}
if chars.peek().is_some() {
hashval = hashval.wrapping_add(hashval.wrapping_shl(5).wrapping_add(' ' as u32));
}
} else {
hashval = hashval.wrapping_add(hashval.wrapping_shl(5).wrapping_add(c as u32));
}
}
hashval
}
pub fn emptyhisttable() {
histtab_lock().write().expect("histtab poisoned").clear();
let has_ring = !crate::ported::hist::hist_ring.lock().unwrap().is_empty();
if has_ring {
crate::ported::hist::histremovedups(); }
}
pub fn histstrcmp(s1: &str, s2: &str, reduce_blanks: bool) -> std::cmp::Ordering {
#[inline]
fn is_inblank_narrow(c: char) -> bool {
c == ' ' || c == '\t'
}
let s1 = s1.trim_start_matches(is_inblank_narrow);
let s2 = s2.trim_start_matches(is_inblank_narrow);
if reduce_blanks {
return s1.cmp(s2);
}
let mut c1 = s1.chars().peekable();
let mut c2 = s2.chars().peekable();
loop {
let ch1 = c1.peek().copied();
let ch2 = c2.peek().copied();
match (ch1, ch2) {
(None, None) => return std::cmp::Ordering::Equal, (None, Some(c)) => {
if is_inblank_narrow(c) {
while c2.peek().copied().map(is_inblank_narrow).unwrap_or(false) {
c2.next();
}
if c2.peek().is_none() {
return std::cmp::Ordering::Equal;
}
}
return std::cmp::Ordering::Less;
}
(Some(c), None) => {
if is_inblank_narrow(c) {
while c1.peek().copied().map(is_inblank_narrow).unwrap_or(false) {
c1.next();
}
if c1.peek().is_none() {
return std::cmp::Ordering::Equal;
}
}
return std::cmp::Ordering::Greater;
}
(Some(ch1), Some(ch2)) => {
let ws1 = is_inblank_narrow(ch1);
let ws2 = is_inblank_narrow(ch2);
if ws1 && ws2 {
while c1.peek().copied().map(is_inblank_narrow).unwrap_or(false) {
c1.next();
}
while c2.peek().copied().map(is_inblank_narrow).unwrap_or(false) {
c2.next();
}
} else if ws1 {
while c1.peek().copied().map(is_inblank_narrow).unwrap_or(false) {
c1.next();
}
if c1.peek().is_none() {
return std::cmp::Ordering::Less;
}
return std::cmp::Ordering::Less;
} else if ws2 {
while c2.peek().copied().map(is_inblank_narrow).unwrap_or(false) {
c2.next();
}
if c2.peek().is_none() {
return std::cmp::Ordering::Greater;
}
return std::cmp::Ordering::Greater;
} else if ch1 != ch2 {
return ch1.cmp(&ch2); } else {
c1.next();
c2.next();
}
}
}
}
}
pub fn addhistnode(nam: &str, event_id: i32) -> Option<i32> {
histtab_lock()
.write()
.expect("histtab poisoned")
.insert(nam.to_string(), event_id)
}
pub fn freehistnode(nodeptr: &str) {
histtab_lock()
.write()
.expect("histtab poisoned")
.remove(nodeptr);
}
pub fn freehistdata(idx: usize, unlink: i32) {
let mut ring = hist_ring.lock().unwrap();
let he = match ring.get(idx) {
Some(h) => h,
None => return,
}; let nam = he.node.nam.clone();
let flags = he.node.flags as u32;
if (flags & (HIST_DUP | HIST_TMPSTORE)) == 0 {
let mut tab = histtab_lock().write().expect("histtab poisoned"); tab.remove(&nam);
}
if unlink != 0 {
ring.remove(idx); let new_ct = ring.len() as i64;
drop(ring);
crate::ported::hist::histlinect.store(new_ct, std::sync::atomic::Ordering::SeqCst);
}
}
pub fn dircache_set(name: &mut Option<String>, value: Option<&str>) {
let mut cache = dircache_lock().lock().expect("dircache poisoned");
if value.is_none() {
let key = match name.as_deref() {
None => return, Some(s) => s.to_string(),
};
if cache.is_empty() {
*name = None; return; }
if let Some(idx) = cache.iter().position(|e| e.name == key) {
cache[idx].refs -= 1; if cache[idx].refs == 0 {
cache.remove(idx); DIRCACHE_LASTENTRY.store(usize::MAX, Ordering::SeqCst); }
*name = None; return; }
*name = None; } else {
let mut v = value.unwrap().to_string();
if !v.starts_with('/') {
let cwd = zgetcwd(); v = format!("{}/{}", cwd, v); if let Some(resolved) = xsymlink(&v) {
v = resolved; } }
let last_idx = DIRCACHE_LASTENTRY.load(Ordering::SeqCst);
if last_idx != usize::MAX && last_idx < cache.len() && cache[last_idx].name == v {
*name = Some(cache[last_idx].name.clone()); cache[last_idx].refs += 1; return; }
if cache.is_empty() {
cache.push(dircache_entry {
name: v.clone(),
refs: 1,
}); DIRCACHE_LASTENTRY.store(0usize, Ordering::SeqCst);
*name = Some(v);
return;
}
if let Some(idx) = cache.iter().position(|e| e.name == v) {
*name = Some(cache[idx].name.clone()); cache[idx].refs += 1; DIRCACHE_LASTENTRY.store(idx, Ordering::SeqCst);
return;
}
cache.push(dircache_entry {
name: v.clone(),
refs: 1,
});
let new_idx = cache.len() - 1;
DIRCACHE_LASTENTRY.store(new_idx, Ordering::SeqCst);
*name = Some(v);
}
}
#[allow(non_camel_case_types)]
#[derive(Debug, Clone)]
pub struct dircache_entry {
pub name: String, pub refs: i32, }
#[derive(Debug)]
pub struct cmdnam_table {
table: HashMap<String, cmdnam>,
path_checked_index: usize,
path: Vec<String>,
hash_executables_only: bool,
}
#[derive(Debug)]
pub struct shfunc_table {
table: HashMap<String, Box<shfunc>>,
}
#[derive(Debug)]
pub struct reswd_table {
table: HashMap<String, reswd>,
}
#[derive(Debug)]
pub struct alias_table {
table: indexmap::IndexMap<String, alias>,
}
static DIRCACHE_INNER: std::sync::OnceLock<std::sync::Mutex<Vec<dircache_entry>>> =
std::sync::OnceLock::new();
static DIRCACHE_LASTENTRY: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(usize::MAX);
pub fn cmdnam_hashed(name: &str, path: &str) -> cmdnam {
cmdnam {
node: hashnode {
next: None,
nam: name.to_string(),
flags: HASHED as i32,
},
name: None,
cmd: Some(path.to_string()),
}
}
pub fn cmdnam_unhashed(name: &str, path_segments: Vec<String>) -> cmdnam {
cmdnam {
node: hashnode {
next: None,
nam: name.to_string(),
flags: 0,
},
name: Some(path_segments),
cmd: None,
}
}
pub fn shfunc_with_body(name: &str, body: &str) -> shfunc {
shfunc {
node: hashnode {
next: None,
nam: name.to_string(),
flags: 0,
},
filename: None,
lineno: 0,
funcdef: None,
redir: None,
sticky: None,
body: Some(body.to_string()),
}
}
pub fn shfunc_autoload(name: &str) -> shfunc {
shfunc {
node: hashnode {
next: None,
nam: name.to_string(),
flags: PM_UNDEFINED as i32,
},
filename: None,
lineno: 0,
funcdef: None,
redir: None,
sticky: None,
body: None,
}
}
pub fn cmdnamtab_lock() -> &'static std::sync::RwLock<cmdnam_table> {
static CMDNAMTAB: std::sync::OnceLock<std::sync::RwLock<cmdnam_table>> =
std::sync::OnceLock::new();
CMDNAMTAB.get_or_init(|| std::sync::RwLock::new(cmdnam_table::new()))
}
pub static pathchecked: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
pub fn aliastab_lock() -> &'static std::sync::RwLock<alias_table> {
static ALIASTAB: std::sync::OnceLock<std::sync::RwLock<alias_table>> =
std::sync::OnceLock::new();
ALIASTAB.get_or_init(|| std::sync::RwLock::new(alias_table::with_defaults()))
}
pub fn sufaliastab_lock() -> &'static std::sync::RwLock<alias_table> {
static SUFALIASTAB: std::sync::OnceLock<std::sync::RwLock<alias_table>> =
std::sync::OnceLock::new();
SUFALIASTAB.get_or_init(|| std::sync::RwLock::new(alias_table::new()))
}
pub fn reswdtab_lock() -> &'static std::sync::RwLock<reswd_table> {
static reswdTAB: std::sync::OnceLock<std::sync::RwLock<reswd_table>> =
std::sync::OnceLock::new();
reswdTAB.get_or_init(|| std::sync::RwLock::new(reswd_table::new()))
}
pub fn histtab_lock() -> &'static std::sync::RwLock<HashMap<String, i32>> {
static HISTTAB: std::sync::OnceLock<std::sync::RwLock<HashMap<String, i32>>> =
std::sync::OnceLock::new();
HISTTAB.get_or_init(|| std::sync::RwLock::new(HashMap::new()))
}
pub fn shfunctab_lock() -> &'static std::sync::RwLock<shfunc_table> {
static shfuncTAB: std::sync::OnceLock<std::sync::RwLock<shfunc_table>> =
std::sync::OnceLock::new();
shfuncTAB.get_or_init(|| std::sync::RwLock::new(shfunc_table::new()))
}
fn simple_glob_match(pattern: &str, name: &str) -> bool {
crate::ported::pattern::patcompile(
pattern,
crate::ported::zsh_h::PAT_HEAPDUP as i32,
None,
)
.map_or(false, |p| crate::ported::pattern::pattry(&p, name))
}
pub fn dircache_lock() -> &'static std::sync::Mutex<Vec<dircache_entry>> {
DIRCACHE_INNER.get_or_init(|| std::sync::Mutex::new(Vec::new()))
}
#[cfg(test)]
mod tests {
use std::cmp::Ordering;
use super::*;
#[test]
fn test_hasher() {
let _g = crate::test_util::global_state_lock();
assert_eq!(hasher(""), 0);
assert_ne!(hasher("test"), 0);
assert_eq!(hasher("test"), hasher("test"));
assert_ne!(hasher("test"), hasher("Test"));
}
#[test]
fn hnamcmp_uses_ztrcmp_meta_aware_compare() {
let _g = crate::test_util::global_state_lock();
assert_eq!(hnamcmp("apple", "banana"), Ordering::Less);
assert_eq!(hnamcmp("banana", "apple"), Ordering::Greater);
assert_eq!(hnamcmp("equal", "equal"), Ordering::Equal);
assert_eq!(hnamcmp("", "a"), Ordering::Less);
assert_eq!(hnamcmp("a", ""), Ordering::Greater);
let meta_a_bytes: Vec<u8> = vec![0x83, 0x41]; let meta_a = unsafe { std::str::from_utf8_unchecked(&meta_a_bytes) };
assert_eq!(
hnamcmp("a", meta_a),
Ordering::Equal,
"c:345 — Meta-encoded 'a' (0x83 0x41) compares equal to real 'a'"
);
}
#[test]
fn test_histhasher() {
let _g = crate::test_util::global_state_lock();
assert_eq!(histhasher(" hello world "), histhasher("hello world"));
assert_ne!(histhasher("hello world"), histhasher("helloworld"));
}
#[test]
fn histhasher_inblank_is_narrow_space_tab_only() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
histhasher("\t hello"),
histhasher("hello"),
"c:1369 — leading space+tab stripped before mixing"
);
assert_eq!(
histhasher("a \t b"),
histhasher("a b"),
"c:1373 — interior inblank runs collapse to single space"
);
assert_ne!(
histhasher("a\nb"),
histhasher("a b"),
"c:50 — newline is NOT inblank; hashes as its own char"
);
assert_ne!(
histhasher("a\rb"),
histhasher("ab"),
"CR not in inblank; must mix as a character, not collapse"
);
assert_ne!(
histhasher("a\u{00A0}b"),
histhasher("ab"),
"NBSP not in inblank; must mix as a character, not collapse"
);
}
#[test]
fn test_histstrcmp() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
histstrcmp(" hello world ", "hello world", false),
std::cmp::Ordering::Equal
);
assert_eq!(
histstrcmp("hello world", "hello world", true),
std::cmp::Ordering::Equal
);
}
#[test]
fn histstrcmp_inblank_is_narrow_space_tab_only() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
histstrcmp("hello\tworld", "hello world", false),
Ordering::Equal,
"c:1411-1413 — tab and space both inblank; mixed runs equal"
);
assert_ne!(
histstrcmp("hello\nworld", "hello world", false),
Ordering::Equal,
"c:50 — newline is NOT inblank; must be treated as ordinary char"
);
assert_ne!(
histstrcmp("hello\rworld", "hello world", false),
Ordering::Equal,
"CR not in inblank; not collapsed with space"
);
assert_ne!(
histstrcmp("hello\u{00A0}world", "hello world", false),
Ordering::Equal,
"NBSP not in inblank; not collapsed"
);
assert_ne!(
histstrcmp("hello world", "hello world", true),
Ordering::Equal,
"c:1405 — HISTREDUCEBLANKS=true → strcmp; runs do NOT collapse"
);
}
#[test]
fn histstrcmp_strips_leading_and_trailing_inblank() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
histstrcmp(" cmd", "\tcmd", false),
Ordering::Equal,
"c:1398-1399 — leading inblank skipped (both kinds)"
);
assert_eq!(
histstrcmp("cmd ", "cmd", false),
Ordering::Equal,
"c:1421 — trailing inblank on left collapses to end-equal"
);
assert_eq!(
histstrcmp("cmd", "cmd\t\t", false),
Ordering::Equal,
"c:1421 — trailing inblank on right collapses to end-equal"
);
}
#[test]
fn test_cmdnam_table() {
let _g = crate::test_util::global_state_lock();
let mut table = cmdnam_table::new();
table.add(cmdnam_hashed("ls", "/bin/ls"));
assert!(table.get("ls").is_some());
assert!(table.get("nonexistent").is_none());
let ls = table.get("ls").unwrap();
assert_ne!((ls.node.flags & HASHED as i32), 0);
assert_eq!((ls.node.flags & DISABLED as i32), 0);
}
#[test]
fn test_shfunc_table() {
let _g = crate::test_util::global_state_lock();
let mut table = shfunc_table::new();
table.add(shfunc_with_body("myfunc", "echo hello"));
table.add(shfunc_autoload("lazy"));
assert!(table.get("myfunc").is_some());
assert_eq!((table.get("myfunc").unwrap().node.flags & PM_UNDEFINED as i32), 0);
assert_ne!((table.get("lazy").unwrap().node.flags & PM_UNDEFINED as i32), 0);
table.disable("myfunc");
assert!(table.get("myfunc").is_none());
assert!(table.get_including_disabled("myfunc").is_some());
table.enable("myfunc");
assert!(table.get("myfunc").is_some());
}
#[test]
fn test_reswd_table() {
let _g = crate::test_util::global_state_lock();
let table = reswd_table::new();
assert!(table.is_reserved("if"));
assert!(table.is_reserved("while"));
assert!(table.is_reserved("[["));
assert!(!table.is_reserved("notreserved"));
let if_rw = table.get("if").unwrap();
assert_eq!(if_rw.token, IF);
}
#[test]
fn test_alias_table() {
let _g = crate::test_util::global_state_lock();
let mut table = alias_table::with_defaults();
assert!(table.get("run-help").is_some());
assert_eq!(table.get("run-help").unwrap().text, "man");
table.add(createaliasnode("G", "| grep", ALIAS_GLOBAL as u32));
let g = table.get("G").unwrap();
assert_ne!((g.node.flags & ALIAS_GLOBAL as i32), 0);
table.add(createaliasnode("pdf", "zathura", ALIAS_SUFFIX as u32));
let p = table.get("pdf").unwrap();
assert_ne!((p.node.flags & ALIAS_SUFFIX as i32), 0);
table.disable("G");
assert!(table.get("G").is_none());
}
#[test]
fn test_dir_cache() {
let _g = crate::test_util::global_state_lock();
let cache = dircache_lock();
{
let mut g = cache.lock().unwrap();
g.clear();
g.push(dircache_entry {
name: "/usr/share/zsh".into(),
refs: 1,
});
g.push(dircache_entry {
name: "/usr/share/zsh".into(),
refs: 1,
});
assert_eq!(g.len(), 2);
assert_eq!(g[0].refs, 1);
}
}
static shfuncTAB_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn fresh_shfunctab() {
let mut tab = shfunctab_lock().write().expect("shfunctab poisoned");
tab.clear();
}
#[test]
fn test_createshfunctable_idempotent() {
let _g = crate::test_util::global_state_lock();
let _g = shfuncTAB_TEST_LOCK.lock();
createshfunctable();
createshfunctable();
let h1 = shfunctab_lock() as *const _;
let h2 = shfunctab_lock() as *const _;
assert_eq!(h1, h2);
}
#[test]
fn test_shfunctab_add_get_remove() {
let _g = crate::test_util::global_state_lock();
let _g = shfuncTAB_TEST_LOCK.lock();
fresh_shfunctab();
{
let mut tab = shfunctab_lock().write().unwrap();
tab.add(shfunc_with_body("greet", "echo hello"));
}
{
let tab = shfunctab_lock().read().unwrap();
assert!(tab.get("greet").is_some());
assert_eq!(
tab.get("greet").unwrap().body.as_deref(),
Some("echo hello")
);
}
let removed = removeshfuncnode("greet");
assert!(removed.is_some());
assert!(shfunctab_lock().read().unwrap().get("greet").is_none());
}
#[test]
fn test_shfunctab_disable_enable() {
let _g = crate::test_util::global_state_lock();
let _g = shfuncTAB_TEST_LOCK.lock();
fresh_shfunctab();
{
let mut tab = shfunctab_lock().write().unwrap();
tab.add(shfunc_with_body("f", "true"));
}
disableshfuncnode("f");
{
let tab = shfunctab_lock().read().unwrap();
assert!(tab.get("f").is_none());
assert!(tab.get_including_disabled("f").is_some());
}
enableshfuncnode("f");
assert!(shfunctab_lock().read().unwrap().get("f").is_some());
removeshfuncnode("f");
}
#[test]
fn test_simple_glob_match() {
let _g = crate::test_util::global_state_lock();
assert!(simple_glob_match("foo", "foo"));
assert!(!simple_glob_match("foo", "bar"));
assert!(simple_glob_match("f*", "foo"));
assert!(simple_glob_match("f*", "f"));
assert!(simple_glob_match("*o", "foo"));
assert!(simple_glob_match("*", ""));
assert!(simple_glob_match("?oo", "foo"));
assert!(!simple_glob_match("?oo", "fo"));
assert!(simple_glob_match("f*o", "frogspawn-suo"));
}
#[test]
fn test_scanmatchshfunc_matches_pattern() {
let _g = crate::test_util::global_state_lock();
let _g = shfuncTAB_TEST_LOCK.lock();
fresh_shfunctab();
{
let mut tab = shfunctab_lock().write().unwrap();
tab.add(shfunc_with_body("foo", "echo a"));
tab.add(shfunc_with_body("foobar", "echo b"));
tab.add(shfunc_with_body("baz", "echo c"));
}
let mut matched: Vec<String> = Vec::new();
let count = scanmatchshfunc(Some("foo*"), |name, _| matched.push(name.to_string()));
assert_eq!(count, 2);
matched.sort();
assert_eq!(matched, vec!["foo".to_string(), "foobar".to_string()]);
let total = scanshfunc(|_, _| {});
assert_eq!(total, 3);
fresh_shfunctab();
}
#[test]
fn test_getshfuncfile_returns_filename() {
let _g = crate::test_util::global_state_lock();
let _g = shfuncTAB_TEST_LOCK.lock();
fresh_shfunctab();
{
let mut tab = shfunctab_lock().write().unwrap();
let mut f = shfunc_with_body("f", "true");
f.filename = Some("/tmp/zshrs-ported/f".to_string());
tab.add(f);
}
assert_eq!(getshfuncfile("f"), Some("/tmp/zshrs-ported/f".to_string()));
assert_eq!(getshfuncfile("nonexistent"), None);
fresh_shfunctab();
}
#[test]
fn test_generic_addhashnode_displaces_old() {
let _g = crate::test_util::global_state_lock();
let mut ht: HashMap<String, alias> = HashMap::new();
addhashnode(&mut ht, "x", createaliasnode("x", "echo a", 0));
let old = addhashnode2(&mut ht, "x", createaliasnode("x", "echo b", 0));
assert!(old.is_some());
assert_eq!(old.unwrap().text, "echo a");
assert_eq!(gethashnode2(&ht, "x").unwrap().text, "echo b");
}
#[test]
fn test_generic_disable_filters_get() {
let _g = crate::test_util::global_state_lock();
let mut ht: HashMap<String, alias> = HashMap::new();
ht.insert("a".to_string(), createaliasnode("a", "1", 0));
assert!(gethashnode(&ht, "a").is_some());
disablehashnode(&mut ht, "a");
assert!(gethashnode(&ht, "a").is_none());
assert!(gethashnode2(&ht, "a").is_some());
enablehashnode(&mut ht, "a");
assert!(gethashnode(&ht, "a").is_some());
}
#[test]
fn test_scanmatchtable_pattern_and_count() {
let _g = crate::test_util::global_state_lock();
let mut ht: HashMap<String, alias> = HashMap::new();
ht.insert("foo".to_string(), createaliasnode("foo", "1", 0));
ht.insert("foobar".to_string(), createaliasnode("foobar", "2", 0));
ht.insert("baz".to_string(), createaliasnode("baz", "3", 0));
let mut hits: Vec<String> = Vec::new();
let count = scanmatchtable(&ht, Some("foo*"), true, 0, 0, |n, _| {
hits.push(n.to_string())
});
assert_eq!(count, 2);
assert_eq!(hits, vec!["foo".to_string(), "foobar".to_string()]);
}
#[test]
fn test_emptyhashtable_clears() {
let _g = crate::test_util::global_state_lock();
let mut ht: HashMap<String, alias> = HashMap::new();
ht.insert("a".to_string(), createaliasnode("a", "1", 0));
ht.insert("b".to_string(), createaliasnode("b", "2", 0));
assert_eq!(ht.len(), 2);
emptyhashtable(&mut ht);
assert_eq!(ht.len(), 0);
}
#[test]
fn test_resizehashtable_reserves_capacity() {
let _g = crate::test_util::global_state_lock();
let mut ht: HashMap<String, i32> = HashMap::new();
let initial_cap = ht.capacity();
resizehashtable(&mut ht, 200);
assert!(ht.capacity() >= 200);
assert!(ht.capacity() >= initial_cap);
}
#[test]
fn test_aliastab_singleton_has_defaults() {
let _g = crate::test_util::global_state_lock();
let tab = aliastab_lock().read().unwrap();
assert!(tab.get_including_disabled("run-help").is_some());
assert!(tab.get_including_disabled("which-command").is_some());
}
#[test]
fn test_createaliasnode_sets_flags() {
let _g = crate::test_util::global_state_lock();
let a = createaliasnode("foo", "echo bar", ALIAS_GLOBAL as u32);
assert_eq!(a.node.nam, "foo");
assert_eq!(a.text, "echo bar");
assert_ne!((a.node.flags & ALIAS_GLOBAL as i32), 0);
}
#[test]
fn test_printaliasnode_smoke() {
let _g = crate::test_util::global_state_lock();
let a = createaliasnode("ll", "ls -la", 0);
printaliasnode(&a, PRINT_NAMEONLY);
printaliasnode(&a, PRINT_WHENCE_WORD);
printaliasnode(&a, PRINT_WHENCE_SIMPLE);
printaliasnode(&a, PRINT_WHENCE_CSH);
printaliasnode(&a, PRINT_WHENCE_VERBOSE);
printaliasnode(&a, PRINT_LIST);
printaliasnode(&a, 0);
}
#[test]
fn test_printreswdnode_smoke() {
let _g = crate::test_util::global_state_lock();
let table = reswd_table::new();
let if_rw = table.get("if").unwrap();
printreswdnode(if_rw, PRINT_WHENCE_WORD);
printreswdnode(if_rw, PRINT_WHENCE_CSH);
printreswdnode(if_rw, PRINT_WHENCE_VERBOSE);
printreswdnode(if_rw, 0);
}
#[test]
fn test_addhistnode_displaces_old() {
let _g = crate::test_util::global_state_lock();
emptyhisttable();
assert_eq!(addhistnode("ls -la", 1), None);
let old = addhistnode("ls -la", 5);
assert_eq!(old, Some(1));
emptyhisttable();
}
#[test]
fn test_freecmdnamnode_removes() {
let _g = crate::test_util::global_state_lock();
emptycmdnamtable();
{
let mut tab = cmdnamtab_lock().write().unwrap();
tab.add(cmdnam_unhashed("ls", vec!["/bin".to_string()]));
}
assert!(cmdnamtab_lock().read().unwrap().get("ls").is_some());
freecmdnamnode("ls");
assert!(cmdnamtab_lock().read().unwrap().get("ls").is_none());
}
#[test]
fn test_dircache_set_refcounts() {
let _g = crate::test_util::global_state_lock();
let mut k: Option<String> = None;
dircache_set(&mut k, Some("/usr/bin"));
let mut k2: Option<String> = None;
dircache_set(&mut k2, Some("/usr/bin"));
let cache_size = dircache_lock().lock().unwrap().len();
assert!(cache_size >= 1);
}
#[test]
fn createaliasnode_round_trips_name_and_text() {
let _g = crate::test_util::global_state_lock();
let a = createaliasnode("ls-color", "ls --color=auto", 0);
assert_eq!(a.text, "ls --color=auto");
assert_eq!(a.node.nam, "ls-color");
}
#[test]
fn alias_corpus_create_regular_alias() {
let _g = crate::test_util::global_state_lock();
let a = createaliasnode("ll", "ls -la", 0);
assert_eq!(a.node.nam, "ll");
assert_eq!(a.text, "ls -la");
let f = a.node.flags as i32;
assert_eq!(f & (crate::ported::zsh_h::ALIAS_GLOBAL | crate::ported::zsh_h::ALIAS_SUFFIX), 0,
"regular alias has no GLOBAL/SUFFIX bits");
}
#[test]
fn alias_corpus_create_global_alias_carries_flag() {
let _g = crate::test_util::global_state_lock();
let a = createaliasnode("G", "global text",
crate::ported::zsh_h::ALIAS_GLOBAL as u32);
let f = a.node.flags as i32;
assert_ne!(f & crate::ported::zsh_h::ALIAS_GLOBAL, 0,
"ALIAS_GLOBAL set");
}
#[test]
fn alias_corpus_create_suffix_alias_carries_flag() {
let _g = crate::test_util::global_state_lock();
let a = createaliasnode("S", "suffix text",
crate::ported::zsh_h::ALIAS_SUFFIX as u32);
let f = a.node.flags as i32;
assert_ne!(f & crate::ported::zsh_h::ALIAS_SUFFIX, 0,
"ALIAS_SUFFIX set");
}
#[test]
fn alias_corpus_create_empty_text_preserved() {
let _g = crate::test_util::global_state_lock();
let a = createaliasnode("noop", "", 0);
assert_eq!(a.text, "");
}
#[test]
fn alias_corpus_create_multi_word_text_preserved() {
let _g = crate::test_util::global_state_lock();
let a = createaliasnode("rmf", "rm -rf --no-preserve-root", 0);
assert_eq!(a.text, "rm -rf --no-preserve-root");
}
#[test]
fn aliastab_seeds_run_help_and_which_command_defaults() {
let _g = crate::test_util::global_state_lock();
createaliastables();
let tab = aliastab_lock().read().expect("aliastab poisoned");
assert!(tab.get("run-help").is_some(), "run-help default missing");
assert!(
tab.get("which-command").is_some(),
"which-command default missing"
);
}
#[test]
fn hasher_is_deterministic_across_calls() {
let _g = crate::test_util::global_state_lock();
assert_eq!(hasher("foo"), hasher("foo"));
assert_eq!(hasher(""), hasher(""));
assert_ne!(hasher("ls"), hasher("cd"));
assert_ne!(hasher("foo"), hasher("bar"));
}
#[test]
fn hasher_empty_string_hashes_to_zero() {
let _g = crate::test_util::global_state_lock();
assert_eq!(hasher(""), 0);
}
#[test]
fn hasher_single_byte_equals_byte_value() {
let _g = crate::test_util::global_state_lock();
assert_eq!(hasher("a"), b'a' as u32);
assert_eq!(hasher("Z"), b'Z' as u32);
assert_eq!(hasher("0"), b'0' as u32);
}
#[test]
fn hasher_two_byte_matches_bernstein_polynomial() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
hasher("ab"),
97u32
.wrapping_add(97u32.wrapping_shl(5))
.wrapping_add(b'b' as u32)
);
assert_eq!(hasher("ab"), 3299);
let ls_expected = {
let mut h: u32 = 0;
for &c in b"ls" {
h = h.wrapping_add(h.wrapping_shl(5)).wrapping_add(c as u32);
}
h
};
assert_eq!(hasher("ls"), ls_expected);
}
#[test]
fn hasher_processes_utf8_bytes_not_codepoints() {
let _g = crate::test_util::global_state_lock();
let expected = {
let mut h: u32 = 0;
for &c in &[0xC3u8, 0xA9u8] {
h = h.wrapping_add(h.wrapping_shl(5)).wrapping_add(c as u32);
}
h
};
assert_eq!(
hasher("é"),
expected,
"c:90 — `*(unsigned char *) str++` reads BYTES, not codepoints"
);
}
#[test]
fn addhashnode_then_gethashnode2_round_trips() {
let _g = crate::test_util::global_state_lock();
let mut h: HashMap<String, i32> = HashMap::new();
addhashnode(&mut h, "key1", 42);
assert_eq!(gethashnode2(&h, "key1"), Some(&42));
assert_eq!(gethashnode2(&h, "missing"), None);
}
#[test]
fn removehashnode_returns_value_and_drops_entry() {
let _g = crate::test_util::global_state_lock();
let mut h: HashMap<String, String> = HashMap::new();
addhashnode(&mut h, "key1", "val".to_string());
let removed = removehashnode(&mut h, "key1");
assert_eq!(removed.as_deref(), Some("val"));
assert!(
gethashnode2(&h, "key1").is_none(),
"after removehashnode, lookup must miss"
);
}
#[test]
fn removehashnode_missing_key_returns_none() {
let _g = crate::test_util::global_state_lock();
let mut h: HashMap<String, i32> = HashMap::new();
addhashnode(&mut h, "k1", 1);
let len_before = h.len();
assert!(removehashnode(&mut h, "missing").is_none());
assert_eq!(h.len(), len_before, "missing-key remove must not mutate");
}
#[test]
fn hashtable_corpus_add_new_returns_none() {
let mut h: HashMap<String, i32> = HashMap::new();
assert!(addhashnode2(&mut h, "fresh", 7).is_none());
assert_eq!(gethashnode2(&h, "fresh"), Some(&7));
}
#[test]
fn hashtable_corpus_add_existing_returns_previous_value() {
let mut h: HashMap<String, i32> = HashMap::new();
addhashnode2(&mut h, "k", 1);
let prev = addhashnode2(&mut h, "k", 2);
assert_eq!(prev, Some(1), "old value returned on replace");
assert_eq!(gethashnode2(&h, "k"), Some(&2), "new value installed");
}
#[test]
fn hashtable_corpus_get_missing_returns_none() {
let h: HashMap<String, i32> = HashMap::new();
assert!(gethashnode2(&h, "anything").is_none());
}
#[test]
fn hashtable_corpus_newhashtable_preserves_name() {
let (name, sz) = newhashtable(64, "myht");
assert_eq!(name, "myht");
assert!(sz > 0, "size positive, got {sz}");
}
#[test]
fn hashtable_corpus_many_keys_round_trip() {
let mut h: HashMap<String, i32> = HashMap::new();
for i in 0..100 {
addhashnode(&mut h, &format!("k{i}"), i);
}
for i in 0..100 {
assert_eq!(gethashnode2(&h, &format!("k{i}")), Some(&i));
}
assert_eq!(h.len(), 100);
}
#[test]
fn hashtable_corpus_remove_then_get_is_none() {
let mut h: HashMap<String, String> = HashMap::new();
addhashnode(&mut h, "x", "value".into());
let _ = removehashnode(&mut h, "x");
assert!(gethashnode2(&h, "x").is_none());
}
}