#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
extern crate libsqlite3_sys as ffi;
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
use sqlite_wasm_rs as ffi;
use crate::result::Error::DatabaseError;
use crate::result::*;
use crate::sqlite::SqliteConnection;
use alloc::boxed::Box;
use alloc::string::{String, ToString};
use core::ffi::{c_char, c_int};
#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
type RawApiPointer = *const core::ffi::c_void;
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
type RawApiPointer = *const ffi::sqlite3_api_routines;
type RawAutoExtension = unsafe extern "C" fn(
db: *mut ffi::sqlite3,
pz_err_msg: *mut *mut c_char,
p_api: RawApiPointer,
) -> c_int;
#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
mod auto_extension_ffi {
use super::{RawAutoExtension, c_int};
#[allow(unsafe_code)]
unsafe extern "C" {
pub(super) fn sqlite3_auto_extension(entry_point: Option<RawAutoExtension>) -> c_int;
pub(super) fn sqlite3_cancel_auto_extension(entry_point: Option<RawAutoExtension>)
-> c_int;
}
}
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
use ffi as auto_extension_ffi;
use super::BorrowedSqliteConnection;
#[allow(unsafe_code)]
pub fn register_auto_extension<F>(extension: F) -> QueryResult<()>
where
F: Fn(&'_ mut BorrowedSqliteConnection) -> QueryResult<()> + Sync + 'static,
{
let result =
unsafe { auto_extension_ffi::sqlite3_auto_extension(Some(entry_point(extension))) };
if result == ffi::SQLITE_OK {
Ok(())
} else {
Err(DatabaseError(
DatabaseErrorKind::Unknown,
Box::new(ffi::code_to_str(result).to_string()),
))
}
}
#[allow(unsafe_code)]
pub fn cancel_auto_extension<F>(extension: F) -> bool
where
F: Fn(&'_ mut BorrowedSqliteConnection) -> QueryResult<()> + Sync + 'static,
{
unsafe { auto_extension_ffi::sqlite3_cancel_auto_extension(Some(entry_point(extension))) != 0 }
}
#[allow(unsafe_code)]
pub fn reset_auto_extension() {
unsafe { ffi::sqlite3_reset_auto_extension() }
}
fn entry_point<F>(extension: F) -> RawAutoExtension
where
F: Fn(&'_ mut BorrowedSqliteConnection) -> QueryResult<()> + Sync + 'static,
{
core::mem::forget(extension);
trampoline::<F>
}
#[allow(unsafe_code)]
unsafe extern "C" fn trampoline<F>(
db: *mut ffi::sqlite3,
pz_err_msg: *mut *mut c_char,
_p_api: RawApiPointer,
) -> c_int
where
F: Fn(&'_ mut BorrowedSqliteConnection) -> QueryResult<()> + Sync + 'static,
{
const {
assert!(
core::mem::size_of::<F>() == 0,
"an auto-extension callback must not capture non-zero-sized state. \
Use a `fn` item or a closure that captures only zero-sized values"
);
}
let result: Result<(), String> =
crate::util::std_compat::catch_unwind(core::panic::AssertUnwindSafe(|| {
let Some(db) = core::ptr::NonNull::new(db) else {
return Err(String::from(
"auto-extension received a null database handle",
));
};
let extension: &F = unsafe { core::ptr::NonNull::<F>::dangling().as_ref() };
unsafe { SqliteConnection::with_borrowed_connection(db, extension) }
.map_err(|e| e.to_string())
}))
.unwrap_or_else(|panic| {
Err(match panic_detail(panic) {
Some(message) => alloc::format!("auto-extension panicked: {message}"),
None => String::from("auto-extension panicked"),
})
});
match result {
Ok(()) => ffi::SQLITE_OK,
Err(message) => {
set_error_message(pz_err_msg, &message);
ffi::SQLITE_ERROR
}
}
}
#[cfg(feature = "std")]
fn panic_detail(panic: alloc::boxed::Box<dyn core::any::Any + Send>) -> Option<String> {
panic
.downcast_ref::<&str>()
.map(|s| (*s).to_owned())
.or_else(|| panic.downcast_ref::<String>().cloned())
}
#[cfg(not(feature = "std"))]
fn panic_detail(_panic: ()) -> Option<String> {
None
}
#[allow(unsafe_code)]
fn set_error_message(pz_err_msg: *mut *mut c_char, message: &str) {
if pz_err_msg.is_null() {
return;
}
let bytes = message.as_bytes();
let len = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
let Ok(size) = c_int::try_from(len + 1) else {
return;
};
let buffer = unsafe { ffi::sqlite3_malloc(size) } as *mut u8;
if buffer.is_null() {
return;
}
unsafe {
core::ptr::copy_nonoverlapping(bytes.as_ptr(), buffer, len);
*buffer.add(len) = 0;
*pz_err_msg = buffer as *mut c_char;
}
}
#[cfg(all(test, not(miri)))]
mod tests {
use super::*;
use crate::dsl::sql;
use crate::prelude::*;
use crate::sql_types::Integer;
use crate::sqlite::BorrowedSqliteConnection;
use std::sync::Mutex;
static AUTO_EXT_TEST_LOCK: Mutex<()> = Mutex::new(());
#[allow(unsafe_code, reason = "Test call to inner unsafe function")]
fn test_ext_init(conn: &mut BorrowedSqliteConnection) -> QueryResult<()> {
unsafe {
conn.inner_mut()
.register_collation("TESTCOLL", |a, b| a.cmp(b))
}
}
fn open_memory_connection() -> SqliteConnection {
SqliteConnection::establish(":memory:").expect("Failed to open :memory: connection")
}
fn probe_collation() -> QueryResult<i32> {
let mut conn = open_memory_connection();
sql::<Integer>("SELECT 'a' = 'a' COLLATE TESTCOLL").get_result(&mut conn)
}
struct TestResetGuard;
impl Drop for TestResetGuard {
fn drop(&mut self) {
reset_auto_extension();
}
}
#[test]
fn auto_extension_lifecycle() {
let _lock = AUTO_EXT_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _guard = TestResetGuard;
reset_auto_extension();
register_auto_extension(test_ext_init).unwrap();
assert_eq!(probe_collation().unwrap(), 1);
let removed = cancel_auto_extension(test_ext_init);
assert!(
removed,
"cancel should return true for registered extension"
);
assert!(
probe_collation().is_err(),
"collation should not be available after cancel"
);
let removed = cancel_auto_extension(test_ext_init);
assert!(
!removed,
"cancel should return false for unregistered extension"
);
register_auto_extension(test_ext_init).unwrap();
reset_auto_extension();
assert!(
probe_collation().is_err(),
"collation should not be available after reset"
);
register_auto_extension(test_ext_init).unwrap();
register_auto_extension(test_ext_init).unwrap();
assert_eq!(probe_collation().unwrap(), 1);
}
#[test]
#[allow(unsafe_code)]
fn trampoline_maps_result_to_return_code() {
fn ok_ext(_conn: &mut BorrowedSqliteConnection) -> QueryResult<()> {
Ok(())
}
fn err_ext(_conn: &mut BorrowedSqliteConnection) -> QueryResult<()> {
Err(Error::QueryBuilderError("boom".into()))
}
let ok_tramp = entry_point(ok_ext);
let err_tramp = entry_point(err_ext);
let mut conn = open_memory_connection();
unsafe {
conn.with_raw_connection(|db| {
let mut err: *mut c_char = core::ptr::null_mut();
let rc = ok_tramp(db, &mut err, core::ptr::null());
assert_eq!(rc, ffi::SQLITE_OK);
assert!(err.is_null());
let rc = err_tramp(db, &mut err, core::ptr::null());
assert_eq!(rc, ffi::SQLITE_ERROR);
assert!(!err.is_null());
let message = core::ffi::CStr::from_ptr(err)
.to_string_lossy()
.into_owned();
assert_eq!(message, "boom");
ffi::sqlite3_free(err as *mut core::ffi::c_void);
let mut err: *mut c_char = core::ptr::null_mut();
let rc = ok_tramp(core::ptr::null_mut(), &mut err, core::ptr::null());
assert_eq!(rc, ffi::SQLITE_ERROR);
if !err.is_null() {
ffi::sqlite3_free(err as *mut core::ffi::c_void);
}
})
}
}
#[test]
fn callback_zero_sized_capture_is_never_dropped() {
use std::sync::atomic::{AtomicUsize, Ordering};
static DROPS: AtomicUsize = AtomicUsize::new(0);
struct Guard;
impl Drop for Guard {
fn drop(&mut self) {
DROPS.fetch_add(1, Ordering::SeqCst);
}
}
let _lock = AUTO_EXT_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _reset = TestResetGuard;
reset_auto_extension();
let guard = Guard;
#[allow(unsafe_code, reason = "Test call to inner unsafe function")]
register_auto_extension(move |conn: &mut BorrowedSqliteConnection| {
let _ = &guard;
unsafe {
conn.inner_mut()
.register_collation("TESTCOLL", |a, b| a.cmp(b))
}
})
.unwrap();
for _ in 0..5 {
assert_eq!(probe_collation().unwrap(), 1);
}
reset_auto_extension();
assert_eq!(
DROPS.load(Ordering::SeqCst),
0,
"the captured guard's destructor must never run"
);
}
#[test]
fn leaked_failed_cursor_in_auto_extension_fails_open_cleanly() {
use std::cell::Cell;
thread_local!(static ARMED: Cell<bool> = const { Cell::new(false) });
#[allow(unsafe_code, reason = "Test code")]
fn extension(conn: &mut BorrowedSqliteConnection) -> QueryResult<()> {
if ARMED.get() {
unsafe {
crate::sqlite::connection::leak_failed_cursor(conn.inner_mut());
}
}
Ok(())
}
let _lock = AUTO_EXT_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _reset = TestResetGuard;
register_auto_extension(extension).unwrap();
ARMED.set(true);
let conn = SqliteConnection::establish(":memory:");
assert!(matches!(conn, Err(ConnectionError::BadConnection(_))));
}
#[test]
#[cfg(not(all(target_family = "wasm", target_os = "unknown")))] fn panic_in_borrowed_connection_teardown_keeps_handle_open() {
use crate::connection::{Instrumentation, InstrumentationEvent};
use std::cell::Cell;
thread_local!(static ARMED: Cell<bool> = const { Cell::new(false) });
struct PanicOnDrop;
impl Instrumentation for PanicOnDrop {
fn on_connection_event(&mut self, _event: InstrumentationEvent<'_>) {}
}
impl Drop for PanicOnDrop {
fn drop(&mut self) {
if ARMED.get() {
panic!("instrumentation dropped");
}
}
}
fn extension(conn: &mut BorrowedSqliteConnection) -> QueryResult<()> {
if ARMED.get() {
conn.set_instrumentation(PanicOnDrop);
}
Ok(())
}
let _lock = AUTO_EXT_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _reset = TestResetGuard;
register_auto_extension(extension).unwrap();
ARMED.set(true);
let conn = SqliteConnection::establish(":memory:");
ARMED.set(false);
assert!(matches!(conn, Err(ConnectionError::BadConnection(_))));
}
mod hook_tokens {
use std::sync::atomic::{AtomicUsize, Ordering};
pub(super) static FIRED: AtomicUsize = AtomicUsize::new(0);
pub(super) static DROPPED: AtomicUsize = AtomicUsize::new(0);
pub(super) struct Token(pub(super) bool);
impl Token {
pub(super) fn fire(&self) {
FIRED.fetch_add(1, Ordering::SeqCst);
}
}
impl Drop for Token {
fn drop(&mut self) {
DROPPED.fetch_add(1, Ordering::SeqCst);
if self.0 {
panic!("hook closure dropped");
}
}
}
}
#[allow(unsafe_code)]
fn register_all_hooks(conn: &mut BorrowedSqliteConnection, panicking: usize) {
use crate::sqlite::{
AuthorizerDecision, BusyDecision, CommitDecision, ProgressDecision, SqliteChangeOps,
SqliteTraceFlags, SqliteUpdateRouter,
};
use core::num::NonZeroU32;
use hook_tokens::Token;
let conn = unsafe { conn.inner_mut() };
let t = Token(panicking == 0);
conn.on_update(SqliteUpdateRouter::new().on_any(SqliteChangeOps::ALL, move |_| t.fire()));
let t = Token(panicking == 1);
conn.on_commit(move || {
t.fire();
CommitDecision::Proceed
});
let t = Token(panicking == 2);
conn.on_rollback(move || t.fire());
let t = Token(panicking == 3);
conn.on_progress(NonZeroU32::new(1).unwrap(), move || {
t.fire();
ProgressDecision::Continue
});
let t = Token(panicking == 4);
conn.on_wal(move |_, _, _| t.fire());
let t = Token(panicking == 5);
conn.on_busy(move |_| {
t.fire();
BusyDecision::GiveUp
});
let t = Token(panicking == 6);
conn.on_authorize(move |_| {
t.fire();
AuthorizerDecision::Allow
});
let t = Token(panicking == 7);
conn.on_trace(SqliteTraceFlags::all(), move |_| t.fire());
let t = Token(panicking == 8);
conn.on_collation_needed(move |_| {
t.fire();
None
});
}
fn trigger_hooks(conn: &mut SqliteConnection) {
use crate::connection::SimpleConnection;
conn.batch_execute(
"CREATE TABLE IF NOT EXISTS t (x); INSERT INTO t VALUES (1);
BEGIN; INSERT INTO t VALUES (2); ROLLBACK;",
)
.unwrap();
let _ = conn.batch_execute("SELECT 'a' = 'b' COLLATE nosuch");
}
#[test]
fn hooks_registered_in_auto_extension_are_unregistered_and_freed() {
use std::cell::Cell;
use std::sync::atomic::Ordering;
thread_local!(static ARMED: Cell<bool> = const { Cell::new(false) });
fn extension(conn: &mut BorrowedSqliteConnection) -> QueryResult<()> {
if ARMED.get() {
register_all_hooks(conn, usize::MAX);
}
Ok(())
}
let _lock = AUTO_EXT_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _reset = TestResetGuard;
hook_tokens::FIRED.store(0, Ordering::SeqCst);
hook_tokens::DROPPED.store(0, Ordering::SeqCst);
register_auto_extension(extension).unwrap();
ARMED.set(true);
let conn = SqliteConnection::establish(":memory:");
ARMED.set(false);
let mut conn = conn.unwrap();
assert_eq!(hook_tokens::DROPPED.load(Ordering::SeqCst), 9);
trigger_hooks(&mut conn);
assert_eq!(hook_tokens::FIRED.load(Ordering::SeqCst), 0);
}
#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
fn assert_panicking_hook_drop_frees_all_hooks(n: usize) {
use std::cell::Cell;
use std::sync::atomic::Ordering;
thread_local!(static PANIC_AT: Cell<usize> = const { Cell::new(usize::MAX) });
thread_local!(static ARMED: Cell<bool> = const { Cell::new(false) });
fn extension(conn: &mut BorrowedSqliteConnection) -> QueryResult<()> {
if ARMED.get() {
register_all_hooks(conn, PANIC_AT.get());
}
Ok(())
}
let _lock = AUTO_EXT_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _reset = TestResetGuard;
hook_tokens::DROPPED.store(0, Ordering::SeqCst);
register_auto_extension(extension).unwrap();
PANIC_AT.set(n);
ARMED.set(true);
let conn = SqliteConnection::establish(":memory:");
ARMED.set(false);
assert!(matches!(conn, Err(ConnectionError::BadConnection(_))));
assert_eq!(hook_tokens::DROPPED.load(Ordering::SeqCst), 9);
}
#[test]
#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
fn panicking_first_hook_drop_frees_all_hooks() {
assert_panicking_hook_drop_frees_all_hooks(0);
}
#[test]
#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
fn panicking_last_hook_drop_frees_all_hooks() {
assert_panicking_hook_drop_frees_all_hooks(8);
}
#[test]
#[allow(unsafe_code)]
#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
fn trampoline_reports_panic_message() {
fn panic_str(_conn: &mut BorrowedSqliteConnection) -> QueryResult<()> {
panic!("boom-str");
}
fn panic_string(_conn: &mut BorrowedSqliteConnection) -> QueryResult<()> {
panic!("boom-{}", 7);
}
fn panic_other(_conn: &mut BorrowedSqliteConnection) -> QueryResult<()> {
std::panic::panic_any(7_u8);
}
let cases: [(RawAutoExtension, &str); 3] = [
(entry_point(panic_str), "auto-extension panicked: boom-str"),
(entry_point(panic_string), "auto-extension panicked: boom-7"),
(entry_point(panic_other), "auto-extension panicked"),
];
let mut conn = open_memory_connection();
unsafe {
conn.with_raw_connection(|db| {
for (tramp, expected) in cases {
let mut err: *mut c_char = core::ptr::null_mut();
let rc = tramp(db, &mut err, core::ptr::null());
assert_eq!(rc, ffi::SQLITE_ERROR);
assert!(!err.is_null());
let message = core::ffi::CStr::from_ptr(err)
.to_string_lossy()
.into_owned();
assert_eq!(message, expected);
ffi::sqlite3_free(err as *mut core::ffi::c_void);
}
})
}
}
#[test]
#[allow(unsafe_code)]
fn error_message_truncates_at_interior_nul() {
let mut err: *mut c_char = core::ptr::null_mut();
set_error_message(&mut err, "before\0after");
assert!(!err.is_null());
unsafe {
let truncated = core::ffi::CStr::from_ptr(err).to_str().unwrap();
assert_eq!(truncated, "before");
ffi::sqlite3_free(err as *mut core::ffi::c_void);
}
set_error_message(core::ptr::null_mut(), "ignored");
}
}