diff --git a/CHANGELOG.md b/CHANGELOG.md index 316662a..390561d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes will be documented in this file. +## 0.5.0 - 2025-11-26 + +- BREAKING: remove register_logger method from Vfs +- return SqliteLogger instance from register_dynamic/register_static + ## 0.4.1 - 2025-06-19 - expose SqliteApi in public API diff --git a/examples/memvfs.rs b/examples/memvfs.rs index dd52a58..402a602 100644 --- a/examples/memvfs.rs +++ b/examples/memvfs.rs @@ -1,10 +1,6 @@ // cargo build --example memvfs --features dynamic -use std::{ - ffi::{CStr, c_void}, - os::raw::c_char, - sync::Arc, -}; +use std::{ffi::c_void, os::raw::c_char, sync::Arc}; use parking_lot::Mutex; use sqlite_plugin::{ @@ -45,33 +41,6 @@ struct MemVfs { impl Vfs for MemVfs { type Handle = File; - fn register_logger(&self, logger: SqliteLogger) { - struct LogCompat { - logger: Mutex, - } - - impl log::Log for LogCompat { - fn enabled(&self, _metadata: &log::Metadata) -> bool { - true - } - - fn log(&self, record: &log::Record) { - let level = match record.level() { - log::Level::Error => SqliteLogLevel::Error, - log::Level::Warn => SqliteLogLevel::Warn, - _ => SqliteLogLevel::Notice, - }; - let msg = format!("{}", record.args()); - self.logger.lock().log(level, msg.as_bytes()); - } - - fn flush(&self) {} - } - - let log = LogCompat { logger: Mutex::new(logger) }; - log::set_boxed_logger(Box::new(log)).expect("failed to setup global logger"); - } - fn open(&self, path: Option<&str>, opts: OpenOpts) -> VfsResult { log::debug!("open: path={:?}, opts={:?}", path, opts); let mode = opts.mode(); @@ -216,6 +185,33 @@ impl Vfs for MemVfs { } } +fn setup_logger(logger: SqliteLogger) { + struct LogCompat { + logger: Mutex, + } + + impl log::Log for LogCompat { + fn enabled(&self, _metadata: &log::Metadata) -> bool { + true + } + + fn log(&self, record: &log::Record) { + let level = match record.level() { + log::Level::Error => SqliteLogLevel::Error, + log::Level::Warn => SqliteLogLevel::Warn, + _ => SqliteLogLevel::Notice, + }; + let msg = format!("{}", record.args()); + self.logger.lock().log(level, &msg); + } + + fn flush(&self) {} + } + + let log = LogCompat { logger: Mutex::new(logger) }; + log::set_boxed_logger(Box::new(log)).expect("failed to setup global logger"); +} + /// This function is called by `SQLite` when the extension is loaded. It registers /// the memvfs VFS with `SQLite`. /// # Safety @@ -226,18 +222,17 @@ pub unsafe extern "C" fn sqlite3_memvfs_init( _pz_err_msg: *mut *mut c_char, p_api: *mut sqlite3_api_routines, ) -> std::os::raw::c_int { - let vfs = MemVfs { files: Default::default() }; - const MEMVFS_NAME: &CStr = c"mem"; - if let Err(err) = unsafe { + match unsafe { register_dynamic( p_api, - MEMVFS_NAME.to_owned(), - vfs, + c"mem".to_owned(), + MemVfs { files: Default::default() }, RegisterOpts { make_default: true }, ) } { - return err; - } + Ok(logger) => setup_logger(logger), + Err(err) => return err, + }; // set the log level to trace log::set_max_level(log::LevelFilter::Trace); diff --git a/examples/test_memvfs.sql b/examples/test_memvfs.sql index 79fb640..2acabcd 100644 --- a/examples/test_memvfs.sql +++ b/examples/test_memvfs.sql @@ -7,6 +7,8 @@ .load target/debug/examples/libmemvfs.so .open main.db +.mode table +.log stdout .databases .vfsinfo diff --git a/src/logger.rs b/src/logger.rs index 4841e04..3cfd4cb 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -3,6 +3,7 @@ use core::ffi::{c_char, c_int}; use crate::vars; +#[allow(non_snake_case)] type Sqlite3Log = unsafe extern "C" fn(iErrCode: c_int, arg2: *const c_char, ...); #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -32,20 +33,13 @@ impl SqliteLogger { Self { log } } - /// Log bytes to the `SQLite3` log handle. - /// This function will write each line separately to `SQLite3`. - /// Note that `SQLite3` silently truncates log lines larger than roughly - /// 230 bytes by default. - pub fn log(&self, level: SqliteLogLevel, buf: &[u8]) { + /// Log bytes directly to the `SQLite3` log handle. + /// Note that `SQLite` silently truncates writes larger than + /// roughly 230 bytes by default. It's recommended that you + /// split your log messages by lines before calling this method. + pub fn log(&self, level: SqliteLogLevel, msg: &str) { let code = level.into_err_code(); - for line in buf.split(|b| *b == b'\n') { - // skip if line only contains whitespace - if line.iter().all(|b| b.is_ascii_whitespace()) { - continue; - } - - let z_format = CString::new(line).unwrap(); - unsafe { (self.log)(code, z_format.as_ptr()) } - } + let z_format = CString::new(msg).unwrap(); + unsafe { (self.log)(code, z_format.as_ptr()) } } } diff --git a/src/mock.rs b/src/mock.rs index 3a853db..c2b6f28 100644 --- a/src/mock.rs +++ b/src/mock.rs @@ -53,9 +53,6 @@ pub trait Hooks { } } -pub struct NoopHooks; -impl Hooks for NoopHooks {} - #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct MockHandle { id: usize, @@ -87,38 +84,45 @@ impl VfsHandle for MockHandle { // MockVfs implements a very simple in-memory VFS for testing purposes. // See the memvfs example for a more complete implementation. pub struct MockVfs { - shared: Arc>, + state: Arc>, } -struct Shared { +pub struct MockState { next_id: usize, files: HashMap, hooks: Box, log: Option, } -impl MockVfs { +impl MockState { pub fn new(hooks: Box) -> Self { - Self { - shared: Arc::new(Mutex::new(Shared { - next_id: 0, - files: HashMap::new(), - hooks, - log: None, - })), + MockState { + next_id: 0, + files: HashMap::new(), + hooks, + log: None, } } - fn shared(&self) -> MutexGuard<'_, Shared> { - self.shared.lock() + pub fn setup_logger(&mut self, logger: SqliteLogger) { + self.log = Some(logger) + } +} + +impl MockVfs { + pub fn new(state: Arc>) -> Self { + Self { state } + } + + fn state(&self) -> MutexGuard<'_, MockState> { + self.state.lock() } } -impl Shared { +impl MockState { fn log(&self, f: fmt::Arguments<'_>) { if let Some(log) = &self.log { - let buf = format!("{f}"); - log.log(SqliteLogLevel::Notice, buf.as_bytes()); + log.log(SqliteLogLevel::Notice, &format!("{f}")); } else { panic!("MockVfs is missing registered log handler") } @@ -135,34 +139,29 @@ impl Vfs for MockVfs { // a simple usize that represents a file handle. type Handle = MockHandle; - fn register_logger(&self, logger: SqliteLogger) { - let mut shared = self.shared(); - shared.log = Some(logger); - } - fn canonical_path<'a>(&self, path: Cow<'a, str>) -> VfsResult> { - let mut shared = self.shared(); - shared.log(format_args!("canonical_path: path={path:?}")); - shared.hooks.canonical_path(&path); + let mut state = self.state(); + state.log(format_args!("canonical_path: path={path:?}")); + state.hooks.canonical_path(&path); Ok(path) } fn open(&self, path: Option<&str>, opts: flags::OpenOpts) -> VfsResult { - let mut shared = self.shared(); - shared.log(format_args!("open: path={path:?} opts={opts:?}")); - shared.hooks.open(&path, &opts); + let mut state = self.state(); + state.log(format_args!("open: path={path:?} opts={opts:?}")); + state.hooks.open(&path, &opts); - let id = shared.next_id(); + let id = state.next_id(); let file_handle = MockHandle::new(id, opts.mode().is_readonly()); if let Some(path) = path { // if file is already open return existing handle - for (handle, file) in shared.files.iter() { + for (handle, file) in state.files.iter() { if file.name == path { return Ok(*handle); } } - shared.files.insert( + state.files.insert( file_handle, File { name: path.to_owned(), @@ -175,32 +174,32 @@ impl Vfs for MockVfs { } fn delete(&self, path: &str) -> VfsResult<()> { - let mut shared = self.shared(); - shared.log(format_args!("delete: path={path:?}")); - shared.hooks.delete(path); - shared.files.retain(|_, file| file.name != path); + let mut state = self.state(); + state.log(format_args!("delete: path={path:?}")); + state.hooks.delete(path); + state.files.retain(|_, file| file.name != path); Ok(()) } fn access(&self, path: &str, flags: AccessFlags) -> VfsResult { - let mut shared = self.shared(); - shared.log(format_args!("access: path={path:?} flags={flags:?}")); - shared.hooks.access(path, flags); - Ok(shared.files.values().any(|file| file.name == path)) + let mut state = self.state(); + state.log(format_args!("access: path={path:?} flags={flags:?}")); + state.hooks.access(path, flags); + Ok(state.files.values().any(|file| file.name == path)) } fn file_size(&self, meta: &mut Self::Handle) -> VfsResult { - let mut shared = self.shared(); - shared.log(format_args!("file_size: handle={meta:?}")); - shared.hooks.file_size(*meta); - Ok(shared.files.get(meta).map_or(0, |file| file.data.len())) + let mut state = self.state(); + state.log(format_args!("file_size: handle={meta:?}")); + state.hooks.file_size(*meta); + Ok(state.files.get(meta).map_or(0, |file| file.data.len())) } fn truncate(&self, meta: &mut Self::Handle, size: usize) -> VfsResult<()> { - let mut shared = self.shared(); - shared.log(format_args!("truncate: handle={meta:?} size={size:?}")); - shared.hooks.truncate(*meta, size); - if let Some(file) = shared.files.get_mut(meta) { + let mut state = self.state(); + state.log(format_args!("truncate: handle={meta:?} size={size:?}")); + state.hooks.truncate(*meta, size); + if let Some(file) = state.files.get_mut(meta) { if size > file.data.len() { file.data.resize(size, 0); } else { @@ -211,15 +210,15 @@ impl Vfs for MockVfs { } fn write(&self, meta: &mut Self::Handle, offset: usize, buf: &[u8]) -> VfsResult { - let mut shared = self.shared(); - shared.log(format_args!( + let mut state = self.state(); + state.log(format_args!( "write: handle={:?} offset={:?} buf.len={}", meta, offset, buf.len() )); - shared.hooks.write(*meta, offset, buf); - if let Some(file) = shared.files.get_mut(meta) { + state.hooks.write(*meta, offset, buf); + if let Some(file) = state.files.get_mut(meta) { if offset + buf.len() > file.data.len() { file.data.resize(offset + buf.len(), 0); } @@ -231,15 +230,15 @@ impl Vfs for MockVfs { } fn read(&self, meta: &mut Self::Handle, offset: usize, buf: &mut [u8]) -> VfsResult { - let mut shared = self.shared(); - shared.log(format_args!( + let mut state = self.state(); + state.log(format_args!( "read: handle={:?} offset={:?} buf.len={}", meta, offset, buf.len() )); - shared.hooks.read(*meta, offset, buf); - if let Some(file) = shared.files.get(meta) { + state.hooks.read(*meta, offset, buf); + if let Some(file) = state.files.get(meta) { if offset > file.data.len() { return Ok(0); } @@ -252,19 +251,19 @@ impl Vfs for MockVfs { } fn sync(&self, meta: &mut Self::Handle) -> VfsResult<()> { - let mut shared = self.shared(); - shared.log(format_args!("sync: handle={meta:?}")); - shared.hooks.sync(*meta); + let mut state = self.state(); + state.log(format_args!("sync: handle={meta:?}")); + state.hooks.sync(*meta); Ok(()) } fn close(&self, meta: Self::Handle) -> VfsResult<()> { - let mut shared = self.shared(); - shared.log(format_args!("close: handle={meta:?}")); - shared.hooks.close(meta); - if let Some(file) = shared.files.get(&meta) { + let mut state = self.state(); + state.log(format_args!("close: handle={meta:?}")); + state.hooks.close(meta); + if let Some(file) = state.files.get(&meta) { if file.delete_on_close { - shared.files.remove(&meta); + state.files.remove(&meta); } } Ok(()) @@ -275,22 +274,22 @@ impl Vfs for MockVfs { meta: &mut Self::Handle, pragma: Pragma<'_>, ) -> Result, PragmaErr> { - let mut shared = self.shared(); - shared.log(format_args!("pragma: handle={meta:?} pragma={pragma:?}")); - shared.hooks.pragma(*meta, pragma) + let mut state = self.state(); + state.log(format_args!("pragma: handle={meta:?} pragma={pragma:?}")); + state.hooks.pragma(*meta, pragma) } fn sector_size(&self) -> i32 { - let mut shared = self.shared(); - shared.log(format_args!("sector_size")); - shared.hooks.sector_size(); + let mut state = self.state(); + state.log(format_args!("sector_size")); + state.hooks.sector_size(); DEFAULT_SECTOR_SIZE } fn device_characteristics(&self) -> i32 { - let mut shared = self.shared(); - shared.log(format_args!("device_characteristics")); - shared.hooks.device_characteristics(); + let mut state = self.state(); + state.log(format_args!("device_characteristics")); + state.hooks.device_characteristics(); DEFAULT_DEVICE_CHARACTERISTICS } } diff --git a/src/vfs.rs b/src/vfs.rs index 1da53cf..f07579c 100644 --- a/src/vfs.rs +++ b/src/vfs.rs @@ -142,11 +142,6 @@ pub trait VfsHandle: Send { pub trait Vfs: Send + Sync { type Handle: VfsHandle; - /// Register the provided logger with this Vfs. - /// This function is guaranteed to only be called once per - /// register_{static,dynamic} call. - fn register_logger(&self, logger: SqliteLogger); - /// construct a canonical version of the given path fn canonical_path<'a>(&self, path: Cow<'a, str>) -> VfsResult> { Ok(path) @@ -230,7 +225,7 @@ impl SqliteApi { }) } - /// Copies the provided string into a memory buffer allocated by sqlite3_mprintf. + /// Copies the provided string into a memory buffer allocated by `sqlite3_mprintf`. /// Writes the pointer to the memory buffer to `out` if `out` is not null. /// # Safety /// 1. the out pointer must not be null @@ -250,11 +245,16 @@ impl SqliteApi { } pub struct RegisterOpts { + /// If true, make this vfs the default vfs for `SQLite`. pub make_default: bool, } #[cfg(feature = "static")] -pub fn register_static(name: CString, vfs: T, opts: RegisterOpts) -> VfsResult<()> { +pub fn register_static( + name: CString, + vfs: T, + opts: RegisterOpts, +) -> VfsResult { register_inner(SqliteApi::new_static(), name, vfs, opts) } @@ -268,7 +268,7 @@ pub unsafe fn register_dynamic( name: CString, vfs: T, opts: RegisterOpts, -) -> VfsResult<()> { +) -> VfsResult { let api = unsafe { p_api.as_ref() }.ok_or(vars::SQLITE_INTERNAL)?; let sqlite_api = unsafe { SqliteApi::new_dynamic(api)? }; register_inner(sqlite_api, name, vfs, opts) @@ -279,7 +279,7 @@ fn register_inner( name: CString, vfs: T, opts: RegisterOpts, -) -> VfsResult<()> { +) -> VfsResult { let version = unsafe { (sqlite_api.libversion_number)() }; if version < MIN_SQLITE_VERSION_NUMBER { panic!( @@ -310,7 +310,7 @@ fn register_inner( xUnfetch: None, }; - vfs.register_logger(SqliteLogger::new(sqlite_api.log)); + let logger = SqliteLogger::new(sqlite_api.log); let p_name = ManuallyDrop::new(name).as_ptr(); let base_vfs = unsafe { (sqlite_api.find)(null_mut()) }; @@ -356,7 +356,7 @@ fn register_inner( }; Err(result) } else { - Ok(()) + Ok(logger) } } @@ -653,9 +653,8 @@ unsafe extern "C" fn x_dlsym( p_vfs: *mut ffi::sqlite3_vfs, p_handle: *mut c_void, z_symbol: *const c_char, -) -> Option< - unsafe extern "C" fn(arg1: *mut ffi::sqlite3_vfs, arg2: *mut c_void, zSymbol: *const c_char), -> { +) -> Option +{ if let Ok(vfs) = unwrap_base_vfs!(p_vfs, T) { if let Some(x_dlsym) = vfs.xDlSym { return unsafe { x_dlsym(vfs, p_handle, z_symbol) }; @@ -728,7 +727,8 @@ mod tests { flags::{CreateMode, OpenKind, OpenMode}, mock::*, }; - use alloc::vec::Vec; + use alloc::{sync::Arc, vec::Vec}; + use parking_lot::Mutex; use rusqlite::{Connection, OpenFlags}; use std::{boxed::Box, io::Write, println}; @@ -766,14 +766,18 @@ mod tests { } } - let vfs = MockVfs::new(Box::new(H {})); - register_static( + let shared = Arc::new(Mutex::new(MockState::new(Box::new(H {})))); + let vfs = MockVfs::new(shared.clone()); + let logger = register_static( CString::new("mock").unwrap(), vfs, RegisterOpts { make_default: true }, ) .map_err(|_| "failed to register vfs")?; + // setup the logger + shared.lock().setup_logger(logger); + // create a sqlite connection using the mock vfs let conn = Connection::open_with_flags_and_vfs( "main.db",