mod rows; #[allow(dead_code)] mod statement; mod types; use limbo_core::{Connection, Database, LimboError}; use std::{ ffi::{c_char, c_void}, rc::Rc, str::FromStr, sync::Arc, }; /// # Safety /// Safe to be called from Go with null terminated DSN string. /// performs null check on the path. #[no_mangle] pub unsafe extern "C" fn db_open(path: *const c_char) -> *mut c_void { if path.is_null() { println!("Path is null"); return std::ptr::null_mut(); } let path = unsafe { std::ffi::CStr::from_ptr(path) }; let path = path.to_str().unwrap(); let db_options = parse_query_str(path); if let Ok(io) = get_io(&db_options.path) { let db = Database::open_file(io.clone(), &db_options.path.to_string()); match db { Ok(db) => { let conn = db.connect(); return LimboConn::new(conn, io).to_ptr(); } Err(e) => { println!("Error opening database: {}", e); return std::ptr::null_mut(); } }; } std::ptr::null_mut() } #[allow(dead_code)] struct LimboConn { conn: Rc, io: Arc, err: Option, } impl<'conn> LimboConn { fn new(conn: Rc, io: Arc) -> Self { LimboConn { conn, io, err: None, } } #[allow(clippy::wrong_self_convention)] fn to_ptr(self) -> *mut c_void { Box::into_raw(Box::new(self)) as *mut c_void } fn from_ptr(ptr: *mut c_void) -> &'conn mut LimboConn { if ptr.is_null() { panic!("Null pointer"); } unsafe { &mut *(ptr as *mut LimboConn) } } fn get_error(&mut self) -> *const c_char { if let Some(err) = &self.err { let err = format!("{}", err); let c_str = std::ffi::CString::new(err).unwrap(); self.err = None; c_str.into_raw() as *const c_char } else { std::ptr::null() } } } /// Get the error value from the connection, if any, as a null /// terminated string. The caller is responsible for freeing the /// memory with `free_string`. #[no_mangle] pub extern "C" fn db_get_error(ctx: *mut c_void) -> *const c_char { if ctx.is_null() { return std::ptr::null(); } let conn = LimboConn::from_ptr(ctx); conn.get_error() } /// Close the database connection /// # Safety /// safely frees the connection's memory #[no_mangle] pub unsafe extern "C" fn db_close(db: *mut c_void) { if !db.is_null() { let _ = unsafe { Box::from_raw(db as *mut LimboConn) }; } } #[allow(clippy::arc_with_non_send_sync)] fn get_io(db_location: &DbType) -> Result, LimboError> { Ok(match db_location { DbType::Memory => Arc::new(limbo_core::MemoryIO::new()?), _ => { return Ok(Arc::new(limbo_core::PlatformIO::new()?)); } }) } #[allow(dead_code)] struct DbOptions { path: DbType, params: Parameters, } #[derive(Default, Debug, Clone)] enum DbType { File(String), #[default] Memory, } impl std::fmt::Display for DbType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { DbType::File(path) => write!(f, "{}", path), DbType::Memory => write!(f, ":memory:"), } } } #[derive(Debug, Clone, Default)] struct Parameters { mode: Mode, cache: Option, vfs: Option, nolock: bool, immutable: bool, modeof: Option, } impl FromStr for Parameters { type Err = (); fn from_str(s: &str) -> Result { if !s.contains('?') { return Ok(Parameters::default()); } let mut params = Parameters::default(); for param in s.split('?').nth(1).unwrap().split('&') { let mut kv = param.split('='); match kv.next() { Some("mode") => params.mode = kv.next().unwrap().parse().unwrap(), Some("cache") => params.cache = Some(kv.next().unwrap().parse().unwrap()), Some("vfs") => params.vfs = Some(kv.next().unwrap().to_string()), Some("nolock") => params.nolock = true, Some("immutable") => params.immutable = true, Some("modeof") => params.modeof = Some(kv.next().unwrap().to_string()), _ => {} } } Ok(params) } } #[derive(Default, Debug, Clone, Copy)] enum Cache { Shared, #[default] Private, } impl FromStr for Cache { type Err = (); fn from_str(s: &str) -> Result { match s { "shared" => Ok(Cache::Shared), _ => Ok(Cache::Private), } } } #[allow(clippy::enum_variant_names)] #[derive(Default, Debug, Clone, Copy)] enum Mode { ReadOnly, ReadWrite, #[default] ReadWriteCreate, } impl FromStr for Mode { type Err = (); fn from_str(s: &str) -> Result { match s { "readonly" | "ro" => Ok(Mode::ReadOnly), "readwrite" | "rw" => Ok(Mode::ReadWrite), "readwritecreate" | "rwc" => Ok(Mode::ReadWriteCreate), _ => Ok(Mode::default()), } } } // At this point we don't have configurable parameters but many // DSN's are going to have query parameters fn parse_query_str(mut path: &str) -> DbOptions { if path == ":memory:" { return DbOptions { path: DbType::Memory, params: Parameters::default(), }; } if path.starts_with("sqlite://") { path = &path[10..]; } if path.contains('?') { let parameters = Parameters::from_str(path).unwrap(); let path = &path[..path.find('?').unwrap()]; DbOptions { path: DbType::File(path.to_string()), params: parameters, } } else { DbOptions { path: DbType::File(path.to_string()), params: Parameters::default(), } } }