use std::collections::HashMap;
use std::sync::OnceLock;
use crate::ported::utils::zwarnnam;
use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_int, c_void};
use std::path::{Path, PathBuf};
use std::ptr;
use std::sync::{Arc, Mutex, RwLock};
use once_cell::sync::Lazy;
pub const PM_UPTODATE: u32 = crate::ported::zsh_h::PM_DONTIMPORT_SUID;
pub fn bin_ztie(nam: &str, args: &[String], ops: &crate::ported::zsh_h::options, _func: i32) -> i32 { let pmname: &str;
let mut read_write: i32 = 0; let _pmflags: u32 = crate::ported::zsh_h::PM_REMOVABLE | crate::ported::zsh_h::PM_SINGLE;
if !crate::ported::zsh_h::OPT_ISSET(ops, b'd') {
crate::ported::utils::zwarnnam(nam, &format!("you must pass `-d {}'", BACKTYPE));
return 1; }
if !crate::ported::zsh_h::OPT_ISSET(ops, b'f') {
crate::ported::utils::zwarnnam(nam, "you must pass `-f' with a filename");
return 1; }
let readonly = crate::ported::zsh_h::OPT_ISSET(ops, b'r');
if readonly {
read_write |= 1; } else {
read_write |= 2; }
let _ = read_write;
let db_type = crate::ported::zsh_h::OPT_ARG(ops, b'd').unwrap_or("");
if db_type != BACKTYPE {
crate::ported::utils::zwarnnam(nam, &format!("unsupported backend type `{}'", db_type));
return 1; }
let resource_name = crate::ported::zsh_h::OPT_ARG(ops, b'f').unwrap_or("");
pmname = match args.first() {
Some(s) => s.as_str(),
None => {
crate::ported::utils::zwarnnam(nam, "parameter name required");
return 1;
}
};
let path = if resource_name.starts_with('/') {
PathBuf::from(resource_name)
} else {
match std::env::current_dir() {
Ok(d) => d.join(resource_name),
Err(_) => {
crate::ported::utils::zwarnnam(nam, "current dir lookup failed");
return 1;
}
}
};
{
let params = match TIED_PARAMS.lock() {
Ok(p) => p,
Err(_) => return 1,
};
if params.contains_key(pmname) {
crate::ported::utils::zwarnnam(nam, &format!("parameter {} is already tied", pmname));
return 1;
}
}
let db = match gdbm_database::open(&path, readonly) {
Ok(d) => d,
Err(e) => {
crate::ported::utils::zwarnnam(nam, &format!("error opening database file {} ({})", resource_name, e));
return 1; }
};
let db = Arc::new(db);
let tied = Arc::new(tied_gdbm_param::new(pmname.to_string(), db));
{
let mut params = match TIED_PARAMS.lock() {
Ok(p) => p,
Err(_) => return 1,
};
params.insert(pmname.to_string(), tied);
}
append_tied_name(pmname); 0 }
pub fn bin_zuntie(nam: &str, args: &[String], ops: &crate::ported::zsh_h::options, _func: i32) -> i32 { let mut ret: i32 = 0;
for pmname in args {
let in_table = match TIED_PARAMS.lock() {
Ok(p) => p.contains_key(pmname),
Err(_) => false,
};
if !in_table { crate::ported::utils::zwarnnam(nam, &format!("cannot untie {}", pmname)); ret = 1; continue; }
crate::ported::signals_h::queue_signals();
if crate::ported::zsh_h::OPT_ISSET(ops, b'u') { }
match TIED_PARAMS.lock() {
Ok(mut p) => { p.remove(pmname); }
Err(_) => { ret = 1; }
}
remove_tied_name(pmname);
crate::ported::signals_h::unqueue_signals();
}
ret }
#[allow(unused_variables)]
pub fn bin_zgdbmpath(nam: &str, args: &[String], ops: &crate::ported::zsh_h::options, func: i32) -> i32 { let pmname = match args.first() {
Some(s) => s.as_str(),
None => {
crate::ported::utils::zwarnnam(
nam,
"parameter name (whose path is to be written to $REPLY) is required",
);
return 1;
}
};
let path = match TIED_PARAMS.lock() {
Ok(p) => match p.get(pmname) {
Some(tied) => tied.db.path().to_string_lossy().to_string(),
None => {
crate::ported::utils::zwarnnam(nam, &format!("no such parameter: {}", pmname));
return 1;
}
},
Err(_) => return 1,
};
println!("{}", path);
0 }
pub fn gdbmgetfn(param_name: &str, key: &str) -> String { let params = match TIED_PARAMS.lock() { Ok(p) => p, Err(_) => return String::new() };
let tied = match params.get(param_name) { Some(t) => t.clone(), None => return String::new() };
drop(params);
match tied.get(key) {
Some(v) => v, None => String::new(), }
}
pub fn gdbmsetfn(param_name: &str, key: &str, val: Option<&str>) { let params = match TIED_PARAMS.lock() { Ok(p) => p, Err(_) => return };
let tied = match params.get(param_name) { Some(t) => t.clone(), None => return };
drop(params);
match val {
Some(v) => { let _ = tied.set(key, v); }
None => { let _ = tied.delete(key); }
}
}
pub fn gdbmunsetfn(param_name: &str, key: &str, _um: i32) { gdbmsetfn(param_name, key, None);
}
pub fn getgdbmnode(ht: &str, name: &str) -> bool { let params = match TIED_PARAMS.lock() {
Ok(p) => p,
Err(_) => return false,
};
let tied = match params.get(ht) {
Some(t) => t.clone(),
None => return false,
};
drop(params);
let exists = tied.get(name).is_some();
if !exists {
let _ = tied.set(name, ""); }
exists }
pub fn scangdbmkeys(ht: &str, mut func: impl FnMut(&str, &str, i32), flags: i32) { let params = match TIED_PARAMS.lock() { Ok(p) => p, Err(_) => return };
let tied = match params.get(ht) { Some(t) => t.clone(), None => return };
drop(params);
for key in tied.keys() {
let _ = getgdbmnode(ht, &key); func(ht, &key, flags); }
}
impl From<&[u8]> for Datum {
fn from(data: &[u8]) -> Self {
let ptr = unsafe { libc::malloc(data.len()) as *mut c_char };
if !ptr.is_null() {
unsafe {
ptr::copy_nonoverlapping(data.as_ptr(), ptr as *mut u8, data.len());
}
}
Datum {
dptr: ptr,
dsize: data.len() as c_int,
}
}
}
impl Datum {
fn to_bytes(&self) -> Option<Vec<u8>> {
if self.dptr.is_null() {
None
} else {
let mut result = vec![0u8; self.dsize as usize];
unsafe {
ptr::copy_nonoverlapping(
self.dptr as *const u8,
result.as_mut_ptr(),
self.dsize as usize,
);
}
Some(result)
}
}
fn free(&mut self) {
if !self.dptr.is_null() {
unsafe { libc::free(self.dptr as *mut c_void) };
self.dptr = ptr::null_mut();
self.dsize = 0;
}
}
}
pub fn gdbmhashsetfn(pm: &str, ht: &[(String, String)]) { let param = match TIED_PARAMS.lock().ok().and_then(|m| m.get(pm).cloned()) {
Some(p) => p,
None => return,
};
for (key, value) in ht {
let _ = param.set(key, value); }
}
#[cfg(feature = "gdbm")]
#[link(name = "gdbm")]
extern "C" {
fn gdbm_open(
name: *const c_char,
block_size: c_int,
flags: c_int,
mode: c_int,
fatal_func: Option<extern "C" fn(*const c_char)>,
) -> GdbmFile;
fn gdbm_close(dbf: GdbmFile);
fn gdbm_store(dbf: GdbmFile, key: Datum, content: Datum, flag: c_int) -> c_int;
fn gdbm_fetch(dbf: GdbmFile, key: Datum) -> Datum;
fn gdbmunsetfn(dbf: GdbmFile, key: Datum) -> c_int;
fn gdbm_exists(dbf: GdbmFile, key: Datum) -> c_int;
fn gdbm_firstkey(dbf: GdbmFile) -> Datum;
fn gdbm_nextkey(dbf: GdbmFile, key: Datum) -> Datum;
fn gdbm_reorganize(dbf: GdbmFile) -> c_int;
fn gdbm_fdesc(dbf: GdbmFile) -> c_int;
fn gdbm_strerror(errno: c_int) -> *const c_char;
static gdbm_errno: c_int;
}
pub fn gdbmuntie(pm: &str) { if let Ok(mut params) = TIED_PARAMS.lock() {
params.remove(pm); }
}
impl gdbm_database {
#[cfg(feature = "gdbm")]
pub fn open(path: &Path, readonly: bool) -> Result<Self, String> {
let c_path = CString::new(path.to_string_lossy().as_bytes()).map_err(|_| "Invalid path")?;
let flags = GDBM_SYNC | if readonly { GDBM_READER } else { GDBM_WRCREAT };
let dbf = unsafe { gdbm_open(c_path.as_ptr(), 0, flags, 0o666, None) };
if dbf.is_null() {
let err = unsafe {
let err_ptr = gdbm_strerror(gdbm_errno);
if err_ptr.is_null() {
"Unknown error".to_string()
} else {
CStr::from_ptr(err_ptr).to_string_lossy().to_string()
}
};
return Err(format!(
"error opening database file {} ({})",
path.display(),
err
));
}
Ok(gdbm_database {
dbf,
path: path.to_path_buf(),
readonly,
})
}
#[cfg(not(feature = "gdbm"))]
pub fn open(_path: &Path, _readonly: bool) -> Result<Self, String> {
Err("GDBM support not compiled in".to_string())
}
#[cfg(feature = "gdbm")]
pub fn get(&self, key: &str) -> Option<String> { let key_bytes = key.as_bytes();
let key_datum = Datum::from(key_bytes);
let exists = unsafe {
gdbm_exists(
self.dbf,
Datum {
dptr: key_datum.dptr,
dsize: key_datum.dsize,
},
)
};
if exists == 0 {
unsafe { libc::free(key_datum.dptr as *mut c_void) };
return None;
}
let mut content = unsafe {
gdbm_fetch(
self.dbf,
Datum {
dptr: key_datum.dptr,
dsize: key_datum.dsize,
},
)
};
unsafe { libc::free(key_datum.dptr as *mut c_void) };
let result = content
.to_bytes()
.map(|bytes| String::from_utf8_lossy(&bytes).to_string());
content.free();
result
}
#[cfg(not(feature = "gdbm"))]
pub fn get(&self, _key: &str) -> Option<String> {
None
}
#[cfg(feature = "gdbm")]
pub fn set(&self, key: &str, value: &str) -> Result<(), String> { if self.readonly {
return Err("Database is read-only".to_string());
}
let key_datum = Datum::from(key.as_bytes());
let content_datum = Datum::from(value.as_bytes());
let ret = unsafe {
gdbm_store(
self.dbf,
Datum {
dptr: key_datum.dptr,
dsize: key_datum.dsize,
},
Datum {
dptr: content_datum.dptr,
dsize: content_datum.dsize,
},
GDBM_REPLACE,
)
};
unsafe {
libc::free(key_datum.dptr as *mut c_void);
libc::free(content_datum.dptr as *mut c_void);
}
if ret != 0 {
Err("Failed to store value".to_string())
} else {
Ok(())
}
}
#[cfg(not(feature = "gdbm"))]
pub fn set(&self, _key: &str, _value: &str) -> Result<(), String> {
Err("GDBM support not compiled in".to_string())
}
#[cfg(feature = "gdbm")]
pub fn delete(&self, key: &str) -> Result<(), String> { if self.readonly {
return Err("Database is read-only".to_string());
}
let key_datum = Datum::from(key.as_bytes());
let ret = unsafe {
gdbmunsetfn(
self.dbf,
Datum {
dptr: key_datum.dptr,
dsize: key_datum.dsize,
},
)
};
unsafe { libc::free(key_datum.dptr as *mut c_void) };
if ret != 0 {
Err("Key not found".to_string())
} else {
Ok(())
}
}
#[cfg(not(feature = "gdbm"))]
pub fn delete(&self, _key: &str) -> Result<(), String> {
Err("GDBM support not compiled in".to_string())
}
#[cfg(feature = "gdbm")]
pub fn keys(&self) -> Vec<String> {
let mut keys = Vec::new();
let mut key = unsafe { gdbm_firstkey(self.dbf) };
while !key.dptr.is_null() {
if let Some(bytes) = key.to_bytes() {
keys.push(String::from_utf8_lossy(&bytes).to_string());
}
let prev_key = key;
key = unsafe {
gdbm_nextkey(
self.dbf,
Datum {
dptr: prev_key.dptr,
dsize: prev_key.dsize,
},
)
};
unsafe { libc::free(prev_key.dptr as *mut c_void) };
}
keys
}
#[cfg(not(feature = "gdbm"))]
pub fn keys(&self) -> Vec<String> {
Vec::new()
}
#[cfg(feature = "gdbm")]
pub fn clear(&self) -> Result<(), String> {
if self.readonly {
return Err("Database is read-only".to_string());
}
let keys = self.keys();
for key in keys {
let _ = self.delete(&key);
}
unsafe { gdbm_reorganize(self.dbf) };
Ok(())
}
#[cfg(not(feature = "gdbm"))]
pub fn clear(&self) -> Result<(), String> {
Err("GDBM support not compiled in".to_string())
}
pub fn path(&self) -> &Path {
&self.path
}
#[cfg(feature = "gdbm")]
pub fn fd(&self) -> i32 {
unsafe { gdbm_fdesc(self.dbf) }
}
#[cfg(not(feature = "gdbm"))]
pub fn fd(&self) -> i32 {
-1
}
}
#[cfg(feature = "gdbm")]
impl Drop for gdbm_database {
fn drop(&mut self) {
if !self.dbf.is_null() {
unsafe { gdbm_close(self.dbf) };
self.dbf = ptr::null_mut();
}
}
}
#[cfg(not(feature = "gdbm"))]
impl Drop for gdbm_database {
fn drop(&mut self) {}
}
unsafe impl Send for gdbm_database {}
unsafe impl Sync for gdbm_database {}
#[allow(non_camel_case_types)]
pub struct tied_gdbm_param {
pub name: String,
pub db: Arc<gdbm_database>,
pub cache: RwLock<HashMap<String, String>>,
}
impl tied_gdbm_param {
pub fn new(name: String, db: Arc<gdbm_database>) -> Self {
tied_gdbm_param {
name,
db,
cache: RwLock::new(HashMap::new()),
}
}
pub fn get(&self, key: &str) -> Option<String> {
if let Ok(cache) = self.cache.read() {
if let Some(val) = cache.get(key) {
return Some(val.clone());
}
}
if let Some(val) = self.db.get(key) {
if let Ok(mut cache) = self.cache.write() {
cache.insert(key.to_string(), val.clone());
}
Some(val)
} else {
None
}
}
pub fn set(&self, key: &str, value: &str) -> Result<(), String> {
self.db.set(key, value)?;
if let Ok(mut cache) = self.cache.write() {
cache.insert(key.to_string(), value.to_string());
}
Ok(())
}
pub fn delete(&self, key: &str) -> Result<(), String> {
self.db.delete(key)?;
if let Ok(mut cache) = self.cache.write() {
cache.remove(key);
}
Ok(())
}
pub fn keys(&self) -> Vec<String> {
self.db.keys()
}
pub fn to_hash(&self) -> HashMap<String, String> {
let mut result = HashMap::new();
for key in self.keys() {
if let Some(val) = self.get(&key) {
result.insert(key, val);
}
}
result
}
pub fn from_hash(&self, hash: &HashMap<String, String>) -> Result<(), String> {
self.db.clear()?;
for (key, val) in hash {
self.db.set(key, val)?;
}
if let Ok(mut cache) = self.cache.write() {
cache.clear();
}
Ok(())
}
}
pub fn gdbmhashunsetfn(param_name: &str) { gdbmuntie(param_name); }
#[allow(unused_variables)]
pub fn setup_(m: *const module) -> i32 { 0
}
pub fn features_(m: *const module, features: &mut Vec<String>) -> i32 {
*features = featuresarray(m, module_features());
0
}
pub fn enables_(m: *const module, enables: &mut Option<Vec<i32>>) -> i32 {
handlefeatures(m, module_features(), enables)
}
#[allow(unused_variables)]
pub fn boot_(m: *const module) -> i32 { if let Ok(mut tied) = ZGDBM_TIED.lock() { tied.clear();
}
0
}
pub fn cleanup_(m: *const module) -> i32 {
setfeatureenables(m, module_features(), None)
}
#[allow(unused_variables)]
pub fn finish_(m: *const module) -> i32 { 0
}
pub fn unmetafy_zalloc(to_copy: &str) -> (String, usize) { let s = crate::ported::utils::unmeta(to_copy);
let len = s.len();
(s, len)
}
pub fn myfreeparamnode(param_name: &str, key: &str) { gdbmunsetfn(param_name, key, 1); }
const BACKTYPE: &str = "db/gdbm";
const GDBM_READER: c_int = 0;
use crate::ported::zsh_h::module;
const GDBM_WRITER: c_int = 1;
const GDBM_WRCREAT: c_int = 2;
const GDBM_NEWDB: c_int = 3;
const GDBM_SYNC: c_int = 0x20;
const GDBM_REPLACE: c_int = 1;
#[repr(C)]
struct Datum {
dptr: *mut c_char,
dsize: c_int,
}
type GdbmFile = *mut c_void;
#[allow(non_camel_case_types)]
#[derive(Debug)]
pub struct gdbm_database {
dbf: GdbmFile,
path: PathBuf,
readonly: bool,
}
pub(crate) static TIED_PARAMS: Lazy<Mutex<HashMap<String, Arc<tied_gdbm_param>>>> =
Lazy::new(|| Mutex::new(HashMap::new()));
pub static ZGDBM_TIED: std::sync::Mutex<Vec<String>> = std::sync::Mutex::new(Vec::new());
pub fn append_tied_name(name: &str) -> i32 { if let Ok(mut tied) = ZGDBM_TIED.lock() {
tied.push(name.to_string()); }
0 }
pub fn remove_tied_name(name: &str) -> i32 { if let Ok(mut tied) = ZGDBM_TIED.lock() {
if let Some(pos) = tied.iter().position(|n| n == name) { tied.remove(pos); }
}
0
}
use crate::ported::zsh_h::features as features_t;
static MODULE_FEATURES: OnceLock<Mutex<features_t>> = OnceLock::new();
fn featuresarray(_m: *const module, _f: &Mutex<features_t>) -> Vec<String> {
vec!["b:ztie".to_string(), "b:zuntie".to_string(), "b:zgdbmpath".to_string(), "p:zgdbm_tied".to_string()]
}
fn handlefeatures(
_m: *const module,
_f: &Mutex<features_t>,
enables: &mut Option<Vec<i32>>,
) -> i32 {
if enables.is_none() {
*enables = Some(vec![1; 4]);
}
0
}
fn setfeatureenables(
_m: *const module,
_f: &Mutex<features_t>,
_e: Option<&[i32]>,
) -> i32 {
0
}
fn module_features() -> &'static Mutex<features_t> {
MODULE_FEATURES.get_or_init(|| Mutex::new(features_t {
bn_list: None,
bn_size: 3,
cd_list: None,
cd_size: 0,
mf_list: None,
mf_size: 0,
pd_list: None,
pd_size: 1,
n_abstract: 0,
}))
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
#[test]
#[cfg(feature = "gdbm")]
fn test_gdbm_basic_operations() {
let dir = tempdir().unwrap();
let db_path = dir.path().join("test.gdbm");
let db = gdbm_database::open(&db_path, false).unwrap();
db.set("key1", "value1").unwrap();
assert_eq!(db.get("key1"), Some("value1".to_string()));
assert_eq!(db.get("nonexistent"), None);
db.delete("key1").unwrap();
assert_eq!(db.get("key1"), None);
db.set("a", "1").unwrap();
db.set("b", "2").unwrap();
db.set("c", "3").unwrap();
let keys = db.keys();
assert_eq!(keys.len(), 3);
assert!(keys.contains(&"a".to_string()));
assert!(keys.contains(&"b".to_string()));
assert!(keys.contains(&"c".to_string()));
db.clear().unwrap();
assert_eq!(db.keys().len(), 0);
}
#[test]
#[cfg(feature = "gdbm")]
fn test_tied_param() {
let dir = tempdir().unwrap();
let db_path = dir.path().join("tied.gdbm");
let db = Arc::new(gdbm_database::open(&db_path, false).unwrap());
let tied = tied_gdbm_param::new("mydb".to_string(), db);
tied.set("foo", "bar").unwrap();
assert_eq!(tied.get("foo"), Some("bar".to_string()));
let hash = tied.to_hash();
assert_eq!(hash.get("foo"), Some(&"bar".to_string()));
}
fn empty_ops() -> crate::ported::zsh_h::options {
crate::ported::zsh_h::options {
ind: [0u8; crate::ported::zsh_h::MAX_OPS],
args: Vec::new(), argscount: 0, argsalloc: 0,
}
}
#[test]
fn pm_uptodate_aliases_pm_dontimport_suid() {
assert_eq!(PM_UPTODATE, crate::ported::zsh_h::PM_DONTIMPORT_SUID);
}
#[test]
fn module_entry_points_return_zero() {
assert_eq!(setup_(std::ptr::null()), 0);
assert_eq!(boot_(std::ptr::null()), 0);
assert_eq!(cleanup_(std::ptr::null()), 0);
assert_eq!(finish_(std::ptr::null()), 0);
}
#[test]
fn ztie_with_no_args_returns_one() {
let ops = empty_ops();
assert_eq!(bin_ztie("ztie", &[], &ops, 0), 1);
}
#[test]
fn zuntie_with_no_args_returns_zero() {
let ops = empty_ops();
assert_eq!(bin_zuntie("zuntie", &[], &ops, 0), 0);
}
#[test]
fn zuntie_unknown_param_returns_one() {
let ops = empty_ops();
let r = bin_zuntie("zuntie", &["zshrs_test_not_tied".to_string()], &ops, 0);
assert_eq!(r, 1);
}
#[test]
fn gdbmgetfn_unknown_db_returns_empty() {
assert_eq!(gdbmgetfn("zshrs_test_no_such_db_xyz", "key"), "");
}
#[test]
fn getgdbmnode_unknown_returns_false() {
assert!(!getgdbmnode("zshrs_test_no_such_db_xyz", "key"));
}
#[test]
fn gdbmsetfn_unknown_db_is_safe() {
gdbmsetfn("zshrs_test_no_such_db_setfn", "key", Some("value"));
}
#[test]
fn gdbmunsetfn_unknown_db_is_safe() {
gdbmunsetfn("zshrs_test_no_such_db_unsetfn", "key", 0);
}
#[test]
fn gdbmuntie_unknown_param_is_safe() {
gdbmuntie("zshrs_test_no_such_param_untie");
}
#[test]
fn gdbmhashunsetfn_unknown_param_is_safe() {
gdbmhashunsetfn("zshrs_test_no_such_param_hash_unset");
}
#[test]
fn scangdbmkeys_unknown_db_yields_no_entries() {
let mut count = 0;
scangdbmkeys("zshrs_test_no_such_db_scan",
|_k, _v, _f| count += 1, 0);
assert_eq!(count, 0, "unknown DB must yield no entries");
}
#[test]
fn gdbmhashsetfn_unknown_param_empty_entries_is_safe() {
gdbmhashsetfn("zshrs_test_no_such_param_hashset", &[]);
}
#[test]
fn bin_zgdbmpath_with_no_args_returns_nonzero() {
let ops = empty_ops();
let r = bin_zgdbmpath("zgdbmpath", &[], &ops, 0);
assert_ne!(r, 0, "zgdbmpath with no args must error");
}
#[test]
fn bin_zgdbmpath_unknown_param_returns_nonzero() {
let ops = empty_ops();
let r = bin_zgdbmpath("zgdbmpath",
&["zshrs_test_not_a_tied_param".to_string()], &ops, 0);
assert_ne!(r, 0, "zgdbmpath on untied param must error");
}
}