In this exercise your task is to create a Rust binding, or foreign function interface (FFI) to the LevelDB database library. Typically, and also in this exercise, "foreign" means "C".
You will learn how to:
handle pointers passed to or from the foreign language
use low-level C bindings
utilize Rust's ownership system to provide safety on top of those raw primitives
This exercise requires knowledge of a few Rust and C concepts:
Conceptually, LevelDB is a key-value store , AKA a persistent dictionary or map.
How is ownership handled in C?
Hint
When does a "double free" occur?
Solution
Ownership is handled only informally - typically an API's documentation and/or function names (e.g. "create", "new") will indicate whether you are responsible to free up the memory passed to you, or it is somebody else's problem. Unclear ownership (via multiple mutable pointers to the same memory), API misunderstandings or other kinds of human error can easily lead to memory being freed too often or too little, resulting in crashes or leaks.
Binding to C is divided into two parts: a minimal low-level interface (called "sys crate") and a higher level wrapper crate.
The sys crate is responsible for linking to the C library and exposing its contents unchanged.
The higher level crate uses the sys crate to provide a more Rust-friendly interface by safely wrapping the inherently unsafe
raw parts.
Writing a sys crate yourself is beyond the scope of this exercise. We will be using the leveldb-sys
crate provided for you .
Additionally, you'll also need the libc
crate which provides C types and other required definitions.
To use them, you will need to specify leveldb-sys
and libc
in the [dependencies]
section of your project's Cargo.toml
.
# in Cargo.toml
[dependencies]
+leveldb-sys = "*"
+libc = "*"
❗ Note on specifying dependencies using the asterisk
Declaring a dependency as * ("any version") is generally not recommended - we're doing it here to prevent stale version numbers and assume it's safe because these specific crates are very unlikely to introduce breaking changes. An alternative would be to install cargo-edit and then use the contained cargo-add command to add a dependency on the most recent release version:
$ cargo add leveldb-sys
$ cargo add libc
Building leveldb-sys
requires CMake and a C++ compiler (gcc, clang, Visual Studio etc.) to be installed on your system.
🔎 Should you ever need to write your own sys crate you can find instructions for doing so here .
Conceptually, a LevelDB database is a directory (the "database name") where all its required files are stored. "Opening" it means passing a path (whose last part is the database name) and some options to leveldb_open
, notably create_if_missing
, which will create the database directory in case it does not exist.
You'll also need these functions and enums from the leveldb-sys
crate:
The LevelDB C header documents some conventions used by its implementation.
If you're stuck, check out the Help and hints below!
✅ Create a new library package project for this group of exercises. Add the required dependencies.
✅ Implement functions for:
opening a database, forwarding the "create if missing" flag
closing it again
✅ Refactor your code and create wrapping structs for the raw leveldb_t
and leveldb_options_t
types, taking care of the required cleanup operations.
It should not be possible to forget the cleanup.
The struct member storing the pointer should use an appropriate Rust type to express the fact that it exclusively stores non-null
pointers.
✅ Create a Database
struct that manages high-level operations.
How's your error handling? open
should not panic - return a custom error instead. To keep things concise, make use of the question mark operator (see also the following section on using it in tests). Convert errors where necessary.
✅ Test the success and error cases.
Note: you can provoke an error by trying to open a database folder to which you don't have write access
✅ What type(s) does your open
function accept as database names? What would offer the most flexibility? Get some inspiration from std::fs::File .
✅ Include the underlying LevelDB error message string in your error type. Mind the ownership!
✅ Rust Strings are valid UTF-8. What issues might occur
as opposed to C strings?
regarding valid file system characters? (a LevelDB database is a directory!)
The most straightforward parameter type for the database name is &str
(why not String
? ). Since we're dealing with paths, what would be an alternative that still has the convenience of using string literals on the caller side?
✅ Change your function signature accordingly.
Hint
Which trait bounding provides the required functionality?
Getting mysterious crashes? Are you maybe dereferencing an uninitialized raw pointer?
c_uchar
is an alias for u8
, and Rust booleans are safe to cast as u8
.
You need to pass an error pointer to open
. The C library potentially mutates it, and initially it should point to null
. For creating a suitable pointer Rust provides you with std::ptr::null_mut
.
A null pointer is equal (==
) to any other null pointer. Raw pointers also offer is_null() for checking.
If an error occurs you own the C string containing the error message. You can either free it or reuse it for future calls - which option is more convenient?
When open
succeeds, it gives you a valid, non-null pointer back. A natural mapping for this type is NonNull::new_unchecked
.
Owned C strings can be created with std::ffi::CString
. Note that unlike C strings, Rust strings can contain null bytes.
To handle paths there's std::path::Path
. For simplicity reasons, assume paths are valid UTF-8, which in the real world isn't always the case.
Errors can be converted with map_err
and From::from
.
String types implement AsRef<Path> , which makes it a good fit for path parameters.
LevelDB, being a database, persists data to disk. When writing tests for your binding, creating this data in a temporary fashion is appropriate, saving you from doing cleanup work yourself. The tempdir crate provides this functionality, but if you added it to [dependencies]
it would also be installed for every user of your library, even if they didn't intend to run your tests. Fortunately, Cargo has a [dev-dependencies]
section for crates that are only required during development:
[dev-dependencies]
tempdir = "*"
You can use this code skeleton to get started:
#![allow(unused)]
fn main () {
use leveldb_sys::*;
use std::ptr;
use std::ffi::CString;
#[test]
fn basic_template () {
let options = unsafe { leveldb_options_create() };
unsafe { leveldb_options_set_create_if_missing(options, true as u8 ) };
let mut err = ptr::null_mut();
let name = CString::new("my_db" ).unwrap();
let (db_ptr, err_ptr) = unsafe {
let db_ptr = leveldb_open(
options,
name.as_ptr(),
&mut err,
);
(db_ptr, err)
};
unsafe { leveldb_options_destroy(options) };
if err_ptr == ptr::null_mut() {
unsafe { leveldb_close(db_ptr) }
} else {
unsafe {
println! ("Error opening database: {}" , *err_ptr);
}
}
}
}
#![allow(unused)]
fn main () {
use libc::{c_void, size_t};
use std::ffi::CString;
use std::path::Path;
use std::ptr;
use std::ptr::NonNull;
use leveldb_sys::{
leveldb_close, leveldb_create_iterator, leveldb_free, leveldb_get, leveldb_iter_destroy,
leveldb_iter_next, leveldb_iter_seek_to_first, leveldb_iter_valid, leveldb_iter_value,
leveldb_iterator_t, leveldb_open, leveldb_options_create, leveldb_options_destroy,
leveldb_options_set_create_if_missing, leveldb_options_t, leveldb_put,
leveldb_readoptions_create, leveldb_readoptions_destroy, leveldb_readoptions_t, leveldb_t,
leveldb_writeoptions_create, leveldb_writeoptions_destroy, leveldb_writeoptions_t,
};
struct DBHandle {
ptr: NonNull<leveldb_t>,
}
impl Drop for DBHandle {
fn drop (&mut self ) {
unsafe { leveldb_close(self .ptr.as_ptr()) }
}
}
pub struct Options {
ptr: NonNull<leveldb_options_t>,
}
impl Drop for Options {
fn drop (&mut self ) {
unsafe { leveldb_options_destroy(self .ptr.as_ptr()) }
}
}
impl Options {
pub fn new () -> Options {
unsafe {
let ptr = leveldb_options_create();
Options {
ptr: NonNull::new_unchecked(ptr),
}
}
}
pub fn create_if_missing (&mut self , value: bool ) {
unsafe { leveldb_options_set_create_if_missing(self .as_ptr(), value as u8 ) }
}
fn as_ptr (&self ) -> *mut leveldb_options_t {
self .ptr.as_ptr()
}
}
pub struct Database {
handle: DBHandle,
}
unsafe fn into_rust_string (ptr: *const i8 ) -> String {
let error_s = CStr::from_ptr(ptr).to_string_lossy().to_string();
leveldb_free(ptr as *mut c_void);
error_s
}
#[derive(Debug, Eq, PartialEq)]
pub enum Error {
OpenFail(String ),
InvalidString,
}
impl Database {
pub fn open <P: AsRef <Path>>(path: P, options: Options) -> Result <Database, Error> {
let mut error = ptr::null_mut();
let c_string = CString::new(path.as_ref().to_str().ok_or(Error::InvalidString)?)
.map_err(|_| Error::InvalidString)?;
unsafe {
let db = leveldb_open(options.as_ptr(), c_string.as_ptr(), &mut error);
if error == ptr::null_mut() {
Ok (Database {
handle: DBHandle {
ptr: NonNull::new_unchecked(db),
},
})
} else {
Err (Error::OpenFail(into_rust_string(error)))
}
}
}
}
#[cfg(test)]
mod test {
use super::*;
use tempdir::TempDir;
#[test]
fn test_open () {
let tmp = TempDir::new("test_open" ).unwrap();
let mut options = Options::new();
options.create_if_missing(true );
let database = Database::open(tmp.path().join("database" ), options);
assert! (database.is_ok());
}
}
}
Now that you have an open database, it's time to interact with it by storing and retrieving data.
You'll need a few more items from the sys crate:
leveldb_readoptions_t
: opaque type to specify read operation options
leveldb_writeoptions_t
: opaque type to specify write operation options
leveldb_readoptions_create
: creates a default readoptions_t
leveldb_readoptions_destroy
: deallocates readoptions_t
leveldb_writeoptions_create
: creates a default writeoptions_t
leveldb_writeoptions_destroy
: deallocates writeoptions_t
leveldb_put
: writes a binary value for a given binary key
leveldb_get
: reads a binary value for a given binary key. Returns a null
pointer for "not found", an owned object otherwise.
leveldb_free
: deallocates a value object returned by leveldb_get
✅ Implement two functions on your Database
type:
pub fn put(&self, key: &[u8], data: &[u8]) -> Result<(), Error>
pub fn get(&self, key: &[u8]) -> Result<Option<Box<[u8]>>, Error>
Be mindful of the API's ownership contract.
✅ Test your implementation.
#![allow(unused)]
fn main () {
pub struct WriteOptions {
ptr: NonNull<leveldb_writeoptions_t>,
}
impl WriteOptions {
pub fn new () -> WriteOptions {
unsafe {
let ptr = leveldb_writeoptions_create();
WriteOptions {
ptr: NonNull::new_unchecked(ptr),
}
}
}
}
impl Drop for WriteOptions {
fn drop (&mut self ) {
unsafe { leveldb_writeoptions_destroy(self .ptr.as_ptr()) }
}
}
pub struct ReadOptions {
ptr: NonNull<leveldb_readoptions_t>,
}
impl ReadOptions {
pub fn new () -> ReadOptions {
unsafe {
let ptr = leveldb_readoptions_create();
ReadOptions {
ptr: NonNull::new_unchecked(ptr),
}
}
}
}
impl Drop for ReadOptions {
fn drop (&mut self ) {
unsafe { leveldb_readoptions_destroy(self .ptr.as_ptr()) }
}
}
#[derive(Debug, Eq, PartialEq)]
pub enum Error {
GetFail(String ),
PutFail(String ),
}
impl Database {
pub fn get (&self , key: &[u8 ]) -> Result <Option <Box <[u8 ]>>, Error> {
unsafe {
let read_options = ReadOptions::new();
let mut len: size_t = 0 ;
let mut error = ptr::null_mut();
let data = leveldb_get(
self .handle.ptr.as_ptr(),
read_options.ptr.as_ptr(),
key.as_ptr() as *const i8 ,
key.len(),
&mut len,
&mut error,
);
if error == ptr::null_mut() {
if data == ptr::null_mut() {
Ok (None )
} else {
let slice = std::slice::from_raw_parts(data as *mut u8 , len);
let result = Box ::from(slice);
leveldb_free(data as *mut c_void);
Ok (Some (result))
}
} else {
Err (Error::GetFail(into_rust_string(error)))
}
}
}
pub fn put (&self , key: &[u8 ], data: &[u8 ]) -> Result <(), Error> {
unsafe {
let write_options = WriteOptions::new();
let mut error = ptr::null_mut();
leveldb_put(
self .handle.ptr.as_ptr(),
write_options.ptr.as_ptr(),
key.as_ptr() as *const i8 ,
key.len(),
data.as_ptr() as *const i8 ,
data.len(),
&mut error,
);
if error == ptr::null_mut() {
Ok (())
} else {
Err (Error::PutFail(into_rust_string(error)))
}
}
}
}
#[cfg(test)]
mod test {
#[test]
fn test_read_write () {
let tmp = TempDir::new("test_read_write" ).unwrap();
let mut options = Options::new();
options.create_if_missing(true );
let database = Database::open(tmp.path().join("database" ), options).unwrap();
let key: &[u8 ] = b"test" ;
let missing_key: &[u8 ] = b"test_missing" ;
let value: &[u8 ] = b"test" ;
database.put(key, value).unwrap();
let result = database.get(key);
assert_eq! (result, Ok (Some (Box ::from(value))));
let result = database.get(missing_key);
assert_eq! (result, Ok (None ));
}
}
}
In this last part we'll create an Iterator
for looping over everything stored in our database.
The iterator functionality is exposed by the sys crate as follows:
leveldb_create_iterator
: Creates an opaque leveldb_iterator_t
handle
leveldb_iter_seek_to_first
: Starts the iteration by seeking to the first item
leveldb_iter_next
: Advances iteration by one element
leveldb_iter_value
: Reads the value at the current iterator position
leveldb_iter_valid
: Indicates whether the iterator is currently valid
An iterator is position invalid before seeking to the first item and after it has advanced beyond the last one. Reading its value returns non-owned data.
✅ Implement an iterator handle. It should fully encapsulate any unsafe
code.
✅ Implement an Iterator
type that holds the necessary state and makes use of the handle.
✅ Implement pub fn iter(&self) -> Iterator
for your Database
struct.
✅ Implement std::iter::Iterator
for your Iterator
type. Its items should be of type Box<[u8]>
.
✅ Write a test case to verify that your iterator returns all items lexicographically sorted by key. Also test with an empty database.
✅ Make the Iterator
type reference the Database that created it. What has changed and what are the benefits?
✅ Bonus task: what could be used instead of the Database
reference that achieves the same goal but consumes no memory?
#![allow(unused)]
fn main () {
struct IteratorHandle {
ptr: NonNull<leveldb_iterator_t>,
}
impl IteratorHandle {
fn new (database: &Database, read_options: ReadOptions) -> IteratorHandle {
unsafe {
let iterator_ptr =
leveldb_create_iterator(database.handle.ptr.as_ptr(), read_options.ptr.as_ptr());
leveldb_iter_seek_to_first(iterator_ptr);
IteratorHandle {
ptr: NonNull::new_unchecked(iterator_ptr),
}
}
}
fn next (&self ) {
unsafe { leveldb_iter_next(self .ptr.as_ptr()) };
}
fn valid (&self ) -> bool {
unsafe { leveldb_iter_valid(self .ptr.as_ptr()) != 0 }
}
fn value (&self ) -> (*const i8 , usize ) {
unsafe {
let mut len = 0 ;
let data = leveldb_iter_value(self .ptr.as_ptr(), &mut len);
(data, len)
}
}
}
impl Drop for IteratorHandle {
fn drop (&mut self ) {
unsafe { leveldb_iter_destroy(self .ptr.as_ptr()) }
}
}
impl Database {
pub fn iter (&self ) -> Iterator <'_ > {
let read_options = ReadOptions::new();
let handle = IteratorHandle::new(self , read_options);
Iterator {
handle: handle,
start: true ,
database: self ,
}
}
}
pub struct Iterator <'iterator > {
handle: IteratorHandle,
start: bool ,
#[allow(unused)]
database: &'iterator Database,
}
impl <'iterator > Iterator <'iterator > {
fn read_current (&self ) -> Option <Box <[u8 ]>> {
unsafe {
if !self .handle.valid() {
return None ;
};
let data = self .handle.value();
let slice = std::slice::from_raw_parts(data.0 as *mut u8 , data.1 );
Some (Box ::from(slice))
}
}
}
impl <'iterator > std::iter::Iterator for Iterator <'iterator > {
type Item = Box <[u8 ]>;
fn next (&mut self ) -> Option <Self::Item> {
if self .start {
self .start = false ;
self .read_current()
} else {
self .handle.next();
self .read_current()
}
}
}
#[cfg(test)]
mod test {
#[test]
fn test_iter () {
let tmp = TempDir::new("test_iter" ).unwrap();
let mut options = Options::new();
options.create_if_missing(true );
let database = Database::open(tmp.path().join("database" ), options).unwrap();
let key1: &[u8 ] = b"test1" ;
let key2: &[u8 ] = b"test2" ;
let key3: &[u8 ] = b"test3" ;
let value1: &[u8 ] = b"value1" ;
let value2: &[u8 ] = b"value2" ;
let value3: &[u8 ] = b"value3" ;
database.put(key1, value1).unwrap();
database.put(key2, value2).unwrap();
database.put(key3, value3).unwrap();
let mut iter = database.iter();
assert_eq! (iter.next(), Some (Box ::from(value1)));
assert_eq! (iter.next(), Some (Box ::from(value2)));
assert_eq! (iter.next(), Some (Box ::from(value3)));
assert_eq! (iter.next(), None );
}
}
}
Lifetime relationships can be expressed without occupying space in application memory by using PhantomData<T>
:
#![allow(unused)]
fn main () {
use std::marker::PhantomData;
pub struct Iterator <'iterator > {
handle: IteratorHandle,
start: bool ,
phantom: PhantomData<&'iterator Database>,
}
Iterator {
handle: handle,
start: true ,
phantom: PhantomData,
}
}
#![allow(unused)]
fn main () {
use libc::{c_void, size_t};
use std::ffi::{CStr, CString};
use std::path::Path;
use std::ptr;
use std::ptr::NonNull;
use leveldb_sys::{
leveldb_close, leveldb_create_iterator, leveldb_free, leveldb_get, leveldb_iter_destroy,
leveldb_iter_next, leveldb_iter_seek_to_first, leveldb_iter_valid, leveldb_iter_value,
leveldb_iterator_t, leveldb_open, leveldb_options_create, leveldb_options_destroy,
leveldb_options_set_create_if_missing, leveldb_options_t, leveldb_put,
leveldb_readoptions_create, leveldb_readoptions_destroy, leveldb_readoptions_t, leveldb_t,
leveldb_writeoptions_create, leveldb_writeoptions_destroy, leveldb_writeoptions_t,
};
struct DBHandle {
ptr: NonNull<leveldb_t>,
}
impl Drop for DBHandle {
fn drop (&mut self ) {
unsafe { leveldb_close(self .ptr.as_ptr()) }
}
}
pub struct Options {
ptr: NonNull<leveldb_options_t>,
}
impl Drop for Options {
fn drop (&mut self ) {
unsafe { leveldb_options_destroy(self .ptr.as_ptr()) }
}
}
impl Options {
pub fn new () -> Options {
unsafe {
let ptr = leveldb_options_create();
Options {
ptr: NonNull::new_unchecked(ptr),
}
}
}
pub fn create_if_missing (&mut self , value: bool ) {
unsafe { leveldb_options_set_create_if_missing(self .as_ptr(), value as u8 ) }
}
fn as_ptr (&self ) -> *mut leveldb_options_t {
self .ptr.as_ptr()
}
}
pub struct Database {
handle: DBHandle,
}
#[derive(Debug, Eq, PartialEq)]
pub enum Error {
OpenFail(String ),
GetFail(String ),
PutFail(String ),
InvalidString,
}
unsafe fn into_rust_string (ptr: *const i8 ) -> String {
let error_s = CStr::from_ptr(ptr).to_string_lossy().to_string();
leveldb_free(ptr as *mut c_void);
error_s
}
impl Database {
pub fn open <P: AsRef <Path>>(path: P, options: Options) -> Result <Database, Error> {
let mut error = ptr::null_mut();
let c_string = CString::new(path.as_ref().to_str().ok_or(Error::InvalidString)?)
.map_err(|_| Error::InvalidString)?;
unsafe {
let db = leveldb_open(options.as_ptr(), c_string.as_ptr(), &mut error);
if error == ptr::null_mut() {
Ok (Database {
handle: DBHandle {
ptr: NonNull::new_unchecked(db),
},
})
} else {
Err (Error::OpenFail(into_rust_string(error)))
}
}
}
pub fn get (&self , key: &[u8 ]) -> Result <Option <Box <[u8 ]>>, Error> {
unsafe {
let read_options = ReadOptions::new();
let mut len: size_t = 0 ;
let mut error = ptr::null_mut();
let data = leveldb_get(
self .handle.ptr.as_ptr(),
read_options.ptr.as_ptr(),
key.as_ptr() as *const i8 ,
key.len(),
&mut len,
&mut error,
);
if error == ptr::null_mut() {
if data == ptr::null_mut() {
Ok (None )
} else {
let slice = std::slice::from_raw_parts(data as *mut u8 , len);
let result = Box ::from(slice);
leveldb_free(data as *mut c_void);
Ok (Some (result))
}
} else {
Err (Error::GetFail(into_rust_string(error)))
}
}
}
pub fn put (&self , key: &[u8 ], data: &[u8 ]) -> Result <(), Error> {
unsafe {
let write_options = WriteOptions::new();
let mut error = ptr::null_mut();
leveldb_put(
self .handle.ptr.as_ptr(),
write_options.ptr.as_ptr(),
key.as_ptr() as *const i8 ,
key.len(),
data.as_ptr() as *const i8 ,
data.len(),
&mut error,
);
if error == ptr::null_mut() {
Ok (())
} else {
Err (Error::PutFail(into_rust_string(error)))
}
}
}
pub fn iter (&self ) -> Iterator <'_ > {
let read_options = ReadOptions::new();
let handle = IteratorHandle::new(self , read_options);
Iterator {
handle: handle,
start: true ,
database: self ,
}
}
}
pub struct Iterator <'iterator > {
handle: IteratorHandle,
start: bool ,
#[allow(unused)]
database: &'iterator Database,
}
impl <'iterator > Iterator <'iterator > {
fn read_current (&self ) -> Option <Box <[u8 ]>> {
unsafe {
if !self .handle.valid() {
return None ;
};
let data = self .handle.value();
let slice = std::slice::from_raw_parts(data.0 as *mut u8 , data.1 );
Some (Box ::from(slice))
}
}
}
pub struct WriteOptions {
ptr: NonNull<leveldb_writeoptions_t>,
}
impl WriteOptions {
pub fn new () -> WriteOptions {
unsafe {
let ptr = leveldb_writeoptions_create();
WriteOptions {
ptr: NonNull::new_unchecked(ptr),
}
}
}
}
impl Drop for WriteOptions {
fn drop (&mut self ) {
unsafe { leveldb_writeoptions_destroy(self .ptr.as_ptr()) }
}
}
pub struct ReadOptions {
ptr: NonNull<leveldb_readoptions_t>,
}
impl ReadOptions {
pub fn new () -> ReadOptions {
unsafe {
let ptr = leveldb_readoptions_create();
ReadOptions {
ptr: NonNull::new_unchecked(ptr),
}
}
}
}
impl Drop for ReadOptions {
fn drop (&mut self ) {
unsafe { leveldb_readoptions_destroy(self .ptr.as_ptr()) }
}
}
struct IteratorHandle {
ptr: NonNull<leveldb_iterator_t>,
}
impl IteratorHandle {
fn new (database: &Database, read_options: ReadOptions) -> IteratorHandle {
unsafe {
let iterator_ptr =
leveldb_create_iterator(database.handle.ptr.as_ptr(), read_options.ptr.as_ptr());
leveldb_iter_seek_to_first(iterator_ptr);
IteratorHandle {
ptr: NonNull::new_unchecked(iterator_ptr),
}
}
}
fn next (&self ) {
unsafe { leveldb_iter_next(self .ptr.as_ptr()) };
}
fn valid (&self ) -> bool {
unsafe { leveldb_iter_valid(self .ptr.as_ptr()) != 0 }
}
fn value (&self ) -> (*const i8 , usize ) {
unsafe {
let mut len = 0 ;
let data = leveldb_iter_value(self .ptr.as_ptr(), &mut len);
(data, len)
}
}
}
impl Drop for IteratorHandle {
fn drop (&mut self ) {
unsafe { leveldb_iter_destroy(self .ptr.as_ptr()) }
}
}
impl <'iterator > std::iter::Iterator for Iterator <'iterator > {
type Item = Box <[u8 ]>;
fn next (&mut self ) -> Option <Self::Item> {
if self .start {
self .start = false ;
self .read_current()
} else {
self .handle.next();
self .read_current()
}
}
}
#[cfg(test)]
mod test {
use core::panic;
use super::*;
use tempdir::TempDir;
#[test]
fn test_open () {
let tmp = TempDir::new("test_open" ).unwrap();
let mut options = Options::new();
options.create_if_missing(true );
let database = Database::open(tmp.path().join("database" ), options);
assert! (database.is_ok());
}
#[test]
fn test_create_open_fails () {
let mut options = Options::new();
options.create_if_missing(true );
let database = Database::open("/invalid/location" , options);
match database {
Err (Error::OpenFail(_)) => {}
_ => panic! (),
}
}
#[test]
fn test_open_nonexistent_fails () {
let options = Options::new();
let database = Database::open("/invalid/location" , options);
match database {
Err (Error::OpenFail(_)) => {}
_ => panic! (),
}
}
#[test]
fn test_read_write () {
let tmp = TempDir::new("test_read_write" ).unwrap();
let mut options = Options::new();
options.create_if_missing(true );
let database = Database::open(tmp.path().join("database" ), options).unwrap();
let key: &[u8 ] = b"test" ;
let missing_key: &[u8 ] = b"test_missing" ;
let value: &[u8 ] = b"test" ;
database.put(key, value).unwrap();
let result = database.get(key);
assert_eq! (result, Ok (Some (Box ::from(value))));
let result = database.get(missing_key);
assert_eq! (result, Ok (None ));
}
#[test]
fn test_iter () {
let tmp = TempDir::new("test_iter" ).unwrap();
let mut options = Options::new();
options.create_if_missing(true );
let database = Database::open(tmp.path().join("database" ), options).unwrap();
let key1: &[u8 ] = b"test1" ;
let key2: &[u8 ] = b"test2" ;
let key3: &[u8 ] = b"test3" ;
let value1: &[u8 ] = b"value1" ;
let value2: &[u8 ] = b"value2" ;
let value3: &[u8 ] = b"value3" ;
database.put(key1, value1).unwrap();
database.put(key2, value2).unwrap();
database.put(key3, value3).unwrap();
let mut iter = database.iter();
assert_eq! (iter.next(), Some (Box ::from(value1)));
assert_eq! (iter.next(), Some (Box ::from(value2)));
assert_eq! (iter.next(), Some (Box ::from(value3)));
assert_eq! (iter.next(), None );
}
}
}