gdbm/
lib.rs

1//! Safe interface to GNU dbm.
2//!
3//! The main documentation can be found at [`Gdbm`].
4#[macro_use]
5extern crate bitflags;
6extern crate gdbm_sys;
7extern crate libc;
8
9use std::error::Error as StdError;
10use std::io::Error;
11use std::fmt;
12use std::ffi::{CStr, CString, IntoStringError, NulError};
13use std::os::unix::ffi::OsStrExt;
14use std::os::unix::io::{AsRawFd, RawFd};
15use std::path::Path;
16use std::str::Utf8Error;
17use std::string::FromUtf8Error;
18
19use libc::{c_uint, c_void, free};
20
21use gdbm_sys::*;
22
23/// Custom error handling for the library
24#[derive(Debug)]
25pub enum GdbmError {
26    FromUtf8Error(FromUtf8Error),
27    Utf8Error(Utf8Error),
28    NulError(NulError),
29    Error(String),
30    IoError(Error),
31    IntoStringError(IntoStringError),
32}
33
34impl fmt::Display for GdbmError {
35    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
36        write!(f, "{}", self)
37    }
38}
39
40impl StdError for GdbmError {
41    fn description(&self) -> &str {
42        match *self {
43            GdbmError::FromUtf8Error(ref _e) => "invalid utf-8 sequence",
44            GdbmError::Utf8Error(ref _e) => "invalid utf-8 sequence",
45            GdbmError::NulError(ref _e) => "nul byte found in provided data",
46            GdbmError::Error(ref _e) => "gdbm error",
47            GdbmError::IoError(ref _e) => "I/O error",
48            GdbmError::IntoStringError(ref _e) => "error",
49        }
50    }
51    fn cause(&self) -> Option<&dyn StdError> {
52        match *self {
53            GdbmError::FromUtf8Error(ref e) => e.source(),
54            GdbmError::Utf8Error(ref e) => e.source(),
55            GdbmError::NulError(ref e) => e.source(),
56            GdbmError::Error(_) => None,
57            GdbmError::IoError(ref e) => e.source(),
58            GdbmError::IntoStringError(ref e) => e.source(),
59        }
60    }
61}
62impl GdbmError {
63    /// Create a new GdbmError with a String message
64    fn new(err: impl Into<String>) -> GdbmError {
65        GdbmError::Error(err.into())
66    }
67
68    /// Convert a GdbmError into a String representation.
69    pub fn to_string(&self) -> String {
70        match *self {
71            GdbmError::FromUtf8Error(ref err) => err.utf8_error().to_string(),
72            GdbmError::Utf8Error(ref err) => err.to_string(),
73            GdbmError::NulError(ref err) => err.to_string(),
74            GdbmError::Error(ref err) => err.to_string(),
75            GdbmError::IoError(ref err) => err.to_string(),
76            GdbmError::IntoStringError(ref err) => err.to_string(),
77        }
78    }
79}
80
81impl From<NulError> for GdbmError {
82    fn from(err: NulError) -> GdbmError {
83        GdbmError::NulError(err)
84    }
85}
86impl From<FromUtf8Error> for GdbmError {
87    fn from(err: FromUtf8Error) -> GdbmError {
88        GdbmError::FromUtf8Error(err)
89    }
90}
91impl From<::std::str::Utf8Error> for GdbmError {
92    fn from(err: ::std::str::Utf8Error) -> GdbmError {
93        GdbmError::Utf8Error(err)
94    }
95}
96impl From<IntoStringError> for GdbmError {
97    fn from(err: IntoStringError) -> GdbmError {
98        GdbmError::IntoStringError(err)
99    }
100}
101impl From<Error> for GdbmError {
102    fn from(err: Error) -> GdbmError {
103        GdbmError::IoError(err)
104    }
105}
106
107
108fn get_error() -> String {
109    unsafe {
110        let error_ptr = gdbm_strerror(*gdbm_errno_location());
111        let err_string = CStr::from_ptr(error_ptr);
112        return err_string.to_string_lossy().into_owned();
113    }
114}
115
116fn datum(what: &str, data: impl AsRef<[u8]>) -> Result<datum, GdbmError> {
117    let data = data.as_ref();
118    if data.len() > i32::MAX as usize {
119        return Err(GdbmError::new(format!("{} too large", what)));
120    }
121    // Note that we cast data.as_ptr(), which is a *const u8, to
122    // a *mut i8. This is an artefact of the gdbm C interface where
123    // 'dptr' is not 'const'. However gdbm does treat it as
124    // const/immutable, so the cast is safe.
125    Ok(datum {
126        dptr: data.as_ptr() as *mut i8,
127        dsize: data.len() as i32,
128    })
129}
130
131bitflags! {
132    /// Flags for [`Gdbm::new`]
133    pub struct Open: c_uint {
134        /// Read only database access
135        const READER  = 0;
136        /// Read and Write access to the database
137        const WRITER  = 1;
138        /// Read, write and create the database if it does not already exist
139        const WRCREAT = 2;
140        /// Create a new database regardless of whether one exised.  Gives read and write access
141        const NEWDB   = 3;
142        const FAST = 16;
143        /// Sync all operations to disk
144        const SYNC = 32;
145        /// Prevents the library from locking the database file
146        const NOLOCK = 64;
147    }
148}
149
150bitflags! {
151    struct Store: c_uint {
152        const INSERT  = 0;
153        const REPLACE  = 1;
154    }
155}
156
157/// An open `gdbm` database.
158///
159/// Note that a lot of the methods take arguments of type `impl AsRef<[u8]>`.
160/// This means you can pass types `String`, `&str`, `&String`, `&[u8]` directly.
161///
162#[derive(Debug)]
163pub struct Gdbm {
164    db_handle: GDBM_FILE, /* int gdbm_export (GDBM_FILE, const char *, int, int);
165                           * int gdbm_export_to_file (GDBM_FILE dbf, FILE *fp);
166                           * int gdbm_import (GDBM_FILE, const char *, int);
167                           * int gdbm_import_from_file (GDBM_FILE dbf, FILE *fp, int flag);
168                           * int gdbm_count (GDBM_FILE dbf, gdbm_count_t *pcount);
169                           * int gdbm_version_cmp (int const a[], int const b[]);
170                           * */
171}
172
173// Safety: Gdbm does have thread-local data, but it's only used to set
174// the gdbm_errno. We access that internally directly after calling
175// into the gdbm library, it's not used to keep state besides that.
176unsafe impl Send for Gdbm {}
177
178impl Drop for Gdbm {
179    fn drop(&mut self) {
180        if self.db_handle.is_null() {
181            // No cleanup needed
182            return;
183        }
184        unsafe {
185            gdbm_close(self.db_handle);
186        }
187    }
188}
189
190/// With locking disabled (if gdbm_open was called with ‘GDBM_NOLOCK’), the user may want
191/// to perform their own file locking on the database file in order to prevent multiple
192/// writers operating on the same file simultaneously.
193impl AsRawFd for Gdbm {
194    fn as_raw_fd(&self) -> RawFd {
195        unsafe {
196            gdbm_fdesc(self.db_handle) as RawFd
197        }
198    }
199}
200
201impl Gdbm {
202    /// Open a DBM with location.
203    ///
204    /// `mode` is the unix mode used when a database is created. See
205    /// [chmod](http://www.manpagez.com/man/2/chmod) and
206    /// [open](http://www.manpagez.com/man/2/open).
207    pub fn new(
208        path: impl AsRef<Path>,
209        block_size: u32,
210        flags: Open,
211        mode: u32
212    ) -> Result<Gdbm, GdbmError> {
213        if block_size > i32::MAX as u32 {
214            return Err(GdbmError::new("block_size too large"));
215        }
216        if mode > i32::MAX as u32 {
217            return Err(GdbmError::new("invalid mode"));
218        }
219        let path = CString::new(path.as_ref().as_os_str().as_bytes())?;
220        unsafe {
221            let db_ptr = gdbm_open(path.as_ptr() as *mut i8,
222                                   block_size as i32,
223                                   flags.bits as i32,
224                                   mode as i32,
225                                   None);
226            if db_ptr.is_null() {
227                return Err(GdbmError::new("gdbm_open failed".to_string()));
228            }
229            Ok(Gdbm { db_handle: db_ptr })
230        }
231    }
232
233    /// Store a record in the database.
234    ///
235    /// If `replace` is `false`, and the key already exists in the
236    /// database, the record is not stored and `false` is returned.
237    /// Otherwise `true` is returned.
238    pub fn store(
239        &self,
240        key: impl AsRef<[u8]>,
241        content: impl AsRef<[u8]>,
242        replace: bool,
243    ) -> Result<bool, GdbmError> {
244        let key_datum = datum("key", key)?;
245        let content_datum = datum("content", content)?;
246        let flag = if replace { Store::REPLACE } else { Store::INSERT };
247        let result = unsafe {
248            gdbm_store(self.db_handle, key_datum, content_datum, flag.bits as i32)
249        };
250        if result < 0 {
251            return Err(GdbmError::new(get_error()));
252        }
253        Ok(result == 0)
254    }
255
256    /// Retrieve a record from the database.
257    pub fn fetch_data(&self, key: impl AsRef<[u8]>) -> Result<Vec<u8>, GdbmError> {
258        // datum gdbm_fetch(dbf, key);
259        let key_datum = datum("key", key)?;
260        unsafe {
261            let content = gdbm_fetch(self.db_handle, key_datum);
262            if content.dptr.is_null() {
263                return Err(GdbmError::new(get_error()));
264            } else if content.dsize < 0 {
265                return Err(GdbmError::new("content has negative size"));
266            } else {
267                let ptr = content.dptr as *const u8;
268                let len = content.dsize as usize;
269                let slice = std::slice::from_raw_parts(ptr, len);
270                let vec = slice.to_vec();
271
272                // Free the malloc'd content that the library gave us
273                // Rust will manage this memory
274                free(content.dptr as *mut c_void);
275
276                return Ok(vec);
277            }
278        }
279    }
280
281    /// Retrieve a string record from the database.
282    ///
283    /// If it exists, but the content is not a valid UTF-8 string,
284    /// `FromUtf8Error` will be returned.
285    pub fn fetch_string(&self, key: impl AsRef<[u8]>) -> Result<String, GdbmError> {
286        let vec = self.fetch_data(key)?;
287        let s = String::from_utf8(vec)?;
288        Ok(s)
289    }
290
291    /// Retrieve a string record from the database and strip trailing '\0'.
292    ///
293    /// C libraries might store strings with their trailing '\0' byte.
294    /// This method is like `fetch_string` but it strips that byte.
295    pub fn fetch_cstring(&self, key: impl AsRef<[u8]>) -> Result<String, GdbmError> {
296        let vec = self.fetch_data(key)?;
297        let mut s = String::from_utf8(vec)?;
298        if s.ends_with("\0") {
299            s.pop();
300        }
301        Ok(s)
302    }
303
304    /// Delete a record from the database.
305    ///
306    /// Returns `false` if the key was not present, `true` if it
307    /// was present and the record was deleted.
308    pub fn delete(&self, key: impl AsRef<[u8]>) -> Result<bool, GdbmError> {
309        let key_datum = datum("key", key)?;
310        let result = unsafe {
311            gdbm_delete(self.db_handle, key_datum)
312        };
313        if result < 0 {
314            if unsafe { *gdbm_errno_location() } == 0 {
315                return Ok(false);
316            }
317            return Err(GdbmError::new(get_error()));
318        }
319        Ok(true)
320    }
321
322    // TODO: Make an iterator out of this to hide the datum handling
323    // pub fn firstkey(&self, key: &str) -> Result<String, GdbmError> {
324    // unsafe {
325    // let content = gdbm_firstkey(self.db_handle);
326    // if content.dptr.is_null() {
327    // return Err(GdbmError::new(get_error()));
328    // } else {
329    // let c_string = CStr::from_ptr(content.dptr);
330    // let data = c_string.to_str()?.to_string();
331    // Free the malloc'd content that the library gave us
332    // Rust will manage this memory
333    // free(content.dptr as *mut c_void);
334    //
335    // return Ok(data);
336    // }
337    // }
338    // }
339    // pub fn nextkey(&self, key: &str) -> Result<String, GdbmError> {
340    // unsafe {
341    // datum gdbm_nextkey(dbf, key);
342    //
343    // }
344    // }
345    //
346    // int gdbm_reorganize(dbf);
347    pub fn sync(&self) {
348        unsafe {
349            gdbm_sync(self.db_handle);
350        }
351    }
352
353    /// Check to see if a record with this key exists in the database
354    pub fn exists(&self, key: impl AsRef<[u8]>) -> Result<bool, GdbmError> {
355        let key_datum = datum("key", key)?;
356        unsafe {
357            let result = gdbm_exists(self.db_handle, key_datum);
358            if result == 0 {
359                Ok(true)
360            } else {
361                if *gdbm_errno_location() == 0 {
362                    return Ok(true);
363                } else {
364                    return Err(GdbmError::new(get_error()));
365                }
366            }
367        }
368    }
369    // pub fn setopt(&self, key: &str) -> Result<(), GdbmError> {
370    // unsafe {
371    // int gdbm_setopt(dbf, option, value, size);
372    //
373    // }
374    // }
375    //
376}