1#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
2extern crate libsqlite3_sys as ffi;
3#[cfg(all(target_family = "wasm", target_os = "unknown"))]
4use sqlite_wasm_rs as ffi;
5
6use crate::result::Error::DatabaseError;
7use crate::result::*;
8use crate::sqlite::SqliteConnection;
9use alloc::boxed::Box;
10use alloc::string::{String, ToString};
11use core::ffi::{c_char, c_int};
12#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
13type RawApiPointer = *const core::ffi::c_void;
14#[cfg(all(target_family = "wasm", target_os = "unknown"))]
15type RawApiPointer = *const ffi::sqlite3_api_routines;
16
17type RawAutoExtension = unsafe extern "C" fn(
19 db: *mut ffi::sqlite3,
20 pz_err_msg: *mut *mut c_char,
21 p_api: RawApiPointer,
22) -> c_int;
23
24#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
26mod auto_extension_ffi {
27 use super::{RawAutoExtension, c_int};
28
29 #[allow(unsafe_code)]
31 unsafe extern "C" {
32 pub(super) fn sqlite3_auto_extension(entry_point: Option<RawAutoExtension>) -> c_int;
33 pub(super) fn sqlite3_cancel_auto_extension(entry_point: Option<RawAutoExtension>)
34 -> c_int;
35 }
36}
37
38#[cfg(all(target_family = "wasm", target_os = "unknown"))]
39use ffi as auto_extension_ffi;
40
41use super::BorrowedSqliteConnection;
42
43#[allow(unsafe_code)]
101pub fn register_auto_extension<F>(extension: F) -> QueryResult<()>
102where
103 F: Fn(&'_ mut BorrowedSqliteConnection) -> QueryResult<()> + Sync + 'static,
104{
105 let result =
107 unsafe { auto_extension_ffi::sqlite3_auto_extension(Some(entry_point(extension))) };
108 if result == ffi::SQLITE_OK {
109 Ok(())
110 } else {
111 Err(DatabaseError(
112 DatabaseErrorKind::Unknown,
113 Box::new(ffi::code_to_str(result).to_string()),
114 ))
115 }
116}
117
118#[allow(unsafe_code)]
127pub fn cancel_auto_extension<F>(extension: F) -> bool
128where
129 F: Fn(&'_ mut BorrowedSqliteConnection) -> QueryResult<()> + Sync + 'static,
130{
131 unsafe { auto_extension_ffi::sqlite3_cancel_auto_extension(Some(entry_point(extension))) != 0 }
133}
134
135#[allow(unsafe_code)]
141pub fn reset_auto_extension() {
142 unsafe { ffi::sqlite3_reset_auto_extension() }
143}
144
145fn entry_point<F>(extension: F) -> RawAutoExtension
149where
150 F: Fn(&'_ mut BorrowedSqliteConnection) -> QueryResult<()> + Sync + 'static,
151{
152 core::mem::forget(extension);
153 trampoline::<F>
154}
155
156#[allow(unsafe_code)]
160unsafe extern "C" fn trampoline<F>(
161 db: *mut ffi::sqlite3,
162 pz_err_msg: *mut *mut c_char,
163 _p_api: RawApiPointer,
164) -> c_int
165where
166 F: Fn(&'_ mut BorrowedSqliteConnection) -> QueryResult<()> + Sync + 'static,
167{
168 const {
169 if !(core::mem::size_of::<F>() == 0) {
{
::core::panicking::panic_fmt(format_args!("an auto-extension callback must not capture non-zero-sized state. Use a `fn` item or a closure that captures only zero-sized values"));
}
};assert!(
170 core::mem::size_of::<F>() == 0,
171 "an auto-extension callback must not capture non-zero-sized state. \
172 Use a `fn` item or a closure that captures only zero-sized values"
173 );
174 }
175
176 let result: Result<(), String> =
179 crate::util::std_compat::catch_unwind(core::panic::AssertUnwindSafe(|| {
180 let Some(db) = core::ptr::NonNull::new(db) else {
181 return Err(String::from(
182 "auto-extension received a null database handle",
183 ));
184 };
185 let extension: &F = unsafe { core::ptr::NonNull::<F>::dangling().as_ref() };
190 unsafe { SqliteConnection::with_borrowed_connection(db, extension) }
193 .map_err(|e| e.to_string())
194 }))
195 .unwrap_or_else(|panic| {
196 Err(match panic_detail(panic) {
197 Some(message) => ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("auto-extension panicked: {0}",
message))
})alloc::format!("auto-extension panicked: {message}"),
198 None => String::from("auto-extension panicked"),
199 })
200 });
201
202 match result {
203 Ok(()) => ffi::SQLITE_OK,
204 Err(message) => {
205 set_error_message(pz_err_msg, &message);
206 ffi::SQLITE_ERROR
207 }
208 }
209}
210
211#[cfg(feature = "std")]
214fn panic_detail(panic: alloc::boxed::Box<dyn core::any::Any + Send>) -> Option<String> {
215 panic
216 .downcast_ref::<&str>()
217 .map(|s| (*s).to_owned())
218 .or_else(|| panic.downcast_ref::<String>().cloned())
219}
220
221#[cfg(not(feature = "std"))]
222fn panic_detail(_panic: ()) -> Option<String> {
223 None
224}
225
226#[allow(unsafe_code)]
230fn set_error_message(pz_err_msg: *mut *mut c_char, message: &str) {
231 if pz_err_msg.is_null() {
232 return;
233 }
234
235 let bytes = message.as_bytes();
236 let len = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
237
238 let Ok(size) = c_int::try_from(len + 1) else {
241 return;
242 };
243 let buffer = unsafe { ffi::sqlite3_malloc(size) } as *mut u8;
244 if buffer.is_null() {
245 return;
246 }
247
248 unsafe {
249 core::ptr::copy_nonoverlapping(bytes.as_ptr(), buffer, len);
250 *buffer.add(len) = 0;
251 *pz_err_msg = buffer as *mut c_char;
252 }
253}
254
255#[cfg(all(test, not(miri)))]
260mod tests {
261 use super::*;
262 use crate::dsl::sql;
263 use crate::prelude::*;
264 use crate::sql_types::Integer;
265 use crate::sqlite::BorrowedSqliteConnection;
266 use std::sync::Mutex;
267
268 static AUTO_EXT_TEST_LOCK: Mutex<()> = Mutex::new(());
274
275 #[allow(unsafe_code, reason = "Test call to inner unsafe function")]
278 fn test_ext_init(conn: &mut BorrowedSqliteConnection) -> QueryResult<()> {
279 unsafe {
280 conn.inner_mut()
281 .register_collation("TESTCOLL", |a, b| a.cmp(b))
282 }
283 }
284
285 fn open_memory_connection() -> SqliteConnection {
286 SqliteConnection::establish(":memory:").expect("Failed to open :memory: connection")
287 }
288
289 fn probe_collation() -> QueryResult<i32> {
291 let mut conn = open_memory_connection();
292 sql::<Integer>("SELECT 'a' = 'a' COLLATE TESTCOLL").get_result(&mut conn)
293 }
294
295 struct TestResetGuard;
298
299 impl Drop for TestResetGuard {
300 fn drop(&mut self) {
301 reset_auto_extension();
302 }
303 }
304
305 #[test]
306 fn auto_extension_lifecycle() {
307 let _lock = AUTO_EXT_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
308 let _guard = TestResetGuard;
309 reset_auto_extension();
310
311 register_auto_extension(test_ext_init).unwrap();
313 assert_eq!(probe_collation().unwrap(), 1);
314
315 let removed = cancel_auto_extension(test_ext_init);
317 assert!(
318 removed,
319 "cancel should return true for registered extension"
320 );
321 assert!(
322 probe_collation().is_err(),
323 "collation should not be available after cancel"
324 );
325
326 let removed = cancel_auto_extension(test_ext_init);
328 assert!(
329 !removed,
330 "cancel should return false for unregistered extension"
331 );
332
333 register_auto_extension(test_ext_init).unwrap();
335 reset_auto_extension();
336 assert!(
337 probe_collation().is_err(),
338 "collation should not be available after reset"
339 );
340
341 register_auto_extension(test_ext_init).unwrap();
343 register_auto_extension(test_ext_init).unwrap();
344 assert_eq!(probe_collation().unwrap(), 1);
345 }
347
348 #[test]
352 #[allow(unsafe_code)]
353 fn trampoline_maps_result_to_return_code() {
354 fn ok_ext(_conn: &mut BorrowedSqliteConnection) -> QueryResult<()> {
355 Ok(())
356 }
357 fn err_ext(_conn: &mut BorrowedSqliteConnection) -> QueryResult<()> {
358 Err(Error::QueryBuilderError("boom".into()))
359 }
360
361 let ok_tramp = entry_point(ok_ext);
362 let err_tramp = entry_point(err_ext);
363
364 let mut conn = open_memory_connection();
365 unsafe {
368 conn.with_raw_connection(|db| {
369 let mut err: *mut c_char = core::ptr::null_mut();
370
371 let rc = ok_tramp(db, &mut err, core::ptr::null());
373 assert_eq!(rc, ffi::SQLITE_OK);
374 assert!(err.is_null());
375
376 let rc = err_tramp(db, &mut err, core::ptr::null());
378 assert_eq!(rc, ffi::SQLITE_ERROR);
379 assert!(!err.is_null());
380 let message = core::ffi::CStr::from_ptr(err)
381 .to_string_lossy()
382 .into_owned();
383 assert_eq!(message, "boom");
384 ffi::sqlite3_free(err as *mut core::ffi::c_void);
385
386 let mut err: *mut c_char = core::ptr::null_mut();
388 let rc = ok_tramp(core::ptr::null_mut(), &mut err, core::ptr::null());
389 assert_eq!(rc, ffi::SQLITE_ERROR);
390 if !err.is_null() {
391 ffi::sqlite3_free(err as *mut core::ffi::c_void);
392 }
393 })
394 }
395 }
396
397 #[test]
402 fn callback_zero_sized_capture_is_never_dropped() {
403 use std::sync::atomic::{AtomicUsize, Ordering};
404
405 static DROPS: AtomicUsize = AtomicUsize::new(0);
406
407 struct Guard;
409 impl Drop for Guard {
410 fn drop(&mut self) {
411 DROPS.fetch_add(1, Ordering::SeqCst);
412 }
413 }
414
415 let _lock = AUTO_EXT_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
416 let _reset = TestResetGuard;
417 reset_auto_extension();
418
419 let guard = Guard;
420 #[allow(unsafe_code, reason = "Test call to inner unsafe function")]
423 register_auto_extension(move |conn: &mut BorrowedSqliteConnection| {
424 let _ = &guard;
425 unsafe {
426 conn.inner_mut()
427 .register_collation("TESTCOLL", |a, b| a.cmp(b))
428 }
429 })
430 .unwrap();
431
432 for _ in 0..5 {
433 assert_eq!(probe_collation().unwrap(), 1);
434 }
435 reset_auto_extension();
436
437 assert_eq!(
438 DROPS.load(Ordering::SeqCst),
439 0,
440 "the captured guard's destructor must never run"
441 );
442 }
443
444 #[test]
445 fn leaked_failed_cursor_in_auto_extension_fails_open_cleanly() {
446 use std::cell::Cell;
447
448 thread_local!(static ARMED: Cell<bool> = const { Cell::new(false) });
450
451 #[allow(unsafe_code, reason = "Test code")]
452 fn extension(conn: &mut BorrowedSqliteConnection) -> QueryResult<()> {
453 if ARMED.get() {
454 unsafe {
455 crate::sqlite::connection::leak_failed_cursor(conn.inner_mut());
456 }
457 }
458 Ok(())
459 }
460
461 let _lock = AUTO_EXT_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
462 let _reset = TestResetGuard;
463 register_auto_extension(extension).unwrap();
464 ARMED.set(true);
465
466 let conn = SqliteConnection::establish(":memory:");
468 assert!(matches!(conn, Err(ConnectionError::BadConnection(_))));
469 }
470
471 #[test]
472 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))] fn panic_in_borrowed_connection_teardown_keeps_handle_open() {
474 use crate::connection::{Instrumentation, InstrumentationEvent};
475 use std::cell::Cell;
476
477 thread_local!(static ARMED: Cell<bool> = const { Cell::new(false) });
479
480 struct PanicOnDrop;
481
482 impl Instrumentation for PanicOnDrop {
483 fn on_connection_event(&mut self, _event: InstrumentationEvent<'_>) {}
484 }
485
486 impl Drop for PanicOnDrop {
487 fn drop(&mut self) {
488 if ARMED.get() {
489 panic!("instrumentation dropped");
490 }
491 }
492 }
493
494 fn extension(conn: &mut BorrowedSqliteConnection) -> QueryResult<()> {
495 if ARMED.get() {
496 conn.set_instrumentation(PanicOnDrop);
497 }
498 Ok(())
499 }
500
501 let _lock = AUTO_EXT_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
502 let _reset = TestResetGuard;
503 register_auto_extension(extension).unwrap();
504 ARMED.set(true);
505 let conn = SqliteConnection::establish(":memory:");
506 ARMED.set(false);
507
508 assert!(matches!(conn, Err(ConnectionError::BadConnection(_))));
510 }
511
512 mod hook_tokens {
515 use std::sync::atomic::{AtomicUsize, Ordering};
516 pub(super) static FIRED: AtomicUsize = AtomicUsize::new(0);
517 pub(super) static DROPPED: AtomicUsize = AtomicUsize::new(0);
518 pub(super) struct Token(pub(super) bool);
520 impl Token {
521 pub(super) fn fire(&self) {
522 FIRED.fetch_add(1, Ordering::SeqCst);
523 }
524 }
525 impl Drop for Token {
526 fn drop(&mut self) {
527 DROPPED.fetch_add(1, Ordering::SeqCst);
528 if self.0 {
529 panic!("hook closure dropped");
530 }
531 }
532 }
533 }
534
535 #[allow(unsafe_code)]
538 fn register_all_hooks(conn: &mut BorrowedSqliteConnection, panicking: usize) {
539 use crate::sqlite::{
540 AuthorizerDecision, BusyDecision, CommitDecision, ProgressDecision, SqliteChangeOps,
541 SqliteTraceFlags, SqliteUpdateRouter,
542 };
543 use core::num::NonZeroU32;
544 use hook_tokens::Token;
545 let conn = unsafe { conn.inner_mut() };
546 let t = Token(panicking == 0);
547 conn.on_update(SqliteUpdateRouter::new().on_any(SqliteChangeOps::ALL, move |_| t.fire()));
548 let t = Token(panicking == 1);
549 conn.on_commit(move || {
550 t.fire();
551 CommitDecision::Proceed
552 });
553 let t = Token(panicking == 2);
554 conn.on_rollback(move || t.fire());
555 let t = Token(panicking == 3);
556 conn.on_progress(NonZeroU32::new(1).unwrap(), move || {
557 t.fire();
558 ProgressDecision::Continue
559 });
560 let t = Token(panicking == 4);
561 conn.on_wal(move |_, _, _| t.fire());
562 let t = Token(panicking == 5);
563 conn.on_busy(move |_| {
564 t.fire();
565 BusyDecision::GiveUp
566 });
567 let t = Token(panicking == 6);
568 conn.on_authorize(move |_| {
569 t.fire();
570 AuthorizerDecision::Allow
571 });
572 let t = Token(panicking == 7);
573 conn.on_trace(SqliteTraceFlags::all(), move |_| t.fire());
574 let t = Token(panicking == 8);
575 conn.on_collation_needed(move |_| {
576 t.fire();
577 None
578 });
579 }
580
581 fn trigger_hooks(conn: &mut SqliteConnection) {
584 use crate::connection::SimpleConnection;
585 conn.batch_execute(
586 "CREATE TABLE IF NOT EXISTS t (x); INSERT INTO t VALUES (1);
587 BEGIN; INSERT INTO t VALUES (2); ROLLBACK;",
588 )
589 .unwrap();
590 let _ = conn.batch_execute("SELECT 'a' = 'b' COLLATE nosuch");
591 }
592
593 #[test]
595 fn hooks_registered_in_auto_extension_are_unregistered_and_freed() {
596 use std::cell::Cell;
597 use std::sync::atomic::Ordering;
598 thread_local!(static ARMED: Cell<bool> = const { Cell::new(false) });
599 fn extension(conn: &mut BorrowedSqliteConnection) -> QueryResult<()> {
600 if ARMED.get() {
601 register_all_hooks(conn, usize::MAX);
602 }
603 Ok(())
604 }
605 let _lock = AUTO_EXT_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
606 let _reset = TestResetGuard;
607 hook_tokens::FIRED.store(0, Ordering::SeqCst);
608 hook_tokens::DROPPED.store(0, Ordering::SeqCst);
609 register_auto_extension(extension).unwrap();
610 ARMED.set(true);
611 let conn = SqliteConnection::establish(":memory:");
612 ARMED.set(false);
613 let mut conn = conn.unwrap();
614 assert_eq!(hook_tokens::DROPPED.load(Ordering::SeqCst), 9);
615 trigger_hooks(&mut conn);
616 assert_eq!(hook_tokens::FIRED.load(Ordering::SeqCst), 0);
617 }
618
619 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
623 fn assert_panicking_hook_drop_frees_all_hooks(n: usize) {
624 use std::cell::Cell;
625 use std::sync::atomic::Ordering;
626 thread_local!(static PANIC_AT: Cell<usize> = const { Cell::new(usize::MAX) });
627 thread_local!(static ARMED: Cell<bool> = const { Cell::new(false) });
628 fn extension(conn: &mut BorrowedSqliteConnection) -> QueryResult<()> {
629 if ARMED.get() {
630 register_all_hooks(conn, PANIC_AT.get());
631 }
632 Ok(())
633 }
634 let _lock = AUTO_EXT_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
635 let _reset = TestResetGuard;
636 hook_tokens::DROPPED.store(0, Ordering::SeqCst);
637 register_auto_extension(extension).unwrap();
638 PANIC_AT.set(n);
639 ARMED.set(true);
640 let conn = SqliteConnection::establish(":memory:");
641 ARMED.set(false);
642 assert!(matches!(conn, Err(ConnectionError::BadConnection(_))));
643 assert_eq!(hook_tokens::DROPPED.load(Ordering::SeqCst), 9);
644 }
645
646 #[test]
647 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
648 fn panicking_first_hook_drop_frees_all_hooks() {
649 assert_panicking_hook_drop_frees_all_hooks(0);
650 }
651
652 #[test]
653 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
654 fn panicking_last_hook_drop_frees_all_hooks() {
655 assert_panicking_hook_drop_frees_all_hooks(8);
656 }
657
658 #[test]
663 #[allow(unsafe_code)]
664 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
665 fn trampoline_reports_panic_message() {
666 fn panic_str(_conn: &mut BorrowedSqliteConnection) -> QueryResult<()> {
667 panic!("boom-str");
668 }
669 fn panic_string(_conn: &mut BorrowedSqliteConnection) -> QueryResult<()> {
670 panic!("boom-{}", 7);
671 }
672 fn panic_other(_conn: &mut BorrowedSqliteConnection) -> QueryResult<()> {
673 std::panic::panic_any(7_u8);
674 }
675
676 let cases: [(RawAutoExtension, &str); 3] = [
677 (entry_point(panic_str), "auto-extension panicked: boom-str"),
678 (entry_point(panic_string), "auto-extension panicked: boom-7"),
679 (entry_point(panic_other), "auto-extension panicked"),
680 ];
681
682 let mut conn = open_memory_connection();
683 unsafe {
685 conn.with_raw_connection(|db| {
686 for (tramp, expected) in cases {
687 let mut err: *mut c_char = core::ptr::null_mut();
688 let rc = tramp(db, &mut err, core::ptr::null());
689 assert_eq!(rc, ffi::SQLITE_ERROR);
690 assert!(!err.is_null());
691 let message = core::ffi::CStr::from_ptr(err)
692 .to_string_lossy()
693 .into_owned();
694 assert_eq!(message, expected);
695 ffi::sqlite3_free(err as *mut core::ffi::c_void);
696 }
697 })
698 }
699 }
700
701 #[test]
702 #[allow(unsafe_code)]
703 fn error_message_truncates_at_interior_nul() {
704 let mut err: *mut c_char = core::ptr::null_mut();
705 set_error_message(&mut err, "before\0after");
706 assert!(!err.is_null());
707 unsafe {
709 let truncated = core::ffi::CStr::from_ptr(err).to_str().unwrap();
710 assert_eq!(truncated, "before");
711 ffi::sqlite3_free(err as *mut core::ffi::c_void);
712 }
713
714 set_error_message(core::ptr::null_mut(), "ignored");
716 }
717}