libcqdb/
lib.rs

1#![allow(non_camel_case_types)]
2#![allow(clippy::missing_safety_doc)]
3use std::{
4    ffi::CStr,
5    fs::File,
6    io::BufWriter,
7    os::raw::{c_char, c_int, c_uint, c_void},
8    ptr,
9};
10
11use cqdb::{CQDBWriter, Flag, CQDB};
12use libc::FILE;
13
14#[macro_use]
15mod macros;
16
17/// No flag
18pub const CQDB_NONE: c_uint = 0;
19/// A reverse lookup array is omitted
20pub const CQDB_ONEWAY: c_uint = 1;
21
22/// Success
23pub const CQDB_SUCCESS: c_int = 0;
24/// Invalid id parameters
25pub const CQDB_ERROR_INVALIDID: c_int = -1018;
26/// Error in file write operations.
27pub const CQDB_ERROR_FILEWRITE: c_int = -1021;
28/// String not found
29pub const CQDB_ERROR_NOTFOUND: c_int = -1023;
30
31/// CQDB Reader API
32#[derive(Debug, Clone, Copy)]
33#[repr(C)]
34pub struct tag_cqdb {
35    _unused: [u8; 0],
36}
37
38pub type cqdb_t = tag_cqdb;
39
40#[derive(Debug, Clone, Copy)]
41#[repr(C)]
42struct tag_cqdb_writer_inner {
43    _unused: [u8; 0],
44}
45
46/// CQDB Writer API
47#[derive(Debug, Clone, Copy)]
48#[repr(C)]
49pub struct tag_cqdb_writer {
50    file: *mut FILE,
51    inner: *mut tag_cqdb_writer_inner,
52}
53
54pub type cqdb_writer_t = tag_cqdb_writer;
55
56ffi_fn! {
57    /// Delete the CQDB reader.
58    fn cqdb_delete(db: *mut cqdb_t) {
59        if !db.is_null() {
60            unsafe { Box::from_raw(db as *mut CQDB) };
61        }
62    }
63}
64
65ffi_fn! {
66    /// Open a new CQDB reader on a memory block.
67    fn cqdb_reader(buffer: *const c_void, size: usize) -> *mut cqdb_t {
68        let buf = unsafe { std::slice::from_raw_parts(buffer as *const u8, size) };
69        if let Ok(db) = CQDB::new(buf) {
70            Box::into_raw(Box::new(db)) as *mut cqdb_t
71        } else {
72            ptr::null_mut()
73        }
74    }
75}
76
77ffi_fn! {
78    /// Get the number of associations in the database.
79    fn cqdb_num(db: *mut cqdb_t) -> c_int {
80        let db = db as *mut CQDB;
81        unsafe {
82            (*db).num() as c_int
83        }
84    }
85}
86
87ffi_fn! {
88    /// Retrieve the identifier associated with a string.
89    ///
90    /// Returns the non-negative identifier if successful, negative status code otherwise.
91    fn cqdb_to_id(db: *mut cqdb_t, s: *const c_char) -> c_int {
92        let db = db as *mut CQDB;
93        unsafe {
94            let c_str = CStr::from_ptr(s).to_str().unwrap();
95            (*db).to_id(c_str).map(|id| id as c_int).unwrap_or(CQDB_ERROR_NOTFOUND)
96        }
97    }
98}
99
100ffi_fn! {
101    /// Retrieve the string associated with an identifier.
102    ///
103    /// Pointer to the string associated with the identifier if successful; otherwise NULL.
104    fn cqdb_to_string(db: *mut cqdb_t, id: c_int) -> *const c_char {
105        let db = db as *mut CQDB;
106        if let Some(s) = unsafe { (*db).to_str(id as u32) } {
107            // Safety
108            // This is safe because s is borrowed from the original buffer
109            s.as_ptr() as *const c_char
110        } else {
111            ptr::null_mut()
112        }
113    }
114}
115
116ffi_fn! {
117    /// Create a new CQDB writer on a seekable stream.
118    ///
119    /// This function initializes a database on the seekable stream and returns the pointer to a `::cqdb_writer_t` instanceto write the database.
120    /// The stream must have the writable and binary flags.
121    /// The database creation flag must be zero except when the reverse lookup array is unnecessary;
122    /// specifying `::CQDB_ONEWAY` flag will save the storage space for the reverse lookup array.
123    /// Once calling this function, one should avoid accessing the seekable stream directly until calling `cqdb_writer_close()`.
124    fn cqdb_writer(fp: *mut FILE, flag: c_int) -> *mut cqdb_writer_t {
125        unsafe {
126            let file = new_file_from_libc(fp);
127            let buf_writer = BufWriter::new(file);
128            let flag = if flag as c_uint == CQDB_ONEWAY {
129                Flag::ONEWAY
130            } else {
131                Flag::NONE
132            };
133            let writer = match CQDBWriter::with_flag(buf_writer, flag) {
134                Ok(writer) => {
135                    let inner = Box::into_raw(Box::new(writer)) as *mut tag_cqdb_writer_inner;
136                    Box::into_raw(Box::new(cqdb_writer_t { file: fp, inner }))
137                }
138                Err(_) => ptr::null_mut(),
139            };
140            writer
141        }
142    }
143}
144
145#[cfg(unix)]
146unsafe fn new_file_from_libc(fp: *mut FILE) -> File {
147    use std::os::unix::io::FromRawFd;
148
149    // Safely get the file descriptor associated with FILE by fflush()ing its contents first
150    // Reference: https://stackoverflow.com/a/31688641
151    libc::fflush(fp);
152    // `from_raw_fd` takes the ownship of the file descriptor, use `libc::dup` to get a new fd
153    let fd = libc::dup(libc::fileno(fp));
154    File::from_raw_fd(fd)
155}
156
157#[cfg(windows)]
158unsafe fn new_file_from_libc(fp: *mut FILE) -> File {
159    use std::os::windows::io::{FromRawHandle, RawHandle};
160
161    // Safely get the file descriptor associated with FILE by fflush()ing its contents first
162    // Reference: https://stackoverflow.com/a/31688641
163    libc::fflush(fp);
164    let fd = libc::dup(libc::fileno(fp));
165    let handle = libc::get_osfhandle(fd) as RawHandle;
166    // `from_raw_handle` takes the ownship of the file descriptor, use `libc::dup` to get a new fd
167    File::from_raw_handle(handle)
168}
169
170ffi_fn! {
171    /// Close a CQDB writer.
172    ///
173    /// This function finalizes the database on the stream.
174    /// If successful, the data remaining on the memory is flushed to the stream;
175    /// the stream position is moved to the end of the chunk.
176    /// If an unexpected error occurs, this function tries to rewind the stream position to
177    /// the original position when the function `cqdb_writer()` was called.
178    fn cqdb_writer_close(dbw: *mut cqdb_writer_t) -> c_int {
179        if !dbw.is_null() {
180            unsafe {
181                let inner = (*dbw).inner as *mut CQDBWriter<BufWriter<File>>;
182                // Drop CQDBWriter
183                Box::from_raw(inner);
184                // Re-sync file position so that ftell works correctly
185                // Reference: https://stackoverflow.com/a/31688641
186                let offset = libc::lseek(libc::fileno((*dbw).file), 0, libc::SEEK_CUR);
187                libc::fseek((*dbw).file, offset, libc::SEEK_SET);
188                Box::from_raw(dbw);
189            }
190        }
191        CQDB_SUCCESS
192    }
193}
194
195ffi_fn! {
196    /// Put a string/identifier association to the database.
197    ///
198    /// This function append a string/identifier association into the database.
199    /// Make sure that the string and/or identifier have never been inserted to the database
200    /// and that the identifier is a non-negative value.
201    fn cqdb_writer_put(
202        dbw: *mut cqdb_writer_t,
203        s: *const c_char,
204        id: c_int
205    ) -> c_int {
206        if id < 0 {
207            return CQDB_ERROR_INVALIDID;
208        }
209        unsafe {
210            let dbw = (*dbw).inner as *mut CQDBWriter<BufWriter<File>>;
211            let c_str = CStr::from_ptr(s).to_str().unwrap();
212            if let Err(_) = (*dbw).put(c_str, id as u32) {
213                return CQDB_ERROR_FILEWRITE;
214            }
215        }
216        CQDB_SUCCESS
217    }
218}
219
220#[cfg(test)]
221mod test {
222    use super::*;
223    use std::{ffi::CString, fs};
224
225    #[test]
226    fn test_cqdb_read_cqdb_ffi() {
227        let name = CString::new("../tests/output/cqdb-ffi-1.cqdb").unwrap();
228        let mode = CString::new("wb").unwrap();
229        unsafe {
230            let fp = libc::fopen(name.as_ptr(), mode.as_ptr());
231            assert!(!fp.is_null());
232            let writer = cqdb_writer(fp, 0);
233            assert!(!writer.is_null());
234            for i in 0..100 {
235                let s = CString::new(format!("{:08}", i)).unwrap();
236                assert_eq!(0, cqdb_writer_put(writer, s.as_ptr(), i));
237            }
238            let s = CString::new(format!("{:08}", 10)).unwrap();
239            assert_eq!(
240                CQDB_ERROR_INVALIDID,
241                cqdb_writer_put(writer, s.as_ptr(), -1)
242            );
243            assert_eq!(0, cqdb_writer_close(writer));
244            libc::fclose(fp);
245        }
246        let buf = std::fs::read("../tests/output/cqdb-ffi-1.cqdb").unwrap();
247        let db = CQDB::new(&buf).unwrap();
248        assert_eq!(100, db.num());
249    }
250
251    #[test]
252    fn test_cqdb_ffi_read_cqdb_writer() {
253        let file = fs::File::create("../tests/output/cqdb-ffi-2.cqdb").unwrap();
254        let mut writer = CQDBWriter::new(file).unwrap();
255        for id in 0..100 {
256            let key = format!("{:08}", id);
257            writer.put(&key, id).unwrap();
258        }
259        drop(writer);
260
261        let buf = fs::read("../tests/output/cqdb-ffi-2.cqdb").unwrap();
262        unsafe {
263            let db = cqdb_reader(buf.as_ptr() as _, buf.len());
264            assert!(!db.is_null());
265            let size = cqdb_num(db);
266            // Forward lookups, strings to integer indentifiers
267            for id in 0..size {
268                let key = CString::new(format!("{:08}", id)).unwrap();
269                let j = cqdb_to_id(db, key.as_ptr());
270                assert_eq!(id, j);
271            }
272            let key = CString::new("non-exists-key").unwrap();
273            let j = cqdb_to_id(db, key.as_ptr());
274            assert!(j < 0);
275
276            // Backward lookups: integer identifiers to strings.
277            for id in 0..size {
278                let ptr = cqdb_to_string(db, id);
279                assert!(!ptr.is_null());
280                let key = CStr::from_ptr(ptr).to_str().unwrap();
281                assert_eq!(key, format!("{:08}", id));
282            }
283            let ptr = cqdb_to_string(db, size + 100);
284            assert!(ptr.is_null());
285
286            cqdb_delete(db);
287        }
288    }
289}