1use 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
21const 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#[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
82type 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#[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
356pub 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
431static TIED_PARAMS: Lazy<Mutex<HashMap<String, Arc<TiedGdbmParam>>>> =
433 Lazy::new(|| Mutex::new(HashMap::new()));
434
435pub 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
444pub 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 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 {
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 let db = GdbmDatabase::open(&path, readonly)?;
481 let db = Arc::new(db);
482
483 let tied = Arc::new(TiedGdbmParam::new(param_name.clone(), db));
485
486 {
488 let mut params = TIED_PARAMS.lock().map_err(|_| "lock error")?;
489 params.insert(param_name.clone(), tied);
490 }
491
492 Ok(())
493}
494
495pub 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
525pub 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
539pub 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
548pub 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
557pub fn gdbm_get(param_name: &str, key: &str) -> Option<String> {
559 get_tied_param(param_name).and_then(|p| p.get(key))
560}
561
562pub 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
569pub 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
576pub 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 let db = GdbmDatabase::open(&db_path, false).unwrap();
595
596 db.set("key1", "value1").unwrap();
598 assert_eq!(db.get("key1"), Some("value1".to_string()));
599
600 assert_eq!(db.get("nonexistent"), None);
602
603 db.delete("key1").unwrap();
605 assert_eq!(db.get("key1"), None);
606
607 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 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}