use std::marker::PhantomData; use std::path::Path; use std::str::FromStr; use crate::wal::{ffi::make_wal_manager, Wal, WalManager}; #[cfg(not(feature = "rusqlite"))] type RawConnection = *mut crate::ffi::sqlite3; #[cfg(feature = "rusqlite")] type RawConnection = rusqlite::Connection; #[cfg(not(feature = "rusqlite"))] pub type OpenFlags = std::ffi::c_int; #[cfg(feature = "rusqlite")] pub type OpenFlags = rusqlite::OpenFlags; #[cfg(feature = "rusqlite")] type Error = rusqlite::Error; #[cfg(not(feature = "rusqlite"))] type Error = crate::Error; #[derive(Clone, Debug, Default)] pub enum Cipher { // AES 256 Bit CBC - No HMAC (wxSQLite3) #[default] Aes256Cbc, } impl FromStr for Cipher { type Err = libsql_ffi::Error; fn from_str(s: &str) -> Result { match s { "aes256cbc" => Ok(Cipher::Aes256Cbc), _ => Err(Self::Err::new(21)), } } } impl Cipher { #[cfg(feature = "encryption")] pub fn cipher_id(&self) -> i32 { let name = match self { Cipher::Aes256Cbc => "aes256cbc\0", }; unsafe { sqlite3mc_cipher_index(name.as_ptr() as _) } } } #[derive(Clone, Debug)] pub struct EncryptionConfig { pub cipher: Cipher, pub encryption_key: bytes::Bytes, } impl EncryptionConfig { pub fn new(cipher: Cipher, encryption_key: bytes::Bytes) -> Self { Self { cipher, encryption_key, } } #[cfg(feature = "encryption")] pub fn cipher_id(&self) -> i32 { self.cipher.cipher_id() } } #[derive(Debug)] pub struct Connection { conn: RawConnection, _pth: PhantomData, } #[cfg(feature = "rusqlite")] impl std::ops::Deref for Connection { type Target = rusqlite::Connection; fn deref(&self) -> &Self::Target { &self.conn } } #[cfg(feature = "rusqlite")] impl std::ops::DerefMut for Connection { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.conn } } #[cfg(feature = "rusqlite")] impl Connection { /// returns a dummy, in-memory connection. For testing purposes only pub fn test() -> Self { let conn = rusqlite::Connection::open_in_memory().unwrap(); Self { conn, _pth: PhantomData, } } } #[cfg(feature = "encryption")] extern "C" { fn sqlite3mc_cipher_index(cipher: *const std::ffi::c_void) -> std::ffi::c_int; fn sqlite3mc_config( db: *mut libsql_ffi::sqlite3, cipher: *const std::ffi::c_void, nKey: std::ffi::c_int, ) -> std::ffi::c_int; fn sqlite3_key( db: *mut libsql_ffi::sqlite3, pKey: *const std::ffi::c_void, nKey: std::ffi::c_int, ) -> std::ffi::c_int; fn sqlite3_rekey( db: *mut libsql_ffi::sqlite3, pKey: *const std::ffi::c_void, nKey: std::ffi::c_int, ) -> std::ffi::c_int; fn libsql_leak_pager(db: *mut libsql_ffi::sqlite3) -> *mut crate::ffi::Pager; fn libsql_generate_initial_vector(seed: u32, iv: *mut u8); fn libsql_generate_aes256_key(user_password: *const u8, password_length: u32, digest: *mut u8); } pub fn pghdr_creator( data: &mut [u8; 4096], _db: *mut libsql_ffi::sqlite3, ) -> libsql_ffi::libsql_pghdr { #[cfg(feature = "encryption")] let pager = crate::connection::leak_pager(_db); #[cfg(not(feature = "encryption"))] let pager = std::ptr::null_mut(); libsql_ffi::libsql_pghdr { pPage: std::ptr::null_mut(), pData: data.as_mut_ptr() as _, pExtra: std::ptr::null_mut(), pCache: std::ptr::null_mut(), pDirty: std::ptr::null_mut(), pPager: pager, pgno: 1, pageHash: 0x02, // DIRTY flags: 0, nRef: 0, pDirtyNext: std::ptr::null_mut(), pDirtyPrev: std::ptr::null_mut(), } } #[cfg(feature = "encryption")] /// # Safety /// db must point to a vaid sqlite database pub unsafe fn set_encryption_cipher(db: *mut libsql_ffi::sqlite3, cipher_id: i32) -> i32 { unsafe { sqlite3mc_config(db, "default:cipher\0".as_ptr() as _, cipher_id) as i32 } } #[cfg(feature = "encryption")] /// # Safety /// db must point to a vaid sqlite database pub unsafe fn set_encryption_key(db: *mut libsql_ffi::sqlite3, key: &[u8]) -> i32 { unsafe { sqlite3_key(db, key.as_ptr() as _, key.len() as _) as i32 } } #[cfg(feature = "encryption")] /// # Safety /// db must point to a valid sqlite database pub fn reset_encryption_key(db: *mut libsql_ffi::sqlite3, key: &[u8]) -> i32 { unsafe { sqlite3_rekey(db, key.as_ptr() as _, key.len() as _) as i32 } } #[cfg(feature = "encryption")] pub fn leak_pager(db: *mut libsql_ffi::sqlite3) -> *mut crate::ffi::Pager { unsafe { libsql_leak_pager(db) } } #[cfg(feature = "encryption")] pub fn generate_initial_vector(seed: u32, iv: &mut [u8]) { unsafe { libsql_generate_initial_vector(seed, iv.as_mut_ptr()) } } #[cfg(feature = "encryption")] pub fn generate_aes256_key(user_password: &[u8], digest: &mut [u8]) { unsafe { libsql_generate_aes256_key( user_password.as_ptr(), user_password.len() as u32, digest.as_mut_ptr(), ) } } pub const NO_AUTOCHECKPOINT: u32 = 0; impl Connection { /// Opens a database with the regular wal methods in the directory pointed to by path pub fn open( path: impl AsRef, flags: OpenFlags, wal_manager: T, auto_checkpoint: u32, encryption_config: Option, ) -> Result where T: WalManager, { tracing::trace!( "Opening a connection with regular WAL at {}", path.as_ref().display() ); #[cfg(feature = "rusqlite")] let conn = { let conn = if cfg!(feature = "unix-excl-vfs") { rusqlite::Connection::open_with_flags_vfs_and_wal( path, flags, "unix-excl", make_wal_manager(wal_manager), ) } else { rusqlite::Connection::open_with_flags_and_wal( path, flags, make_wal_manager(wal_manager), ) }?; if !cfg!(feature = "encryption") && encryption_config.is_some() { return Err(Error::SqliteFailure( rusqlite::ffi::Error::new(21), Some("encryption feature is not enabled, the database will not be encrypted on disk" .to_string()), )); } #[cfg(feature = "encryption")] if let Some(cfg) = encryption_config { let cipher_id = cfg.cipher_id(); if unsafe { set_encryption_cipher(conn.handle(), cipher_id) } == -1 { return Err(Error::SqliteFailure( rusqlite::ffi::Error::new(21), Some("failed to set encryption cipher".into()), )); }; if unsafe { set_encryption_key(conn.handle(), &cfg.encryption_key) } != rusqlite::ffi::SQLITE_OK { return Err(Error::SqliteFailure( rusqlite::ffi::Error::new(21), Some("failed to set encryption key".into()), )); }; } conn.pragma_update(None, "journal_mode", "WAL")?; unsafe { let rc = rusqlite::ffi::sqlite3_wal_autocheckpoint(conn.handle(), auto_checkpoint as _); if rc != 0 { return Err(rusqlite::Error::SqliteFailure( rusqlite::ffi::Error::new(rc), Some("failed to set auto_checkpoint".into()), )); } } conn.busy_timeout(std::time::Duration::from_millis(100))?; conn }; #[cfg(not(feature = "rusqlite"))] let conn = unsafe { #[cfg(unix)] let path = { use std::os::unix::ffi::OsStrExt; std::ffi::CString::new(path.as_ref().as_os_str().as_bytes()).map_err(|_| { crate::error::Error::Bug( "invalid database path containing an internal nul byte", ) })? }; #[cfg(not(unix))] let path = path .as_ref() .to_str() .ok_or_else(|| crate::error::Error::Bug("database path is not valid unicode")) .and_then(|x| { std::ffi::CString::new(x).map_err(|_| { crate::error::Error::Bug( "invalid database path containing an internal nul byte", ) }) })?; let mut conn: *mut crate::ffi::sqlite3 = std::ptr::null_mut(); // We pass a pointer to the WAL methods data to the database connection. This means // that the reference must outlive the connection. This is guaranteed by the marker in // the returned connection. let vfs = if cfg!(feature = "unix-excl-vfs") { "unix-excl\0".as_ptr() as *const _ } else { std::ptr::null_mut() }; let mut rc = libsql_ffi::libsql_open_v3( path.as_ptr(), &mut conn as *mut _, flags, vfs, make_wal_manager(wal_manager), ); if !cfg!(feature = "encryption") && encryption_config.is_some() { return Err(Error::Bug( "encryption feature is not enabled, the database will not be encrypted on disk", )); } #[cfg(feature = "encryption")] if let Some(cfg) = encryption_config { let cipher_id = cfg.cipher_id(); if set_encryption_cipher(conn, cipher_id) == -1 { return Err(Error::Bug("failed to set encryption cipher")); } if set_encryption_key(conn, &cfg.encryption_key) != libsql_ffi::SQLITE_OK { return Err(Error::Bug("failed to set encryption key")); } } if rc == 0 { rc = libsql_ffi::sqlite3_wal_autocheckpoint(conn, auto_checkpoint as _); } if rc != 0 { libsql_ffi::sqlite3_close(conn); return Err(crate::Error::from(rc)); } assert!(!conn.is_null()); crate::ffi::sqlite3_busy_timeout(conn, 5000); conn }; Ok(Connection { conn, _pth: PhantomData, }) } /// Returns the raw sqlite handle /// /// # Safety /// The caller is responsible for the returned pointer. pub unsafe fn handle(&self) -> *mut libsql_ffi::sqlite3 { #[cfg(feature = "rusqlite")] { self.conn.handle() } #[cfg(not(feature = "rusqlite"))] { self.conn } } pub fn db_change_counter(&self) -> Result { let mut file_ptr: *mut libsql_ffi::sqlite3_file = std::ptr::null_mut(); let rc = unsafe { libsql_ffi::sqlite3_file_control( self.handle(), "main\0".as_ptr() as *const _, libsql_ffi::SQLITE_FCNTL_FILE_POINTER, &mut file_ptr as *mut _ as *mut _, ) }; if rc != libsql_ffi::SQLITE_OK { return Err(rc); } let counter = unsafe { let mut counter: u32 = 0; let file = &*file_ptr; (*file.pMethods).xRead.unwrap()(file_ptr, &mut counter as *mut _ as *mut _, 4, 24); u32::from_be(counter) }; Ok(counter) } fn reserved_bytes(&self, reserve: Option) -> Result { let mut reserve_value = reserve.unwrap_or(0) as std::ffi::c_int; let rc = unsafe { libsql_ffi::sqlite3_file_control( self.handle(), "main\0".as_ptr() as *const _, libsql_ffi::SQLITE_FCNTL_RESERVE_BYTES, &mut reserve_value as *mut _ as *mut _, ) }; if rc != libsql_ffi::SQLITE_OK { return Err(rc); } Ok(reserve_value as i32) } pub fn set_reserved_bytes(&self, reserved_bytes: i32) -> Result<(), std::ffi::c_int> { self.reserved_bytes(Some(reserved_bytes))?; Ok(()) } pub fn get_reserved_bytes(&self) -> Result { self.reserved_bytes(None) } } // pub struct Connection<'a> { // pub conn: *mut crate::ffi::sqlite3, // _pth: PhantomData<&'a mut ()>, // } // // /// The `Connection` struct is `Send` because `sqlite3` is thread-safe. // unsafe impl<'a> Send for Connection<'a> {} // unsafe impl<'a> Sync for Connection<'a> {} // // impl<'a> Connection<'a> { // /// returns a dummy, in-memory connection. For testing purposes only // pub fn test(_: &mut ()) -> Self { // let mut conn: *mut crate::ffi::sqlite3 = std::ptr::null_mut(); // let rc = unsafe { // crate::ffi::sqlite3_open(":memory:\0".as_ptr() as *const _, &mut conn as *mut _) // }; // assert_eq!(rc, 0); // assert!(!conn.is_null()); // Self { // conn, // _pth: PhantomData, // } // } // // /// Opens a database with the regular wal methods, given a path to the database file. // pub fn open( // path: impl AsRef, // flags: c_int, // // we technically _only_ need to know about W, but requiring a static ref to the wal_hook ensures that // // it has been instantiated and lives for long enough // _wal_hook: &'static WalMethodsHook, // hook_ctx: &'a mut W::Context, // ) -> Result { // let path = path.as_ref(); // tracing::trace!( // "Opening a connection with regular WAL at {}", // path.display() // ); // // let conn_str = format!("file:{}?_journal_mode=WAL", path.display()); // let filename = CString::new(conn_str).unwrap(); // let mut conn: *mut crate::ffi::sqlite3 = std::ptr::null_mut(); // // unsafe { // // We pass a pointer to the WAL methods data to the database connection. This means // // that the reference must outlive the connection. This is guaranteed by the marker in // // the returned connection. // let rc = crate::ffi::libsql_open_v2( // filename.as_ptr(), // &mut conn as *mut _, // flags, // std::ptr::null_mut(), // W::name().as_ptr(), // hook_ctx as *mut _ as *mut _, // ); // // if rc != 0 { // crate::ffi::sqlite3_close(conn); // return Err(crate::Error::LibError(rc)); // } // // assert!(!conn.is_null()); // }; // // unsafe { // crate::ffi::sqlite3_busy_timeout(conn, 5000); // } // // Ok(Connection { // conn, // _pth: PhantomData, // }) // } // // pub fn is_autocommit(&self) -> bool { // unsafe { crate::ffi::sqlite3_get_autocommit(self.conn) != 0 } // } // } // // impl Drop for Connection<'_> { // fn drop(&mut self) { // if self.conn.is_null() { // tracing::debug!("Trying to close a null connection"); // return; // } // unsafe { // crate::ffi::sqlite3_close(self.conn as *mut _); // } // } // }