Skip to main content

zsh/
db_gdbm.rs

1//! GDBM database bindings for zsh
2//!
3//! Port of zsh/Src/Modules/db_gdbm.c
4//!
5//! Provides builtins:
6//! - ztie: Tie a parameter to a GDBM database
7//! - zuntie: Untie a parameter from a GDBM database
8//! - zgdbmpath: Get the path of a tied GDBM database
9
10use std::collections::HashMap;
11use std::ffi::{CStr, CString};
12use std::os::raw::{c_char, c_int, c_void};
13use std::path::{Path, PathBuf};
14use std::ptr;
15use std::sync::{Arc, Mutex, RwLock};
16
17use once_cell::sync::Lazy;
18
19const BACKTYPE: &str = "db/gdbm";
20
21/// GDBM open flags
22const GDBM_READER: c_int = 0;
23const GDBM_WRITER: c_int = 1;
24const GDBM_WRCREAT: c_int = 2;
25const GDBM_NEWDB: c_int = 3;
26const GDBM_SYNC: c_int = 0x20;
27const GDBM_REPLACE: c_int = 1;
28
29/// Datum structure for GDBM
30#[repr(C)]
31struct Datum {
32    dptr: *mut c_char,
33    dsize: c_int,
34}
35
36impl Datum {
37    fn null() -> Self {
38        Datum {
39            dptr: ptr::null_mut(),
40            dsize: 0,
41        }
42    }
43
44    fn from_bytes(data: &[u8]) -> Self {
45        let ptr = unsafe { libc::malloc(data.len()) as *mut c_char };
46        if !ptr.is_null() {
47            unsafe {
48                ptr::copy_nonoverlapping(data.as_ptr(), ptr as *mut u8, data.len());
49            }
50        }
51        Datum {
52            dptr: ptr,
53            dsize: data.len() as c_int,
54        }
55    }
56
57    fn to_bytes(&self) -> Option<Vec<u8>> {
58        if self.dptr.is_null() {
59            None
60        } else {
61            let mut result = vec![0u8; self.dsize as usize];
62            unsafe {
63                ptr::copy_nonoverlapping(
64                    self.dptr as *const u8,
65                    result.as_mut_ptr(),
66                    self.dsize as usize,
67                );
68            }
69            Some(result)
70        }
71    }
72
73    fn free(&mut self) {
74        if !self.dptr.is_null() {
75            unsafe { libc::free(self.dptr as *mut c_void) };
76            self.dptr = ptr::null_mut();
77            self.dsize = 0;
78        }
79    }
80}
81
82/// Opaque GDBM file handle
83type GdbmFile = *mut c_void;
84
85#[cfg(feature = "gdbm")]
86#[link(name = "gdbm")]
87extern "C" {
88    fn gdbm_open(
89        name: *const c_char,
90        block_size: c_int,
91        flags: c_int,
92        mode: c_int,
93        fatal_func: Option<extern "C" fn(*const c_char)>,
94    ) -> GdbmFile;
95    fn gdbm_close(dbf: GdbmFile);
96    fn gdbm_store(dbf: GdbmFile, key: Datum, content: Datum, flag: c_int) -> c_int;
97    fn gdbm_fetch(dbf: GdbmFile, key: Datum) -> Datum;
98    fn gdbm_delete(dbf: GdbmFile, key: Datum) -> c_int;
99    fn gdbm_exists(dbf: GdbmFile, key: Datum) -> c_int;
100    fn gdbm_firstkey(dbf: GdbmFile) -> Datum;
101    fn gdbm_nextkey(dbf: GdbmFile, key: Datum) -> Datum;
102    fn gdbm_reorganize(dbf: GdbmFile) -> c_int;
103    fn gdbm_fdesc(dbf: GdbmFile) -> c_int;
104    fn gdbm_strerror(errno: c_int) -> *const c_char;
105    static gdbm_errno: c_int;
106}
107
108/// A GDBM database handle wrapper
109#[derive(Debug)]
110pub struct GdbmDatabase {
111    dbf: GdbmFile,
112    path: PathBuf,
113    readonly: bool,
114}
115
116impl GdbmDatabase {
117    #[cfg(feature = "gdbm")]
118    pub fn open(path: &Path, readonly: bool) -> Result<Self, String> {
119        let c_path = CString::new(path.to_string_lossy().as_bytes()).map_err(|_| "Invalid path")?;
120
121        let flags = GDBM_SYNC | if readonly { GDBM_READER } else { GDBM_WRCREAT };
122
123        let dbf = unsafe { gdbm_open(c_path.as_ptr(), 0, flags, 0o666, None) };
124
125        if dbf.is_null() {
126            let err = unsafe {
127                let err_ptr = gdbm_strerror(gdbm_errno);
128                if err_ptr.is_null() {
129                    "Unknown error".to_string()
130                } else {
131                    CStr::from_ptr(err_ptr).to_string_lossy().to_string()
132                }
133            };
134            return Err(format!(
135                "error opening database file {} ({})",
136                path.display(),
137                err
138            ));
139        }
140
141        Ok(GdbmDatabase {
142            dbf,
143            path: path.to_path_buf(),
144            readonly,
145        })
146    }
147
148    #[cfg(not(feature = "gdbm"))]
149    pub fn open(_path: &Path, _readonly: bool) -> Result<Self, String> {
150        Err("GDBM support not compiled in".to_string())
151    }
152
153    #[cfg(feature = "gdbm")]
154    pub fn get(&self, key: &str) -> Option<String> {
155        let key_bytes = key.as_bytes();
156        let key_datum = Datum::from_bytes(key_bytes);
157
158        let exists = unsafe {
159            gdbm_exists(
160                self.dbf,
161                Datum {
162                    dptr: key_datum.dptr,
163                    dsize: key_datum.dsize,
164                },
165            )
166        };
167
168        if exists == 0 {
169            unsafe { libc::free(key_datum.dptr as *mut c_void) };
170            return None;
171        }
172
173        let mut content = unsafe {
174            gdbm_fetch(
175                self.dbf,
176                Datum {
177                    dptr: key_datum.dptr,
178                    dsize: key_datum.dsize,
179                },
180            )
181        };
182
183        unsafe { libc::free(key_datum.dptr as *mut c_void) };
184
185        let result = content
186            .to_bytes()
187            .map(|bytes| String::from_utf8_lossy(&bytes).to_string());
188
189        content.free();
190        result
191    }
192
193    #[cfg(not(feature = "gdbm"))]
194    pub fn get(&self, _key: &str) -> Option<String> {
195        None
196    }
197
198    #[cfg(feature = "gdbm")]
199    pub fn set(&self, key: &str, value: &str) -> Result<(), String> {
200        if self.readonly {
201            return Err("Database is read-only".to_string());
202        }
203
204        let key_datum = Datum::from_bytes(key.as_bytes());
205        let content_datum = Datum::from_bytes(value.as_bytes());
206
207        let ret = unsafe {
208            gdbm_store(
209                self.dbf,
210                Datum {
211                    dptr: key_datum.dptr,
212                    dsize: key_datum.dsize,
213                },
214                Datum {
215                    dptr: content_datum.dptr,
216                    dsize: content_datum.dsize,
217                },
218                GDBM_REPLACE,
219            )
220        };
221
222        unsafe {
223            libc::free(key_datum.dptr as *mut c_void);
224            libc::free(content_datum.dptr as *mut c_void);
225        }
226
227        if ret != 0 {
228            Err("Failed to store value".to_string())
229        } else {
230            Ok(())
231        }
232    }
233
234    #[cfg(not(feature = "gdbm"))]
235    pub fn set(&self, _key: &str, _value: &str) -> Result<(), String> {
236        Err("GDBM support not compiled in".to_string())
237    }
238
239    #[cfg(feature = "gdbm")]
240    pub fn delete(&self, key: &str) -> Result<(), String> {
241        if self.readonly {
242            return Err("Database is read-only".to_string());
243        }
244
245        let key_datum = Datum::from_bytes(key.as_bytes());
246
247        let ret = unsafe {
248            gdbm_delete(
249                self.dbf,
250                Datum {
251                    dptr: key_datum.dptr,
252                    dsize: key_datum.dsize,
253                },
254            )
255        };
256
257        unsafe { libc::free(key_datum.dptr as *mut c_void) };
258
259        if ret != 0 {
260            Err("Key not found".to_string())
261        } else {
262            Ok(())
263        }
264    }
265
266    #[cfg(not(feature = "gdbm"))]
267    pub fn delete(&self, _key: &str) -> Result<(), String> {
268        Err("GDBM support not compiled in".to_string())
269    }
270
271    #[cfg(feature = "gdbm")]
272    pub fn keys(&self) -> Vec<String> {
273        let mut keys = Vec::new();
274
275        let mut key = unsafe { gdbm_firstkey(self.dbf) };
276
277        while !key.dptr.is_null() {
278            if let Some(bytes) = key.to_bytes() {
279                keys.push(String::from_utf8_lossy(&bytes).to_string());
280            }
281
282            let prev_key = key;
283            key = unsafe {
284                gdbm_nextkey(
285                    self.dbf,
286                    Datum {
287                        dptr: prev_key.dptr,
288                        dsize: prev_key.dsize,
289                    },
290                )
291            };
292            unsafe { libc::free(prev_key.dptr as *mut c_void) };
293        }
294
295        keys
296    }
297
298    #[cfg(not(feature = "gdbm"))]
299    pub fn keys(&self) -> Vec<String> {
300        Vec::new()
301    }
302
303    #[cfg(feature = "gdbm")]
304    pub fn clear(&self) -> Result<(), String> {
305        if self.readonly {
306            return Err("Database is read-only".to_string());
307        }
308
309        let keys = self.keys();
310        for key in keys {
311            let _ = self.delete(&key);
312        }
313
314        unsafe { gdbm_reorganize(self.dbf) };
315        Ok(())
316    }
317
318    #[cfg(not(feature = "gdbm"))]
319    pub fn clear(&self) -> Result<(), String> {
320        Err("GDBM support not compiled in".to_string())
321    }
322
323    pub fn path(&self) -> &Path {
324        &self.path
325    }
326
327    #[cfg(feature = "gdbm")]
328    pub fn fd(&self) -> i32 {
329        unsafe { gdbm_fdesc(self.dbf) }
330    }
331
332    #[cfg(not(feature = "gdbm"))]
333    pub fn fd(&self) -> i32 {
334        -1
335    }
336}
337
338#[cfg(feature = "gdbm")]
339impl Drop for GdbmDatabase {
340    fn drop(&mut self) {
341        if !self.dbf.is_null() {
342            unsafe { gdbm_close(self.dbf) };
343            self.dbf = ptr::null_mut();
344        }
345    }
346}
347
348#[cfg(not(feature = "gdbm"))]
349impl Drop for GdbmDatabase {
350    fn drop(&mut self) {}
351}
352
353unsafe impl Send for GdbmDatabase {}
354unsafe impl Sync for GdbmDatabase {}
355
356/// A tied parameter backed by GDBM
357pub struct TiedGdbmParam {
358    pub name: String,
359    pub db: Arc<GdbmDatabase>,
360    pub cache: RwLock<HashMap<String, String>>,
361}
362
363impl TiedGdbmParam {
364    pub fn new(name: String, db: Arc<GdbmDatabase>) -> Self {
365        TiedGdbmParam {
366            name,
367            db,
368            cache: RwLock::new(HashMap::new()),
369        }
370    }
371
372    pub fn get(&self, key: &str) -> Option<String> {
373        if let Ok(cache) = self.cache.read() {
374            if let Some(val) = cache.get(key) {
375                return Some(val.clone());
376            }
377        }
378
379        if let Some(val) = self.db.get(key) {
380            if let Ok(mut cache) = self.cache.write() {
381                cache.insert(key.to_string(), val.clone());
382            }
383            Some(val)
384        } else {
385            None
386        }
387    }
388
389    pub fn set(&self, key: &str, value: &str) -> Result<(), String> {
390        self.db.set(key, value)?;
391        if let Ok(mut cache) = self.cache.write() {
392            cache.insert(key.to_string(), value.to_string());
393        }
394        Ok(())
395    }
396
397    pub fn delete(&self, key: &str) -> Result<(), String> {
398        self.db.delete(key)?;
399        if let Ok(mut cache) = self.cache.write() {
400            cache.remove(key);
401        }
402        Ok(())
403    }
404
405    pub fn keys(&self) -> Vec<String> {
406        self.db.keys()
407    }
408
409    pub fn to_hash(&self) -> HashMap<String, String> {
410        let mut result = HashMap::new();
411        for key in self.keys() {
412            if let Some(val) = self.get(&key) {
413                result.insert(key, val);
414            }
415        }
416        result
417    }
418
419    pub fn from_hash(&self, hash: &HashMap<String, String>) -> Result<(), String> {
420        self.db.clear()?;
421        for (key, val) in hash {
422            self.db.set(key, val)?;
423        }
424        if let Ok(mut cache) = self.cache.write() {
425            cache.clear();
426        }
427        Ok(())
428    }
429}
430
431/// Global registry of tied GDBM parameters
432static TIED_PARAMS: Lazy<Mutex<HashMap<String, Arc<TiedGdbmParam>>>> =
433    Lazy::new(|| Mutex::new(HashMap::new()));
434
435/// Get list of tied parameter names
436pub fn zgdbm_tied() -> Vec<String> {
437    if let Ok(params) = TIED_PARAMS.lock() {
438        params.keys().cloned().collect()
439    } else {
440        Vec::new()
441    }
442}
443
444/// Tie a parameter to a GDBM database
445///
446/// Usage: ztie -d db/gdbm -f /path/to/db.gdbm [-r] PARAM_NAME
447pub fn ztie(
448    args: &[String],
449    readonly: bool,
450    db_type: Option<&str>,
451    file_path: Option<&str>,
452) -> Result<(), String> {
453    let db_type = db_type.ok_or("you must pass `-d db/gdbm'")?;
454    let file_path = file_path.ok_or("you must pass `-f' with a filename")?;
455
456    if db_type != BACKTYPE {
457        return Err(format!("unsupported backend type `{}'", db_type));
458    }
459
460    let param_name = args.first().ok_or("parameter name required")?;
461
462    // Resolve path
463    let path = if file_path.starts_with('/') {
464        PathBuf::from(file_path)
465    } else {
466        std::env::current_dir()
467            .map_err(|e| e.to_string())?
468            .join(file_path)
469    };
470
471    // Check if already tied
472    {
473        let params = TIED_PARAMS.lock().map_err(|_| "lock error")?;
474        if params.contains_key(param_name) {
475            return Err(format!("parameter {} is already tied", param_name));
476        }
477    }
478
479    // Open database
480    let db = GdbmDatabase::open(&path, readonly)?;
481    let db = Arc::new(db);
482
483    // Create tied parameter
484    let tied = Arc::new(TiedGdbmParam::new(param_name.clone(), db));
485
486    // Register
487    {
488        let mut params = TIED_PARAMS.lock().map_err(|_| "lock error")?;
489        params.insert(param_name.clone(), tied);
490    }
491
492    Ok(())
493}
494
495/// Untie a parameter from its GDBM database
496///
497/// Usage: zuntie [-u] PARAM_NAME...
498pub fn zuntie(args: &[String], force_unset: bool) -> Result<(), String> {
499    let mut errors = Vec::new();
500
501    for param_name in args {
502        let mut params = match TIED_PARAMS.lock() {
503            Ok(p) => p,
504            Err(_) => {
505                errors.push(format!("cannot untie {}: lock error", param_name));
506                continue;
507            }
508        };
509
510        if !params.contains_key(param_name) {
511            errors.push(format!("cannot untie {}", param_name));
512            continue;
513        }
514
515        params.remove(param_name);
516    }
517
518    if errors.is_empty() {
519        Ok(())
520    } else {
521        Err(errors.join("\n"))
522    }
523}
524
525/// Get the path of a tied GDBM database
526///
527/// Usage: zgdbmpath PARAM_NAME
528/// Sets $REPLY to the path
529pub fn zgdbmpath(param_name: &str) -> Result<String, String> {
530    let params = TIED_PARAMS.lock().map_err(|_| "lock error")?;
531
532    let tied = params
533        .get(param_name)
534        .ok_or_else(|| format!("no such parameter: {}", param_name))?;
535
536    Ok(tied.db.path().to_string_lossy().to_string())
537}
538
539/// Check if a parameter is tied to GDBM
540pub fn is_gdbm_tied(param_name: &str) -> bool {
541    if let Ok(params) = TIED_PARAMS.lock() {
542        params.contains_key(param_name)
543    } else {
544        false
545    }
546}
547
548/// Get a tied parameter by name
549pub fn get_tied_param(param_name: &str) -> Option<Arc<TiedGdbmParam>> {
550    if let Ok(params) = TIED_PARAMS.lock() {
551        params.get(param_name).cloned()
552    } else {
553        None
554    }
555}
556
557/// Get value from a tied parameter
558pub fn gdbm_get(param_name: &str, key: &str) -> Option<String> {
559    get_tied_param(param_name).and_then(|p| p.get(key))
560}
561
562/// Set value in a tied parameter
563pub fn gdbm_set(param_name: &str, key: &str, value: &str) -> Result<(), String> {
564    let param = get_tied_param(param_name)
565        .ok_or_else(|| format!("not a tied gdbm hash: {}", param_name))?;
566    param.set(key, value)
567}
568
569/// Delete key from a tied parameter
570pub fn gdbm_delete(param_name: &str, key: &str) -> Result<(), String> {
571    let param = get_tied_param(param_name)
572        .ok_or_else(|| format!("not a tied gdbm hash: {}", param_name))?;
573    param.delete(key)
574}
575
576/// Get all keys from a tied parameter
577pub fn gdbm_keys(param_name: &str) -> Option<Vec<String>> {
578    get_tied_param(param_name).map(|p| p.keys())
579}
580
581#[cfg(test)]
582mod tests {
583    use super::*;
584    use std::fs;
585    use tempfile::tempdir;
586
587    #[test]
588    #[cfg(feature = "gdbm")]
589    fn test_gdbm_basic_operations() {
590        let dir = tempdir().unwrap();
591        let db_path = dir.path().join("test.gdbm");
592
593        // Open database
594        let db = GdbmDatabase::open(&db_path, false).unwrap();
595
596        // Set and get
597        db.set("key1", "value1").unwrap();
598        assert_eq!(db.get("key1"), Some("value1".to_string()));
599
600        // Non-existent key
601        assert_eq!(db.get("nonexistent"), None);
602
603        // Delete
604        db.delete("key1").unwrap();
605        assert_eq!(db.get("key1"), None);
606
607        // Multiple keys
608        db.set("a", "1").unwrap();
609        db.set("b", "2").unwrap();
610        db.set("c", "3").unwrap();
611
612        let keys = db.keys();
613        assert_eq!(keys.len(), 3);
614        assert!(keys.contains(&"a".to_string()));
615        assert!(keys.contains(&"b".to_string()));
616        assert!(keys.contains(&"c".to_string()));
617
618        // Clear
619        db.clear().unwrap();
620        assert_eq!(db.keys().len(), 0);
621    }
622
623    #[test]
624    #[cfg(feature = "gdbm")]
625    fn test_tied_param() {
626        let dir = tempdir().unwrap();
627        let db_path = dir.path().join("tied.gdbm");
628
629        let db = Arc::new(GdbmDatabase::open(&db_path, false).unwrap());
630        let tied = TiedGdbmParam::new("mydb".to_string(), db);
631
632        tied.set("foo", "bar").unwrap();
633        assert_eq!(tied.get("foo"), Some("bar".to_string()));
634
635        let hash = tied.to_hash();
636        assert_eq!(hash.get("foo"), Some(&"bar".to_string()));
637    }
638}